package console import ( "context" "errors" "fmt" "net/http" "os" "testing" "time" "github.com/carlosframework/platform/internal/dnsrec" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/mail" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" ) func TestClaimPurgeStoreInventoryRefusesBeforeEffects(t *testing.T) { for _, tc := range []struct{ name, key, body string }{ {"malformed key", "stores/bad/nested.json", `{"schema":1,"app":"titogo"}`}, {"unsupported schema", "stores/prod.json", `{"schema":2,"app":"titogo","environment":"prod"}`}, {"identity mismatch", "stores/prod.json", `{"schema":1,"app":"other","environment":"prod"}`}, {"default unsupported schema", "store.json", `{"schema":2,"app":"titogo"}`}, {"default identity mismatch", "store.json", `{"schema":1,"app":"other"}`}, } { for _, stage := range []string{"request", "teardown"} { t.Run(tc.name+"/"+stage, func(t *testing.T) { cx := newClaimCase(t) cx.purgeWorld(t) p := cx.srv.cfg.ClaimPurge ctx := t.Context() const id = "strict-store-purge" appID := cx.appIDs[claimAppRef{Account: "bab", App: "titogo"}] var h *hostops.OperationHandle if stage == "teardown" { got, err := p.Request(ctx, "bab", "titogo", id, "owner", claimFxOwner) if got != id || err != nil && !errors.Is(err, hostops.ErrPending) { t.Fatalf("request: %s %v", got, err) } var err2 error h, err2 = p.Authority.Control.ResumeExclusive(ctx, "bab", "titogo", appID, id, hostops.OpPurging, p.Owner) if err2 != nil { t.Fatal(err2) } j, _, err := hostops.ReadPurgeJournal(ctx, cx.rel, "bab", "titogo", id, appID) if err != nil { t.Fatal(err) } j.Phase = "claimed" // Stage fixture: host barriers are tested separately. key, _ := hostops.PurgeKey("bab", "titogo") putMailJSON(t, cx.rel, key, j) } key := release.AppPrefix("bab", "titogo") + tc.key if _, err := cx.rel.Put(ctx, key, []byte(tc.body), ""); err != nil { t.Fatal(err) } mark := cx.rec.mark() var err error if stage == "request" { _, err = p.Request(ctx, "bab", "titogo", id, "owner", claimFxOwner) journal, _ := hostops.PurgeKey("bab", "titogo") if _, _, e := cx.rel.Get(ctx, journal); !errors.Is(e, os.ErrNotExist) { t.Errorf("invalid inventory reserved purge: %v", e) } } else { err = p.stage(ctx, h, "teardown", func(ctx context.Context) error { scope := mail.ClaimMailScope{Account: "bab", App: "titogo", AppID: appID, Operation: id} return mail.WithClaimMailResource(mail.WithClaimMailScope(ctx, scope), cx.srv.claimMailStore(), func(held context.Context) error { return p.teardown(held, h, "bab", "titogo", id, appID) }) }) } if err == nil || errors.Is(err, hostops.ErrPending) { t.Errorf("invalid inventory was not refused: %v", err) } for _, e := range cx.rec.since(mark) { if !e.Control && (e.Unscoped || len(e.Scopes) > 0) { t.Errorf("effect before validation: %v", e) } } if _, _, e := cx.rel.Get(ctx, key); e != nil { t.Errorf("invalid estate record removed: %v", e) } }) } } } func TestClaimStoreReadersValidateEnabledMode(t *testing.T) { cx := newClaimCase(t) for _, tc := range []struct{ name, body string }{ {"schema", `{"schema":2,"app":"titogo"}`}, {"identity", `{"schema":1,"app":"other"}`}, } { t.Run(tc.name, func(t *testing.T) { key := objstore.Key("bab", "titogo") _, tag, _ := cx.rel.Get(t.Context(), key) if _, err := cx.rel.Put(t.Context(), key, []byte(tc.body), tag); err != nil { t.Fatal(err) } if _, _, err := objstore.GetIn(t.Context(), cx.srv.cfg.Releases, "bab", "titogo", ""); err == nil { t.Fatal("enabled reader accepted invalid record") } if _, _, err := objstore.GetIn(t.Context(), cx.rel, "bab", "titogo", ""); err != nil { t.Fatalf("legacy reader changed: %v", err) } mark := cx.rec.mark() if err := objstore.TeardownIn(t.Context(), cx.srv.cfg.Releases, cx.srv.cfg.StoreProvisioner, nil, "bab", "titogo", ""); err == nil { t.Fatal("teardown accepted invalid record") } for _, e := range cx.rec.since(mark) { if !e.Control { t.Errorf("teardown effect: %v", e) } } }) } } func TestClaimBackendLegacyDomainClaims(t *testing.T) { for _, schema := range []int{0, 1, -1, 2} { for _, bad := range []string{"", "domain", "account", "app"} { t.Run(string(rune('a'+schema+1))+"/"+bad, func(t *testing.T) { raw := release.NewDirStore(t.TempDir()) ctx := mailGuardPermits(t, raw, hostops.AppScope{Account: "bab", App: "titogo"}) w := claimTestWriter(t, raw) lease, err := w.AcquireMailGate(ctx, hostops.MailGateOwner{Console: "c", Enrollment: "e", Controller: "k"}, hostops.MailGateScope{}) if err != nil { t.Fatal(err) } ctx = hostops.WithMailGateLease(ctx, lease) const domain = "legacy.example.com" c := mail.DomainClaim{Schema: schema, Domain: domain, Account: "bab", App: "titogo", CreatedAt: time.Now()} switch bad { case "domain": c.Domain = "other.example.com" case "account": c.Account = "" case "app": c.App = "../other" } putMailJSON(t, raw, mail.DomainClaimKey(domain), c) mem := mail.NewMemProvisioner("1") dns := dnsrec.NewMemWriter() g := backendGuard{data: raw, control: w} p := newClaimMailProvisioner(mem, g) d := newClaimDNSWriter(dns, g) _, sesErr := p.EnsureIdentity(ctx, "eu-west-1", domain, "", false) dnsErr := d.EnsureRecord(ctx, dnsrec.Record{Name: "_dmarc." + domain, Type: "TXT", Values: []string{"v=DMARC1; p=none"}}) valid := (schema == 0 || schema == 1) && bad == "" if valid { if sesErr != nil || dnsErr != nil { t.Fatalf("legacy supported claim: SES=%v DNS=%v", sesErr, dnsErr) } } else { if sesErr == nil || dnsErr == nil { t.Fatalf("invalid claim accepted: SES=%v DNS=%v", sesErr, dnsErr) } if len(mem.Identities) != 0 || len(dns.Records) != 0 { t.Fatal("invalid claim reached backend") } } }) } } } // The same worker fixture checks actual HTTP delivery, not just its SQL receipt. func TestClaimMailPushCountsOutboundEffects(t *testing.T) { cx := newClaimCase(t) door := claimFixtures["worker StartMailEventPush"].door door.setup(t, cx) ref := door.apps[0] restore := cx.foreign(t, ref) w := cx.measure(t, door) if problems := claimForeignProblems(ref, door.apps, w); len(problems) > 0 { t.Errorf("foreign delivery: %v", problems) } restore() w = cx.measure(t, door) sends := 0 for _, e := range w.events { if e.Surface == "http" && e.Method == http.MethodPost { sends++ } } if sends != 2 { t.Fatalf("open worker sent %d requests, want 2", sends) } } type claimDeliveryTransport struct { call func(*http.Request) (*http.Response, error) } func (tr claimDeliveryTransport) RoundTrip(r *http.Request) (*http.Response, error) { return tr.call(r) } func TestClaimMailPushHoldsAdmissionThroughDelivery(t *testing.T) { for _, ambiguous := range []bool{false, true} { t.Run(fmt.Sprint(ambiguous), func(t *testing.T) { cx := newClaimCase(t) door := claimFixtures["worker StartMailEventPush"].door door.setup(t, cx) appID := cx.appIDs[door.apps[0]] calls := 0 old := mailPushClient.Transport mailPushClient.Transport = claimDeliveryTransport{call: func(r *http.Request) (*http.Response, error) { calls++ if err := hostops.RequireAppMutation(r.Context(), cx.rel, "bab", "titogo", appID); err != nil { t.Errorf("outbound call lacks permit: %v", err) } a, err := hostops.ReadCanonicalAdmission(r.Context(), cx.rel, "bab", "titogo") if err != nil || a.Value.Op == nil || a.Value.Op.Intent["action"] != "mail-event-delivery" { t.Errorf("delivery admission: %+v %v", a.Value, err) } _, err = cx.srv.cfg.ClaimAuthority.Control.BeginExclusive(r.Context(), "bab", "titogo", appID, hostops.AppOperation{ID: "competing", Kind: hostops.OpPurging, AppID: appID, Phase: "reserved"}, claimTestOwner) if err == nil { t.Error("purge entered while HTTP delivery was in flight") } if ambiguous { return nil, errors.New("lost response") } return old.RoundTrip(r) }} t.Cleanup(func() { mailPushClient.Transport = old }) if err := cx.srv.mailPushPass(t.Context(), time.Now); err != nil { t.Fatal(err) } a, err := hostops.ReadCanonicalAdmission(t.Context(), cx.rel, "bab", "titogo") if err != nil { t.Fatal(err) } if ambiguous { if calls != 1 || a.Value.Op == nil || a.Value.State != "closed" { t.Fatalf("ambiguous call released: calls=%d admission=%+v", calls, a.Value) } } else { if calls != 2 || a.Value.Op != nil || a.Value.State != "open" { t.Fatalf("joined delivery unsettled: calls=%d admission=%+v", calls, a.Value) } rows, err := cx.st.dueMailEventDeliveriesFor(t.Context(), "bab", "titogo", time.Now(), mailPushPerApp) if err != nil || len(rows) != 0 { t.Fatalf("delivery did not settle SQL: %v %v", rows, err) } } }) } } type claimStoreInventoryFault struct { release.Store list []string listErr, errorRead error } func (s claimStoreInventoryFault) List(context.Context, string) ([]string, error) { return s.list, s.listErr } func (s claimStoreInventoryFault) Get(ctx context.Context, key string) ([]byte, string, error) { if s.errorRead != nil { return nil, "", s.errorRead } return s.Store.Get(ctx, key) } func TestClaimStoreInventoryCompleteListing(t *testing.T) { raw := release.NewDirStore(t.TempDir()) key := objstore.KeyIn("bab", "titogo", "prod") putMailJSON(t, raw, key, objstore.Record{Schema: 1, App: "titogo", Environment: "prod"}) sentinel := errors.New("partial listing") for _, tc := range []struct { name string keys []string listErr, readErr error want bool }{ {"valid duplicate", []string{key, key}, nil, nil, true}, {"partial", []string{key}, sentinel, nil, false}, {"missing listed", []string{key}, nil, os.ErrNotExist, false}, {"failed read", []string{key}, nil, sentinel, false}, {"foreign prefix", []string{"apps/zed/other/stores/prod.json"}, nil, nil, false}, {"stray", []string{key, "apps/bab/titogo/stores/notes.txt"}, nil, nil, false}, } { t.Run(tc.name, func(t *testing.T) { s := claimStoreInventoryFault{Store: raw, list: tc.keys, listErr: tc.listErr, errorRead: tc.readErr} got, err := objstore.StrictStoreEnvironments(t.Context(), s, "bab", "titogo") if tc.want { if err != nil || len(got) != 1 || got[0] != "prod" { t.Fatalf("%v %v", got, err) } } else if err == nil || got != nil { t.Fatalf("incomplete inventory accepted: %v %v", got, err) } }) } }