mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 21:13:00 +08:00
617 lines
23 KiB
Go
617 lines
23 KiB
Go
// Package architecture checks the dependency rules that keep the packages
|
|
// separable.
|
|
//
|
|
// These are asserted rather than documented because a wrong import compiles
|
|
// fine and is only noticed much later, when the package it corrupted can no
|
|
// longer be tested without a router attached.
|
|
package architecture
|
|
|
|
import (
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const modulePath = "github.com/matthewlu070111/smart-srun/core"
|
|
|
|
// coreRoot is the module root, two levels up from tests/architecture.
|
|
func coreRoot(t *testing.T) string {
|
|
t.Helper()
|
|
root, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("resolve core root: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil {
|
|
t.Fatalf("core root %q has no go.mod: %v", root, err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
// packageImports maps each package's import path to the set of paths it imports.
|
|
// Test files are excluded: a test may reach for anything it needs to build a
|
|
// fixture, and holding tests to the production dependency rules would only
|
|
// push fixtures into production code.
|
|
func packageImports(t *testing.T) map[string][]string {
|
|
t.Helper()
|
|
root := coreRoot(t)
|
|
fileSet := token.NewFileSet()
|
|
result := map[string][]string{}
|
|
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if entry.IsDir() || !strings.HasSuffix(path, ".go") ||
|
|
strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
relative, err := filepath.Rel(root, filepath.Dir(path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
importPath := modulePath
|
|
if relative != "." {
|
|
importPath += "/" + filepath.ToSlash(relative)
|
|
}
|
|
|
|
file, err := parser.ParseFile(fileSet, path, nil, parser.ImportsOnly)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, spec := range file.Imports {
|
|
imported := strings.Trim(spec.Path.Value, `"`)
|
|
if !slices.Contains(result[importPath], imported) {
|
|
result[importPath] = append(result[importPath], imported)
|
|
}
|
|
}
|
|
if _, seen := result[importPath]; !seen {
|
|
result[importPath] = nil
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk: %v", err)
|
|
}
|
|
if len(result) == 0 {
|
|
t.Fatal("no packages found; the walk is looking in the wrong place")
|
|
}
|
|
return result
|
|
}
|
|
|
|
// domain is the vocabulary every other package shares. Giving it I/O would make
|
|
// every type that mentions it untestable without the thing it reached for.
|
|
func TestDomainHasNoIO(t *testing.T) {
|
|
// "net" is forbidden but "net/netip" is not, and the difference is the
|
|
// point: netip is value types with no dialing, listening or resolving in
|
|
// it, so a Binding can name an address without the package that holds it
|
|
// gaining the ability to open a socket.
|
|
forbidden := []string{
|
|
"os", "os/exec", "net", "net/http", "io/fs", "database/sql",
|
|
"os/user", "syscall", "golang.org/x/sys/unix", "bufio",
|
|
}
|
|
imports := packageImports(t)[modulePath+"/internal/domain"]
|
|
if imports == nil {
|
|
t.Fatal("domain package not found")
|
|
}
|
|
for _, imported := range imports {
|
|
if slices.Contains(forbidden, imported) {
|
|
t.Errorf("domain imports %q; it must stay pure so every type that "+
|
|
"mentions it can be tested without that dependency", imported)
|
|
}
|
|
if strings.HasPrefix(imported, modulePath) {
|
|
t.Errorf("domain imports %q; the vocabulary package depends on "+
|
|
"nothing inside the module", imported)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The direction that matters: the layers that make decisions may use the layers
|
|
// that hold data, never the reverse. An adapter reaching back into the
|
|
// application is how a "small exception" becomes a cycle.
|
|
func TestDependencyDirection(t *testing.T) {
|
|
// deciders are the layers that make decisions, plus the assembly above
|
|
// them. Nothing below them may depend on them, which is most of what the
|
|
// table below repeats.
|
|
deciders := []string{"internal/policy", "internal/application",
|
|
"internal/observe", "internal/daemon", "internal/wifi",
|
|
"internal/wireless"}
|
|
below := func(extra ...string) []string {
|
|
return append(append([]string{}, deciders...), extra...)
|
|
}
|
|
|
|
rules := map[string][]string{
|
|
"internal/domain": below("internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "internal/openwrt",
|
|
"internal/transport", "internal/auth", "internal/strategy",
|
|
"internal/presets", "cmd"),
|
|
// presets decodes a published document into values and decides nothing
|
|
// else. It may speak domain and nothing above it: a catalogue reader
|
|
// that could reach the transport would fetch on its own, and then the
|
|
// rules about which source wins and when a cache may be replaced would
|
|
// need a network to test.
|
|
"internal/presets": below("internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "internal/openwrt",
|
|
"internal/transport", "internal/auth", "internal/strategy", "cmd"),
|
|
"internal/protocol": below("internal/config", "internal/control",
|
|
"internal/cli", "internal/openwrt", "internal/transport", "cmd"),
|
|
// transport carries bytes out of one line. It takes a Binding as a
|
|
// value and knows nothing about where that came from, so it does not
|
|
// depend on the adapter that produced it -- which is what lets it be
|
|
// tested with a hand-made binding and no router.
|
|
"internal/transport": below("internal/config", "internal/control",
|
|
"internal/cli", "internal/openwrt", "internal/auth", "cmd"),
|
|
// auth performs one transaction over a line somebody else chose. It
|
|
// must not read configuration or answer RPCs, and it must not reach the
|
|
// adapter: which line to use is decided above it.
|
|
"internal/auth": below("internal/config", "internal/control",
|
|
"internal/cli", "internal/openwrt", "cmd"),
|
|
// strategy is declarative. It describes a school's parameters and
|
|
// extension points; it does not perform I/O of any kind.
|
|
"internal/strategy": below("internal/config", "internal/control",
|
|
"internal/cli", "internal/openwrt", "internal/transport", "cmd"),
|
|
// The adapter is a leaf. It reads the router and speaks domain; it does
|
|
// not read the user's configuration or answer RPCs. An adapter that
|
|
// reaches back into the layers that use it is how a "small exception"
|
|
// becomes a cycle, and it would also make every one of those layers
|
|
// need a router to test.
|
|
"internal/openwrt": below("internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "cmd"),
|
|
"internal/config": below("internal/control", "internal/cli",
|
|
"internal/openwrt", "cmd"),
|
|
// policy is arithmetic over domain values and nothing else. Every
|
|
// scheduling question it answers has to be answerable without a router,
|
|
// a socket or a configuration file, which is what lets its tests run a
|
|
// hundred thousand failures and cross midnight in microseconds.
|
|
"internal/policy": {"internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "internal/openwrt",
|
|
"internal/transport", "internal/auth", "internal/strategy",
|
|
"internal/application", "internal/observe", "internal/daemon",
|
|
"internal/wifi", "internal/wireless", "cmd"},
|
|
// wifi is the same kind of package as policy, for the other half of the
|
|
// scheduling problem: which access point, rather than when. It decides
|
|
// from values only, so its tests run on a machine with no wireless
|
|
// hardware -- which is every machine this is developed on. A wifi
|
|
// package that could scan would need a radio to test, and the guest
|
|
// this project tests on does not have one either.
|
|
"internal/wifi": {"internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "internal/openwrt",
|
|
"internal/transport", "internal/auth", "internal/strategy",
|
|
"internal/application", "internal/observe", "internal/daemon",
|
|
"internal/policy", "internal/wireless", "cmd"},
|
|
// wireless performs the one change this program makes to a device it does
|
|
// not own. It may read the adapter and ask wifi what to join, and it may
|
|
// not reach the layers that schedule it: a transaction that could submit
|
|
// an action could start itself, and the global lock spec 04 requires
|
|
// would have nothing to protect.
|
|
"internal/wireless": below("internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "internal/transport",
|
|
"internal/auth", "internal/strategy", "cmd"),
|
|
// observe is a projection. It may name what policy decided, but it does
|
|
// not decide anything itself and it never reaches the network -- a
|
|
// status poll that probed would turn an idle browser tab into
|
|
// continuous authentication traffic.
|
|
"internal/observe": {"internal/config", "internal/protocol",
|
|
"internal/control", "internal/cli", "internal/openwrt",
|
|
"internal/transport", "internal/auth", "internal/application",
|
|
"internal/daemon", "internal/wifi", "cmd"},
|
|
// application coordinates. It may use everything below it; what it may
|
|
// not do is reach up into the transports that call it.
|
|
// application declares the Wireless interface it needs and does not
|
|
// import the package that implements it, for the same reason it does not
|
|
// import openwrt: the assembly belongs to daemon, and a coordinator that
|
|
// could reach the transaction could not be tested without one.
|
|
"internal/application": {"internal/control", "internal/cli",
|
|
"internal/openwrt", "internal/wireless", "internal/daemon", "cmd"},
|
|
"internal/control": {"internal/cli", "internal/daemon", "cmd"},
|
|
// daemon assembles the running service. It is allowed to know about
|
|
// everything below it -- that is its job -- and nothing about the
|
|
// command line above it: a service that reached into the CLI could not
|
|
// be started any other way.
|
|
"internal/daemon": {"internal/cli", "cmd"},
|
|
"internal/cli": {"cmd"},
|
|
}
|
|
|
|
imports := packageImports(t)
|
|
for pkg, forbidden := range rules {
|
|
full := modulePath + "/" + pkg
|
|
for _, imported := range imports[full] {
|
|
for _, banned := range forbidden {
|
|
if strings.HasPrefix(imported, modulePath+"/"+banned) {
|
|
t.Errorf("%s imports %s; dependencies point one way only",
|
|
pkg, imported)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The protocol layer is bytes in, bytes out. Spec 04 requires timestamps and
|
|
// callback names to arrive as arguments: a package that read the clock or the
|
|
// network could only be tested against a live gateway, which is exactly what
|
|
// fixed protocol vectors exist to avoid.
|
|
func TestProtocolReadsNothing(t *testing.T) {
|
|
forbidden := []string{
|
|
"net", "net/http", "net/url", "os", "os/exec", "time", "math/rand",
|
|
"math/rand/v2", "crypto/rand", "io/ioutil", "bufio",
|
|
}
|
|
|
|
imports := packageImports(t)
|
|
found := false
|
|
for pkg, imported := range imports {
|
|
if !strings.HasPrefix(pkg, modulePath+"/internal/protocol") {
|
|
continue
|
|
}
|
|
found = true
|
|
for _, name := range imported {
|
|
if slices.Contains(forbidden, name) {
|
|
t.Errorf("%s imports %q; the protocol layer takes its inputs as "+
|
|
"arguments so its output is decided entirely by them", pkg, name)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("no protocol package found")
|
|
}
|
|
}
|
|
|
|
// Every system command goes through the one runner.
|
|
//
|
|
// That runner is what makes a command cancellable, bounded in output and time,
|
|
// reaped along with its children, and free of a shell. A second place that
|
|
// starts a process gets none of it, and the way that happens is somebody
|
|
// needing one more query and reaching for exec.Command because it is three
|
|
// lines. Restricting the import to the file that owns the guarantees makes the
|
|
// shortcut visible instead of easy.
|
|
func TestOnlyTheRunnerStartsProcesses(t *testing.T) {
|
|
// command.go declares the runner; the platform files carry the syscalls it
|
|
// needs for process groups.
|
|
allowed := map[string]bool{
|
|
"command.go": true, "platform_unix.go": true, "platform_other.go": true,
|
|
}
|
|
|
|
root := filepath.Join(coreRoot(t), "internal", "openwrt")
|
|
fileSet := token.NewFileSet()
|
|
found := false
|
|
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
t.Fatalf("read the adapter directory: %v", err)
|
|
}
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if entry.IsDir() || !strings.HasSuffix(name, ".go") ||
|
|
strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
found = true
|
|
file, err := parser.ParseFile(fileSet, filepath.Join(root, name), nil,
|
|
parser.ImportsOnly)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", name, err)
|
|
}
|
|
for _, spec := range file.Imports {
|
|
imported := strings.Trim(spec.Path.Value, `"`)
|
|
if (imported == "os/exec" || imported == "syscall") && !allowed[name] {
|
|
t.Errorf("%s imports %q; system commands go through Runner, "+
|
|
"which is what bounds their output and reaps their children",
|
|
name, imported)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("no adapter sources found; the walk is looking in the wrong place")
|
|
}
|
|
}
|
|
|
|
// Every committed fixture is read by something.
|
|
//
|
|
// A fixture nobody opens suggests coverage that does not exist: it looks like
|
|
// the case is covered when nothing asserts anything about it. These files are
|
|
// captured from real devices and can be regenerated from the raw capture, so
|
|
// the honest state is to commit the ones in use.
|
|
//
|
|
// It lives here rather than beside the tests that read them because it is an
|
|
// assertion about the repository, and the adapter's own tests are cross-
|
|
// compiled and run inside a real OpenWrt guest, where there is no source tree.
|
|
func TestEveryCommittedFixtureIsUsed(t *testing.T) {
|
|
root := coreRoot(t)
|
|
fixtures := filepath.Join(root, "testdata", "openwrt")
|
|
tests := filepath.Join(root, "internal", "openwrt")
|
|
|
|
sources := map[string]string{}
|
|
entries, err := os.ReadDir(tests)
|
|
if err != nil {
|
|
t.Fatalf("read the adapter directory: %v", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if !strings.HasSuffix(entry.Name(), "_test.go") {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(tests, entry.Name()))
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", entry.Name(), err)
|
|
}
|
|
sources[entry.Name()] = string(data)
|
|
}
|
|
if len(sources) == 0 {
|
|
t.Fatal("no test sources found; this check would pass vacuously")
|
|
}
|
|
|
|
found := 0
|
|
err = filepath.WalkDir(fixtures, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil || entry.IsDir() {
|
|
return err
|
|
}
|
|
relative, err := filepath.Rel(fixtures, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
relative = filepath.ToSlash(relative)
|
|
found++
|
|
for _, source := range sources {
|
|
if strings.Contains(source, relative) {
|
|
return nil
|
|
}
|
|
}
|
|
t.Errorf("testdata/openwrt/%s is committed but no test reads it", relative)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk: %v", err)
|
|
}
|
|
if found < 10 {
|
|
t.Fatalf("only %d fixtures found; the walk is looking in the wrong place",
|
|
found)
|
|
}
|
|
}
|
|
|
|
// There is one HTTP client, and it is the bound one.
|
|
//
|
|
// The M06 card says a school strategy may not carry its own; the same applies
|
|
// to the authentication transaction. A second client would have its own pool,
|
|
// its own timeouts and -- the part that matters -- its own idea of which line
|
|
// to leave by, which is the whole thing the transport package exists to fix. A
|
|
// strategy that needed a request nobody anticipated is a reason to widen the
|
|
// interface it is given, not to open a socket.
|
|
func TestOnlyTheTransportBuildsHTTPClients(t *testing.T) {
|
|
// The transport package is where the one client lives. Everything else
|
|
// that could plausibly want a request of its own is listed here.
|
|
restricted := []string{"internal/auth", "internal/strategy",
|
|
"internal/discovery", "internal/presets", "internal/update",
|
|
"internal/policy", "internal/observe", "internal/application",
|
|
"internal/wifi", "internal/wireless"}
|
|
// presets was already named here before the package existed, which is what
|
|
// that list is for -- it names the packages that must never make their own
|
|
// way onto the network, whether or not they have been written yet.
|
|
|
|
// Imports that mean "I am about to make my own way onto the network".
|
|
// net/http is allowed: a Request has to be built somewhere. net/netip is
|
|
// values only.
|
|
forbiddenImports := []string{"net", "crypto/tls", "net/http/httputil"}
|
|
// And the constructions that would build a client out of net/http alone.
|
|
forbiddenText := []string{"http.DefaultClient", "http.DefaultTransport",
|
|
"http.Client{", "http.Transport{", "&http.Client", "&http.Transport"}
|
|
|
|
root := coreRoot(t)
|
|
fileSet := token.NewFileSet()
|
|
checked := 0
|
|
|
|
for _, relative := range restricted {
|
|
directory := filepath.Join(root, filepath.FromSlash(relative))
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
// The package does not exist yet. The rule applies when it does.
|
|
continue
|
|
}
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if entry.IsDir() || !strings.HasSuffix(name, ".go") ||
|
|
strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
path := filepath.Join(directory, name)
|
|
checked++
|
|
|
|
file, err := parser.ParseFile(fileSet, path, nil, parser.ImportsOnly)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", path, err)
|
|
}
|
|
for _, spec := range file.Imports {
|
|
imported := strings.Trim(spec.Path.Value, `"`)
|
|
if slices.Contains(forbiddenImports, imported) {
|
|
t.Errorf("%s/%s imports %q; the network is reached through "+
|
|
"the bound transport, not directly", relative, name, imported)
|
|
}
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
for _, marker := range forbiddenText {
|
|
if strings.Contains(string(data), marker) {
|
|
t.Errorf("%s/%s contains %q; there is one HTTP client and "+
|
|
"it is the one bound to the line", relative, name, marker)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if checked == 0 {
|
|
t.Fatal("no restricted packages found; this check would pass vacuously")
|
|
}
|
|
}
|
|
|
|
// The fake clock is a test tool, and it has to stay one.
|
|
//
|
|
// It exists so scheduling can be tested without waiting: a sixty-second cap and
|
|
// a window that crosses midnight are both unreachable in a unit test on a real
|
|
// clock. What it must never become is a way for production code to control
|
|
// time, because then the thing being tested is no longer the thing that ships.
|
|
// Keeping it in its own package makes the rule checkable rather than hopeful.
|
|
func TestTheFakeClockIsTestOnly(t *testing.T) {
|
|
fake := modulePath + "/internal/policy/faketime"
|
|
|
|
for pkg, imports := range packageImports(t) {
|
|
if pkg == fake {
|
|
continue
|
|
}
|
|
if slices.Contains(imports, fake) {
|
|
t.Errorf("%s imports the fake clock outside a test; production code "+
|
|
"takes a policy.Clock and the daemon passes the real one", pkg)
|
|
}
|
|
}
|
|
|
|
// And something has to use it, or the rule above passes because the package
|
|
// is dead rather than because it is contained.
|
|
root := coreRoot(t)
|
|
users := 0
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil || entry.IsDir() || !strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
data, readErr := os.ReadFile(path)
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
if strings.Contains(string(data), fake) {
|
|
users++
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk: %v", err)
|
|
}
|
|
if users == 0 {
|
|
t.Error("no test imports the fake clock; either it is unused or this " +
|
|
"check is looking in the wrong place")
|
|
}
|
|
}
|
|
|
|
// cmd wires things together and exits. Business logic there would be reachable
|
|
// only by running the binary.
|
|
func TestCommandPackageOnlyWires(t *testing.T) {
|
|
root := coreRoot(t)
|
|
path := filepath.Join(root, "cmd", "srunnet", "main.go")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read main.go: %v", err)
|
|
}
|
|
lines := strings.Count(string(data), "\n")
|
|
if lines > 120 {
|
|
t.Errorf("cmd/srunnet/main.go is %d lines; it should construct "+
|
|
"dependencies and exit, not carry logic", lines)
|
|
}
|
|
}
|
|
|
|
// Third-party dependencies are a supply-chain decision, so each one gets
|
|
// recorded deliberately rather than arriving with a convenient helper.
|
|
func TestNoUnreviewedThirdPartyDependencies(t *testing.T) {
|
|
// Reviewed and allowed. Spec 02 permits golang.org/x/sys/unix for Linux
|
|
// syscalls and x/net/html plus its x/text charset support for portal
|
|
// parsing. Nothing else may appear without being added here and to the
|
|
// dependency record.
|
|
allowed := []string{
|
|
"golang.org/x/sys/unix",
|
|
"golang.org/x/net/html",
|
|
"golang.org/x/net/html/charset",
|
|
"golang.org/x/text/encoding",
|
|
"golang.org/x/text/encoding/htmlindex",
|
|
"golang.org/x/text/transform",
|
|
}
|
|
|
|
for pkg, imports := range packageImports(t) {
|
|
for _, imported := range imports {
|
|
if strings.HasPrefix(imported, modulePath) {
|
|
continue
|
|
}
|
|
// A standard library path has no dot in its first element.
|
|
first, _, _ := strings.Cut(imported, "/")
|
|
if !strings.Contains(first, ".") {
|
|
continue
|
|
}
|
|
if !slices.Contains(allowed, imported) {
|
|
t.Errorf("%s imports the unreviewed third-party package %q",
|
|
pkg, imported)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A shared mutable bag is how ownership stops being traceable: any package can
|
|
// write any key, and nothing can be tested in isolation afterwards.
|
|
func TestNoGlobalServiceLocator(t *testing.T) {
|
|
root := coreRoot(t)
|
|
banned := []string{"ServiceLocator", "CoreAPI", "GlobalContext", "AppContext"}
|
|
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil || entry.IsDir() || !strings.HasSuffix(path, ".go") ||
|
|
strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
data, readErr := os.ReadFile(path)
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
text := string(data)
|
|
for _, name := range banned {
|
|
if strings.Contains(text, "type "+name) {
|
|
relative, _ := filepath.Rel(root, path)
|
|
t.Errorf("%s declares %s; dependencies are passed explicitly",
|
|
filepath.ToSlash(relative), name)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk: %v", err)
|
|
}
|
|
}
|
|
|
|
// The system-route probe client is the one deliberate exception to "there is
|
|
// one HTTP client, and it is the bound one" (issue #64). It answers only the
|
|
// Internet check after the bound line has verified the session on a
|
|
// single-uplink router, so it is built in exactly one place -- the daemon's
|
|
// wiring -- and handed to the worker as a portal.Fetcher. Anything else that
|
|
// constructed it could send a campus request, or a credential, by the default
|
|
// route.
|
|
func TestOnlyTheDaemonBuildsTheSystemProbeClient(t *testing.T) {
|
|
allowed := map[string]bool{
|
|
"internal/transport/system.go": true,
|
|
"internal/daemon/worker.go": true,
|
|
}
|
|
root := coreRoot(t)
|
|
users := 0
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil || entry.IsDir() || !strings.HasSuffix(path, ".go") ||
|
|
strings.HasSuffix(path, "_test.go") {
|
|
return err
|
|
}
|
|
data, readErr := os.ReadFile(path)
|
|
if readErr != nil {
|
|
return readErr
|
|
}
|
|
if !strings.Contains(string(data), "NewSystemProbeClient") {
|
|
return nil
|
|
}
|
|
relative, _ := filepath.Rel(root, path)
|
|
relative = filepath.ToSlash(relative)
|
|
if !allowed[relative] {
|
|
t.Errorf("%s builds the unbound system-route client; only the "+
|
|
"daemon's wiring may, for the Internet check alone", relative)
|
|
}
|
|
users++
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk: %v", err)
|
|
}
|
|
if users != len(allowed) {
|
|
t.Errorf("found the system probe client in %d allowed files, want %d; "+
|
|
"this check is looking in the wrong place", users, len(allowed))
|
|
}
|
|
}
|