oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1package memoriatest
2
3import (
4 "fmt"
5 "go/ast"
6 "go/parser"
7 "go/token"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "testing"
13)
14
15// Every plain-text error answer left in the app is here, by file and
16// function, with why it is not a page (shared chrome §3.3). A new
17// http.Error or http.NotFound anywhere else fails this gate: it is either
18// a page (errorPage) or it is classified here with its reason.
19//
20// Keyed by function, not line, so an edit above a site does not churn the
21// list; the count per function catches a second site added beside one.
22var plainTextErrors = map[string]struct {
23 n int
24 why string
25}{
26 "app.go:requireSession": {1, "a boot misconfiguration (no sign-in wired): every request is refused, and there is no viewer to draw chrome for"},
27 "auth.go:beginGuard": {1, "the same misconfiguration on the sign-in POST (no owners list)"},
28 "errors.go:plainError": {1, "the plain answer itself, for a request that did not ask for a page"},
29 "errors.go:methodNotAllowed": {1, "405 to a method no route takes, with Allow set: never a navigation (a GET of a POST-only path gets the 404 page)"},
30 "handlers.go:writeOutcome": {1, "the upload tray's JSON path: a bare non-200 is what the tray reads as \"no answer\" (spec §3.3 keeps it)"},
31 "native.go:nativeSearch": {4, "the native client's JSON API; it shows these strings itself"},
32 "native.go:nativeNote": {3, "the native client's JSON API; it shows these strings itself"},
33 "render.go:renderStatus": {1, "a page template failed to execute: the error page is built from the same layout, so the plainest answer is the one left"},
34 "render.go:renderFragment": {1, "search.js's results fragment: a fetch, not a navigation, and search.js reads a non-200 as a search that did not run"},
35}
36
37func TestEveryPlainTextErrorIsClassified(t *testing.T) {
38 dir := filepath.Join("..", "memoria")
39 entries, err := os.ReadDir(dir)
40 if err != nil {
41 t.Fatal(err)
42 }
43 found := map[string]int{}
44 seen := 0
45 for _, e := range entries {
46 name := e.Name()
47 if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
48 continue
49 }
50 fset := token.NewFileSet()
51 file, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.SkipObjectResolution)
52 if err != nil {
53 t.Fatal(err)
54 }
55 for _, decl := range file.Decls {
56 fn, ok := decl.(*ast.FuncDecl)
57 if !ok || fn.Body == nil {
58 continue
59 }
60 ast.Inspect(fn.Body, func(n ast.Node) bool {
61 call, ok := n.(*ast.CallExpr)
62 if !ok {
63 return true
64 }
65 sel, ok := call.Fun.(*ast.SelectorExpr)
66 if !ok {
67 return true
68 }
69 pkg, ok := sel.X.(*ast.Ident)
70 if !ok || pkg.Name != "http" || (sel.Sel.Name != "Error" && sel.Sel.Name != "NotFound") {
71 return true
72 }
73 seen++
74 found[name+":"+fn.Name.Name]++
75 return true
76 })
77 }
78 }
79 if seen == 0 {
80 t.Fatal("found no http.Error at all: the walk is broken, not the app clean")
81 }
82 var problems []string
83 for key, n := range found {
84 want, ok := plainTextErrors[key]
85 switch {
86 case !ok:
87 problems = append(problems, fmt.Sprintf("%s: %d unclassified plain-text error(s) — render errorPage, or classify it here with its reason", key, n))
88 case want.n != n:
89 problems = append(problems, fmt.Sprintf("%s: %d plain-text error(s), classified %d", key, n, want.n))
90 case want.why == "":
91 problems = append(problems, key+": classified with no reason")
92 }
93 }
94 for key := range plainTextErrors {
95 if found[key] == 0 {
96 problems = append(problems, key+": classified, but no longer there — remove it from the list")
97 }
98 }
99 sort.Strings(problems)
100 for _, p := range problems {
101 t.Error(p)
102 }
103}
104