| new file mode 100644 |
| index 000000000..f81ab92fd |
| --- /dev/null |
| +++ b/internal/console/regioncandidates_test.go |
| @@ -0,0 +1,389 @@ |
| +package console |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "io" |
| + "log" |
| + "os" |
| + "strconv" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// mustTestSecretsKey is Task 9's (regionconfig_test.go). |
| +var testKeyPub, testKeyFP = mustTestSecretsKey() |
| + |
| +func bp(b bool) *bool { return &b } |
| + |
| +type edgeOpts struct { |
| + hib, shared, inbound *bool // nil = the writer predates the field |
| + fp string |
| +} |
| + |
| +var eligibleBox = edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: testKeyFP} |
| + |
| +// edgeJSON writes one box's edges file the way internal/host does. |
| +func edgeJSON(box, eip, awsRegion, placement string, o edgeOpts) string { |
| + m := map[string]any{"v": 1, "box": box, "eip": eip, "region": awsRegion, "hosts": map[string]any{}} |
| + if placement != "" { |
| + m["placement"] = placement |
| + } |
| + if o.hib != nil { |
| + m["hibernation_configured"] = *o.hib |
| + } |
| + if o.shared != nil { |
| + m["serves_shared_pool"] = *o.shared |
| + } |
| + if o.inbound != nil { |
| + m["accepts_inbound"] = *o.inbound |
| + } |
| + if o.fp != "" { |
| + m["secrets_key_fp"] = o.fp |
| + } |
| + b, _ := json.Marshal(m) |
| + return string(b) |
| +} |
| + |
| +var ( |
| + homeBox = edgeJSON("ie-box", "5.6.7.8", "eu-west-1", "", eligibleBox) |
| + ukEligible = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", eligibleBox) |
| + ukNoKey = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}) |
| + ukWrongKey = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true), fp: "sha256:" + strings.Repeat("0", 64)}) |
| + ukOldWriter = edgeJSON("uk-box", "1.2.3.4", "eu-west-2", "uk", edgeOpts{}) |
| + auPoolOnly = edgeJSON("au-box", "9.9.9.9", "ap-southeast-2", "au", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP}) |
| + usNoInbound = edgeJSON("us-box", "8.8.4.4", "us-east-1", "us", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}) |
| + nzNoHosting = edgeJSON("nz-box", "6.6.6.6", "ap-southeast-6", "nz", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP}) |
| + jpNoEIP = edgeJSON("jp-box", "", "ap-northeast-1", "jp", eligibleBox) |
| + fleetBoxFile = edgeJSON("fl-box", "4.4.4.4", "eu-west-2", "fleet/acme", eligibleBox) |
| +) |
| + |
| +// setEdges creates OR replaces edges files. putEdges is create-only (an |
| +// empty ifMatch on DirStore and S3), so it cannot change a box's file. |
| +func setEdges(t *testing.T, fx *consoleFixture, files map[string]string) { |
| + t.Helper() |
| + ctx := context.Background() |
| + for name, body := range files { |
| + key := "control/edges/" + name + ".json" |
| + etag := "" |
| + if _, cur, err := fx.rel.Get(ctx, key); err == nil { |
| + etag = cur |
| + } else if !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal(err) |
| + } |
| + if _, err := fx.rel.Put(ctx, key, []byte(body), etag); err != nil { |
| + t.Fatalf("set %s: %v", key, err) |
| + } |
| + } |
| +} |
| + |
| +type failingList struct{ release.Store } |
| + |
| +func (failingList) List(context.Context, string) ([]string, error) { |
| + return nil, errors.New("bucket unreachable") |
| +} |
| + |
| +func captureLog(w io.Writer) (restore func()) { |
| + prev, flags := log.Writer(), log.Flags() |
| + log.SetOutput(w) |
| + return func() { log.SetOutput(prev); log.SetFlags(flags) } |
| +} |
| + |
| +// views is a COMPLETE snapshot of the given edges-file bodies. |
| +func views(t *testing.T, bodies ...string) edgesSnapshot { |
| + t.Helper() |
| + snap := edgesSnapshot{Complete: true} |
| + for _, b := range bodies { |
| + var f edgesFilePlacementView |
| + if err := json.Unmarshal([]byte(b), &f); err != nil { |
| + t.Fatal(err) |
| + } |
| + snap.Files = append(snap.Files, f) |
| + } |
| + return snap |
| +} |
| + |
| +func TestRegionCandidatesNeedPositiveEligibility(t *testing.T) { |
| + ca := edgeJSON("ca-box", "7.7.7.7", "ca-central-1", "ca", edgeOpts{}) |
| + cands := regionCandidates(views(t, homeBox, ukEligible, auPoolOnly, usNoInbound, ca, nzNoHosting, jpNoEIP, fleetBoxFile), "ie", testKeyFP) |
| + want := map[string]string{"": "", "uk": "", "au": "not-eligible", "us": "not-eligible", |
| + "ca": "no-secrets-key", "nz": "not-hosting", "jp": "absent"} |
| + if len(cands) != len(want) { |
| + t.Fatalf("candidates = %+v, want exactly %v (no fleet code)", cands, want) |
| + } |
| + for code, reason := range want { |
| + c, ok := findCandidate(cands, code) |
| + if !ok { |
| + t.Fatalf("no candidate %q", code) |
| + } |
| + if got := c.unavailableReason(); got != reason { |
| + t.Errorf("%q: reason %q, want %q", code, got, reason) |
| + } |
| + } |
| + if h, _ := findCandidate(cands, ""); !h.Home || h.AWSRegion != "eu-west-1" || !h.Eligible { |
| + t.Errorf("home = %+v", h) |
| + } |
| +} |
| + |
| +// R9: a box must hold the deployment's secrets key to receive placed |
| +// instances, and only a CURRENT writer can prove it does not. |
| +func TestRegionCandidatesSecretsKey(t *testing.T) { |
| + cases := []struct { |
| + name string |
| + body string |
| + expected string |
| + reason string |
| + noKeyProven bool |
| + }{ |
| + {"matching key", ukEligible, testKeyFP, "", false}, |
| + {"no key, current writer", ukNoKey, testKeyFP, "no-secrets-key", true}, |
| + {"wrong key, current writer", ukWrongKey, testKeyFP, "no-secrets-key", true}, |
| + {"old writer: unknown", ukOldWriter, testKeyFP, "no-secrets-key", false}, |
| + {"console cannot judge", ukEligible, "", "no-secrets-key", false}, |
| + } |
| + for _, c := range cases { |
| + cand, _ := findCandidate(regionCandidates(views(t, homeBox, c.body), "ie", c.expected), "uk") |
| + if got := cand.unavailableReason(); got != c.reason || cand.NoKeyProven != c.noKeyProven { |
| + t.Errorf("%s: reason %q proven %v, want %q %v", c.name, got, cand.NoKeyProven, c.reason, c.noKeyProven) |
| + } |
| + } |
| + // One box with the key and one without: the region is not PROVEN |
| + // keyless, and is eligible through the box that holds it. |
| + ukKeyless2 := edgeJSON("uk-box-2", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}) |
| + cand, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, ukKeyless2), "ie", testKeyFP), "uk") |
| + if !cand.Eligible || cand.NoKeyProven { |
| + t.Errorf("mixed region = %+v", cand) |
| + } |
| +} |
| + |
| +// codex plan-R2 1: a negative proof needs EVERY box. With one file |
| +// unreadable or malformed, a readable keyless box proves nothing. |
| +func TestNoKeyProvenNeedsACompleteRead(t *testing.T) { |
| + incomplete := views(t, homeBox, ukNoKey) |
| + incomplete.Complete = false |
| + if c, _ := findCandidate(regionCandidates(incomplete, "ie", testKeyFP), "uk"); c.NoKeyProven { |
| + t.Fatal("an incomplete read proved a region keyless") |
| + } |
| + ctx := context.Background() |
| + for name, extra := range map[string]string{"malformed": "{not json", "unversioned": `{"box":"uk-box-2"}`} { |
| + s := release.NewDirStore(t.TempDir()) |
| + for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": extra} { |
| + if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + snap, err := readEdgesSnapshot(ctx, s) |
| + if err != nil || snap.Complete { |
| + t.Fatalf("%s: snapshot complete=%v err=%v, want incomplete", name, snap.Complete, err) |
| + } |
| + if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven { |
| + t.Fatalf("%s: proved keyless with a box unassessed", name) |
| + } |
| + } |
| + s := release.NewDirStore(t.TempDir()) |
| + for key, body := range map[string]string{"ie-box": homeBox, "uk-box": ukNoKey, "uk-box-2": ukEligible} { |
| + if _, err := s.Put(ctx, "control/edges/"+key+".json", []byte(body), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + snap, err := readEdgesSnapshot(ctx, failingGet{Store: s, key: "control/edges/uk-box-2.json"}) |
| + if err != nil || snap.Complete { |
| + t.Fatalf("failed GET: complete=%v err=%v, want incomplete", snap.Complete, err) |
| + } |
| + if c, _ := findCandidate(regionCandidates(snap, "ie", testKeyFP), "uk"); c.NoKeyProven { |
| + t.Fatal("a failed GET let the readable keyless box prove the region keyless") |
| + } |
| +} |
| + |
| +type failingGet struct { |
| + release.Store |
| + key string |
| +} |
| + |
| +func (f failingGet) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if key == f.key { |
| + return nil, "", errors.New("read timeout") |
| + } |
| + return f.Store.Get(ctx, key) |
| +} |
| + |
| +// codex plan-R2 2: a candidate's coordinates come from its ELIGIBLE boxes |
| +// only; ties among them go to the lexically smallest AWS region. |
| +func TestCandidateLocationComesFromEligibleBoxes(t *testing.T) { |
| + syd1 := edgeJSON("uk-syd-1", "2.2.2.1", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}) |
| + syd2 := edgeJSON("uk-syd-2", "2.2.2.2", "ap-southeast-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}) |
| + c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, syd1, syd2), "ie", testKeyFP), "uk") |
| + if !c.Eligible || c.AWSRegion != "eu-west-2" { |
| + t.Fatalf("mixed eligibility located uk at %q, want the eligible box's eu-west-2", c.AWSRegion) |
| + } |
| + dub := edgeJSON("uk-dub", "2.2.2.3", "eu-west-1", "uk", eligibleBox) |
| + if c, _ := findCandidate(regionCandidates(views(t, homeBox, ukEligible, dub), "ie", testKeyFP), "uk"); c.AWSRegion != "eu-west-1" { |
| + t.Fatalf("eligible tie = %q, want the lexically smallest eu-west-1", c.AWSRegion) |
| + } |
| +} |
| + |
| +func TestRegionCandidatesFoldTheHomesName(t *testing.T) { |
| + coded := edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox) |
| + cands := regionCandidates(views(t, homeBox, coded), "ie", testKeyFP) |
| + if len(cands) != 1 || !cands[0].Home { |
| + t.Fatalf("candidates = %+v, want the home alone", cands) |
| + } |
| +} |
| + |
| +// spec §6.3, §6.5: a candidate with no point is logged once per code, and |
| +// the line carries no caller coordinates (there are none to carry). |
| +func TestRegionCandidatesLogAMissingPointOnce(t *testing.T) { |
| + var buf bytes.Buffer |
| + restore := captureLog(&buf) |
| + defer restore() |
| + noPointSaid.Delete("odd") // the throttle is process-wide; -count=N reruns this test |
| + odd := edgeJSON("odd-box", "3.3.3.3", "xx-nowhere-1", "odd", eligibleBox) |
| + regionCandidates(views(t, homeBox, odd), "ie", testKeyFP) |
| + regionCandidates(views(t, homeBox, odd), "ie", testKeyFP) |
| + if n := strings.Count(buf.String(), `region "odd"`); n != 1 { |
| + t.Fatalf("missing-point line logged %d times, want once: %s", n, buf.String()) |
| + } |
| +} |
| + |
| +func TestReadEdgesSnapshotReportsListFailure(t *testing.T) { |
| + if _, err := readEdgesSnapshot(context.Background(), failingList{}); err == nil { |
| + t.Fatal("a failed listing must be an error, not an empty answer (codex R2-2)") |
| + } |
| + snap, err := readEdgesSnapshot(context.Background(), release.NewDirStore(t.TempDir())) |
| + if err != nil || len(snap.Files) != 0 || !snap.Complete { |
| + t.Fatalf("an empty prefix = %+v %v, want complete and empty", snap, err) |
| + } |
| +} |
| + |
| +func TestAppPolicyStates(t *testing.T) { |
| + fx, _ := instanceFixture(t, "code-policy") |
| + acct := &Account{ID: OpsAccountID, Sqid: "bab"} |
| + ctx := context.Background() |
| + if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyAbsent || !p.allows("uk") { |
| + t.Fatalf("no manifest = %+v", p) |
| + } |
| + m := titoManifest() |
| + m.Pool, m.Regions = "", []string{"uk"} |
| + enableInstances(t, fx, "titogo", m) |
| + if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyKnown || p.allows("") || !p.allows("uk") || p.pooled() { |
| + t.Fatalf("known = %+v", p) |
| + } |
| + m.Pool = "tito" |
| + enableInstances(t, fx, "titogo", m) |
| + if !fx.srv.appPolicy(ctx, acct, "titogo").pooled() { |
| + t.Fatal("a pooled manifest must read as pooled") |
| + } |
| + if err := fx.st.SetAppPlacement(OpsAccountID, "titogo", "fleet/acme"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.Placed != "fleet/acme" { |
| + t.Fatalf("placed = %q", p.Placed) |
| + } |
| + _, etag, err := fx.rel.Get(ctx, provision.ManifestKey("bab", "titogo")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := fx.rel.Put(ctx, provision.ManifestKey("bab", "titogo"), []byte("{not json"), etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyUnreadable || p.allows("") { |
| + t.Fatalf("unreadable = %+v, want unreadable and nothing allowed", p) |
| + } |
| + fx.srv.cfg.Releases = nil |
| + if p := fx.srv.appPolicy(ctx, acct, "titogo"); p.State != policyUnreadable || p.allows("uk") { |
| + t.Fatalf("no bucket = %+v, want unreadable and nothing allowed", p) |
| + } |
| +} |
| + |
| +// splitCapability pairs two boxes in one region that between them publish |
| +// every eligibility fact while neither publishes them all (codex |
| +// plan-R3 4). Task 11 reuses it for the guess. |
| +var splitCapability = map[string][2]string{ |
| + "keyless ordinary box beside a keyed named-pool-only box": { |
| + edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}), |
| + edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(false), inbound: bp(true), fp: testKeyFP}), |
| + }, |
| + "keyed no-inbound box beside a keyless inbound box": { |
| + edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(false), fp: testKeyFP}), |
| + edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}), |
| + }, |
| + "keyed non-hosting box beside a keyless hosting box": { |
| + edgeJSON("uk-a", "1.2.3.4", "eu-west-2", "uk", edgeOpts{hib: bp(false), shared: bp(true), inbound: bp(true), fp: testKeyFP}), |
| + edgeJSON("uk-b", "1.2.3.5", "eu-west-2", "uk", edgeOpts{hib: bp(true), shared: bp(true), inbound: bp(true)}), |
| + }, |
| +} |
| + |
| +// Eligibility is ONE box's facts, never the region's union of them. |
| +func TestEligibilityNeverCombinesBoxes(t *testing.T) { |
| + for name, boxes := range splitCapability { |
| + cands := regionCandidates(views(t, homeBox, boxes[0], boxes[1]), "ie", testKeyFP) |
| + if c, _ := findCandidate(cands, "uk"); c.Eligible || c.unavailableReason() == "" { |
| + t.Errorf("%s: uk = %+v, want ineligible", name, c) |
| + } |
| + if n := eligibleCount(cands); n != 1 { |
| + t.Errorf("%s: %d eligible candidates, want the home alone", name, n) |
| + } |
| + } |
| +} |
| + |
| +// candsOrHome answers from this console's home and secrets key, and falls |
| +// back to the home alone when the listing fails or there is no bucket. |
| +func TestCandsOrHomeUsesTheServerAndFallsBackToTheHome(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + ctx := context.Background() |
| + fx.srv.cfg.HomeRegion, fx.srv.cfg.SecretsPub = "ie", testKeyPub |
| + setEdges(t, fx, map[string]string{"ie-box": homeBox, "uk-box": ukEligible, "ie2": edgeJSON("ie2", "5.6.7.9", "eu-west-1", "ie", eligibleBox)}) |
| + cands := fx.srv.candsOrHome(ctx) |
| + if len(cands) != 2 || eligibleCount(cands) != 2 { |
| + t.Fatalf("candidates = %+v, want the home (ie folded in) and uk, both eligible", cands) |
| + } |
| + fx.srv.cfg.SecretsPub = "" |
| + if c, _ := findCandidate(fx.srv.candsOrHome(ctx), "uk"); c.Eligible || c.unavailableReason() != "no-secrets-key" { |
| + t.Fatalf("no console key: uk = %+v, want no-secrets-key", c) |
| + } |
| + for name, store := range map[string]release.Store{"failed listing": failingList{Store: fx.rel}, "no bucket": nil} { |
| + fx.srv.cfg.Releases = store |
| + if cands := fx.srv.candsOrHome(ctx); len(cands) != 1 || !cands[0].Home || !cands[0].Eligible { |
| + t.Errorf("%s: candidates = %+v, want the home alone", name, cands) |
| + } |
| + } |
| +} |
| + |
| +// O6, O7 and O13 to O15 are this file's diagnostics, taken verbatim from |
| +// the approved copy record and compared as quoted literals. |
| +func TestRegionCandidatesCopyMatchesTheRecord(t *testing.T) { |
| + raw, err := os.ReadFile("../../docs/superpowers/plans/2026-10-02-nearest-region-copy.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var rec struct { |
| + Entries []struct { |
| + ID, File, Text string |
| + } `json:"entries"` |
| + } |
| + if err := json.Unmarshal(raw, &rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + src, err := os.ReadFile("regioncandidates.go") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var ids []string |
| + for _, e := range rec.Entries { |
| + if e.File != "internal/console/regioncandidates.go" { |
| + continue |
| + } |
| + ids = append(ids, e.ID) |
| + if !strings.Contains(string(src), strconv.Quote(e.Text)) { |
| + t.Errorf("%s: regioncandidates.go has no literal %s", e.ID, strconv.Quote(e.Text)) |
| + } |
| + } |
| + if got := strings.Join(ids, ","); got != "O6,O7,O13,O14,O15" { |
| + t.Errorf("the record maps %s to regioncandidates.go, want O6,O7,O13,O14,O15", got) |
| + } |
| +} |