yak(boundary-repo-as-sibling-boundary-web-repo-forbidden-tgrc): boundary: Repo as sibling boundary, web->Repo forbidden
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
defmodule Firehose.BoundaryTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Mix.Dep.Lock
|
||||
|
||||
# Boundary enforcement itself is a compile-time property: the pin on it is the
|
||||
# warnings-fatal compile in `make verify` (and the recorded violation
|
||||
# demonstration in the yak notes). These tests pin the *declarations* that
|
||||
# compile gate reads, so a later edit that quietly re-opens the web -> Repo
|
||||
# hole shows up as a failure here instead of only as an architectural drift.
|
||||
#
|
||||
# They read the `Boundary` module attribute that `use Boundary` persists. That
|
||||
# is the same attribute boundary's own classifier reads
|
||||
# (deps/boundary/lib/boundary/definition.ex: `Module.register_attribute(...,
|
||||
# Boundary, persist: true)`), so this is the declaration as compiled, not a
|
||||
# grep of the source text.
|
||||
describe "boundary dependency" do
|
||||
test "boundary is a direct dependency, not one inherited from ex_crap" do
|
||||
assert {:boundary, requirement, opts} =
|
||||
List.keyfind(Mix.Project.config()[:deps], :boundary, 0)
|
||||
|
||||
assert opts[:runtime] == false, "boundary is compile-time only; it must not ship at runtime"
|
||||
assert Version.match?(locked_version(:boundary), requirement)
|
||||
end
|
||||
end
|
||||
|
||||
describe "a sibling Repo boundary" do
|
||||
test "Firehose.Repo is its own top-level boundary with no dependencies" do
|
||||
definition = boundary(Firehose.Repo)
|
||||
|
||||
assert definition.opts[:top_level?] == true,
|
||||
"Firehose.Repo must be a sibling of Firehose/FirehoseWeb, not nested inside Firehose"
|
||||
|
||||
assert deps(Firehose.Repo) == [],
|
||||
"the Repo is the bottom of the graph: it may depend on nothing"
|
||||
end
|
||||
|
||||
test "Firehose may use Firehose.Repo" do
|
||||
assert Firehose.Repo in deps(Firehose)
|
||||
end
|
||||
|
||||
test "FirehoseWeb may not use Firehose.Repo" do
|
||||
refute Firehose.Repo in deps(FirehoseWeb),
|
||||
"web -> Repo is the hole this boundary closes; only contexts may touch the Repo"
|
||||
end
|
||||
end
|
||||
|
||||
describe "the context boundary's surface" do
|
||||
test "Firehose exports everything, so the web tier can keep using context modules" do
|
||||
assert boundary(Firehose).opts[:exports] == :all
|
||||
end
|
||||
end
|
||||
|
||||
# Application boot is the only thing that reaches from the context tier into
|
||||
# the web tier. Making it its own top-level boundary (boundary's README
|
||||
# prescribes exactly this for `mix phx.new` layouts) is what keeps the graph
|
||||
# acyclic; the alternative -- FirehoseWeb in Firehose's deps -- is rejected by
|
||||
# boundary with "dependency cycle found: Firehose -> FirehoseWeb -> Firehose".
|
||||
describe "the application boundary" do
|
||||
test "Firehose.Application depends on both tiers, and neither tier on the other's boot code" do
|
||||
assert boundary(Firehose.Application).opts[:top_level?] == true
|
||||
|
||||
assert Enum.sort(deps(Firehose.Application)) == [Firehose, FirehoseWeb]
|
||||
|
||||
refute FirehoseWeb in deps(Firehose),
|
||||
"Firehose depending on FirehoseWeb is the dependency cycle"
|
||||
end
|
||||
end
|
||||
|
||||
describe "enforcement wiring" do
|
||||
# A compiler that is registered after :elixir looks wired but never reports
|
||||
# anything, so the gate would be decoration. Position is the property.
|
||||
test "the boundary compiler runs before :elixir in this environment" do
|
||||
compilers = Mix.Project.config()[:compilers]
|
||||
|
||||
assert :boundary in compilers, "boundary compiler is not wired in #{Mix.env()}"
|
||||
|
||||
assert Enum.find_index(compilers, &(&1 == :boundary)) <
|
||||
Enum.find_index(compilers, &(&1 == :elixir)),
|
||||
"boundary must be listed before :elixir: it hooks after_compiler(:elixir, ...)"
|
||||
end
|
||||
end
|
||||
|
||||
defp boundary(module) do
|
||||
assert [definition] = module.__info__(:attributes)[Boundary],
|
||||
"#{inspect(module)} declares no boundary"
|
||||
|
||||
definition
|
||||
end
|
||||
|
||||
defp deps(module), do: Keyword.get(boundary(module).opts, :deps, [])
|
||||
|
||||
defp locked_version(app) do
|
||||
Lock.read()
|
||||
|> Map.fetch!(app)
|
||||
|> elem(2)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user