oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1// Package lint holds build-time guards over this repo's own source —
2// properties that used to be enforced only by prose in a doc comment,
3// promoted to something `go test ./...` (and so scripts/wire-gate.sh's
4// "go: vet, test, gofmt" step) cannot pass without.
5package lint
6
7import (
8 "go/ast"
9 "go/parser"
10 "go/token"
11 "os"
12 "path/filepath"
13 "strconv"
14 "strings"
15 "testing"
16)
17
18// bannedSessionsUserIDPackages and bannedSessionsUserIDFunc identify
19// the one call this repo's own code must never make. See
20// TestNoSessionsUserID's own doc comment for why. Both module paths:
21// native.go keeps one import of the pre-move package
22// (TestNoPreMoveRastrillo), and its UserID is the same trap.
23var bannedSessionsUserIDPackages = map[string]bool{
24 "amadan.net/rastrillo/rastrillo/sessions": true,
25 "github.com/carlosframework/rastrillo/sessions": true,
26}
27
28const bannedSessionsUserIDFunc = "UserID"
29
30// repoRoot resolves the module root from this test's own directory —
31// internal/lint, two levels below it — the same "../.." relative-path
32// convention testing.go's ReadFixture already uses from internal/docs
33// (also two levels below the root), which works because `go test`
34// always runs with the package's own directory as its working
35// directory.
36func repoRoot(t *testing.T) string {
37 t.Helper()
38 root, err := filepath.Abs(filepath.Join("..", ".."))
39 if err != nil {
40 t.Fatalf("lint: resolve repo root: %v", err)
41 }
42 if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil {
43 t.Fatalf("lint: %s does not look like the module root (no go.mod): %v", root, err)
44 }
45 return root
46}
47
48// skipDir reports whether WalkDir should not descend into a
49// directory: version control, and the two large generated/vendored
50// trees this repo's own build produces (fixtures/node_modules from
51// `npm ci`, core/target from `cargo build`) — neither holds Go source
52// this app wrote, and both are large enough that walking them would
53// only slow this test down for nothing.
54func skipDir(name string) bool {
55 switch name {
56 case ".git", "node_modules", "target":
57 return true
58 }
59 return false
60}
61
62// TestNoSessionsUserID is a build-time guard, not a comment: this
63// repo's own code must never call sessions.UserID (rastrillo/auth's
64// underlying sessions package), because under the magic-link plugin
65// this app uses, the session Subject is the verified email address
66// itself — sessions.UserID's own contract can only ever return
67// (0, false) here, on every request, with no way for that to change
68// short of switching auth plugins. A version of this app's own code
69// that called it anyway would not error or panic; it would silently
70// scope a query to person/user id 0 and behave like an empty result —
71// the same failure class rastrillo's own scope.Owned has on a table
72// that grows sharing (store.go's own doc comment on `owned`): wrong
73// answers that look like "you just have nothing here," not a crash
74// that gets noticed.
75//
76// Four comments (models.go, store.go, handlers.go, testing.go) have
77// said "never sessions.UserID" since Task 1/2. A comment is a code
78// review that has to succeed every single time a future change is
79// made near this code; this test is what makes a violation impossible
80// to land instead of merely discouraged. It walks this repo's own Go
81// AST rather than grepping for the symbol, deliberately: a text scan
82// would have to carve out an exemption for every one of those four
83// comments (and grow that exemption list forever), and could never
84// tell a real call apart from a comment or a string that merely
85// mentions the name. Flagging only actual call expressions — Fun
86// resolving, through the file's own import spec, to
87// sessions.UserID — needs no exemption list at all and cannot be
88// fooled by a comment quoting the symbol, including this test's own.
89//
90// Credit: adapted from the Sheets session's TestNoSessionsUserID,
91// the same guard for the same reason under the same auth plugin.
92func TestNoSessionsUserID(t *testing.T) {
93 root := repoRoot(t)
94 fset := token.NewFileSet()
95
96 err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
97 if err != nil {
98 return err
99 }
100 if d.IsDir() {
101 if path != root && skipDir(d.Name()) {
102 return filepath.SkipDir
103 }
104 return nil
105 }
106 if !strings.HasSuffix(path, ".go") {
107 return nil
108 }
109
110 f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
111 if err != nil {
112 return err
113 }
114
115 // aliases maps this file's own local identifier for the
116 // banned package to true (an ordinary named or default
117 // import) — resolved per file, because the same package can
118 // legally be imported under different local names in
119 // different files. dotImported is set instead when the file
120 // dot-imports it, which puts UserID in scope unqualified
121 // (a bare *ast.Ident call, no selector at all) rather than as
122 // sessions.UserID — the AST shape a plain "sessions.UserID"
123 // text search would never even think to look for, and
124 // exactly the kind of gap a symbol-precise walk closes for
125 // free.
126 aliases := map[string]bool{}
127 dotImported := false
128 for _, imp := range f.Imports {
129 importPath, err := strconv.Unquote(imp.Path.Value)
130 if err != nil || !bannedSessionsUserIDPackages[importPath] {
131 continue
132 }
133 switch {
134 case imp.Name == nil:
135 aliases["sessions"] = true // the package's own default name
136 case imp.Name.Name == ".":
137 dotImported = true
138 case imp.Name.Name == "_":
139 // blank-imported for side effects only; nothing in
140 // this file can reference it by name at all.
141 default:
142 aliases[imp.Name.Name] = true
143 }
144 }
145 if len(aliases) == 0 && !dotImported {
146 // This file does not import the banned package under any
147 // name — nothing it writes can possibly call it, however
148 // often it mentions the name in a comment or a string.
149 return nil
150 }
151
152 ast.Inspect(f, func(n ast.Node) bool {
153 call, ok := n.(*ast.CallExpr)
154 if !ok {
155 return true
156 }
157 switch fn := call.Fun.(type) {
158 case *ast.SelectorExpr:
159 if fn.Sel.Name != bannedSessionsUserIDFunc {
160 return true
161 }
162 id, ok := fn.X.(*ast.Ident)
163 if !ok || !aliases[id.Name] {
164 return true
165 }
166 case *ast.Ident:
167 if !dotImported || fn.Name != bannedSessionsUserIDFunc {
168 return true
169 }
170 default:
171 return true
172 }
173 pos := fset.Position(call.Pos())
174 rel, relErr := filepath.Rel(root, pos.Filename)
175 if relErr != nil {
176 rel = pos.Filename
177 }
178 t.Errorf(
179 "%s:%d: calls sessions.UserID — banned in this app: under the magic-link plugin the session Subject is the verified email address, so this can only ever return (0, false) here; use auth.From(r).Address and Store.EnsurePerson instead (see handlers.go's owner helper)",
180 rel, pos.Line,
181 )
182 return true
183 })
184 return nil
185 })
186 if err != nil {
187 t.Fatalf("lint: walk %s: %v", root, err)
188 }
189}
190