package console // The member half of the email API (apimail.go, tenant-email task 17). What // is pinned here, in the order it would hurt if lost: a stranger probing any // of the ten doors learns nothing (the byte-identical 404 the whole // /api/cli surface keeps, and — new to this file — the same 404 when this // deployment has no SES estate at all); the standalone-credential reply is // the only one that ever carries a password, and it is uncacheable; a // re-declare that changes nothing writes no audit line and bumps no serial; // and the enable door always leaves a grant behind BEFORE it provisions // anything, so the limiter never runs an app on the wrong cap. import ( "context" "encoding/json" "net/http" "net/url" "strings" "testing" "time" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/release" ) func emailFixture(t *testing.T, code string) (*consoleFixture, string) { t.Helper() fx := newConsoleFixture(t) now := time.Now() if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.AddMember(OpsAccountID, "alice@keymail.dev", RoleMember, now); err != nil { t.Fatal(err) } return fx, cliToken(t, fx, "alice@keymail.dev", code) } func emailURL(fx *consoleFixture, app string) string { return fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/" + app + "/email" } // emailArmed is emailFixture with a whole deployment behind it: an // in-memory SES/IAM backend, an in-memory DNS writer, a secrets key the // console can seal with, a platform suffix, and an operator allowlist. It // hands back the fake provisioner and DNS writer so a test can assert on // the estate they actually built. func emailArmed(t *testing.T, code string) (*consoleFixture, *mail.MemProvisioner, *dnsrec.MemWriter, string) { t.Helper() fx, token := emailFixture(t, code) pub, _, _ := sealedFor(t, "irrelevant") fake := mail.NewMemProvisioner("1") writer := dnsrec.NewMemWriter() fx.srv.cfg.SecretsPub = pub fx.srv.cfg.MailProvisioner = fake fx.srv.cfg.DNSWriter = writer fx.srv.cfg.MailDeployment = mail.Deployment{Region: "eu-west-1", PlatformSuffix: "oncarlos.com"} // Spelled with different capitals on purpose, matching storeArmed: an // operator list is typed by a human into a unit file, and addresses are // case-insensitive. fx.srv.cfg.MailOperators = []string{"Ops@Keymail.dev"} return fx, fake, writer, token } // emailOperatorToken mints a token for a genuine fleet operator (the // store.IsOperator bit set) on MailOperators — and DELIBERATELY does NOT // add them as a member of fx's account. I3: the pause door must be // drivable by an operator who is not a member of the account they are // pausing, which is the exact scenario the abuse brake exists for, so the // standard operator fixture proves that by construction rather than by a // separate test remembering to skip AddMember. func emailOperatorToken(t *testing.T, fx *consoleFixture, code string) string { t.Helper() if err := fx.st.SetOperator("ops@keymail.dev", true); err != nil { t.Fatal(err) } return cliToken(t, fx, "ops@keymail.dev", code) } func declareEmail(t *testing.T, fx *consoleFixture, token, app, from string) { t.Helper() status, out := postJSON(t, emailURL(fx, app), map[string]any{"from": from}, token) if status != http.StatusOK { t.Fatalf("declare %s = %d %v", app, status, out) } } func ensureEmailDomain(t *testing.T, fx *consoleFixture, token, app, domain string) map[string]any { t.Helper() status, out := postJSON(t, emailURL(fx, app)+"/domains", map[string]any{"domain": domain}, token) if status != http.StatusOK { t.Fatalf("ensure domain %s = %d %v", domain, status, out) } return out } func verifyEmailDomain(t *testing.T, fx *consoleFixture, token, app, domain string) { t.Helper() status, out := postJSON(t, emailURL(fx, app)+"/domains/"+domain+"/verify", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("verify domain %s = %d %v", domain, status, out) } } func enableEmail(t *testing.T, fx *consoleFixture, token, app string) map[string]any { t.Helper() status, out := postJSON(t, emailURL(fx, app)+"/enable", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("enable %s = %d %v", app, status, out) } return out } // provisionEmail runs the whole self-serve happy path — declare, ensure a // platform-suffixed domain, drive the fake to verified, verify, enable — // leaving app with a live, delivered credential. func provisionEmail(t *testing.T, fx *consoleFixture, fake *mail.MemProvisioner, token, app, domain, from string) { t.Helper() declareEmail(t, fx, token, app, from) fake.StatusOnEnsure = mail.Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"} ensureEmailDomain(t, fx, token, app, domain) verifyEmailDomain(t, fx, token, app, domain) enableEmail(t, fx, token, app) } // pendingList reads the "pending" field of a domain-ensure reply — a list // of record OBJECTS (name/type/values/ttl), not plain strings, so it needs // its own reader rather than strList. func pendingList(t *testing.T, out map[string]any) []any { t.Helper() raw, ok := out["pending"].([]any) if !ok { t.Fatalf("pending = %#v, want a list", out["pending"]) } return raw } // emailRecord reads the record straight out of the bucket — the check that // a 200 actually landed, and that a refusal left nothing behind. func emailRecord(t *testing.T, fx *consoleFixture, app string) *mail.Record { t.Helper() rec, _, err := mail.Get(context.Background(), fx.rel, release.OpsAccount, app) if err != nil { t.Fatalf("read email record: %v", err) } return rec } // ---- the gate --------------------------------------------------------------- // TestEmailDoorsAre404ForNonMembers: on all ten doors, a token that belongs // to no account, an account segment that doesn't exist, and an app the // account never claimed are one and the same 404 — byte-identical to // http.NotFound's own output. This runs on an UNARMED fixture (no // MailProvisioner, no secrets key) on purpose: the member gate must fire // before any of that is even consulted. func TestEmailDoorsAre404ForNonMembers(t *testing.T) { fx, token := emailFixture(t, "code-email-gate") eve := cliToken(t, fx, "eve@keymail.dev", "code-email-gate-eve") doors := []struct { name string call func(base, bearer string) (int, string) }{ {"GET status", func(base, bearer string) (int, string) { return getRaw(t, base, bearer) }}, {"GET events", func(base, bearer string) (int, string) { return getRaw(t, base+"/events", bearer) }}, {"GET events push", func(base, bearer string) (int, string) { return getRaw(t, base+"/events/push", bearer) }}, {"PUT events push", func(base, bearer string) (int, string) { return putRaw(t, base+"/events/push", map[string]any{"path": "/mail/events"}, bearer) }}, {"DELETE events push", func(base, bearer string) (int, string) { status, _, body := rawReq(t, http.MethodDelete, base+"/events/push", bearer, "") return status, body }}, {"POST declare", func(base, bearer string) (int, string) { return postRaw(t, base, map[string]any{"from": "hello@example.com"}, bearer) }}, {"POST domains", func(base, bearer string) (int, string) { return postRaw(t, base+"/domains", map[string]any{"domain": "example.com"}, bearer) }}, {"POST domains verify", func(base, bearer string) (int, string) { return postRaw(t, base+"/domains/example.com/verify", map[string]any{}, bearer) }}, {"DELETE domains", func(base, bearer string) (int, string) { status, _, body := rawReq(t, http.MethodDelete, base+"/domains/example.com", bearer, "") return status, body }}, {"POST enable", func(base, bearer string) (int, string) { return postRaw(t, base+"/enable", map[string]any{}, bearer) }}, {"POST rotate", func(base, bearer string) (int, string) { return postRaw(t, base+"/rotate", map[string]any{}, bearer) }}, {"POST credentials", func(base, bearer string) (int, string) { return postRaw(t, base+"/credentials", map[string]any{"name": "laptop"}, bearer) }}, {"DELETE credentials", func(base, bearer string) (int, string) { status, _, body := rawReq(t, http.MethodDelete, base+"/credentials/laptop", bearer, "") return status, body }}, {"POST pause", func(base, bearer string) (int, string) { return postRaw(t, base+"/pause", map[string]any{"paused": true}, bearer) }}, } for _, d := range doors { t.Run(d.name, func(t *testing.T) { // (a) a member of nothing, against a real account and a real app. statusA, bodyA := d.call(emailURL(fx, "titogo"), eve) // (b) a member, against an account that does not exist. statusB, bodyB := d.call(fx.ts.URL+"/api/cli/apps/zzzz/titogo/email", token) // (c) a member of the account, against an app it never claimed. statusC, bodyC := d.call(emailURL(fx, "ghost-app"), token) if statusA != http.StatusNotFound || statusB != http.StatusNotFound || statusC != http.StatusNotFound { t.Fatalf("%s gate = %d / %d / %d, want 404 for all three", d.name, statusA, statusB, statusC) } if bodyA != bodyB || bodyB != bodyC { t.Fatalf("%s refusals are not byte-identical:\n(a) %q\n(b) %q\n(c) %q", d.name, bodyA, bodyB, bodyC) } if bodyA != "404 page not found\n" { t.Fatalf("%s refusal body = %q, want the stdlib http.NotFound output", d.name, bodyA) } }) } } // TestEmailDoorsAre404WithNoProvisioner: a deployment with no SES estate // answers 404 on every door, never a panic on the nil interface and never // the 503 store's sibling doors give for a missing backend — see // apimail.go's file comment for why the two differ. func TestEmailDoorsAre404WithNoProvisioner(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-noprovisioner") opsToken := emailOperatorToken(t, fx, "code-email-noprovisioner-ops") fx.srv.cfg.MailProvisioner = nil calls := []struct { name string do func() (int, string) }{ {"GET status", func() (int, string) { return getRaw(t, emailURL(fx, "titogo"), token) }}, {"GET events", func() (int, string) { return getRaw(t, emailURL(fx, "titogo")+"/events", token) }}, {"GET events push", func() (int, string) { return getRaw(t, emailURL(fx, "titogo")+"/events/push", token) }}, {"PUT events push", func() (int, string) { return putRaw(t, emailURL(fx, "titogo")+"/events/push", map[string]any{"path": "/mail/events"}, token) }}, {"DELETE events push", func() (int, string) { status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, "titogo")+"/events/push", token, "") return status, body }}, {"POST declare", func() (int, string) { return postRaw(t, emailURL(fx, "titogo"), map[string]any{"from": "hello@example.com"}, token) }}, {"POST domains", func() (int, string) { return postRaw(t, emailURL(fx, "titogo")+"/domains", map[string]any{"domain": "example.com"}, token) }}, {"POST domains verify", func() (int, string) { return postRaw(t, emailURL(fx, "titogo")+"/domains/example.com/verify", map[string]any{}, token) }}, {"DELETE domains", func() (int, string) { status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, "titogo")+"/domains/example.com", token, "") return status, body }}, {"POST enable", func() (int, string) { return postRaw(t, emailURL(fx, "titogo")+"/enable", map[string]any{}, token) }}, {"POST rotate", func() (int, string) { return postRaw(t, emailURL(fx, "titogo")+"/rotate", map[string]any{}, token) }}, {"POST credentials", func() (int, string) { return postRaw(t, emailURL(fx, "titogo")+"/credentials", map[string]any{"name": "laptop"}, token) }}, {"DELETE credentials", func() (int, string) { status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, "titogo")+"/credentials/laptop", token, "") return status, body }}, // The pause door needs an operator too — proving the 404 fires // AFTER operator authority is established, not in place of it. {"POST pause", func() (int, string) { return postRaw(t, emailURL(fx, "titogo")+"/pause", map[string]any{"paused": true}, opsToken) }}, } for _, c := range calls { t.Run(c.name, func(t *testing.T) { status, body := c.do() if status != http.StatusNotFound { t.Fatalf("%s with a nil provisioner = %d %q, want 404 (never a panic)", c.name, status, body) } }) } } // TestOperatorStatusBuysNothingOnMemberDoors: I3 moved the pause door off // apiAccountApp's member gate onto store.IsOperator — this proves that // move did not leak into the other nine. A fleet operator (IsOperator // true) who is NOT a member of the target account gets the exact same // byte-identical 404 an ordinary stranger gets on a WRITE (declare) and a // READ (status) member door. Operator status must buy nothing here. func TestOperatorStatusBuysNothingOnMemberDoors(t *testing.T) { fx, _ := emailFixture(t, "code-email-operator-not-member") ops := emailOperatorToken(t, fx, "code-email-operator-not-member-ops") status, body := postRaw(t, emailURL(fx, "titogo"), map[string]any{"from": "hello@example.com"}, ops) if status != http.StatusNotFound { t.Fatalf("declare by a non-member operator = %d %q, want 404", status, body) } if body != "404 page not found\n" { t.Fatalf("declare refusal body = %q, want the stdlib http.NotFound output", body) } status, body = getRaw(t, emailURL(fx, "titogo"), ops) if status != http.StatusNotFound { t.Fatalf("status by a non-member operator = %d %q, want 404", status, body) } if body != "404 page not found\n" { t.Fatalf("status refusal body = %q, want the stdlib http.NotFound output", body) } } // ---- declare and its changed-gating ------------------------------------ // TestAPIEmailDeclareWritesTheRecord: the happy path. From is required (a // bare env_prefix with no From is refused), the record lands in the // bucket, and declaring never bumps the serial — nothing a box reads has // changed. func TestAPIEmailDeclareWritesTheRecord(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-declare") app := "titogo" if rec := emailRecord(t, fx, app); rec != nil { t.Fatalf("a record exists before any declaration: %+v", rec) } status, out := postJSON(t, emailURL(fx, app), map[string]any{"from": "hello@titogo.example"}, token) if status != http.StatusOK { t.Fatalf("declare = %d %v", status, out) } declared := block(t, out, "declared") if declared["from"] != "hello@titogo.example" || declared["by"] != "alice@keymail.dev" { t.Fatalf("declared = %v", declared) } if declared["env_prefix"] != mail.DefaultEnvPrefix { t.Fatalf("env_prefix = %v, want the default %q", declared["env_prefix"], mail.DefaultEnvPrefix) } if out["grant"] != nil || out["provisioned"] != nil { t.Fatalf("grant/provisioned = %v/%v, want null before enable", out["grant"], out["provisioned"]) } if got := serial(t, fx); got != 0 { t.Fatalf("serial after a declaration = %d, want 0 — declaring delivers nothing to a box", got) } auditedBy(t, fx, app, "email-declare", "alice@keymail.dev", "hello@titogo.example") } // TestAPIEmailStatusCarriesTheDefaultHostEvenWhenNotDeclared (I4, review // follow-up): `carlos email enable` with no -from needs the app's default // sending host on its VERY FIRST call — before anything has ever been // declared, which is exactly the 404 handleAPIEmailStatus gives. The field // must ride along on that 404, not only on a 200. func TestAPIEmailStatusCarriesTheDefaultHostEvenWhenNotDeclared(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-default-host-404") app := "titogo" if rec := emailRecord(t, fx, app); rec != nil { t.Fatalf("a record exists before any declaration: %+v", rec) } status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusNotFound { t.Fatalf("status = %d %v, want 404 for an undeclared app", status, out) } want := "titogo.bab.oncarlos.com" if got, _ := out["default_host"].(string); got != want { t.Fatalf("default_host = %q, want %q", got, want) } } // TestAPIEmailStatusCarriesTheDefaultHostOnceDeclared: the same field on // the ordinary 200 path, once something has actually been declared. func TestAPIEmailStatusCarriesTheDefaultHostOnceDeclared(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-default-host-200") app := "titogo" declareEmail(t, fx, token, app, "hello@titogo.example") status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } want := "titogo.bab.oncarlos.com" if got, _ := out["default_host"].(string); got != want { t.Fatalf("default_host = %q, want %q", got, want) } } // TestAPIEmailStatusDefaultHostEmptyWithNoPlatformSuffix: a deployment with // no CARLOS_MAIL_PLATFORM_SUFFIX configured has no zone to default into — // default_host must stay empty, which is what keeps the CLI's explicit // "no From address" refusal intact rather than defaulting to a bogus host. func TestAPIEmailStatusDefaultHostEmptyWithNoPlatformSuffix(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-default-host-empty") fx.srv.cfg.MailDeployment.PlatformSuffix = "" app := "titogo" status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusNotFound { t.Fatalf("status = %d %v, want 404 for an undeclared app", status, out) } if got, _ := out["default_host"].(string); got != "" { t.Fatalf("default_host = %q, want empty with no platform suffix configured", got) } } // TestAPIEmailStatusResumesAStaleCapPause: R2, review follow-up. The SES // event path (enforceMailLimits) is enforceMailLimits' only production // caller, and a paused app's IAM key is deactivated — it sends nothing, so // no SES event ever arrives to run that check. The status READ door must // run the same day-roll check itself, or an app alone on its own domain // would see its cap pause as permanent, contradicting what the CLI tells // the member. Proves the wiring end to end, over HTTP — not just the // shared function in isolation. func TestAPIEmailStatusResumesAStaleCapPause(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-status-resume") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) rec, etag, err := mail.Get(context.Background(), fx.rel, release.OpsAccount, app) if err != nil { t.Fatal(err) } rec.Provisioned.Active = false rec.Provisioned.PauseKind = mail.PauseKindCap rec.Provisioned.At = time.Now().AddDate(0, 0, -1) // yesterday, UTC if _, err := mail.Put(context.Background(), fx.rel, release.OpsAccount, app, rec, etag); err != nil { t.Fatal(err) } status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if out["active"] != true { t.Fatalf("active = %v, want true — a cap pause from yesterday must clear on a status read", out["active"]) } got := emailRecord(t, fx, app) if !got.Provisioned.Active { t.Fatal("the record on disk is still paused after the status read resumed it") } if got.Provisioned.PauseKind != "" { t.Fatalf("PauseKind = %q on disk after resume, want cleared", got.Provisioned.PauseKind) } } // TestAPIEmailStatusLeavesARatePauseAlone: the same door must NEVER resume // a RATE pause — that pause exists because the tenant is abusive, not // because a budget rolled over, and it needs a human. func TestAPIEmailStatusLeavesARatePauseAlone(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-status-no-resume-rate") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) rec, etag, err := mail.Get(context.Background(), fx.rel, release.OpsAccount, app) if err != nil { t.Fatal(err) } rec.Provisioned.Active = false rec.Provisioned.PauseKind = mail.PauseKindRate rec.Provisioned.At = time.Now().AddDate(0, 0, -1) // yesterday, UTC if _, err := mail.Put(context.Background(), fx.rel, release.OpsAccount, app, rec, etag); err != nil { t.Fatal(err) } status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if out["active"] != false { t.Fatalf("active = %v, want false — a rate pause must never auto-resume", out["active"]) } got := emailRecord(t, fx, app) if got.Provisioned.Active { t.Fatal("the record on disk was resumed — a rate pause must never auto-resume") } if got.Provisioned.PauseKind != mail.PauseKindRate { t.Fatalf("PauseKind = %q on disk, want it to survive as %q", got.Provisioned.PauseKind, mail.PauseKindRate) } } // findSender pulls one sender's entry out of a decoded status reply's // senders array by id. Over HTTP the array decodes as []any of // map[string]any, unlike the in-process emailReply callers that see // []map[string]any directly. func findSender(t *testing.T, out map[string]any, id string) map[string]any { t.Helper() raw, _ := out["senders"].([]any) for _, r := range raw { if m, ok := r.(map[string]any); ok && m["id"] == id { return m } } t.Fatalf("no sender %q in %#v", id, out["senders"]) return nil } // putSiblingSender writes a second, non-app sender straight to its own // object, the way step 5's per-customer instance enable will, already // carrying a pause so a status-read test can seed one without going through // a walk that does not exist yet. func putSiblingSender(t *testing.T, fx *consoleFixture, app, pauseKind string, pausedAt time.Time) { t.Helper() if _, err := mail.PutSender(context.Background(), fx.rel, release.OpsAccount, app, &mail.Sender{ ID: "book.example.com", Scope: mail.ScopeInstance, Source: mail.SourceDomain, Host: "book.example.com", Declared: &mail.Declared{EnvPrefix: "TITOGO_SMTP", From: "noreply@book.example.com", At: pausedAt, By: "o@example.test"}, Grant: &mail.GrantBlock{Domains: []string{"book.example.com"}, DailyCap: 50, Source: "tier:free", At: pausedAt, By: "tier"}, Provisioned: &mail.Provisioned{ IAMUser: "sibling-user", KeyID: "AKIA-SIB", Active: false, PauseKind: pauseKind, At: pausedAt, }, }, ""); err != nil { t.Fatal(err) } } // TestAPIEmailStatusResumesAStaleCapPauseOnANonAppSender: task 13 review, // round 2. The event path (enforceMailLimits) never runs for a sender that // is already paused, because a paused sender's IAM key sends nothing and so // generates no SES event to run it. The status door's own resume used to // look at the app sender only; it must do the same day-roll check for every // sender it is about to report, or a per-customer instance sender (step 5) // would pause at its cap and never resume, since nothing else ever reads it. func TestAPIEmailStatusResumesAStaleCapPauseOnANonAppSender(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-status-resume-sibling") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) putSiblingSender(t, fx, app, mail.PauseKindCap, time.Now().AddDate(0, 0, -1)) status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } // The app sender's own top-level fields are exactly as before: the // sibling's pause and resume must not touch them. if out["active"] != true || out["daily_cap"] != float64(50) { t.Fatalf("the top-level fields moved: active=%v daily_cap=%v", out["active"], out["daily_cap"]) } sibling := findSender(t, out, "book.example.com") if sibling["active"] != true { t.Fatalf("sibling active = %v, want true. a cap pause from yesterday must clear on a status read, same as the app sender", sibling["active"]) } if _, ok := sibling["pause_kind"]; ok { t.Fatalf("sibling pause_kind = %v, want absent once resumed", sibling["pause_kind"]) } sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, app, "book.example.com") if err != nil { t.Fatal(err) } if sn.Provisioned == nil || !sn.Provisioned.Active { t.Fatal("the sibling sender on disk is still paused after the status read resumed it") } if sn.Provisioned.PauseKind != "" { t.Fatalf("PauseKind = %q on disk after resume, want cleared", sn.Provisioned.PauseKind) } } // TestAPIEmailStatusLeavesARatePauseAloneOnANonAppSender: the same door must // never resume a RATE pause on a sibling sender either. That pause exists // because the sender is abusive, not because a budget rolled over, and it // needs a human, whichever sender it sits on. func TestAPIEmailStatusLeavesARatePauseAloneOnANonAppSender(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-status-no-resume-rate-sibling") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) putSiblingSender(t, fx, app, mail.PauseKindRate, time.Now().AddDate(0, 0, -1)) status, out := getJSON(t, emailURL(fx, app), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } sibling := findSender(t, out, "book.example.com") if sibling["active"] != false { t.Fatalf("sibling active = %v, want false. a rate pause must never auto-resume, sibling or not", sibling["active"]) } if sibling["pause_kind"] != mail.PauseKindRate { t.Fatalf("sibling pause_kind = %v, want %q to survive", sibling["pause_kind"], mail.PauseKindRate) } sn, _, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, app, "book.example.com") if err != nil { t.Fatal(err) } if sn.Provisioned == nil || sn.Provisioned.Active { t.Fatal("the sibling sender on disk was resumed. a rate pause must never auto-resume") } if sn.Provisioned.PauseKind != mail.PauseKindRate { t.Fatalf("PauseKind = %q on disk, want it to survive as %q", sn.Provisioned.PauseKind, mail.PauseKindRate) } } // TestAPIEmailDeclareRefusesAnUnaddressedFrom: a From with no "@" is refused // before anything is written. func TestAPIEmailDeclareRefusesAnUnaddressedFrom(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-declare-badfrom") status, out := postJSON(t, emailURL(fx, "titogo"), map[string]any{"from": "not-an-address"}, token) if status != http.StatusBadRequest { t.Fatalf("declare with a bad From = %d %v, want 400", status, out) } if rec := emailRecord(t, fx, "titogo"); rec != nil { t.Fatalf("a refused declaration wrote a record: %+v", rec) } } // TestNoOpDeclareDoesNotBumpTheSerial: a re-declare of the SAME env prefix // and From writes no audit line — declaring never bumps the serial either // way, changed or not, but the audit trail must not be buried under a no-op // re-run. A declaration that DOES change something still audits. func TestNoOpDeclareDoesNotBumpTheSerial(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-noop") app := "titogo" body := map[string]any{"env_prefix": "TITOGO_SMTP", "from": "hello@titogo.example"} status, out := postJSON(t, emailURL(fx, app), body, token) if status != http.StatusOK { t.Fatalf("declare = %d %v", status, out) } if got := serial(t, fx); got != 0 { t.Fatalf("serial after declare = %d, want 0", got) } before, err := fx.st.Events(OpsAccountID, app, 20) if err != nil { t.Fatal(err) } // A no-op re-declare: identical prefix, identical From. status, out = postJSON(t, emailURL(fx, app), body, token) if status != http.StatusOK { t.Fatalf("no-op re-declare = %d %v", status, out) } if got := serial(t, fx); got != 0 { t.Fatalf("serial after a no-op re-declare = %d, want 0", got) } after, err := fx.st.Events(OpsAccountID, app, 20) if err != nil { t.Fatal(err) } if len(after) != len(before) { t.Fatalf("event count = %d after a no-op re-declare, want unchanged from %d\nbefore=%+v\nafter=%+v", len(after), len(before), before, after) } // A genuinely changed re-declare DOES audit — the gating is on the // content, not a blanket "never audit a re-declare". body["from"] = "other@titogo.example" status, out = postJSON(t, emailURL(fx, app), body, token) if status != http.StatusOK { t.Fatalf("changed re-declare = %d %v", status, out) } changed, err := fx.st.Events(OpsAccountID, app, 20) if err != nil { t.Fatal(err) } if len(changed) != len(after)+1 { t.Fatalf("event count = %d after a changed re-declare, want %d", len(changed), len(after)+1) } } // ---- domains -------------------------------------------------------------- // TestAPIEmailDomainEnsurePublishesForAPlatformDomain: a domain under this // deployment's own suffix is auto-published — pending is empty, and the // records actually landed in the fake DNS writer. func TestAPIEmailDomainEnsurePublishesForAPlatformDomain(t *testing.T) { fx, _, writer, token := emailArmed(t, "code-email-domain-platform") app, domain := "titogo", "titogo.bab.oncarlos.com" declareEmail(t, fx, token, app, "hello@"+domain) out := ensureEmailDomain(t, fx, token, app, domain) dom := block(t, out, "domain") if dom["kind"] != mail.KindPlatform { t.Fatalf("kind = %v, want platform", dom["kind"]) } if pending := pendingList(t, out); len(pending) != 0 { t.Fatalf("pending = %v, want empty — a platform domain publishes itself", pending) } if len(writer.Records) == 0 { t.Fatal("no records landed in the DNS writer for a platform domain") } auditedBy(t, fx, app, "email-domain-ensure", "alice@keymail.dev", domain) if got := serial(t, fx); got != 0 { t.Fatalf("serial after ensuring a domain = %d, want 0 — nothing in the app's own config changed", got) } } // TestAPIEmailDomainEnsureReturnsPendingForACustomDomain: a domain outside // this deployment's suffix is never auto-published — the member gets the // record set back to publish by hand, and nothing lands in the DNS writer. func TestAPIEmailDomainEnsureReturnsPendingForACustomDomain(t *testing.T) { fx, _, writer, token := emailArmed(t, "code-email-domain-custom") app, domain := "titogo", "mail.example.com" declareEmail(t, fx, token, app, "hello@"+domain) out := ensureEmailDomain(t, fx, token, app, domain) dom := block(t, out, "domain") if dom["kind"] != mail.KindCustom { t.Fatalf("kind = %v, want custom", dom["kind"]) } if pending := pendingList(t, out); len(pending) == 0 { t.Fatal("pending is empty for a custom domain — the member has nothing to publish") } if len(writer.Records) != 0 { t.Fatalf("records landed in the DNS writer for a CUSTOM domain: %v", writer.Records) } } // TestAPIEmailDomainEnsureWithNilDNSWriterDegradesPublishingOnly proves // mailDeploymentFor's corrected adaptation (I4, review round 2): a // deployment with no DNSWriter must not panic on a platform-suffixed // domain, and it must hand back pending records for the member to publish // by hand — Config.DNSWriter's own documented contract — but Kind must stay // "platform", NOT get silently relabeled "custom". The first cut of this // fix cleared PlatformSuffix to sidestep the nil writer, which also turned // off C1's ownership check for this whole deployment shape (with no suffix, // DomainKindFor can only ever answer custom, so platformNameOwnedBy never // runs at all). Kind staying "platform" is what proves the ownership check // ran — see TestAPIEmailDomainEnsureRefusesAPlatformNameWithNoConfiguredWriter // for the direct security assertion. func TestAPIEmailDomainEnsureWithNilDNSWriterDegradesPublishingOnly(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-domain-nowriter") fx.srv.cfg.DNSWriter = nil app, domain := "titogo", "titogo.bab.oncarlos.com" // under the platform suffix, owned by fx.sqid ("bab") declareEmail(t, fx, token, app, "hello@"+domain) out := ensureEmailDomain(t, fx, token, app, domain) dom := block(t, out, "domain") if dom["kind"] != mail.KindPlatform { t.Fatalf("kind = %v, want platform — no DNSWriter must degrade PUBLISHING only, never the domain's kind", dom["kind"]) } if pending := pendingList(t, out); len(pending) == 0 { t.Fatal("pending is empty — a platform domain with no DNSWriter must still hand back records") } } // TestAPIEmailDomainEnsureRefusesAPlatformNameWithNoConfiguredWriter (I4, // review round 2): the ownership check must run even in a deployment with // no DNSWriter configured. Before this fix, mailDeploymentFor cleared // PlatformSuffix whenever DNSWriter was nil, which silently disabled // platformNameOwnedBy for the whole deployment — any account could take // the global claim on any OTHER account's platform name in that shape, // locking the real owner out of their own name (denial-of-name; no DKIM // gets published, so not a send capability, but still a 409 against the // rightful owner). func TestAPIEmailDomainEnsureRefusesAPlatformNameWithNoConfiguredWriter(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-domain-nowriter-ownership") fx.srv.cfg.DNSWriter = nil app := "titogo" declareEmail(t, fx, token, app, "hello@x.aaa.oncarlos.com") status, out := postJSON(t, emailURL(fx, app)+"/domains", map[string]any{"domain": "x.aaa.oncarlos.com"}, token) if status != http.StatusBadRequest { t.Fatalf("ensure a platform name for a DIFFERENT account (aaa), fx account is bab = %d, want 400: %v", status, out) } } // TestAPIEmailDomainVerifyRequiresAnEnsuredDomain: verifying a domain that // was never ensured is a speaking 404, not a crash on a missing region. func TestAPIEmailDomainVerifyRequiresAnEnsuredDomain(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-verify-missing") status, out := postJSON(t, emailURL(fx, "titogo")+"/domains/never-ensured.example.com/verify", map[string]any{}, token) if status != http.StatusNotFound { t.Fatalf("verify an unensured domain = %d %v, want 404", status, out) } } // TestAPIEmailDomainVerifyRecordsSESVerdict: driving the fake to verified // and calling verify moves the domain's region to StatusVerified. func TestAPIEmailDomainVerifyRecordsSESVerdict(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-verify") app, domain := "titogo", "titogo.bab.oncarlos.com" declareEmail(t, fx, token, app, "hello@"+domain) fake.StatusOnEnsure = mail.Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"} ensureEmailDomain(t, fx, token, app, domain) status, out := postJSON(t, emailURL(fx, app)+"/domains/"+domain+"/verify", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("verify = %d %v", status, out) } dom := block(t, out, "domain") regions, _ := dom["regions"].(map[string]any) region, _ := regions["eu-west-1"].(map[string]any) if region["status"] != mail.StatusVerified { t.Fatalf("region status = %v, want verified", region["status"]) } auditedBy(t, fx, app, "email-domain-verify", "alice@keymail.dev", "verified") } // ---- domains rm: the release door ----------------------------------------- // removeEmailDomain drives the DELETE door and hands back the status and // the decoded reply — a 200 and a refusal are both interesting here, so it // never fails the test itself. func removeEmailDomain(t *testing.T, fx *consoleFixture, token, app, domain string) (int, map[string]any) { t.Helper() status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, app)+"/domains/"+domain, token, "") out := map[string]any{} if err := json.Unmarshal([]byte(body), &out); err != nil { t.Fatalf("remove %s reply is not JSON (%d): %q", domain, status, body) } return status, out } // secondApp claims another app in the same account, so a domain can have // two referrers — the state the whole sibling rule exists for. func secondApp(t *testing.T, fx *consoleFixture, name string) string { t.Helper() if _, err := fx.st.CreateAppIn(OpsAccountID, name, "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } return name } // TestAPIEmailDomainRemoveReleasesTheGlobalClaim: the defect this door // exists for. A claimed domain was unreachable to every other CARLOS // account — including the one that actually controls its DNS — until the // app holding it was PERMANENTLY DELETED, because mail.Teardown was the // only caller that could ever release a claim. After this, the claim object // is gone and the domain is free. func TestAPIEmailDomainRemoveReleasesTheGlobalClaim(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-domain-rm-claim") app, domain := "titogo", "mail.example.com" declareEmail(t, fx, token, app, "hello@elsewhere.example.com") ensureEmailDomain(t, fx, token, app, domain) claims, err := mail.ListDomainClaims(context.Background(), fx.srv.cfg.Releases) if err != nil || len(claims) != 1 || claims[0].Domain != domain { t.Fatalf("fixture broken: claims = %+v (err %v)", claims, err) } status, out := removeEmailDomain(t, fx, token, app, domain) if status != http.StatusOK { t.Fatalf("remove = %d %v, want 200", status, out) } if out["torn_down"] != true { t.Fatalf("torn_down = %v, want true — this was the only referrer", out["torn_down"]) } claims, err = mail.ListDomainClaims(context.Background(), fx.srv.cfg.Releases) if err != nil || len(claims) != 0 { t.Fatalf("the global claim survived the remove: %+v (err %v)", claims, err) } auditedBy(t, fx, app, "email-domain-remove", "alice@keymail.dev", domain) if got := serial(t, fx); got != 0 { t.Fatalf("serial after removing a domain = %d, want 0 — nothing in the app's own config changed", got) } } // TestAPIEmailDomainRemoveKeepsADomainASiblingAppUses: with another app // still referring to the domain, the door removes ONLY the referral. The // identity, the DNS and the claim all stay — releasing them here would // revoke a sibling app's verified domain silently. func TestAPIEmailDomainRemoveKeepsADomainASiblingAppUses(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-domain-rm-sibling") domain := "shared.example.com" other := secondApp(t, fx, "marketing") declareEmail(t, fx, token, "titogo", "hello@elsewhere.example.com") ensureEmailDomain(t, fx, token, "titogo", domain) ensureEmailDomain(t, fx, token, other, domain) status, out := removeEmailDomain(t, fx, token, "titogo", domain) if status != http.StatusOK { t.Fatalf("remove = %d %v, want 200", status, out) } if out["torn_down"] != false { t.Fatalf("torn_down = %v, want false — %s still uses the domain", out["torn_down"], other) } if got := strList(t, out, "remaining_apps"); len(got) != 1 || got[0] != other { t.Fatalf("remaining_apps = %v, want [%s]", got, other) } claims, err := mail.ListDomainClaims(context.Background(), fx.srv.cfg.Releases) if err != nil || len(claims) != 1 { t.Fatalf("the claim was released while a sibling app still sends from the domain: %+v (err %v)", claims, err) } if len(fake.Identities) == 0 { t.Fatal("the sibling app's SES identity was destroyed") } } // TestAPIEmailDomainRemoveRefusesTheAppsOwnFromDomain: mail.Enable rejects a // From that is not at a granted domain, so removing the From's own domain // would break sending at the app's next re-enable rather than at the // command that caused it. 409, naming the From and the verb that moves it. func TestAPIEmailDomainRemoveRefusesTheAppsOwnFromDomain(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-domain-rm-from") app, domain := "titogo", "mail.example.com" declareEmail(t, fx, token, app, "hello@"+domain) ensureEmailDomain(t, fx, token, app, domain) status, out := removeEmailDomain(t, fx, token, app, domain) if status != http.StatusConflict { t.Fatalf("remove of the From's own domain = %d %v, want 409", status, out) } msg, _ := out["error"].(string) if !strings.Contains(msg, "hello@"+domain) { t.Fatalf("refusal does not name the From address: %q", msg) } if !strings.Contains(msg, "--from") { t.Fatalf("refusal does not name the verb that moves the From: %q", msg) } claims, err := mail.ListDomainClaims(context.Background(), fx.srv.cfg.Releases) if err != nil || len(claims) != 1 { t.Fatalf("a refused remove still released the claim: %+v (err %v)", claims, err) } if rec, _, err := mail.GetDomain(context.Background(), fx.srv.cfg.Releases, fx.sqid, domain); err != nil || rec == nil { t.Fatalf("a refused remove still deleted the domain record: %+v (err %v)", rec, err) } } // TestAPIEmailDomainRemoveIs404ForADomainTheAppDoesNotUse: naming a domain // this app does not send from is a 404, never a quiet 200. The same 404 // covers a domain claimed by ANOTHER account, deliberately in the same // words — see mail.ErrDomainNotReferenced. func TestAPIEmailDomainRemoveIs404ForADomainTheAppDoesNotUse(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-domain-rm-404") app := "titogo" declareEmail(t, fx, token, app, "hello@elsewhere.example.com") status, out := removeEmailDomain(t, fx, token, app, "never-added.example.com") if status != http.StatusNotFound { t.Fatalf("remove of an unused domain = %d %v, want 404", status, out) } if msg, _ := out["error"].(string); !strings.Contains(msg, "carlos email status") { t.Fatalf("404 does not point at the verb that lists the app's domains: %q", msg) } } // TestAPIEmailDomainRemoveReportsACustomDomainsOrphanedRecords: a custom // domain's records live in the customer's zone and are the customer's to // remove — mail.Teardown's rule, kept. The door deletes none of them and // hands the whole set back so the member can see what just went stale. func TestAPIEmailDomainRemoveReportsACustomDomainsOrphanedRecords(t *testing.T) { fx, _, writer, token := emailArmed(t, "code-email-domain-rm-custom-dns") app, domain := "titogo", "mail.example.com" declareEmail(t, fx, token, app, "hello@elsewhere.example.com") ensureEmailDomain(t, fx, token, app, domain) if len(writer.Records) != 0 { t.Fatalf("fixture broken: CARLOS published a custom domain's records: %v", writer.Records) } status, out := removeEmailDomain(t, fx, token, app, domain) if status != http.StatusOK { t.Fatalf("remove = %d %v, want 200", status, out) } orphaned, ok := out["orphaned"].([]any) if !ok || len(orphaned) != 6 { t.Fatalf("orphaned = %v, want the whole six-record set back", out["orphaned"]) } } // TestAPIEmailDomainRemoveDeletesAPlatformDomainsRecords: the other half of // the same rule — records CARLOS published, CARLOS removes, and reports // nothing orphaned because nothing is. func TestAPIEmailDomainRemoveDeletesAPlatformDomainsRecords(t *testing.T) { fx, _, writer, token := emailArmed(t, "code-email-domain-rm-platform-dns") app, domain := "titogo", "titogo.bab.oncarlos.com" declareEmail(t, fx, token, app, "hello@elsewhere.example.com") ensureEmailDomain(t, fx, token, app, domain) if len(writer.Records) == 0 { t.Fatal("fixture broken: nothing was published for a platform domain") } status, out := removeEmailDomain(t, fx, token, app, domain) if status != http.StatusOK { t.Fatalf("remove = %d %v, want 200", status, out) } if len(writer.Records) != 0 { t.Fatalf("a platform domain's published records survived: %v", writer.Records) } if orphaned, _ := out["orphaned"].([]any); len(orphaned) != 0 { t.Fatalf("orphaned = %v, want empty for a platform domain", orphaned) } } // ---- enable and the F2 grant wiring ---------------------------------------- // TestEnableAppliesTheTierGrantBeforeProvisioning: F2's ruling, pinned. The // enable door must leave a `grant` block on the record naming the tier's // cap — without it the limiter silently runs the app on the default free // cap with no record of why. func TestEnableAppliesTheTierGrantBeforeProvisioning(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-grant") app, domain := "titogo", "titogo.bab.oncarlos.com" declareEmail(t, fx, token, app, "hello@"+domain) fake.StatusOnEnsure = mail.Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"} ensureEmailDomain(t, fx, token, app, domain) verifyEmailDomain(t, fx, token, app, domain) out := enableEmail(t, fx, token, app) grant := block(t, out, "grant") if grant["daily_cap"] != float64(TierCap("free")) { t.Fatalf("grant.daily_cap = %v, want %d", grant["daily_cap"], TierCap("free")) } if grant["source"] != "tier:free" { t.Fatalf("grant.source = %v, want tier:free", grant["source"]) } if out["daily_cap"] != float64(TierCap("free")) { t.Fatalf("daily_cap = %v, want %d", out["daily_cap"], TierCap("free")) } rec := emailRecord(t, fx, app) if rec.Grant == nil { t.Fatal("record has no grant block after enable") } if rec.Grant.DailyCap != TierCap("free") || rec.Grant.Source != "tier:free" { t.Fatalf("record grant = %+v, want cap %d source tier:free", rec.Grant, TierCap("free")) } if rec.Provisioned == nil || rec.Provisioned.KeyID == "" { t.Fatalf("record provisioned = %+v, want a delivered credential", rec.Provisioned) } auditedBy(t, fx, app, "email-enable", "alice@keymail.dev", "enabled") if got := serial(t, fx); got == 0 { t.Fatal("serial did not bump after enable delivered config") } } // TestAPIEmailEnableRequiresAVerifiedDomain: enabling before any domain has // verified is refused, not silently short-circuited. func TestAPIEmailEnableRequiresAVerifiedDomain(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-enable-nodomain") app := "titogo" declareEmail(t, fx, token, app, "hello@titogo.bab.oncarlos.com") status, out := postJSON(t, emailURL(fx, app)+"/enable", map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("enable with no verified domain = %d %v, want 409", status, out) } } // ---- rotate ----------------------------------------------------------------- // TestAPIEmailRotateThenFinish: rotate leaves both keys live // (pending_finish true) and delivers a new key; finish closes the window // and bumps no serial. func TestAPIEmailRotateThenFinish(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-rotate") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) firstKey := emailRecord(t, fx, app).Provisioned.KeyID status, out := postJSON(t, emailURL(fx, app)+"/rotate", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("rotate = %d %v", status, out) } if out["pending_finish"] != true { t.Fatalf("pending_finish = %v, want true right after a rotate", out["pending_finish"]) } rec := emailRecord(t, fx, app) if rec.Provisioned.KeyID == firstKey { t.Fatal("rotate did not mint a new key") } if rec.Provisioned.PrevKeyID != firstKey { t.Fatalf("PrevKeyID = %q, want %q", rec.Provisioned.PrevKeyID, firstKey) } serialAfterRotate := serial(t, fx) if serialAfterRotate == 0 { t.Fatal("serial did not bump after rotate delivered a new key") } status, out = postJSON(t, emailURL(fx, app)+"/rotate?finish=1", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("rotate finish = %d %v", status, out) } if out["pending_finish"] != false { t.Fatalf("pending_finish = %v, want false after finish", out["pending_finish"]) } if got := serial(t, fx); got != serialAfterRotate { t.Fatalf("serial after finish = %d, want unchanged from %d — finish delivers nothing", got, serialAfterRotate) } } // ---- standalone credentials ------------------------------------------------- // TestStandaloneResponseIsNoStoreAndTheOnlyOneWithAPassword: the credential // create reply is uncacheable and the only reply in this file carrying a // password. Every sibling reply in the same flow is checked and must not. func TestStandaloneResponseIsNoStoreAndTheOnlyOneWithAPassword(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-standalone") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) status, headers, body := rawReq(t, http.MethodPost, emailURL(fx, app)+"/credentials", token, `{"name":"laptop"}`) if status != http.StatusOK { t.Fatalf("credentials create = %d %s", status, body) } if got := headers.Get("Cache-Control"); got != "no-store" { t.Fatalf("Cache-Control = %q, want no-store", got) } var out map[string]any if err := json.Unmarshal([]byte(body), &out); err != nil { t.Fatalf("body is not JSON: %v\n%s", err, body) } pass, _ := out["password"].(string) if pass == "" { t.Fatalf("password = %q, want a real credential", pass) } user, _ := out["username"].(string) if user == "" { t.Fatal("username (the key id) is empty") } // No sibling reply in this same flow ever carries the substring // "password" — declare, status, enable, rotate. _, declareBody := postRaw(t, emailURL(fx, app), map[string]any{"from": "hello@" + domain}, token) if strings.Contains(declareBody, "password") { t.Fatalf("declare reply carries a password: %s", declareBody) } _, statusBody := getRaw(t, emailURL(fx, app), token) if strings.Contains(statusBody, "password") { t.Fatalf("status reply carries a password: %s", statusBody) } _, enableBody := postRaw(t, emailURL(fx, app)+"/enable", map[string]any{}, token) if strings.Contains(enableBody, "password") { t.Fatalf("enable reply carries a password: %s", enableBody) } _, rotateBody := postRaw(t, emailURL(fx, app)+"/rotate", map[string]any{}, token) if strings.Contains(rotateBody, "password") { t.Fatalf("rotate reply carries a password: %s", rotateBody) } } // TestStatusReplyNeverCarriesAPassword: a dedicated check on the status // door specifically, with a standalone credential already minted (the one // place a password could leak from if rec.Standalone were ever echoed // wrong) — key_id is expected and fine; password is not. func TestStatusReplyNeverCarriesAPassword(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-status-password") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) if status, out := postJSON(t, emailURL(fx, app)+"/credentials", map[string]any{"name": "laptop"}, token); status != http.StatusOK { t.Fatalf("credentials create = %d %v", status, out) } status, body := getRaw(t, emailURL(fx, app), token) if status != http.StatusOK { t.Fatalf("status = %d %s", status, body) } if strings.Contains(body, "password") { t.Fatalf("status reply carries a password: %s", body) } var out map[string]any if err := json.Unmarshal([]byte(body), &out); err != nil { t.Fatalf("body is not JSON: %v", err) } prov := block(t, out, "provisioned") if s, _ := prov["key_id"].(string); s == "" { t.Fatalf("provisioned.key_id = %v, want the delivered key id (not a secret)", prov["key_id"]) } standalone, _ := out["standalone"].([]any) if len(standalone) != 1 { t.Fatalf("standalone = %v, want the one minted credential", out["standalone"]) } } // TestAPIEmailCredentialsRevokeUnknownNameIs404: revoking a name that was // never minted is a speaking 404, not a silent no-op 200. func TestAPIEmailCredentialsRevokeUnknownNameIs404(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-revoke-missing") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, app)+"/credentials/never-minted", token, "") if status != http.StatusNotFound { t.Fatalf("revoke an unminted credential = %d %s, want 404", status, body) } } // TestAPIEmailCredentialsRevokeRemovesIt: revoke actually deletes the // credential and does not disturb the delivered one. func TestAPIEmailCredentialsRevokeRemovesIt(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-revoke") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) deliveredKey := emailRecord(t, fx, app).Provisioned.KeyID if status, out := postJSON(t, emailURL(fx, app)+"/credentials", map[string]any{"name": "laptop"}, token); status != http.StatusOK { t.Fatalf("credentials create = %d %v", status, out) } status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, app)+"/credentials/laptop", token, "") if status != http.StatusOK { t.Fatalf("revoke = %d %s", status, body) } rec := emailRecord(t, fx, app) if len(rec.Standalone) != 0 { t.Fatalf("standalone = %+v, want empty after revoke", rec.Standalone) } if rec.Provisioned.KeyID != deliveredKey { t.Fatal("revoking a standalone credential disturbed the delivered one") } } // ---- pause (operator) ------------------------------------------------------- // TestAPIEmailPauseNeedsAnOperator: a member of the account — even the one // who declared and enabled — cannot pause or raise a cap. The refusal is // the family's byte-identical 404 (I3: this door never speaks a 403 — // that would be an oracle telling a non-operator prober the route // exists), and nothing is changed. func TestAPIEmailPauseNeedsAnOperator(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause-operator") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) status, body := postRaw(t, emailURL(fx, app)+"/pause", map[string]any{"paused": true}, token) if status != http.StatusNotFound { t.Fatalf("pause by a non-operator member = %d %q, want 404", status, body) } if body != "404 page not found\n" { t.Fatalf("refusal body = %q, want the stdlib http.NotFound output", body) } rec := emailRecord(t, fx, app) if !rec.Provisioned.Active { t.Fatal("a refused pause changed the record's active state") } } // TestAPIEmailPauseOperatorNotOnMailOperatorsListIsRefused (I2): a genuine // fleet operator (store.IsOperator true) is STILL refused when // CARLOS_MAIL_OPERATORS does not name them — the list is fail-closed, not // "any fleet operator may." This is the invariant protecting the SHIPPED // deployment shape, where CARLOS_MAIL_OPERATORS is typically unset. func TestAPIEmailPauseOperatorNotOnMailOperatorsListIsRefused(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause-notlisted") fx.srv.cfg.MailOperators = nil // the shipped-binary default app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) if err := fx.st.SetOperator("random-operator@keymail.dev", true); err != nil { t.Fatal(err) } randomOp := cliToken(t, fx, "random-operator@keymail.dev", "code-email-pause-notlisted-op") status, body := postRaw(t, emailURL(fx, app)+"/pause", map[string]any{"paused": true}, randomOp) if status != http.StatusNotFound { t.Fatalf("pause by a fleet operator not on MailOperators = %d %q, want 404", status, body) } rec := emailRecord(t, fx, app) if !rec.Provisioned.Active { t.Fatal("a refused pause changed the record's active state") } } // TestAPIEmailPauseOperatorNeedNotBeAMember (I3): the abuse brake's whole // reason for existing — an operator with NO membership in the account can // still pause it. Confirms non-membership explicitly, rather than relying // on emailOperatorToken never having called AddMember. func TestAPIEmailPauseOperatorNeedNotBeAMember(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause-nonmember") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) ops := emailOperatorToken(t, fx, "code-email-pause-nonmember-ops") role, err := fx.st.Role(OpsAccountID, "ops@keymail.dev") if err != nil { t.Fatal(err) } if role != "" { t.Fatalf("role = %q, want the operator to hold NO role in this account", role) } status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"paused": true}, ops) if status != http.StatusOK { t.Fatalf("pause by a non-member operator = %d %v, want 200", status, out) } if out["active"] != false { t.Fatalf("active = %v, want false after a non-member operator's pause", out["active"]) } } // TestAPIEmailPauseCapRaiseDoesNotResume (I1): a cap-raise-only request // ({"daily_cap": N}, no "paused" field) must not decode a missing Paused // as false and silently resume a tenant an earlier request paused. func TestAPIEmailPauseCapRaiseDoesNotResume(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause-capnoresume") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) ops := emailOperatorToken(t, fx, "code-email-pause-capnoresume-ops") if status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"paused": true}, ops); status != http.StatusOK { t.Fatalf("pause = %d %v", status, out) } if emailRecord(t, fx, app).Provisioned.Active { t.Fatal("sanity check failed: record is not paused after the pause call") } status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"daily_cap": 5000}, ops) if status != http.StatusOK { t.Fatalf("cap-raise-only = %d %v", status, out) } if out["active"] != false { t.Fatalf("active = %v, want false — a cap-raise-only request must not resume", out["active"]) } rec := emailRecord(t, fx, app) if rec.Provisioned.Active { t.Fatal("a cap-raise-only request resumed a paused tenant") } if rec.Grant.DailyCap != 5000 || rec.Grant.Source != "operator" { t.Fatalf("grant = %+v, want the cap raise to still have applied", rec.Grant) } } // TestAPIEmailPauseRefusesAResumeInPerInstanceMode: the operator door // answers a permanent conflict, never the generic "try again" 502, when // asked to resume an app sender that has moved to per-instance senders. // Nothing in this release sets PerInstance itself, so this hand-sets it — // the point is that a retry can never succeed here, and the operator needs // to be told that plainly rather than sent to retry forever. func TestAPIEmailPauseRefusesAResumeInPerInstanceMode(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause-perinstance") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) ops := emailOperatorToken(t, fx, "code-email-pause-perinstance-ops") if status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"paused": true}, ops); status != http.StatusOK { t.Fatalf("pause = %d %v", status, out) } sn, etag, err := mail.GetSender(context.Background(), fx.rel, release.OpsAccount, app, mail.SenderAppID) if err != nil { t.Fatal(err) } sn.PerInstance = true if _, err := mail.PutSender(context.Background(), fx.rel, release.OpsAccount, app, sn, etag); err != nil { t.Fatal(err) } status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"paused": false}, ops) if status != http.StatusConflict { t.Fatalf("resume in per-instance mode = %d %v, want 409", status, out) } msg, _ := out["error"].(string) if !strings.Contains(msg, "per-instance") { t.Fatalf("error = %q, want it to name per-instance mode", msg) } if strings.Contains(strings.ToLower(msg), "try again") { t.Fatalf("refusal promised a retry would help, but this refusal is permanent: %q", msg) } if emailRecord(t, fx, app).Provisioned.Active { t.Fatal("the refused resume left the record active") } } // TestAPIEmailPauseRefusesAnEmptyBody: neither paused nor a positive // daily_cap is a request that would change nothing — refused rather than // silently answering 200 having done nothing. func TestAPIEmailPauseRefusesAnEmptyBody(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause-empty") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) ops := emailOperatorToken(t, fx, "code-email-pause-empty-ops") status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{}, ops) if status != http.StatusBadRequest { t.Fatalf("empty pause body = %d %v, want 400", status, out) } } // TestAPIEmailPauseTogglesTheKeyAndRaisesTheCap: an operator can pause, // resume and raise the cap through the same door. Raising the cap applies // mail.ApplyGrant with source "operator" (F2), and that grant SURVIVES a // later tier-sourced enable — proving the wiring passes the right source // rather than re-implementing ApplyGrant's own precedence rule. func TestAPIEmailPauseTogglesTheKeyAndRaisesTheCap(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-pause") app, domain := "titogo", "titogo.bab.oncarlos.com" provisionEmail(t, fx, fake, token, app, domain, "hello@"+domain) ops := emailOperatorToken(t, fx, "code-email-pause-ops") status, out := postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"paused": true}, ops) if status != http.StatusOK { t.Fatalf("pause = %d %v", status, out) } if out["active"] != false { t.Fatalf("active = %v, want false after a pause", out["active"]) } rec := emailRecord(t, fx, app) if rec.Provisioned.Active { t.Fatal("record still active after a pause") } auditedBy(t, fx, app, "email-pause", "ops@keymail.dev", "paused") status, out = postJSON(t, emailURL(fx, app)+"/pause", map[string]any{"paused": false, "daily_cap": 5000}, ops) if status != http.StatusOK { t.Fatalf("resume+raise = %d %v", status, out) } if out["active"] != true { t.Fatalf("active = %v, want true after a resume", out["active"]) } grant := block(t, out, "grant") if grant["daily_cap"] != float64(5000) || grant["source"] != "operator" { t.Fatalf("grant = %v, want cap 5000 source operator", grant) } // A later enable's TIER-sourced grant must not silently flatten the // operator's raise back to the free cap. enableEmail(t, fx, token, app) rec = emailRecord(t, fx, app) if rec.Grant.Source != "operator" || rec.Grant.DailyCap != 5000 { t.Fatalf("grant after a later enable = %+v, want the operator's raise to survive", rec.Grant) } } // TestAPIEmailDomainEnsureRefusesADomainClaimedByAnotherAccount (I3, Task // 17a review follow-up): a domain another account already claimed must // come back as a clean 409, not the door's generic 502 — the refusal is // PERMANENT (nothing changes between attempts), so telling the member to // retry would be actively misleading. The refusal must also stay neutral: // the holding account's sqid must never appear in the response body. func TestAPIEmailDomainEnsureRefusesADomainClaimedByAnotherAccount(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-domain-claimed") app := "titogo" declareEmail(t, fx, token, app, "hello@shared.com") ensureEmailDomain(t, fx, token, app, "shared.com") // fx.sqid now holds the claim // A genuinely SEPARATE account — not just a second app in the same // account — attempts the same domain. now := time.Now() const otherAcctID int64 = 4242 otherSqid := accountSqid(otherAcctID) if err := fx.st.CreateAccount(otherAcctID, otherSqid, "Other", "carol@keymail.dev", now); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(otherAcctID, "otherapp", "carol@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.AddMember(otherAcctID, "carol@keymail.dev", RoleOwner, now); err != nil { t.Fatal(err) } otherToken := cliToken(t, fx, "carol@keymail.dev", "code-email-domain-claimed-other") otherURL := fx.ts.URL + "/api/cli/apps/" + otherSqid + "/otherapp/email" status, _ := postJSON(t, otherURL, map[string]any{"from": "hello@shared.com"}, otherToken) if status != http.StatusOK { t.Fatalf("declare for the other account = %d", status) } status, out := postJSON(t, otherURL+"/domains", map[string]any{"domain": "shared.com"}, otherToken) if status != http.StatusConflict { t.Fatalf("ensure domain by a second account = %d, want 409: %v", status, out) } msg, _ := out["error"].(string) if strings.Contains(msg, fx.sqid) { t.Fatalf("refusal named the holding account's sqid: %q", msg) } if strings.Contains(strings.ToLower(msg), "try again") || strings.Contains(strings.ToLower(msg), "re-run") { t.Fatalf("refusal promised a retry would help, but this refusal is permanent: %q", msg) } // Neutral (#216): the refusal must not confirm that another account // holds the domain, only that this one cannot have it. if msg != mailDomainUnavailableMsg { t.Fatalf("refusal = %q, want the neutral %q", msg, mailDomainUnavailableMsg) } } // TestAPIEmailDomainEnsureRefusesAForeignIdentityWithProofSteps (#216): a // domain already verified in SES outside CARLOS is refused with 409 and the // exact TXT record that would prove control, and the identity is untouched. func TestAPIEmailDomainEnsureRefusesAForeignIdentityWithProofSteps(t *testing.T) { fx, fake, _, token := emailArmed(t, "code-email-domain-foreign") app := "titogo" declareEmail(t, fx, token, app, "hello@tito.io") fake.Identities["eu-west-1|tito.io"] = &mail.MemIdentity{ ARN: "arn:aws:ses:eu-west-1:1:identity/tito.io", DKIMTokens: []string{"c1", "c2", "c3"}, MailFrom: "bounce.tito.io", ConfigSet: "classic", Status: mail.Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"}, } status, out := postJSON(t, emailURL(fx, app)+"/domains", map[string]any{"domain": "tito.io"}, token) if status != http.StatusConflict { t.Fatalf("ensure a foreign identity = %d, want 409: %v", status, out) } msg, _ := out["error"].(string) if !strings.Contains(msg, mail.ProofRecordName("tito.io")) || !strings.Contains(msg, mail.ProofRecordValue(fx.sqid)) { t.Fatalf("refusal does not say how to prove control: %q", msg) } if got := fake.Identities["eu-west-1|tito.io"]; got.MailFrom != "bounce.tito.io" || got.ConfigSet != "classic" { t.Fatalf("the foreign identity was mutated: %+v", got) } } // TestAPIEmailEventsReadsTheLog: the door `carlos email events` calls hands // back the app's per-message log, narrowed by what the query asks, and // refuses a query it cannot read rather than guessing at it. func TestAPIEmailEventsReadsTheLog(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-events") app := "titogo" ctx := context.Background() now := time.Now() for _, c := range []struct{ to, id, typ string }{ {"ann@example.com", "m-ann", mail.EventDelivery}, {"bob@example.com", "m-bob", mail.EventBounce}, } { ev := mail.Event{Type: c.typ, MessageID: c.id, At: now, Outcomes: []mail.Outcome{{Recipient: c.to}}} if err := fx.st.RecordMailMessageEvent(ctx, fx.sqid, app, "sns-"+c.id, ev, true, now); err != nil { t.Fatal(err) } } status, out := getJSON(t, emailURL(fx, app)+"/events", token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } events, _ := out["events"].([]any) if len(events) != 2 { t.Fatalf("events = %v, want both", out["events"]) } if days, _ := out["retention_days"].(float64); days != 30 { t.Errorf("retention_days = %v, want 30 — the reader should be told how far back it can look", out["retention_days"]) } status, out = getJSON(t, emailURL(fx, app)+"/events?to=BOB%40example.com", token) events, _ = out["events"].([]any) if status != http.StatusOK || len(events) != 1 { t.Fatalf("narrowed to bob: %d %v", status, out) } if first, _ := events[0].(map[string]any); first["message_id"] != "m-bob" || first["type"] != mail.EventBounce { t.Errorf("row = %v", first) } // An app with nothing in its log is an empty list, not a null. if status, body := getRaw(t, emailURL(fx, app)+"/events?message=nothing-by-this-id", token); status != http.StatusOK || !strings.Contains(body, `"events":[]`) { t.Errorf("an empty answer = %d %s", status, body) } // And the narrowing that is not about who: a window and a count. old := mail.Event{Type: mail.EventDelivery, MessageID: "m-old", At: now.Add(-48 * time.Hour), Outcomes: []mail.Outcome{{Recipient: "cat@example.com"}}} if err := fx.st.RecordMailMessageEvent(ctx, fx.sqid, app, "sns-m-old", old, true, now); err != nil { t.Fatal(err) } since := url.QueryEscape(now.Add(-24 * time.Hour).UTC().Format(time.RFC3339)) if status, out := getJSON(t, emailURL(fx, app)+"/events?since="+since, token); status != http.StatusOK { t.Fatalf("since: %d %v", status, out) } else if events, _ := out["events"].([]any); len(events) != 2 { t.Errorf("since the last day = %d events, want the two recent ones: %v", len(events), out["events"]) } if status, out := getJSON(t, emailURL(fx, app)+"/events?limit=1", token); status != http.StatusOK { t.Fatalf("limit: %d %v", status, out) } else if events, _ := out["events"].([]any); len(events) != 1 { t.Errorf("limit=1 gave %d events", len(events)) } for _, bad := range []string{"since=yesterday", "limit=0", "limit=100000", "limit=many"} { if status, body := getRaw(t, emailURL(fx, app)+"/events?"+bad, token); status != http.StatusBadRequest { t.Errorf("%s = %d %s, want 400", bad, status, body) } } } // TestAPIEmailPushTurnsOnAndOff: turning the push on delivers the key an app // verifies with into the app's own config, and names where events will go; // turning it off takes both back. func TestAPIEmailPushTurnsOnAndOff(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-push") fx.srv.cfg.AppsDomain = "oncarlos.com" app := "titogo" push := emailURL(fx, app) + "/events/push" status, out := getJSON(t, push, token) if status != http.StatusOK || out["on"] != false { t.Fatalf("before asking: %d %v", status, out) } status, body := putRaw(t, push, map[string]any{"path": "/mail/events"}, token) if status != http.StatusOK { t.Fatalf("turning it on: %d %s", status, body) } var on map[string]any if err := json.Unmarshal([]byte(body), &on); err != nil { t.Fatal(err) } if on["on"] != true || on["path"] != "/mail/events" || on["url"] != "https://titogo."+fx.sqid+".oncarlos.com/mail/events" { t.Errorf("reply = %v", on) } key, _ := fx.st.MailEventsSigningKey() vars := appVars(t, fx, app) if v, ok := vars[MailEventsKeyEnv]; !ok || v.Kind != "env" || v.Value != mailEventsPublicKey(key) { t.Errorf("%s = %+v, want the verifying key as a plain env var", MailEventsKeyEnv, v) } if on["public_key"] != mailEventsPublicKey(key) { t.Errorf("the reply should name the same key the app was given") } for _, bad := range []string{"", "mail/events", "//elsewhere.example/x", "/a/../b", "/with space"} { if status, body := putRaw(t, push, map[string]any{"path": bad}, token); status != http.StatusBadRequest { t.Errorf("path %q = %d %s, want 400", bad, status, body) } } status, _, body = rawReq(t, http.MethodDelete, push, token, "") if status != http.StatusOK || !strings.Contains(body, `"on":false`) { t.Fatalf("turning it off: %d %s", status, body) } if _, ok := appVars(t, fx, app)[MailEventsKeyEnv]; ok { t.Errorf("%s was left in the app's config after the push was turned off", MailEventsKeyEnv) } } // TestAPIEmailPushNeedsSomewhereToSend: a deployment with no apps domain has // no platform host to deliver to, and says so rather than recording a push // that could never arrive. func TestAPIEmailPushNeedsSomewhereToSend(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-push-nodomain") fx.srv.cfg.AppsDomain = "" status, body := putRaw(t, emailURL(fx, "titogo")+"/events/push", map[string]any{"path": "/mail/events"}, token) if status != http.StatusServiceUnavailable { t.Fatalf("with no apps domain = %d %s, want 503", status, body) } if hook, _ := fx.st.MailEventHookFor(context.Background(), fx.sqid, "titogo"); hook != nil { t.Error("a push was recorded with nowhere to send it") } } // TestAPIEmailPushOffTakesTheKeyOutEvenWithNoPush: turning the push on // delivers the key before it records the push, so a failure between the two // leaves a key and no push — and --off must still take the key out. func TestAPIEmailPushOffTakesTheKeyOutEvenWithNoPush(t *testing.T) { fx, _, _, token := emailArmed(t, "code-email-push-halfway") fx.srv.cfg.AppsDomain = "oncarlos.com" app := "titogo" key, _ := fx.st.MailEventsSigningKey() if _, err := appconfig.Apply(context.Background(), fx.rel, fx.sqid, app, map[string]appconfig.Var{MailEventsKeyEnv: {Kind: "env", Value: mailEventsPublicKey(key)}}, nil); err != nil { t.Fatal(err) } status, _, body := rawReq(t, http.MethodDelete, emailURL(fx, app)+"/events/push", token, "") if status != http.StatusOK { t.Fatalf("off: %d %s", status, body) } if _, ok := appVars(t, fx, app)[MailEventsKeyEnv]; ok { t.Errorf("%s was left behind by --off with no push recorded", MailEventsKeyEnv) } } // The reply gains a senders array and changes nothing else, so an older CLI // reading only the top-level fields keeps working byte for byte. func TestEmailReplyCarriesEverySenderAndKeepsItsTopLevelFields(t *testing.T) { fx, now := mailFixture(t) ctx := context.Background() // emailReply reads counts for TODAY (it computes its own day from // time.Now(), not from a caller-supplied clock), so the events seeded // here have to land on the real day, not mailFixture's fixed 2026-08-19. // otherwise MailCounts would legitimately find nothing. day := time.Now().UTC().Format("2006-01-02") if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", mail.SenderAppID, day, mail.EventSend); err != nil { t.Fatal(err) } if err := fx.st.RecordMailEvent(ctx, release.OpsAccount, "harbourmail", "book.example.com", day, mail.EventSend); err != nil { t.Fatal(err) } // A second sender, written straight to its own object the way step 5's // instance enable will. if _, err := mail.PutSender(ctx, fx.rel, release.OpsAccount, "harbourmail", &mail.Sender{ ID: "book.example.com", Scope: mail.ScopeInstance, Source: mail.SourceDomain, Host: "book.example.com", Declared: &mail.Declared{EnvPrefix: "HARBOURMAIL_SMTP", From: "noreply@x.com", At: now, By: "o@example.test"}, Grant: &mail.GrantBlock{Domains: []string{"x.com"}, DailyCap: 1000, Source: "operator", At: now, By: "op"}, Provisioned: &mail.Provisioned{IAMUser: "u2", KeyID: "AKIA9", Active: true, At: now}, }, ""); err != nil { t.Fatal(err) } acct, err := fx.st.AccountBySqid(release.OpsAccount) if err != nil || acct == nil { t.Fatalf("AccountBySqid = %v %v", acct, err) } rec, _, err := mail.Get(ctx, fx.rel, release.OpsAccount, "harbourmail") if err != nil { t.Fatal(err) } out, err := fx.srv.emailReply(ctx, acct, "harbourmail", rec) if err != nil { t.Fatal(err) } // Unchanged: the top-level fields still describe the app. if out["active"] != true || out["daily_cap"] != 50 { t.Fatalf("the top-level fields moved: active=%v daily_cap=%v", out["active"], out["daily_cap"]) } counts, _ := out["counts"].(map[string]any) if counts["sends"] != 2 { t.Fatalf("top-level sends = %v, want the app's whole day across both senders", counts["sends"]) } senders, ok := out["senders"].([]map[string]any) if !ok || len(senders) != 2 { t.Fatalf("senders = %#v, want two", out["senders"]) } if senders[0]["id"] != mail.SenderAppID || senders[0]["scope"] != mail.ScopeApp { t.Fatalf("the app sender is not first: %#v", senders[0]) } if senders[1]["id"] != "book.example.com" || senders[1]["daily_cap"] != 1000 || senders[1]["host"] != "book.example.com" { t.Fatalf("the second sender = %#v", senders[1]) } first, _ := senders[0]["counts"].(map[string]any) second, _ := senders[1]["counts"].(map[string]any) if first["sends"] != 1 || second["sends"] != 1 { t.Fatalf("per-sender counts = %v / %v, want one send each", first["sends"], second["sends"]) } }