package calls import ( "context" "io" "log/slog" "path/filepath" "testing" "time" "amadan.net/rastrillo/rastrillo/db" ) // testApp builds the whole app over a fresh temp database and hands back // the *app itself, which NewForTest deliberately does not. These two // tables are about internals — Authorize and standingFor — that no HTTP // request reaches directly. func testApp(t *testing.T, members ...string) *app { t.Helper() logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, err := db.Open(filepath.Join(t.TempDir(), "a.db"), logger) if err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) _, a, err := buildForTest(t, d, Config{ Env: EnvDevelopment, Origin: "http://app.test", AllowedAddresses: members, Logger: logger, }) if err != nil { t.Fatal(err) } now := time.Date(2026, 9, 8, 10, 0, 0, 0, time.UTC) a.store.clk.set(func() time.Time { return now }) return a } // Review C1-8: refusing an outsider in front of Begin never exercises // Authorize, so nothing proved that Authorize itself refuses an address // with no pending row. Spec §3.2: on the allowlist, or holding an // unexpired pending row whose link is still live. func TestAuthorizeAdmitsMembersAndLivePendingRowsOnly(t *testing.T) { a := testApp(t, "paul@example.com") ctx := context.Background() m, tok, err := a.store.createMeeting(ctx, "x", nil, "person:1") if err != nil { t.Fatal(err) } link, _, _, _ := a.store.liveLink(ctx, tok) if a.authorize("unknown@example.com") { t.Fatal("an address with no pending row was admitted") } if !a.authorize("paul@example.com") { t.Fatal("an allowlisted address was refused") } if !a.authorize("PAUL@Example.com ") { t.Fatal("an allowlisted address was refused on case or whitespace") } if err := a.store.writePending(ctx, "guest@example.com", link); err != nil { t.Fatal(err) } if !a.authorize("guest@example.com") { t.Fatal("an address with a live pending row was refused") } if _, err := a.store.replaceLink(ctx, m.ID, "person:1"); err != nil { t.Fatal(err) } if a.authorize("guest@example.com") { t.Fatal("an address whose link was replaced was still admitted") } } // Review C1-8: the one-per-request entry authority (spec §2.3), asserted // directly rather than through the holding page's status code. func TestStandingTable(t *testing.T) { ctx := context.Background() for _, tc := range []struct { name string allowlisted bool grant bool expired bool want Standing }{ {name: "allowlisted, no grant", allowlisted: true, want: StandingMember}, {name: "allowlisted, live grant", allowlisted: true, grant: true, want: StandingMember}, {name: "allowlisted, expired grant", allowlisted: true, grant: true, expired: true, want: StandingMember}, {name: "outsider, no grant", want: StandingNone}, {name: "outsider, live grant", grant: true, want: StandingGuest}, {name: "outsider, expired grant", grant: true, expired: true, want: StandingNone}, } { t.Run(tc.name, func(t *testing.T) { const address = "someone@example.com" members := []string{"other@example.com"} if tc.allowlisted { members = append(members, address) } a := testApp(t, members...) m, _, err := a.store.createMeeting(ctx, "x", nil, "person:1") if err != nil { t.Fatal(err) } p, err := a.store.ensurePerson(ctx, address) if err != nil { t.Fatal(err) } if tc.grant { if _, err := a.store.mintGrant(ctx, m, p.ID); err != nil { t.Fatal(err) } } if tc.expired { after := a.store.now().Add(grantLife + time.Minute) a.store.clk.set(func() time.Time { return after }) } got, err := a.standingFor(ctx, p, m) if err != nil { t.Fatal(err) } if got != tc.want { t.Fatalf("standing %s, want %s", got, tc.want) } }) } }