| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "go/ast" |
| 6 | "go/format" |
| 7 | "go/parser" |
| 8 | "go/token" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "slices" |
| 12 | "strings" |
| 13 | "testing" |
| 14 | ) |
| 15 | |
| 16 | // The reviewed inventory is committed. The test never regenerates it: new |
| 17 | // constructors or typed control callers need a matching coverage change. |
| 18 | func claimAuthoritySites(t *testing.T) []string { |
| 19 | t.Helper() |
| 20 | selected := map[string]bool{ |
| 21 | "NewDirStore": true, "Open": true, "OpenDB": true, |
| 22 | "NewConditionalRecordStore": true, "NewVersionedBoundary": true, "NewFromConfig": true, "S3ClientFor": true, "NewControlWriter": true, "NewRestoreControl": true, "NewMutationBoundary": true, |
| 23 | "NewObservedMutationBoundary": true, "NewProductionMutationCheck": true, |
| 24 | "NewS3Store": true, "NewS3StoreIn": true, "NewDeploymentS3Store": true, "NewDeploymentS3StoreIn": true, "NewVersionedS3": true, "NewAWSProvisioner": true, |
| 25 | "NewClamAVScanner": true, "NewRoute53Writer": true, "BeginExclusive": true, |
| 26 | "ResumeExclusive": true, "RegisterStart": true, "FinishWakeRetryReset": true, "ReservePurge": true, |
| 27 | "ConsoleMove": true, "InspectMoveLoss": true, "DeclareMoveLoss": true, |
| 28 | "RunRecovery": true, "runMoveRecovery": true, "captureMoveStartBase": true, "resolveUncertain": true, "ConvertMovingToRelocating": true, "ReleaseFencedTarget": true, "VerifyMoveTargetFence": true, "ReportAutomaticLosses": true, |
| 29 | // The console's move pass (moves Plan 5 Task 7): every write of the |
| 30 | // move-recovery grant, and the lost target's stamp. |
| 31 | "ResolveStart": true, "Convert": true, "Claim": true, "Record": true, "StampLostMoveTargetParked": true, |
| 32 | // The pass's fence action (moves after claim Plan 12, Task 5, Q8) |
| 33 | // calls ReleaseFencedTarget, selected above, on the same grant: one |
| 34 | // step of the target's fence per observation, with the grant-only |
| 35 | // release token minted inside it. |
| 36 | // The recovery pass's move log writer (Plan 6 Task 3, fix round 3 |
| 37 | // ruling 1): minted inside RunRecovery for the console's outbox |
| 38 | // drainer; one create-only entry per call, under the entry's own |
| 39 | // move's report prefix, for a host whose claim still names the app. |
| 40 | "runMoveLogDrain": true, "WriteMoveLog": true, |
| 41 | "BeginPurge": true, "SavePurgeJournal": true, "RemovePurgeJournal": true, |
| 42 | "AcquireMailGate": true, "ReleaseMailGate": true, "EnrollNewApp": true, |
| 43 | "EnrollBackfillApp": true, "OpenGated": true, "WithAppMutation": true, |
| 44 | "withClaimSQL": true, "withPreparedClaimSQL": true, "RegisterIngest": true, "RecoverIngest": true, "RecoverSQLTransaction": true, "BindTransaction": true, "PrepareSQLPermit": true, "FenceSQL": true, "FenceSQLOutcome": true, "WithProjection": true, |
| 45 | "BindMutationRunner": true, "claimEnrollmentContext": true, |
| 46 | // Region HA spec 2's automatic enrollment pass (plan 2b Task 5): its |
| 47 | // construction hands this console's roster directory and root |
| 48 | // signer to hostactors.AutoEnroller, its start runs it, and its one |
| 49 | // invocation is the pass that invites and enrolls boxes. |
| 50 | "newAutoEnroller": true, "runEnrollPass": true, "EnrollPass": true, |
| 51 | } |
| 52 | fs := token.NewFileSet() |
| 53 | var sites []string |
| 54 | for _, dir := range []string{".", "../hostops", "../mail", "../../cmd/console"} { |
| 55 | es, err := os.ReadDir(dir) |
| 56 | if err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | for _, e := range es { |
| 60 | if !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") { |
| 61 | continue |
| 62 | } |
| 63 | name := filepath.Join(dir, e.Name()) |
| 64 | f, err := parser.ParseFile(fs, name, nil, 0) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | for _, d := range f.Decls { |
| 69 | fn, ok := d.(*ast.FuncDecl) |
| 70 | if !ok || fn.Body == nil { |
| 71 | continue |
| 72 | } |
| 73 | ast.Inspect(fn.Body, func(n ast.Node) bool { |
| 74 | call, ok := n.(*ast.CallExpr) |
| 75 | if !ok { |
| 76 | return true |
| 77 | } |
| 78 | var method string |
| 79 | switch c := call.Fun.(type) { |
| 80 | case *ast.SelectorExpr: |
| 81 | method = c.Sel.Name |
| 82 | case *ast.Ident: |
| 83 | method = c.Name |
| 84 | } |
| 85 | // Loss paging reads signed control evidence and writes only its |
| 86 | // NonApp delivery journal. Inventory those sites explicitly. |
| 87 | lossPageSite := e.Name() == "claim_loss_pages.go" && (method == "List" || method == "ReadLossRecord" || method == "Exec" || method == "ExecContext" || method == "QueryRowContext" || method == "ClaimLossPageAttempt") |
| 88 | lossPageSchema := e.Name() == "store_gate.go" && fn.Name.Name == "OpenGated" && method == "Exec" |
| 89 | if !selected[method] && !lossPageSite && !lossPageSchema { |
| 90 | return true |
| 91 | } |
| 92 | var b bytes.Buffer |
| 93 | if err := format.Node(&b, fs, call); err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | sites = append(sites, filepath.ToSlash(name)+"|"+fn.Name.Name+"|"+strings.Join(strings.Fields(b.String()), " ")) |
| 97 | return true |
| 98 | }) |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | slices.Sort(sites) |
| 103 | return sites |
| 104 | } |
| 105 | func TestClaimAuthoritySourceCensus(t *testing.T) { |
| 106 | b, err := os.ReadFile("testdata/claim-authority-sites.txt") |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | want := strings.Split(strings.TrimSpace(string(b)), "\n") |
| 111 | slices.Sort(want) |
| 112 | got := claimAuthoritySites(t) |
| 113 | if !slices.Equal(got, want) { |
| 114 | t.Errorf("authority census differs: got %d calls, want %d", len(got), len(want)) |
| 115 | for _, s := range got { |
| 116 | if !slices.Contains(want, s) { |
| 117 | t.Errorf("unreviewed authority use: %s", s) |
| 118 | } |
| 119 | } |
| 120 | for _, s := range want { |
| 121 | if !slices.Contains(got, s) { |
| 122 | t.Errorf("stale authority fixture: %s", s) |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | |