config: fix round 1 - delete dead field, prove the with-relay path
- Deleted Config.RelayConfigured: nothing read it once resolve owned relayConfigured, so its comment was already a lie. - config_test.go no longer prints a parsed region on failure (len and relayConfigured only); the outside-development refusal test now also asserts config.go's own wording, not just relay.Parse's. - New TestAMalformedRelayBuiltFromTheVectorNeverLeaksIt: the config-layer regression test for relay's item 1, built from the vector key in region, key and username_suffix at once with an empty urls list. - New internal/meet/app_relay_test.go: TestHandle.SetRelay flipping relayConfigured through a real build(), build()'s own INFO line asserted to carry the region count and not the key, SetRelay's type-consistency with build's initial atomic.Value store (swapping to a relay and back to none without a panic), and a malformed SetRelay leaving the existing regions untouched. - New internal/meettest/relay_floor_test.go: a room-page floor test (there was none), and SetRelay hiding the floor sentence on both the holding and room pages - the "relay configured, so sentence absent" direction a hardcoded Floor would still have passed. The with-relay wiring in app.go/config.go was already correct (the Task 2 review's own finding); this round is new coverage, not a production fix, except for the deleted field. Verified the config regression test above fails against the pre-fix relay.go and passes against the fixed one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzvnuKhLzudvq7jZ7SvVt2
4 files changed,
+227
−5
internal/meet/app_relay_test.go+126 −0internal/meet/config.go+0 −4internal/meet/config_test.go+39 −1internal/meettest/relay_floor_test.go+62 −0
CI passed — run details
diff --git a/internal/meet/app_relay_test.go b/internal/meet/app_relay_test.go| new file mode 100644 |
| index 0000000..12efd9d |
| --- /dev/null |
| +++ b/internal/meet/app_relay_test.go |
| @@ -0,0 +1,126 @@ |
| +package meet |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "io" |
| + "log/slog" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + |
| + "amadan.net/rastrillo/rastrillo/db" |
| +) |
| + |
| +// openRelayTestDB is a fresh temp database for the app-level relay |
| +// wiring tests below: build() and NewForTest need a real *db.DB, not |
| +// the bare Config resolve() takes. |
| +func openRelayTestDB(t *testing.T, logger *slog.Logger) *db.DB { |
| + t.Helper() |
| + d, err := db.Open(filepath.Join(t.TempDir(), "relay.db"), logger) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { d.Close() }) |
| + return d |
| +} |
| + |
| +// TestSetRelayFlipsRelayConfigured is the with-relay path through |
| +// build: nothing before this exercised SetRelay at all — the config |
| +// package tests only ever checked resolved.relayConfigured, which |
| +// build() never reads back through the app's own atomic. |
| +func TestSetRelayFlipsRelayConfigured(t *testing.T) { |
| + cfg := minimalConfig(t) |
| + cfg.Env = EnvDevelopment |
| + d := openRelayTestDB(t, slog.New(slog.NewTextHandler(io.Discard, nil))) |
| + _, ctl, err := NewForTest(d, cfg) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if ctl.a.relayConfigured() { |
| + t.Fatal("expected no relay at boot") |
| + } |
| + if err := ctl.SetRelay(relayVector(t)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !ctl.a.relayConfigured() || len(ctl.a.relayRegions()) != 1 { |
| + t.Fatalf("SetRelay did not flip relayConfigured: configured=%v regions=%d", ctl.a.relayConfigured(), len(ctl.a.relayRegions())) |
| + } |
| +} |
| + |
| +// TestBuildLogsOneINFOLineWithTheRegionCountAndNotTheKey: build()'s own |
| +// INFO line was untested — make ci passed with a hardcoded Floor and no |
| +// assertion on the log at all, so a mutation that dropped the line, or |
| +// one that logged the raw contract instead of its length, would have |
| +// survived. |
| +func TestBuildLogsOneINFOLineWithTheRegionCountAndNotTheKey(t *testing.T) { |
| + var buf bytes.Buffer |
| + logger := slog.New(slog.NewTextHandler(&buf, nil)) |
| + cfg := minimalConfig(t) |
| + cfg.Logger = logger |
| + cfg.Relay = relayVector(t) |
| + d := openRelayTestDB(t, logger) |
| + if _, _, err := build(context.Background(), d, cfg); err != nil { |
| + t.Fatal(err) |
| + } |
| + log := buf.String() |
| + if !strings.Contains(log, "meet: relay configured") || !strings.Contains(log, "regions=1") { |
| + t.Fatalf("want one INFO line naming the region count, got:\n%s", log) |
| + } |
| + if strings.Contains(log, "uMhwW4P") { |
| + t.Fatal("the relay key reached the log") |
| + } |
| +} |
| + |
| +// TestSetRelayIsTypeConsistentWithBuildsInitialStore: build() stores a |
| +// nil []relay.Region into a.relay when no relay is configured, and |
| +// SetRelay later stores a real []relay.Region into the same |
| +// atomic.Value. atomic.Value panics on a Store whose concrete type |
| +// disagrees with an earlier one, so swapping to a real slice and back |
| +// to empty here without a panic is the proof the two call sites agree. |
| +func TestSetRelayIsTypeConsistentWithBuildsInitialStore(t *testing.T) { |
| + cfg := minimalConfig(t) |
| + cfg.Env = EnvDevelopment |
| + d := openRelayTestDB(t, slog.New(slog.NewTextHandler(io.Discard, nil))) |
| + _, ctl, err := NewForTest(d, cfg) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := ctl.SetRelay(relayVector(t)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !ctl.a.relayConfigured() { |
| + t.Fatal("SetRelay did not take effect") |
| + } |
| + if err := ctl.SetRelay(""); err != nil { |
| + t.Fatal(err) |
| + } |
| + if ctl.a.relayConfigured() { |
| + t.Fatal(`SetRelay("") did not clear the relay`) |
| + } |
| +} |
| + |
| +// TestSetRelayOfAMalformedValueLeavesRegionsUnchanged: SetRelay parses |
| +// before it stores, so a malformed value must return the error and |
| +// leave whatever was there before untouched — never a half-applied |
| +// swap. |
| +func TestSetRelayOfAMalformedValueLeavesRegionsUnchanged(t *testing.T) { |
| + cfg := minimalConfig(t) |
| + cfg.Env = EnvDevelopment |
| + d := openRelayTestDB(t, slog.New(slog.NewTextHandler(io.Discard, nil))) |
| + _, ctl, err := NewForTest(d, cfg) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := ctl.SetRelay(relayVector(t)); err != nil { |
| + t.Fatal(err) |
| + } |
| + before := ctl.a.relayRegions() |
| + if err := ctl.SetRelay("%%%not-base64"); err == nil { |
| + t.Fatal("want an error") |
| + } |
| + after := ctl.a.relayRegions() |
| + if len(after) != len(before) || len(after) == 0 || after[0].Region != before[0].Region { |
| + t.Fatalf("a malformed SetRelay mutated the regions: before len=%d after len=%d", len(before), len(after)) |
| + } |
| +} |
diff --git a/internal/meet/config.go b/internal/meet/config.go| index f092cb6..a09b88b 100644 |
| --- a/internal/meet/config.go |
| +++ b/internal/meet/config.go |
| @@ -44,10 +44,6 @@ type Config struct { |
| // §2). Absent means the floor (§5.5). It is parsed once here and the |
| // decoded value is held in memory and never logged (§5.2). |
| Relay string |
| - // RelayConfigured is derived from Relay by resolve; callers read the |
| - // resolved value, not this field. Kept so existing tests that set it |
| - // still compile; resolve overwrites it. |
| - RelayConfigured bool |
| // Zone is the IANA zone start times are entered and shown in. |
| // MEET_ZONE, default UTC. Per-person zones are deliberately not in |
| // version one. |
diff --git a/internal/meet/config_test.go b/internal/meet/config_test.go| index 1ca701d..613aa1d 100644 |
| --- a/internal/meet/config_test.go |
| +++ b/internal/meet/config_test.go |
| @@ -51,11 +51,49 @@ func TestAMalformedRelayRefusesToStartOutsideDevelopment(t *testing.T) { |
| if err == nil || !strings.Contains(err.Error(), "CARLOS_RELAY") { |
| t.Fatalf("want a refusal naming the variable, got %v", err) |
| } |
| + // Not just any refusal that happens to name the variable: this must |
| + // be config.go's own wording, so a case where relay.Parse's error |
| + // text alone satisfied the assertion above (and resolve silently |
| + // stopped wrapping it) would still be caught. |
| + if !strings.Contains(err.Error(), "is present but malformed") { |
| + t.Fatalf("want config.go's own refusal wording, got %v", err) |
| + } |
| if strings.Contains(err.Error(), "%%%") { |
| t.Fatal("the refusal echoes the value") |
| } |
| } |
| +// TestAMalformedRelayBuiltFromTheVectorNeverLeaksIt is the config-layer |
| +// regression test for relay's Task 1 item 1: element 0's "no urls" |
| +// error used to embed the region field verbatim, so a raw value whose |
| +// region field held the platform vector key leaked it straight into |
| +// resolve's refusal. The key and the username_suffix's incarnation |
| +// segment are planted throughout the one region below so any of the |
| +// three fields' checks reintroducing an echo trips this test. |
| +func TestAMalformedRelayBuiltFromTheVectorNeverLeaksIt(t *testing.T) { |
| + const vectorKey = "uMhwW4P/9O5dQUIs91t2CCHGhwJbeiM0xQMQ2G0CJLU=" |
| + cfg := minimalConfig(t) |
| + cfg.Env = "production" |
| + regions := []map[string]any{{ |
| + "region": vectorKey, |
| + "urls": []string{}, |
| + "key": vectorKey, |
| + "username_suffix": "abc:meet:0123456789abcdef:k1", |
| + }} |
| + b, err := json.Marshal(regions) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + cfg.Relay = base64.StdEncoding.EncodeToString(b) |
| + _, err = resolve(cfg) |
| + if err == nil { |
| + t.Fatal("want a refusal") |
| + } |
| + if strings.Contains(err.Error(), vectorKey) || strings.Contains(err.Error(), "0123456789abcdef") { |
| + t.Fatalf("the refusal echoes the vector: %v", err) |
| + } |
| +} |
| + |
| func TestAMalformedRelayIsIgnoredWithAWarningInDevelopment(t *testing.T) { |
| var buf bytes.Buffer |
| cfg := minimalConfig(t) |
| @@ -84,7 +122,7 @@ func TestAGoodRelayIsParsedAndNeverLogged(t *testing.T) { |
| t.Fatal(err) |
| } |
| if len(out.relay) != 1 || !out.relayConfigured { |
| - t.Fatalf("parsed %+v", out.relay) |
| + t.Fatalf("parsed len=%d relayConfigured=%v", len(out.relay), out.relayConfigured) |
| } |
| if strings.Contains(buf.String(), "uMhwW4P") || strings.Contains(buf.String(), "0123456789abcdef") { |
| t.Fatal("the key or the suffix reached a log line") |
diff --git a/internal/meettest/relay_floor_test.go b/internal/meettest/relay_floor_test.go| new file mode 100644 |
| index 0000000..b299573 |
| --- /dev/null |
| +++ b/internal/meettest/relay_floor_test.go |
| @@ -0,0 +1,62 @@ |
| +package meettest |
| + |
| +import ( |
| + "encoding/base64" |
| + "encoding/json" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// relayVectorForTest is the base64 CARLOS_RELAY value for one region, |
| +// built from relay's own platform vector key and username suffix |
| +// (internal/meet/relay/relay_test.go) so a leak of either into a |
| +// rendered page would be unmistakable. |
| +func relayVectorForTest(t *testing.T) string { |
| + t.Helper() |
| + const vectorKey = "uMhwW4P/9O5dQUIs91t2CCHGhwJbeiM0xQMQ2G0CJLU=" |
| + regions := []map[string]any{{ |
| + "region": "eu-west-1", |
| + "urls": []string{"stun:relay.eu-west-1.oncarlos.com:3478"}, |
| + "key": vectorKey, |
| + "username_suffix": "abc:meet:0123456789abcdef:k1", |
| + }} |
| + b, err := json.Marshal(regions) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return base64.StdEncoding.EncodeToString(b) |
| +} |
| + |
| +// TestRoomPageShowsFloorWithNoRelay: holding_test.go's |
| +// TestHoldingPageShowsTitleStandingAndFloor covers /join; room.html |
| +// renders the identical sentence and nothing proved it — there was no |
| +// room-page floor test before this one. |
| +func TestRoomPageShowsFloorWithNoRelay(t *testing.T) { |
| + x, sqid, _ := memberWithMeeting(t) |
| + c := x.browser(t) |
| + c.signIn("paul@example.com") |
| + body := c.get("/m/" + sqid).Body.String() |
| + if !strings.Contains(body, "no relay yet") { |
| + t.Fatalf("room page lacks the floor sentence with no relay configured:\n%s", body) |
| + } |
| +} |
| + |
| +// TestSetRelayHidesTheFloorSentenceOnBothPages: the floor sentence, |
| +// present on both the holding and room pages with no relay, must |
| +// disappear from both once TestHandle.SetRelay configures one — the |
| +// "relay configured ⇒ sentence absent" direction that hardcoding Floor |
| +// to true would still have passed. |
| +func TestSetRelayHidesTheFloorSentenceOnBothPages(t *testing.T) { |
| + x, sqid, _ := memberWithMeeting(t) |
| + if err := x.ctl.SetRelay(relayVectorForTest(t)); err != nil { |
| + t.Fatal(err) |
| + } |
| + c := x.browser(t) |
| + c.signIn("paul@example.com") |
| + if body := c.get("/m/" + sqid + "/join").Body.String(); strings.Contains(body, "no relay yet") { |
| + t.Fatalf("holding page still shows the floor sentence with a relay configured:\n%s", body) |
| + } |
| + if body := c.get("/m/" + sqid).Body.String(); strings.Contains(body, "no relay yet") { |
| + t.Fatalf("room page still shows the floor sentence with a relay configured:\n%s", body) |
| + } |
| +} |