// SPDX-License-Identifier: AGPL-3.0-or-later package admin import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "comail/internal/enroll" "comail/internal/relay" "comail/internal/relaystore" ) // errFakeSERVFAIL is the sentinel used by the transient-DNS test path to // simulate a resolver failure. Kept as a package-level var so the test body // can assign it to fakeLookuper.err without allocating a new error each time. var errFakeSERVFAIL = errors.New("SERVFAIL: upstream DNS failure") // fakeLookuper is the test double for net.Resolver's LookupTXT. The // admin-API tests never touch real DNS — they stash records in a map // keyed by the well-known _atmos-enroll. name. type fakeLookuper struct { records map[string][]string err error // if non-nil, every lookup returns this error } func (f *fakeLookuper) LookupTXT(_ context.Context, name string) ([]string, error) { if f.err != nil { return nil, f.err } if rs, ok := f.records[name]; ok { return rs, nil } return nil, enroll.ErrNoSuchDomain } // testEnrollAPI builds an admin API wired to a fake DNS lookuper, so tests // can drive the self-service enrollment path end-to-end without real DNS. // Returns the API, the underlying store, and the lookuper the test can // mutate to install TXT records on demand. func testEnrollAPI(t *testing.T) (*API, *relaystore.Store, *fakeLookuper) { t.Helper() store, err := relaystore.New(":memory:") if err != nil { t.Fatalf("New store: %v", err) } t.Cleanup(func() { store.Close() }) lk := &fakeLookuper{records: map[string][]string{}} dv := enroll.NewDomainVerifier(lk, 2*time.Second) api := NewComplete(store, "test-admin-token", "atmos.email", nil, nil, dv) return api, store, lk } // startEnrollment runs the happy-path first half of the wizard: POST // /admin/enroll-start and return the issued token + DNS publish pair. // Helpers keep individual tests focused on what they're actually pinning. func startEnrollment(t *testing.T, api *API, did, domain string) EnrollStartResponse { t.Helper() body, _ := json.Marshal(EnrollStartRequest{DID: did, Domain: domain, TermsAccepted: true}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("enroll-start: status=%d body=%s", w.Code, w.Body.String()) } var resp EnrollStartResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("decode enroll-start: %v", err) } return resp } func testAdminAPI(t *testing.T) (*API, *relaystore.Store) { t.Helper() store, err := relaystore.New(":memory:") if err != nil { t.Fatalf("New store: %v", err) } t.Cleanup(func() { store.Close() }) api := New(store, "test-admin-token", "atmos.email") return api, store } func TestMemberStatus(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:statustest22222222222222" // Enroll a member first now := time.Now().UTC() store.InsertMember(context.Background(), &relaystore.Member{ DID: did, Status: "active", HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }) store.InsertMemberDomain(context.Background(), &relaystore.MemberDomain{ Domain: "example.com", DID: did, APIKeyHash: []byte("hash"), DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }) req := httptest.NewRequest("GET", "/admin/member/"+did, nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) } var resp MemberStatusResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } if resp.DID != did { t.Errorf("DID = %q", resp.DID) } if resp.Status != "active" { t.Errorf("Status = %q, want active", resp.Status) } } func TestMemberStatusNotFound(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("GET", "/admin/member/did:plc:nonexistent2222222222222", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", w.Code) } } func TestSuspendAndReactivate(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:suspendtest2222222222222" now := time.Now().UTC() store.InsertMember(context.Background(), &relaystore.Member{ DID: did, Status: "active", HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }) store.InsertMemberDomain(context.Background(), &relaystore.MemberDomain{ Domain: "example.com", DID: did, APIKeyHash: []byte("hash"), DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }) // Suspend req := httptest.NewRequest("POST", "/admin/member/"+did+"/suspend?reason=testing", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("suspend: status = %d; body: %s", w.Code, w.Body.String()) } member, _ := store.GetMember(req.Context(), did) if member.Status != "suspended" { t.Errorf("status after suspend = %q, want suspended", member.Status) } // Reactivate req = httptest.NewRequest("POST", "/admin/member/"+did+"/reactivate", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("reactivate: status = %d; body: %s", w.Code, w.Body.String()) } member, _ = store.GetMember(req.Context(), did) if member.Status != "active" { t.Errorf("status after reactivate = %q, want active", member.Status) } } func TestDeleteMember(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:deletemetest222222222222" now := time.Now().UTC() store.InsertMember(context.Background(), &relaystore.Member{ DID: did, Status: "active", HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }) store.InsertMemberDomain(context.Background(), &relaystore.MemberDomain{ Domain: "delete-test.com", DID: did, APIKeyHash: []byte("hash"), DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }) // Delete req := httptest.NewRequest("DELETE", "/admin/member/"+did, nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("delete: status = %d; body: %s", w.Code, w.Body.String()) } // Verify gone member, _ := store.GetMember(context.Background(), did) if member != nil { t.Error("member should be deleted") } domains, _ := store.ListMemberDomains(context.Background(), did) if len(domains) != 0 { t.Errorf("domains should be deleted, got %d", len(domains)) } } func TestDeleteMemberNotFound(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("DELETE", "/admin/member/did:plc:doesnotexistabcdefghijkl", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", w.Code) } } func TestDeleteMemberUnauthorized(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("DELETE", "/admin/member/did:plc:doesnotexistabcdefghijkl", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } func TestStatsEndpoint(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("GET", "/admin/stats", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } var resp map[string]any if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } if _, ok := resp["members"]; !ok { t.Error("response missing 'members' field") } } func TestListMembers(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() // Empty list req := httptest.NewRequest("GET", "/admin/members", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("empty list: status = %d, want 200; body: %s", w.Code, w.Body.String()) } var emptyResp struct { Members []MemberStatusResponse `json:"members"` } if err := json.NewDecoder(w.Body).Decode(&emptyResp); err != nil { t.Fatal(err) } if len(emptyResp.Members) != 0 { t.Errorf("expected 0 members, got %d", len(emptyResp.Members)) } // Add two members now := time.Now().UTC() for i, did := range []string{"did:plc:listtest111111111111111", "did:plc:listtest222222222222222"} { store.InsertMember(ctx, &relaystore.Member{ DID: did, Status: relaystore.StatusActive, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: fmt.Sprintf("list%d.example.com", i), DID: did, APIKeyHash: []byte("hash"), DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }) } // List with members req = httptest.NewRequest("GET", "/admin/members", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("list: status = %d, want 200; body: %s", w.Code, w.Body.String()) } var resp struct { Members []MemberStatusResponse `json:"members"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } if len(resp.Members) != 2 { t.Fatalf("expected 2 members, got %d", len(resp.Members)) } if resp.Members[0].DID != "did:plc:listtest111111111111111" { t.Errorf("first member DID = %q", resp.Members[0].DID) } if len(resp.Members[0].Domains) != 1 || resp.Members[0].Domains[0].Domain != "list0.example.com" { t.Errorf("first member domains = %v", resp.Members[0].Domains) } if resp.Members[1].DID != "did:plc:listtest222222222222222" { t.Errorf("second member DID = %q", resp.Members[1].DID) } } func TestListMembersUnauthorized(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("GET", "/admin/members", nil) req.Header.Set("Authorization", "Bearer wrong-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } func TestListMembersMethodNotAllowed(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("POST", "/admin/members", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("status = %d, want 405", w.Code) } } func TestLabelBypassAddRemove(t *testing.T) { store, err := relaystore.New(":memory:") if err != nil { t.Fatal(err) } defer store.Close() lc := relay.NewLabelChecker("http://127.0.0.1:1", nil) api := NewWithLabelChecker(store, "test-admin-token", "atmos.email", lc) // Add bypass req := httptest.NewRequest("POST", "/admin/member/did:web:example.com/bypass-labels", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("add bypass: status = %d; body: %s", w.Code, w.Body.String()) } // Verify it's in the list dids := lc.BypassDIDs() if len(dids) != 1 || dids[0] != "did:web:example.com" { t.Errorf("BypassDIDs = %v, want [did:web:example.com]", dids) } // List bypass via GET req = httptest.NewRequest("GET", "/admin/bypass-labels", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("list bypass: status = %d", w.Code) } var listResp map[string][]string json.NewDecoder(w.Body).Decode(&listResp) if len(listResp["dids"]) != 1 { t.Errorf("list response = %v, want 1 DID", listResp) } // Remove bypass req = httptest.NewRequest("DELETE", "/admin/member/did:web:example.com/bypass-labels", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("remove bypass: status = %d; body: %s", w.Code, w.Body.String()) } dids = lc.BypassDIDs() if len(dids) != 0 { t.Errorf("BypassDIDs after remove = %v, want empty", dids) } } func TestLabelBypassUnauthorized(t *testing.T) { store, err := relaystore.New(":memory:") if err != nil { t.Fatal(err) } defer store.Close() lc := relay.NewLabelChecker("http://127.0.0.1:1", nil) api := NewWithLabelChecker(store, "test-admin-token", "atmos.email", lc) req := httptest.NewRequest("POST", "/admin/member/did:web:test.com/bypass-labels", nil) req.Header.Set("Authorization", "Bearer wrong") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } // Verify that the API key returned from enrollment actually works for verification // --- DNS-TXT self-service enrollment tests --- // // Replaces the earlier sign-the-nonce flow. The self-service wizard now: // 1. POSTs {did, domain} to /admin/enroll-start and receives a token // + "publish this at _atmos-enroll." instructions. // 2. User publishes the TXT record at their registrar. // 3. POSTs {token} to /admin/enroll; the server re-resolves the TXT // and, on match, creates the member. // // Tests below drive that flow end-to-end using a fakeLookuper instead of // real DNS. func TestEnrollStart_CreatesPendingEnrollment(t *testing.T) { api, store, _ := testEnrollAPI(t) did := "did:plc:aaaaaaaabbbbbbbbcccccccc" domain := "mail.example.com" resp := startEnrollment(t, api, did, domain) if resp.Token == "" { t.Fatal("empty token returned from /admin/enroll-start") } if resp.DNSName != "_atmos-enroll."+domain { t.Errorf("DNSName = %q, want _atmos-enroll.%s", resp.DNSName, domain) } if resp.DNSValue != "atmos-verify="+resp.Token { t.Errorf("DNSValue = %q, want atmos-verify=%s", resp.DNSValue, resp.Token) } if resp.ExpiresAt == "" { t.Error("ExpiresAt empty — wizard needs this to tell the user how long they have") } // Pending row must be persisted so the verify step can later look it up. pending, err := store.GetPendingEnrollment(context.Background(), resp.Token) if err != nil || pending == nil { t.Fatalf("pending enrollment not persisted: err=%v pending=%v", err, pending) } if pending.DID != did || pending.Domain != domain { t.Errorf("pending record did=%q domain=%q, want %q/%q", pending.DID, pending.Domain, did, domain) } } func TestEnrollStart_RejectsInvalidDID(t *testing.T) { api, _, _ := testEnrollAPI(t) body, _ := json.Marshal(EnrollStartRequest{DID: "not-a-did", Domain: "example.com"}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestEnrollStart_RejectsInvalidDomain(t *testing.T) { api, _, _ := testEnrollAPI(t) body, _ := json.Marshal(EnrollStartRequest{DID: "did:plc:aaaaaaaabbbbbbbbcccccccc", Domain: "not a domain"}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestEnrollStart_RejectsAlreadyRegisteredDomain(t *testing.T) { // If the domain is already owned by a different member, fail fast so // the user doesn't waste time publishing DNS records for a domain // they can't claim. The message must NOT reveal who owns the domain. api, store, _ := testEnrollAPI(t) ctx := context.Background() now := time.Now().UTC() existingDID := "did:plc:aaaaaaaabbbbbbbbcccccccc" store.InsertMember(ctx, &relaystore.Member{DID: existingDID, Status: relaystore.StatusActive, CreatedAt: now, UpdatedAt: now}) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "taken.example.com", DID: existingDID, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) body, _ := json.Marshal(EnrollStartRequest{DID: "did:plc:ddddddddeeeeeeeeffffffff", Domain: "taken.example.com", TermsAccepted: true}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("status = %d, want 409", w.Code) } respBody := w.Body.String() if strings.Contains(respBody, existingDID) { t.Error("response must not reveal the DID that owns the domain") } if !strings.Contains(respBody, "another account") { t.Errorf("different-DID conflict should mention 'another account', got: %s", respBody) } } func TestEnrollStart_OwnDomainAlreadyRegistered(t *testing.T) { // When a DID tries to enroll a domain it already owns, the message // should direct them to /account instead of a generic "already // registered" error. This is a common UX path: user forgets they // already enrolled and goes through the wizard again. api, store, _ := testEnrollAPI(t) ctx := context.Background() now := time.Now().UTC() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, CreatedAt: now, UpdatedAt: now}) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "mine.example.com", DID: did, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) body, _ := json.Marshal(EnrollStartRequest{DID: did, Domain: "mine.example.com", TermsAccepted: true}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("status = %d, want 409", w.Code) } respBody := w.Body.String() if !strings.Contains(respBody, "already enrolled") { t.Errorf("same-DID conflict should say 'already enrolled', got: %s", respBody) } if !strings.Contains(respBody, "/account") { t.Errorf("same-DID conflict should mention /account, got: %s", respBody) } } func TestEnrollStart_RejectsWithoutTermsAcceptance(t *testing.T) { api, _, _ := testEnrollAPI(t) body, _ := json.Marshal(EnrollStartRequest{DID: "did:plc:aaaaaaaabbbbbbbbcccccccc", Domain: "example.com", TermsAccepted: false}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestEnrollStart_NotImplementedWhenVerifierNil(t *testing.T) { // NewComplete with nil verifier simulates a deployment where operators // have disabled self-service. The endpoint must return 501 so the UI // can render a clear "contact the operator" message. store, err := relaystore.New(":memory:") if err != nil { t.Fatalf("New store: %v", err) } t.Cleanup(func() { store.Close() }) api := NewComplete(store, "tok", "atmos.email", nil, nil, nil) body, _ := json.Marshal(EnrollStartRequest{DID: "did:plc:aaaaaaaabbbbbbbbcccccccc", Domain: "example.com"}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNotImplemented { t.Errorf("status = %d, want 501", w.Code) } } func TestEnrollWithToken_SuccessFlow(t *testing.T) { // Happy path: start → publish TXT → verify → member created. api, store, lk := testEnrollAPI(t) did := "did:plc:aaaaaaaabbbbbbbbcccccccc" domain := "mail.example.com" start := startEnrollment(t, api, did, domain) // Simulate the user publishing the TXT record at their registrar. lk.records["_atmos-enroll."+domain] = []string{start.DNSValue} body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("enroll: status=%d body=%s", w.Code, w.Body.String()) } var er EnrollResponse if err := json.NewDecoder(w.Body).Decode(&er); err != nil { t.Fatalf("decode: %v", err) } if er.APIKey == "" { t.Error("API key missing from response") } if er.DKIM.RSARecord == "" || er.DKIM.EdRecord == "" { t.Error("DKIM records missing from response") } member, err := store.GetMember(context.Background(), did) if err != nil || member == nil { t.Fatalf("member not persisted: err=%v", err) } if member.Status != relaystore.StatusPending { t.Errorf("status = %q, want pending (self-service must require operator approval)", member.Status) } // Phase 2 flips this to true via atproto OAuth. Phase 1 must leave it false. if member.DIDVerified { t.Error("self-service DID must NOT be marked verified — that's the Phase 2 OAuth gate") } if member.TermsVersion != relaystore.CurrentTermsVersion { t.Errorf("TermsVersion = %q, want %q", member.TermsVersion, relaystore.CurrentTermsVersion) } if member.TermsAcceptedAt.IsZero() { t.Error("TermsAcceptedAt not recorded") } dom, _ := store.GetMemberDomain(context.Background(), domain) if dom == nil || dom.DID != did { t.Fatalf("domain not stored with correct DID: %+v", dom) } } func TestEnrollWithToken_ConsumesPendingRow(t *testing.T) { // Critical: the pending row must be deleted on success so the same // token can't be replayed to create a second member on the same domain. api, store, lk := testEnrollAPI(t) did := "did:plc:aaaaaaaabbbbbbbbcccccccc" domain := "consume.example.com" start := startEnrollment(t, api, did, domain) lk.records["_atmos-enroll."+domain] = []string{start.DNSValue} body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("first enroll: status=%d body=%s", w.Code, w.Body.String()) } pending, _ := store.GetPendingEnrollment(context.Background(), start.Token) if pending != nil { t.Fatal("pending row not deleted after successful enrollment — token replay possible") } } func TestEnrollWithToken_MissingBody_400(t *testing.T) { api, _, _ := testEnrollAPI(t) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestEnrollWithToken_EmptyToken_400(t *testing.T) { api, _, _ := testEnrollAPI(t) body, _ := json.Marshal(EnrollRequest{Token: ""}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func TestEnrollWithToken_UnknownToken_404(t *testing.T) { api, _, _ := testEnrollAPI(t) body, _ := json.Marshal(EnrollRequest{Token: "never-created-this-token"}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", w.Code) } } func TestEnrollWithToken_ExpiredToken_410(t *testing.T) { // Tokens older than the TTL must be rejected with 410 Gone so the UI // can render "start over" instead of "publish the record again". api, store, _ := testEnrollAPI(t) did := "did:plc:aaaaaaaabbbbbbbbcccccccc" domain := "expired.example.com" // Directly insert an already-expired pending row to bypass the TTL. token, _ := enroll.NewToken() past := time.Now().UTC().Add(-1 * time.Hour) err := store.CreatePendingEnrollment(context.Background(), &relaystore.PendingEnrollment{ Token: token, DID: did, Domain: domain, CreatedAt: past.Add(-24 * time.Hour), ExpiresAt: past, }) if err != nil { t.Fatal(err) } body, _ := json.Marshal(EnrollRequest{Token: token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusGone { t.Errorf("status = %d, want 410", w.Code) } // The expired row should be cleaned up even on failure so a retry // with the same token produces the same outcome, not spurious success. pending, _ := store.GetPendingEnrollment(context.Background(), token) if pending != nil { t.Error("expired pending row not purged on verify attempt") } } func TestEnrollWithToken_NoTXTRecord_403(t *testing.T) { // User didn't publish the TXT yet (or it hasn't propagated). The UI // needs to distinguish this from "wrong token" so it can tell the // user to check their registrar settings. api, _, _ := testEnrollAPI(t) start := startEnrollment(t, api, "did:plc:aaaaaaaabbbbbbbbcccccccc", "no-record.example.com") body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("status = %d, want 403", w.Code) } if !strings.Contains(w.Body.String(), "_atmos-enroll") { t.Errorf("error body should mention the record name to help the user; got: %s", w.Body.String()) } } func TestEnrollWithToken_WrongValueInTXT_403(t *testing.T) { // TXT record exists but carries a different token — could be an // attacker trying to hijack the domain, or a user who pasted the // wrong value. Either way, must fail distinctly from "no record". api, _, lk := testEnrollAPI(t) start := startEnrollment(t, api, "did:plc:aaaaaaaabbbbbbbbcccccccc", "wrong-token.example.com") lk.records["_atmos-enroll.wrong-token.example.com"] = []string{"atmos-verify=some-other-token"} body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("status = %d, want 403 (token mismatch); body: %s", w.Code, w.Body.String()) } } func TestEnrollWithToken_TransientDNSError_503(t *testing.T) { // SERVFAIL / network issues are distinct from "record missing" — // 503 tells the UI "retry in a moment" rather than "the record isn't // there". Avoids misleading users during DNS outages. api, _, lk := testEnrollAPI(t) start := startEnrollment(t, api, "did:plc:aaaaaaaabbbbbbbbcccccccc", "flaky.example.com") lk.err = errFakeSERVFAIL body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusServiceUnavailable { t.Errorf("status = %d, want 503 (transient DNS); body: %s", w.Code, w.Body.String()) } } // --- Manual approval gate (internal ref) --- func TestSelfServiceEnrollment_CreatesPendingMember(t *testing.T) { // Critical invariant: a self-service enrollment (no admin bearer) MUST // land the member in status=pending. This is the shared-IP reputation // protection — until an operator approves, the member cannot SMTP AUTH. api, store, lk := testEnrollAPI(t) did := "did:plc:aaaaaaaabbbbbbbbcccccccc" domain := "pending.example.com" start := startEnrollment(t, api, did, domain) lk.records["_atmos-enroll."+domain] = []string{start.DNSValue} body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("enroll: status=%d body=%s", w.Code, w.Body.String()) } member, err := store.GetMember(context.Background(), did) if err != nil || member == nil { t.Fatalf("member not found: %v", err) } if member.Status != relaystore.StatusPending { t.Errorf("status = %q, want pending (self-service must start pending)", member.Status) } } func TestMemberApprove_FlipsPendingToActive(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" // Manually create a pending member (simulating the self-service path). now := time.Now().UTC() store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusPending, CreatedAt: now, UpdatedAt: now}) req := httptest.NewRequest("POST", "/admin/member/"+did+"/approve", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("approve status = %d; body: %s", w.Code, w.Body.String()) } member, _ := store.GetMember(ctx, did) if member.Status != relaystore.StatusActive { t.Errorf("after approve, status = %q, want active", member.Status) } } func TestMemberApprove_RequiresAdminToken(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" now := time.Now().UTC() store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusPending, CreatedAt: now, UpdatedAt: now}) // No auth — must be rejected. req := httptest.NewRequest("POST", "/admin/member/"+did+"/approve", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("no auth: status = %d, want 401", w.Code) } // Confirm no mutation happened. m, _ := store.GetMember(ctx, did) if m.Status != relaystore.StatusPending { t.Error("approve without auth mutated status") } } func TestMemberApprove_IdempotentOnAlreadyActive(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" now := time.Now().UTC() store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, CreatedAt: now, UpdatedAt: now}) req := httptest.NewRequest("POST", "/admin/member/"+did+"/approve", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("idempotent approve: status = %d, want 200", w.Code) } } func TestMemberApprove_RejectsSuspended(t *testing.T) { // Suspended members must be reactivated, not approved — different // workflow, different semantic. api, store := testAdminAPI(t) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" now := time.Now().UTC() store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusSuspended, CreatedAt: now, UpdatedAt: now}) req := httptest.NewRequest("POST", "/admin/member/"+did+"/approve", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("approve suspended: status = %d, want 409", w.Code) } } func TestMemberReject_FlipsPendingToSuspended(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" now := time.Now().UTC() store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusPending, CreatedAt: now, UpdatedAt: now}) req := httptest.NewRequest("POST", "/admin/member/"+did+"/reject?reason=spammer", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("reject: status = %d, body: %s", w.Code, w.Body.String()) } member, _ := store.GetMember(ctx, did) if member.Status != relaystore.StatusSuspended { t.Errorf("after reject, status = %q, want suspended", member.Status) } if member.SuspendReason != "spammer" { t.Errorf("reason = %q, want spammer", member.SuspendReason) } } func TestMemberReject_OnlyPending(t *testing.T) { // Active members should be suspended, not rejected. api, store := testAdminAPI(t) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" now := time.Now().UTC() store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, CreatedAt: now, UpdatedAt: now}) req := httptest.NewRequest("POST", "/admin/member/"+did+"/reject", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("reject active: status = %d, want 409", w.Code) } } // --- Member self-serve status tests --- func enrollWithAPIKey(t *testing.T, store *relaystore.Store, did, domain string) string { t.Helper() apiKey, err := relay.GenerateAPIKey() if err != nil { t.Fatalf("generate api key: %v", err) } hash, err := relay.HashAPIKey(apiKey) if err != nil { t.Fatalf("hash api key: %v", err) } now := time.Now().UTC() store.InsertMember(context.Background(), &relaystore.Member{ DID: did, Status: "active", HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }) store.InsertMemberDomain(context.Background(), &relaystore.MemberDomain{ Domain: domain, DID: did, APIKeyHash: hash, DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }) return apiKey } func selfStatusReq(did, apiKey string) *http.Request { req := httptest.NewRequest("GET", "/member/status?did="+did, nil) if apiKey != "" { req.Header.Set("Authorization", "Bearer "+apiKey) } return req } func TestSelfStatusSuccess(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:aaaaaaaabbbbbbbbcccccccc" apiKey := enrollWithAPIKey(t, store, did, "example.com") w := httptest.NewRecorder() api.ServeHTTP(w, selfStatusReq(did, apiKey)) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) } var resp MemberSelfStatusResponse if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } if resp.DID != did { t.Errorf("DID = %q, want %q", resp.DID, did) } if resp.Status != "active" { t.Errorf("Status = %q, want active", resp.Status) } if len(resp.Domains) != 1 || resp.Domains[0] != "example.com" { t.Errorf("Domains = %v, want [example.com]", resp.Domains) } } func TestSelfStatusSuspended(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:ddddddddeeeeeeeeffffffff" apiKey := enrollWithAPIKey(t, store, did, "suspended.com") store.UpdateMemberStatus(context.Background(), did, relaystore.StatusSuspended, "high bounce rate") w := httptest.NewRecorder() api.ServeHTTP(w, selfStatusReq(did, apiKey)) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) } var resp MemberSelfStatusResponse json.NewDecoder(w.Body).Decode(&resp) if resp.Status != "suspended" { t.Errorf("Status = %q, want suspended", resp.Status) } if resp.SuspendReason != "high bounce rate" { t.Errorf("SuspendReason = %q, want 'high bounce rate'", resp.SuspendReason) } } func TestSelfStatusBadAPIKey(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:gggggggghhhhhhhhiiiiiiii" enrollWithAPIKey(t, store, did, "example.com") w := httptest.NewRecorder() api.ServeHTTP(w, selfStatusReq(did, "wrong-key")) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } func TestSelfStatusMissingAuth(t *testing.T) { api, _ := testAdminAPI(t) // No DID, no auth header req := httptest.NewRequest("GET", "/member/status", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("no params: status = %d, want 400", w.Code) } // DID but no auth header req = httptest.NewRequest("GET", "/member/status?did=did:plc:aaaaaaaabbbbbbbbcccccccc", nil) w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("did only: status = %d, want 401", w.Code) } } func TestSelfStatusNotFoundReturns401(t *testing.T) { // Unknown DID returns 401 (not 404) to prevent member existence oracle. api, _ := testAdminAPI(t) w := httptest.NewRecorder() api.ServeHTTP(w, selfStatusReq("did:plc:jjjjjjjjkkkkkkkkllllllll", "somekey")) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401 (not 404 — prevents enumeration)", w.Code) } } func TestSelfStatusInvalidDID(t *testing.T) { api, _ := testAdminAPI(t) w := httptest.NewRecorder() api.ServeHTTP(w, selfStatusReq("notadid", "somekey")) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } } func deliverabilityReq(did, apiKey string) *http.Request { req := httptest.NewRequest("GET", "/member/deliverability?did="+did, nil) if apiKey != "" { req.Header.Set("Authorization", "Bearer "+apiKey) } return req } func TestSelfDeliverabilitySuccess(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:deliveraaaaaaaaaaaaaaaaa" apiKey := enrollWithAPIKey(t, store, did, "example.com") // Seed some messages ctx := context.Background() now := time.Now().UTC() for i := 0; i < 5; i++ { _, _ = store.InsertMessage(ctx, &relaystore.Message{ MemberDID: did, FromAddr: "x@example.com", ToAddr: "y@z.com", MessageID: fmt.Sprintf("", i), Status: relaystore.MsgSent, CreatedAt: now, }) } _, _ = store.InsertMessage(ctx, &relaystore.Message{ MemberDID: did, FromAddr: "x@example.com", ToAddr: "y@z.com", MessageID: "", Status: relaystore.MsgBounced, CreatedAt: now, }) _, _ = store.InsertFeedbackEvent(ctx, &relaystore.FeedbackEvent{ MemberDID: did, EventType: "complaint", CreatedAt: now, }) w := httptest.NewRecorder() api.ServeHTTP(w, deliverabilityReq(did, apiKey)) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) } var resp struct { Sent14d int64 `json:"sent_14d"` Bounced14d int64 `json:"bounced_14d"` Complaints14d int64 `json:"complaints_14d"` BounceRate float64 `json:"bounce_rate"` DailySends []int64 `json:"daily_sends"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } if resp.Sent14d != 6 { t.Errorf("Sent14d = %d, want 6 (5 sent + 1 bounced)", resp.Sent14d) } if resp.Bounced14d != 1 { t.Errorf("Bounced14d = %d, want 1", resp.Bounced14d) } if resp.Complaints14d != 1 { t.Errorf("Complaints14d = %d, want 1", resp.Complaints14d) } if resp.BounceRate != 1.0/6.0 { t.Errorf("BounceRate = %f, want %f", resp.BounceRate, 1.0/6.0) } if len(resp.DailySends) != 14 { t.Errorf("DailySends len = %d, want 14", len(resp.DailySends)) } } func TestSelfDeliverabilityBadAPIKey(t *testing.T) { api, store := testAdminAPI(t) did := "did:plc:deliverbbbbbbbbbbbbbbbbb" enrollWithAPIKey(t, store, did, "example.com") w := httptest.NewRecorder() api.ServeHTTP(w, deliverabilityReq(did, "wrong-key")) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } func TestSelfDeliverabilityMissingAuth(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("GET", "/member/deliverability?did=did:plc:aaaaaaaaaaaaaaaaaaaaaaaa", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("status = %d, want 401", w.Code) } } // --- forward_to admin endpoints --- func TestAdminDomainForwardToSet_RequiresAdminAuth(t *testing.T) { api, store := testAdminAPI(t) enrollWithAPIKey(t, store, "did:plc:aaaaaaaabbbbbbbbcccccccc", "comail.org") body := strings.NewReader(`{"forwardTo":"real@gmail.com"}`) req := httptest.NewRequest("PUT", "/admin/domain/comail.org/forward-to", body) // No Authorization header. w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("no auth: status = %d, want 401", w.Code) } // With bad token. req = httptest.NewRequest("PUT", "/admin/domain/comail.org/forward-to", strings.NewReader(`{"forwardTo":"real@gmail.com"}`)) req.Header.Set("Authorization", "Bearer wrong-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("wrong token: status = %d, want 401", w.Code) } } func TestAdminDomainForwardToSetAndGet(t *testing.T) { api, store := testAdminAPI(t) enrollWithAPIKey(t, store, "did:plc:aaaaaaaabbbbbbbbcccccccc", "comail.org") // PUT sets. req := httptest.NewRequest("PUT", "/admin/domain/comail.org/forward-to", strings.NewReader(`{"forwardTo":"real@gmail.com"}`)) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Fatalf("PUT status = %d, want 204; body: %s", w.Code, w.Body.String()) } // GET confirms. req = httptest.NewRequest("GET", "/admin/domain/comail.org/forward-to", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("GET status = %d, want 200", w.Code) } var got ForwardToRequest if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if got.ForwardTo != "real@gmail.com" { t.Errorf("forward_to = %q, want real@gmail.com", got.ForwardTo) } } func TestAdminDomainForwardTo_RejectsMalformed(t *testing.T) { api, store := testAdminAPI(t) enrollWithAPIKey(t, store, "did:plc:aaaaaaaabbbbbbbbcccccccc", "comail.org") // No @ sign. req := httptest.NewRequest("PUT", "/admin/domain/comail.org/forward-to", strings.NewReader(`{"forwardTo":"notanemail"}`)) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("malformed: status = %d, want 400", w.Code) } } func TestAdminDomainForwardTo_UnknownDomain(t *testing.T) { api, _ := testAdminAPI(t) req := httptest.NewRequest("PUT", "/admin/domain/notregistered.example/forward-to", strings.NewReader(`{"forwardTo":"real@gmail.com"}`)) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("unknown domain: status = %d, want 404", w.Code) } } func TestMemberSelfForwardTo_AuthenticatesViaAPIKey(t *testing.T) { api, store := testAdminAPI(t) apiKey := enrollWithAPIKey(t, store, "did:plc:aaaaaaaabbbbbbbbcccccccc", "comail.org") // Valid API key → should accept. req := httptest.NewRequest("PUT", "/member/forward-to?domain=comail.org", strings.NewReader(`{"forwardTo":"real@gmail.com"}`)) req.Header.Set("Authorization", "Bearer "+apiKey) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Fatalf("valid api key: status = %d, want 204; body: %s", w.Code, w.Body.String()) } // Verify forward_to was actually set. d, _ := store.GetMemberDomain(context.Background(), "comail.org") if d == nil || d.ForwardTo != "real@gmail.com" { t.Errorf("forward_to not persisted; got %q", d.ForwardTo) } } func TestMemberSelfForwardTo_RejectsWrongAPIKey(t *testing.T) { api, store := testAdminAPI(t) enrollWithAPIKey(t, store, "did:plc:aaaaaaaabbbbbbbbcccccccc", "comail.org") req := httptest.NewRequest("PUT", "/member/forward-to?domain=comail.org", strings.NewReader(`{"forwardTo":"attacker@evil.com"}`)) req.Header.Set("Authorization", "Bearer not-the-real-key") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("wrong key: status = %d, want 401", w.Code) } // Confirm no mutation happened. d, _ := store.GetMemberDomain(context.Background(), "comail.org") if d.ForwardTo != "" { t.Errorf("forward_to was modified despite auth failure: %q", d.ForwardTo) } } // --- Multi-domain enrollment limit --- func TestEnrollStart_AllowsDIDWithOneDomain(t *testing.T) { // A DID on the business tier (cap 5) with one domain should be // allowed to start enrollment for a second. Default plan // (self-host) only allows 1 domain per internal ref, so this // test explicitly sets a multi-domain tier. api, store, _ := testEnrollAPI(t) // Opt into per-tier caps; with FeaturePaidTiers off, every plan // collapses to PreTierDomainCap (1) and Business would also block // the second domain. api.SetPaidTiersEnabled(true) ctx := context.Background() now := time.Now().UTC() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, Plan: relaystore.PlanBusiness, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now}) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "first.example.com", DID: did, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) body, _ := json.Marshal(EnrollStartRequest{DID: did, Domain: "second.example.com", TermsAccepted: true}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200; a DID with 1 domain should be allowed a second: %s", w.Code, w.Body.String()) } } func TestEnrollStart_RejectsDIDAtDomainLimit(t *testing.T) { // A DID at its plan's domain cap should be rejected when trying to // start enrollment for one more (internal ref). Self-host tier // is cap 1, so a member with one verified domain on self-host // trips the limit on the second. api, store, _ := testEnrollAPI(t) // Per-tier domain caps are gated by FeaturePaidTiers (chainlink // kill-switch). With the flag off the cap reverts to the legacy // MaxDomainsPerMember=3 — this test specifically exercises the // per-tier path, so opt in. api.SetPaidTiersEnabled(true) ctx := context.Background() now := time.Now().UTC() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, Plan: relaystore.PlanSelfHost, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now}) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "first.example.com", DID: did, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) body, _ := json.Marshal(EnrollStartRequest{DID: did, Domain: "second.example.com", TermsAccepted: true}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("status = %d, want 409; a DID at the domain limit should be rejected", w.Code) } if !strings.Contains(w.Body.String(), "tier") || !strings.Contains(w.Body.String(), "/pricing") { t.Errorf("body should mention the tier and the /pricing CTA, got: %s", w.Body.String()) } } func TestEnrollStart_ExistingMemberSkipsTerms(t *testing.T) { api, store, _ := testEnrollAPI(t) // Opt into per-tier caps; FeaturePaidTiers off collapses Business // to PreTierDomainCap (1) and blocks the second-domain attempt. api.SetPaidTiersEnabled(true) ctx := context.Background() now := time.Now().UTC() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" // Business tier so the second-domain attempt isn't blocked by the // per-tier cap from internal ref (self-host = 1). store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, Plan: relaystore.PlanBusiness, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now}) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "first.example.com", DID: did, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) body, _ := json.Marshal(EnrollStartRequest{DID: did, Domain: "second.example.com", TermsAccepted: false}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200; existing members should not need to re-accept terms: %s", w.Code, w.Body.String()) } } func TestEnrollStart_ExistingMemberInheritsContactEmail(t *testing.T) { api, store, _ := testEnrollAPI(t) // Opt into per-tier caps; with the kill-switch off, Business // collapses to PreTierDomainCap (1) and blocks the second-domain // attempt before the inherit-contact-email path is reached. api.SetPaidTiersEnabled(true) ctx := context.Background() now := time.Now().UTC() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" // Business tier — second-domain attempt must clear the per-tier // cap (internal ref) so the inherit-contact-email contract can // be exercised. store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, Plan: relaystore.PlanBusiness, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now}) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "first.example.com", DID: did, ContactEmail: "dave@example.com", APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) body, _ := json.Marshal(EnrollStartRequest{DID: did, Domain: "second.example.com"}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll-start", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200; existing member with empty contactEmail should inherit from first domain: %s", w.Code, w.Body.String()) } var resp struct{ Token string } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } pending, err := store.GetPendingEnrollment(ctx, resp.Token) if err != nil || pending == nil { t.Fatalf("expected pending enrollment, got err=%v", err) } if pending.ContactEmail != "dave@example.com" { t.Errorf("contactEmail = %q, want %q (inherited from first domain)", pending.ContactEmail, "dave@example.com") } } func TestEnrollWithToken_RejectsDIDAtDomainLimit(t *testing.T) { // Defense-in-depth: even if a pending enrollment sneaks past // enroll-start (e.g. a domain was added between start and // completion), the final enrollment must check the count again. api, store, lk := testEnrollAPI(t) ctx := context.Background() now := time.Now().UTC() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" // Start enrollment BEFORE the DID has any domains — should succeed. start := startEnrollment(t, api, did, "new.example.com") lk.records["_atmos-enroll.new.example.com"] = []string{start.DNSValue} // Now create the member with MaxDomainsPerMember existing domains // (simulating a race: another enrollment completed between our start // and verify). store.InsertMember(ctx, &relaystore.Member{DID: did, Status: relaystore.StatusActive, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now}) for _, d := range []string{"first.example.com", "second.example.com", "third.example.com"} { store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: d, DID: did, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }) } body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("status = %d, want 409; enrollment should be rejected at domain limit", w.Code) } } func TestEnrollWithToken_ExistingDIDAddsDomain(t *testing.T) { // An already-enrolled DID with one domain should be able to add a // second domain via the normal enrollment flow. The member record // already exists, so only a new domain row is inserted. api, store, lk := testEnrollAPI(t) // Opt into per-tier caps so the UpdateMemberPlan(Business) below // actually unlocks a higher cap. With FeaturePaidTiers off every // plan collapses to PreTierDomainCap (1). api.SetPaidTiersEnabled(true) ctx := context.Background() did := "did:plc:aaaaaaaabbbbbbbbcccccccc" // Enroll the first domain via the normal flow. start1 := startEnrollment(t, api, did, "first.example.com") lk.records["_atmos-enroll.first.example.com"] = []string{start1.DNSValue} body1, _ := json.Marshal(EnrollRequest{Token: start1.Token}) w1 := httptest.NewRecorder() api.ServeHTTP(w1, httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body1))) if w1.Code != http.StatusOK { t.Fatalf("first enroll: status=%d body=%s", w1.Code, w1.Body.String()) } // Bump the member to the business tier so the per-tier domain cap // (internal ref) doesn't block the second enrollment. New members // default to self-host (cap 1); the existing-member multi-domain // contract exercised here requires a tier that allows ≥ 2. if err := store.UpdateMemberPlan(ctx, did, relaystore.PlanBusiness); err != nil { t.Fatalf("UpdateMemberPlan: %v", err) } // Enroll the second domain for the same DID. start2 := startEnrollment(t, api, did, "second.example.com") lk.records["_atmos-enroll.second.example.com"] = []string{start2.DNSValue} body2, _ := json.Marshal(EnrollRequest{Token: start2.Token}) w2 := httptest.NewRecorder() api.ServeHTTP(w2, httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body2))) if w2.Code != http.StatusOK { t.Fatalf("second enroll: status=%d body=%s", w2.Code, w2.Body.String()) } // Verify both domains are registered under the same DID. domains, err := store.ListMemberDomains(ctx, did) if err != nil { t.Fatalf("ListMemberDomains: %v", err) } if len(domains) != 2 { t.Errorf("expected 2 domains, got %d", len(domains)) } // The member record should still be the original one (not duplicated). member, err := store.GetMember(ctx, did) if err != nil || member == nil { t.Fatalf("GetMember: %v", err) } } // TestEnroll_FirstDomain_TransitionsNoDomainToPending pins the // account-first onboarding contract: a member who already exists at // status=no_domain (CreateUnverifiedMember → VerifyMemberEmail in the // /signup flow) MUST land at pending after their first domain is // DNS-verified. Without the CAS in handleEnroll the member would stay // at no_domain — silently bypassing operator approval, which is the // whole point of the gate. func TestEnroll_FirstDomain_TransitionsNoDomainToPending(t *testing.T) { api, store, lk := testEnrollAPI(t) ctx := context.Background() // did:plc: + 24 base32-lower (a-z2-7) chars did := "did:plc:firstdomainabcdefghijklm" // Set up the account-first state: member exists at no_domain (e.g., // they completed /signup → email verify but haven't attached a // sending domain yet). now := time.Now().UTC() if err := store.InsertMember(ctx, &relaystore.Member{ DID: did, Status: relaystore.StatusNoDomain, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatalf("InsertMember: %v", err) } // Drive the enroll flow for their first domain. domain := "first.example.com" start := startEnrollment(t, api, did, domain) lk.records["_atmos-enroll."+domain] = []string{start.DNSValue} body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("enroll: status=%d body=%s", w.Code, w.Body.String()) } got, err := store.GetMember(ctx, did) if err != nil || got == nil { t.Fatalf("GetMember: err=%v member=%v", err, got) } if got.Status != relaystore.StatusPending { t.Errorf("Status = %q, want %q (first-domain enrollment must require operator approval)", got.Status, relaystore.StatusPending) } } // TestEnroll_AdditionalDomain_StaysActive pins that an active member // adding a SECOND domain is NOT pushed back through the admin gate: // they're already approved and adding additional domains is a routine // self-service op. func TestEnroll_AdditionalDomain_StaysActive(t *testing.T) { api, store, lk := testEnrollAPI(t) // Opt into per-tier caps so the Business plan below actually // unlocks ≥ 2 domains. With FeaturePaidTiers off every plan // collapses to PreTierDomainCap (1) and blocks the second domain // before the active-stays-active path is reached. api.SetPaidTiersEnabled(true) ctx := context.Background() // did:plc: + 24 base32-lower chars did := "did:plc:additionalactiveeeeeeefg" // Seed an already-approved active member with one existing domain. // Business tier (cap 5 per internal ref) so the second-domain // enrollment isn't rejected by the per-tier cap; this test is // about the active-stays-active contract, not the cap. now := time.Now().UTC() if err := store.InsertMember(ctx, &relaystore.Member{ DID: did, Status: relaystore.StatusActive, Plan: relaystore.PlanBusiness, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatalf("InsertMember: %v", err) } if err := store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "first.example.com", DID: did, APIKeyHash: []byte("h"), DKIMRSAPriv: []byte("r"), DKIMEdPriv: []byte("e"), DKIMSelector: "s", CreatedAt: now, }); err != nil { t.Fatalf("InsertMemberDomain: %v", err) } domain := "second.example.com" start := startEnrollment(t, api, did, domain) lk.records["_atmos-enroll."+domain] = []string{start.DNSValue} body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("enroll: status=%d body=%s", w.Code, w.Body.String()) } got, err := store.GetMember(ctx, did) if err != nil || got == nil { t.Fatalf("GetMember: err=%v member=%v", err, got) } if got.Status != relaystore.StatusActive { t.Errorf("Status = %q, want %q (active member adding second domain must NOT be re-pended)", got.Status, relaystore.StatusActive) } } // TestEnroll_NoDomainTransition_RaceWithApproval simulates the worst- // case race for the account-first first-domain transition: an operator // approves the member at exactly the moment we'd otherwise CAS them to // pending. The CAS-on-status anchor (WHERE status='no_domain') must // leave them at active rather than clobbering the approval back to // pending. // // We can't easily inject a goroutine between EnrollMember and the CAS, // but we can pre-flip the member to active before calling enroll: the // effect is identical (status != no_domain when the CAS runs), and a // regression that ignored prev-status would write pending unconditionally. func TestEnroll_NoDomainTransition_RaceWithApproval(t *testing.T) { api, store, lk := testEnrollAPI(t) ctx := context.Background() did := "did:plc:racewithapprovalabcdefgh" // Member exists; provisionMemberAndDomain reads PrevStatus from // here. We seed it as no_domain so handleEnroll's CAS branch is // taken — but then flip to active before the request runs to // simulate the operator-approve-before-CAS race. now := time.Now().UTC() if err := store.InsertMember(ctx, &relaystore.Member{ DID: did, Status: relaystore.StatusNoDomain, HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatalf("InsertMember: %v", err) } domain := "race.example.com" start := startEnrollment(t, api, did, domain) lk.records["_atmos-enroll."+domain] = []string{start.DNSValue} // Race injection: operator flips to active between provision (which // snapshots PrevStatus=no_domain) and the post-EnrollMember CAS. // We approximate by flipping before the request; the CAS still // observes status=active and must be a no-op. if err := store.UpdateMemberStatus(ctx, did, relaystore.StatusActive, ""); err != nil { t.Fatalf("UpdateMemberStatus: %v", err) } body, _ := json.Marshal(EnrollRequest{Token: start.Token}) req := httptest.NewRequest(http.MethodPost, "/admin/enroll", bytes.NewReader(body)) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("enroll: status=%d body=%s", w.Code, w.Body.String()) } got, _ := store.GetMember(ctx, did) if got == nil { t.Fatal("GetMember returned nil") } if got.Status != relaystore.StatusActive { t.Errorf("Status = %q, want %q (CAS must not stomp a concurrent admin approval)", got.Status, relaystore.StatusActive) } } func TestMemberSelfForwardTo_RejectsAPIKeyForDifferentDomain(t *testing.T) { // Member A has a key. Member B has a domain. A should NOT be able to // set B's forward_to using A's key. api, store := testAdminAPI(t) _ = enrollWithAPIKey(t, store, "did:plc:aaaaaaaabbbbbbbbcccccccc", "a.example") enrollWithAPIKey(t, store, "did:plc:dddddddddddddddddddddddd", "b.example") // Use A's key to target B's domain — must fail with 401. // Regenerate A's key (enrollWithAPIKey returns the key; we need A's specifically). apiKeyA := enrollWithAPIKey(t, store, "did:plc:eeeeeeeeffffffffgggggggg", "another.example") req := httptest.NewRequest("PUT", "/member/forward-to?domain=b.example", strings.NewReader(`{"forwardTo":"attacker@evil.com"}`)) req.Header.Set("Authorization", "Bearer "+apiKeyA) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("cross-domain key: status = %d, want 401 (body: %s)", w.Code, w.Body.String()) } d, _ := store.GetMemberDomain(context.Background(), "b.example") if d.ForwardTo != "" { t.Error("forward_to was cross-domain-modified — authz bug") } } // --- /admin/sender-reputation --- func newSenderReputationAPI(t *testing.T) (*API, *relaystore.Store) { t.Helper() store, err := relaystore.New(":memory:") if err != nil { t.Fatalf("New store: %v", err) } t.Cleanup(func() { store.Close() }) api := New(store, "test-admin-token", "atmos.email") return api, store } func TestSenderReputation_RequiresAdminAuth(t *testing.T) { api, _ := newSenderReputationAPI(t) req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did=did:plc:abcdefghijklmnopqrstuvwx", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("missing auth: status = %d, want 401", w.Code) } req = httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did=did:plc:abcdefghijklmnopqrstuvwx", nil) req.Header.Set("Authorization", "Bearer wrong") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("wrong auth: status = %d, want 401", w.Code) } } func TestSenderReputation_RejectsBadMethod(t *testing.T) { api, _ := newSenderReputationAPI(t) req := httptest.NewRequest(http.MethodPost, "/admin/sender-reputation?did=did:plc:abcdefghijklmnopqrstuvwx", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Fatalf("POST: status = %d, want 405", w.Code) } } func TestSenderReputation_RejectsMissingDID(t *testing.T) { api, _ := newSenderReputationAPI(t) req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("missing did: status = %d, want 400", w.Code) } } func TestSenderReputation_RejectsMalformedDID(t *testing.T) { api, _ := newSenderReputationAPI(t) req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did=not-a-did", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("malformed did: status = %d, want 400", w.Code) } } func TestSenderReputation_RejectsBadSinceFormat(t *testing.T) { api, _ := newSenderReputationAPI(t) req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did=did:plc:abcdefghijklmnopqrstuvwx&since=last-tuesday", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("bad since: status = %d, want 400", w.Code) } } func TestSenderReputation_RejectsSinceBeyondMaxLookback(t *testing.T) { api, _ := newSenderReputationAPI(t) tooOld := time.Now().Add(-2 * 365 * 24 * time.Hour).UTC().Format(time.RFC3339) url := "/admin/sender-reputation?did=did:plc:abcdefghijklmnopqrstuvwx&since=" + tooOld req := httptest.NewRequest(http.MethodGet, url, nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("since too old: status = %d, want 400", w.Code) } } func TestSenderReputation_HappyPath_EmptyStoreReturnsZeroes(t *testing.T) { api, _ := newSenderReputationAPI(t) req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did=did:plc:abcdefghijklmnopqrstuvwx", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) } var rep relaystore.SenderReputation if err := json.NewDecoder(w.Body).Decode(&rep); err != nil { t.Fatalf("decode: %v", err) } if rep.DID != "did:plc:abcdefghijklmnopqrstuvwx" { t.Errorf("DID = %q", rep.DID) } if rep.Total != 0 || rep.Bounces != 0 || rep.Complaints != 0 { t.Errorf("counts = (%d,%d,%d), want all zero", rep.Total, rep.Bounces, rep.Complaints) } } func TestSenderReputation_HappyPath_AggregatesEvents(t *testing.T) { api, store := newSenderReputationAPI(t) ctx := context.Background() did := "did:plc:abcdefghijklmnopqrstuvwx" now := time.Now().UTC() // 3 deliveries + 1 bounce in the default 30-day window for i, action := range []string{"delivery_result", "delivery_result", "delivery_result", "bounce_received"} { if err := store.InsertRelayEvent(ctx, &relaystore.RelayEvent{ ActionID: int64(i + 1), KafkaOffset: int64(i + 1), IngestedAt: now, EventTimestamp: now.Add(-1 * time.Hour), ActionName: action, SenderDID: did, }); err != nil { t.Fatalf("InsertRelayEvent %d: %v", i, err) } } req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did="+did, nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) } var rep relaystore.SenderReputation if err := json.NewDecoder(w.Body).Decode(&rep); err != nil { t.Fatalf("decode: %v", err) } if rep.Total != 3 { t.Errorf("Total = %d, want 3", rep.Total) } if rep.Bounces != 1 { t.Errorf("Bounces = %d, want 1", rep.Bounces) } } func TestSenderReputation_CustomSinceParam(t *testing.T) { api, store := newSenderReputationAPI(t) ctx := context.Background() did := "did:plc:abcdefghijklmnopqrstuvwx" now := time.Now().UTC() // One event 10 days ago. With `since` set to 5 days ago, it should // not be counted. if err := store.InsertRelayEvent(ctx, &relaystore.RelayEvent{ ActionID: 1, KafkaOffset: 1, IngestedAt: now, EventTimestamp: now.Add(-10 * 24 * time.Hour), ActionName: "delivery_result", SenderDID: did, }); err != nil { t.Fatalf("InsertRelayEvent: %v", err) } since := now.Add(-5 * 24 * time.Hour).Format(time.RFC3339) req := httptest.NewRequest(http.MethodGet, "/admin/sender-reputation?did="+did+"&since="+since, nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) } var rep relaystore.SenderReputation if err := json.NewDecoder(w.Body).Decode(&rep); err != nil { t.Fatalf("decode: %v", err) } if rep.Total != 0 { t.Errorf("Total = %d, want 0 (event was outside since=5d window)", rep.Total) } } // --- internal ref: /admin/migrate-dkim-encryption --- // testAdminAPIWithPII mirrors testAdminAPI but configures a PIIKey so // the migrate endpoint has a key to rotate to. The PIIKey value is // deterministic for test reproducibility. func testAdminAPIWithPII(t *testing.T) (*API, *relaystore.Store) { t.Helper() key := make([]byte, 32) for i := range key { key[i] = byte(i + 1) } store, err := relaystore.NewWithPIIKey(":memory:", relaystore.PIIKey(key)) if err != nil { t.Fatalf("NewWithPIIKey: %v", err) } t.Cleanup(func() { store.Close() }) return New(store, "test-admin-token", "atmos.email"), store } func TestMigrateDKIMEncryption_HTTPContract(t *testing.T) { api, store := testAdminAPIWithPII(t) // internal ref follow-up: migration endpoints default off (return 404). // Operators opt in via cfg.EnableMigrationEndpoints / env for the migration // window. Tests exercising the contract enable it explicitly. api.SetMigrationEndpointsEnabled(true) ctx := context.Background() now := time.Now().UTC() // Seed: one member-domain row via the normal path (already encrypted). if err := store.InsertMember(ctx, &relaystore.Member{ DID: "did:plc:httpmig", Status: relaystore.StatusActive, HourlyLimit: 10, DailyLimit: 100, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatal(err) } if err := store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "fresh.example", DID: "did:plc:httpmig", APIKeyHash: []byte("h"), DKIMRSAPriv: append([]byte{0x30, 0x82, 0x04, 0xa4}, []byte("rsa")...), DKIMEdPriv: append([]byte{0x30, 0x2e, 0x02, 0x01}, []byte("ed")...), DKIMSelector: "sel", CreatedAt: now, }); err != nil { t.Fatal(err) } // POST without auth → 401 req := httptest.NewRequest(http.MethodPost, "/admin/migrate-dkim-encryption", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("unauth POST: status=%d, want 401", w.Code) } // GET → 405 even with auth (POST-only mutation endpoint) req = httptest.NewRequest(http.MethodGet, "/admin/migrate-dkim-encryption", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("GET: status=%d, want 405", w.Code) } // POST with auth → 200 + JSON body. Row was already encrypted by // InsertMemberDomain, so we expect Skipped=1, Rotated=0. req = httptest.NewRequest(http.MethodPost, "/admin/migrate-dkim-encryption", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("authed POST: status=%d body=%s", w.Code, w.Body.String()) } var resp relaystore.DKIMEncryptionRotateResult if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } if resp.Rotated != 0 || resp.Skipped != 1 { t.Errorf("result: %+v, want {Rotated:0 Skipped:1}", resp) } } func TestMigrateDKIMEncryption_NoPIIKeyReturns500(t *testing.T) { api, _ := testAdminAPI(t) // no PIIKey api.SetMigrationEndpointsEnabled(true) req := httptest.NewRequest(http.MethodPost, "/admin/migrate-dkim-encryption", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Errorf("no-key endpoint should 500: got status=%d body=%q", w.Code, w.Body.String()) } } // TestDualTokenRotation pins the zero-downtime admin-token rotation // surface: when SetPrevAdminToken is called with a non-empty value, // requireAuth MUST accept either the primary or the previous token. // This is the contract operators rely on to deploy a new token while // keeping the old one briefly valid for downstream consumer updates, // then redeploy with the previous slot cleared. Wrong or empty bearers // continue to 401. func TestDualTokenRotation(t *testing.T) { api, _ := testAdminAPI(t) // primary token = "test-admin-token" api.SetPrevAdminToken("legacy-token-being-rotated-out") // Use /admin/stats — a trivially authed endpoint that returns 200 on // auth success regardless of store contents. for _, tc := range []struct { name string bearer string wantStatus int }{ {"primary token accepted", "test-admin-token", http.StatusOK}, {"previous token accepted", "legacy-token-being-rotated-out", http.StatusOK}, {"unrelated token rejected", "definitely-not-a-token", http.StatusUnauthorized}, {"empty bearer rejected", "", http.StatusUnauthorized}, } { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/admin/stats", nil) if tc.bearer != "" { req.Header.Set("Authorization", "Bearer "+tc.bearer) } w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != tc.wantStatus { t.Errorf("status=%d, want %d (body=%q)", w.Code, tc.wantStatus, w.Body.String()) } }) } // Clearing the previous slot returns the API to single-token mode — // the previously-valid legacy token is now rejected. api.SetPrevAdminToken("") req := httptest.NewRequest(http.MethodGet, "/admin/stats", nil) req.Header.Set("Authorization", "Bearer legacy-token-being-rotated-out") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("after SetPrevAdminToken(\"\"), legacy token should be rejected; got status=%d", w.Code) } } // TestMigrateEndpoints_DefaultOffReturns404 pins the chainlink-#689 // follow-up gating: when an operator hasn't opted into migration mode, // BOTH encryption-rotation endpoints return 404 (not 401, not 405) so a // probing scan cannot confirm they exist. This is the default state on // fresh deployments and again after the migration window closes. func TestMigrateEndpoints_DefaultOffReturns404(t *testing.T) { api, _ := testAdminAPI(t) // NOT enabling — the whole point of this test. for _, path := range []string{ "/admin/migrate-dkim-encryption", "/admin/migrate-forward-to-encryption", } { req := httptest.NewRequest(http.MethodPost, path, nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("default-off %s: status=%d, want 404 (gate should hide existence)", path, w.Code) } } } // --- internal ref: /admin/migrate-forward-to-encryption HTTP contract --- func TestMigrateForwardToEncryption_HTTPContract(t *testing.T) { api, store := testAdminAPIWithPII(t) api.SetMigrationEndpointsEnabled(true) ctx := context.Background() now := time.Now().UTC() if err := store.InsertMember(ctx, &relaystore.Member{ DID: "did:plc:ft-http", Status: relaystore.StatusActive, HourlyLimit: 10, DailyLimit: 100, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatal(err) } if err := store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: "ftc.example", DID: "did:plc:ft-http", APIKeyHash: []byte("h"), DKIMRSAPriv: []byte{0x30}, DKIMEdPriv: []byte{0x30}, DKIMSelector: "s", ForwardTo: "real@example.com", CreatedAt: now, }); err != nil { t.Fatal(err) } // 401 unauth POST req := httptest.NewRequest(http.MethodPost, "/admin/migrate-forward-to-encryption", nil) w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("unauth POST: status=%d", w.Code) } // 405 GET with auth req = httptest.NewRequest(http.MethodGet, "/admin/migrate-forward-to-encryption", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("GET: status=%d", w.Code) } // 200 authed POST — row was inserted via the normal path so it's already encrypted. req = httptest.NewRequest(http.MethodPost, "/admin/migrate-forward-to-encryption", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w = httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("authed POST: status=%d body=%s", w.Code, w.Body.String()) } var resp relaystore.ForwardToEncryptionRotateResult if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatal(err) } if resp.Rotated != 0 || resp.Skipped != 1 { t.Errorf("got %+v, want {Rotated:0 Skipped:1}", resp) } } func TestMigrateForwardToEncryption_NoPIIKeyReturns500(t *testing.T) { api, _ := testAdminAPI(t) // no PII key api.SetMigrationEndpointsEnabled(true) req := httptest.NewRequest(http.MethodPost, "/admin/migrate-forward-to-encryption", nil) req.Header.Set("Authorization", "Bearer test-admin-token") w := httptest.NewRecorder() api.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Errorf("got status=%d, want 500", w.Code) } } // TestSendTestResultAutoFlipsEligibility pins phase 3 of chainlink // #433: when a second receiver reports dkim=pass for a domain (via // the manual paste endpoint here, or via phase 3b's IMAP fetcher in // the future), the relay auto-flips warmup_eligible=1. Threshold = 2 // distinct receivers. func TestSendTestResultAutoFlipsEligibility(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() now := time.Now().UTC() did := "did:plc:autoflip22222222222222222" domain := "autoflip.example" if err := store.InsertMember(ctx, &relaystore.Member{ DID: did, Status: "active", HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }); err != nil { t.Fatalf("InsertMember: %v", err) } if err := store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: domain, DID: did, APIKeyHash: []byte("hash"), DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }); err != nil { t.Fatalf("InsertMemberDomain: %v", err) } // InsertMemberDomain set warmup_eligible=0 (phase 1 default for // new enrollments). Confirm. eligible, err := store.IsWarmupEligible(ctx, domain) if err != nil || eligible { t.Fatalf("initial eligibility: eligible=%v err=%v; want false", eligible, err) } // Record two send-test rows targeting two different receivers. if _, err := store.RecordSendTest(ctx, domain, "test+gmail@gmail.com", "", now); err != nil { t.Fatalf("RecordSendTest gmail: %v", err) } if _, err := store.RecordSendTest(ctx, domain, "test+proton@proton.me", "", now); err != nil { t.Fatalf("RecordSendTest proton: %v", err) } post := func(messageID, header string) int { body := fmt.Sprintf(`{"messageId":%q,"authResults":%q}`, messageID, header) req := httptest.NewRequest(http.MethodPost, "/admin/domain/"+domain+"/test-result", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer test-admin-token") req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() api.ServeHTTP(w, req) return w.Code } // First receiver passes — threshold not yet met (count=1). if code := post("", "mx.google.com; dkim=pass; spf=pass; dmarc=pass"); code != http.StatusOK { t.Fatalf("post 1: status=%d", code) } eligible, err = store.IsWarmupEligible(ctx, domain) if err != nil || eligible { t.Errorf("after 1 pass: eligible=%v err=%v; want false (threshold not met)", eligible, err) } // Second distinct receiver passes — threshold met, auto-flip // should fire. if code := post("", "mail.protonmail.ch; dkim=pass; spf=pass; dmarc=pass"); code != http.StatusOK { t.Fatalf("post 2: status=%d", code) } eligible, err = store.IsWarmupEligible(ctx, domain) if err != nil { t.Fatalf("IsWarmupEligible: %v", err) } if !eligible { t.Errorf("expected auto-flip after 2 distinct receivers passed; got eligible=false") } } // TestSendTestResultNoFlipOnDKIMFail pins that a fail result is // recorded but never triggers the auto-flip code path. func TestSendTestResultNoFlipOnDKIMFail(t *testing.T) { api, store := testAdminAPI(t) ctx := context.Background() now := time.Now().UTC() did := "did:plc:nofail2222222222222222222" domain := "nofail.example" store.InsertMember(ctx, &relaystore.Member{ DID: did, Status: "active", HourlyLimit: 100, DailyLimit: 1000, CreatedAt: now, UpdatedAt: now, }) store.InsertMemberDomain(ctx, &relaystore.MemberDomain{ Domain: domain, DID: did, APIKeyHash: []byte("hash"), DKIMRSAPriv: []byte("rsa"), DKIMEdPriv: []byte("ed"), DKIMSelector: "sel1", CreatedAt: now, }) store.RecordSendTest(ctx, domain, "test+gmail@gmail.com", "", now) store.RecordSendTest(ctx, domain, "test+proton@proton.me", "", now) post := func(messageID, header string) int { body := fmt.Sprintf(`{"messageId":%q,"authResults":%q}`, messageID, header) req := httptest.NewRequest(http.MethodPost, "/admin/domain/"+domain+"/test-result", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer test-admin-token") req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() api.ServeHTTP(w, req) return w.Code } // Two distinct receivers, both fail. Threshold should NOT fire // (we count only dkim=pass). post("", "mx.google.com; dkim=fail; spf=pass; dmarc=fail") post("", "mail.protonmail.ch; dkim=fail; spf=pass; dmarc=fail") eligible, err := store.IsWarmupEligible(ctx, domain) if err != nil { t.Fatalf("IsWarmupEligible: %v", err) } if eligible { t.Errorf("expected eligible=false after two dkim=fail receivers; got true") } }