Files
at-container-registry/pkg/hold/pds/crew_test.go
T
Evan JarrettandClaude Opus 5 12c55ed560 billing: make Stripe webhook delivery idempotent and retryable
Webhook delivery was neither idempotent nor order-safe, and every failure
returned 400, which Stripe does not retry. A transient DB or hold error
therefore dropped a subscription change silently and permanently.

  - New stripe_processed_events table: event_id as primary key dedups
    redelivery, and event_created per customer drops stale out-of-order
    deliveries.
  - HandleWebhook distinguishes ErrWebhookSignature (400, no retry) from
    ErrWebhookProcessing (500, Stripe redelivers). The event handlers
    return errors instead of swallowing them. ErrBillingDisabled maps to
    400: the route is mounted but billing is off, so redelivery can never
    succeed and Stripe should stop rather than retry to exhaustion.
  - Refuse to boot when billing is enabled with an empty
    STRIPE_WEBHOOK_SECRET. Stripe HMACs with the empty key, so an
    attacker can reproduce the signature and the endpoint is forgeable.
  - UpdateCrewTierOnAllHolds retries each hold (3 attempts, linear
    backoff, 5s per request) and returns a joined error so the webhook
    can fail and let Stripe redeliver.

The fan-out contacts holds concurrently rather than in sequence. Serially,
one unreachable hold burns the caller's entire 10s budget on its own
retries (3 x 5s plus backoff) and the holds after it are never contacted;
because Stripe redelivers in the same order, a persistently-down first
hold means the rest are never updated at all.

On the hold, the signature-validated sub claim is now the source of truth
for updateCrewTier: a mismatched body userDid is rejected with 403 rather
than retargeting the grant to another DID. "Not crew on this hold" is a
200 no-op, since the appview fans updates out to every managed hold and a
subscriber is not crew everywhere.

That no-op has to be told apart from a storage failure. GetCrewMember
collapsed both into one generic error, so a CAR-store failure read as
"not a member", answered 200, and let the appview record the event as
processed — losing the tier grant permanently, which is exactly the
failure mode this commit exists to prevent. Missing records now carry an
ErrCrewMemberNotFound sentinel, and anything else returns 500 so Stripe
redelivers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:14:58 -05:00

595 lines
15 KiB
Go

package pds
import (
"bytes"
"errors"
"strings"
"testing"
"atcr.io/pkg/atproto"
)
// TestAddCrewMember tests adding a single crew member
func TestAddCrewMember(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:alice123"
role := "writer"
permissions := []string{"blob:read", "blob:write"}
recordCID, err := pds.AddCrewMember(ctx, memberDID, role, permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// Verify CID is defined
if !recordCID.Defined() {
t.Error("Expected defined CID")
}
// List crew members to verify
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
crew := crewMembers[0]
if crew.Record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, crew.Record.Member)
}
if crew.Record.Role != role {
t.Errorf("Expected role %s, got %s", role, crew.Record.Role)
}
if len(crew.Record.Permissions) != len(permissions) {
t.Fatalf("Expected %d permissions, got %d", len(permissions), len(crew.Record.Permissions))
}
for i, perm := range permissions {
if crew.Record.Permissions[i] != perm {
t.Errorf("Expected permission[%d]=%s, got %s", i, perm, crew.Record.Permissions[i])
}
}
if crew.Record.Type != atproto.CrewCollection {
t.Errorf("Expected type %s, got %s", atproto.CrewCollection, crew.Record.Type)
}
if crew.Record.AddedAt == "" {
t.Error("Expected addedAt to be set")
}
}
// TestGetCrewMember tests retrieving a crew member by rkey
func TestGetCrewMember(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:bob456"
role := "reader"
permissions := []string{"blob:read"}
_, err := pds.AddCrewMember(ctx, memberDID, role, permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// List to get the rkey
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) == 0 {
t.Fatal("Expected at least one crew member")
}
rkey := crewMembers[0].Rkey
// Get crew member by rkey
retrievedCID, crew, err := pds.GetCrewMember(ctx, rkey)
if err != nil {
t.Fatalf("GetCrewMember failed: %v", err)
}
// Verify CID matches
if !crewMembers[0].Cid.Equals(retrievedCID) {
t.Error("Expected retrieved CID to match")
}
// Verify crew data
if crew.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, crew.Member)
}
if crew.Role != role {
t.Errorf("Expected role %s, got %s", role, crew.Role)
}
if len(crew.Permissions) != len(permissions) {
t.Fatalf("Expected %d permissions, got %d", len(permissions), len(crew.Permissions))
}
}
// TestGetCrewMember_NotFound tests error handling for missing crew
func TestGetCrewMember_NotFound(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Try to get non-existent crew member
_, _, err := pds.GetCrewMember(ctx, "nonexistent-rkey")
if err == nil {
t.Fatal("Expected error when getting non-existent crew member")
}
// A missing record must be distinguishable from a storage failure. Callers
// that treat "not crew" as a benign no-op (updateCrewTier) would otherwise
// swallow a CAR-store error as a non-member and silently drop a tier grant.
if !errors.Is(err, ErrCrewMemberNotFound) {
t.Errorf("Expected ErrCrewMemberNotFound, got: %v", err)
}
if !strings.Contains(err.Error(), "nonexistent-rkey") {
t.Errorf("Expected the rkey in the error, got: %s", err)
}
}
// TestListCrewMembers_Empty tests listing when no crew exists
func TestListCrewMembers_Empty(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// List crew members (should be empty)
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 0 {
t.Errorf("Expected 0 crew members, got %d", len(crewMembers))
}
}
// TestListCrewMembers_Multiple tests listing with multiple crew members
func TestListCrewMembers_Multiple(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add multiple crew members
members := []struct {
did string
role string
permissions []string
}{
{
did: "did:plc:alice123",
role: "admin",
permissions: []string{"blob:read", "blob:write", "crew:admin"},
},
{
did: "did:plc:bob456",
role: "writer",
permissions: []string{"blob:read", "blob:write"},
},
{
did: "did:plc:charlie789",
role: "reader",
permissions: []string{"blob:read"},
},
}
for _, m := range members {
_, err := pds.AddCrewMember(ctx, m.did, m.role, m.permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed for %s: %v", m.did, err)
}
}
// List all crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != len(members) {
t.Fatalf("Expected %d crew members, got %d", len(members), len(crewMembers))
}
// Verify each crew member (order may vary, so check by DID)
foundMembers := make(map[string]*CrewMemberWithKey)
for _, cm := range crewMembers {
foundMembers[cm.Record.Member] = cm
}
for _, m := range members {
crew, found := foundMembers[m.did]
if !found {
t.Errorf("Expected to find crew member %s", m.did)
continue
}
if crew.Record.Role != m.role {
t.Errorf("Expected role %s for %s, got %s", m.role, m.did, crew.Record.Role)
}
if len(crew.Record.Permissions) != len(m.permissions) {
t.Errorf("Expected %d permissions for %s, got %d", len(m.permissions), m.did, len(crew.Record.Permissions))
}
// Verify rkey is set
if crew.Rkey == "" {
t.Errorf("Expected non-empty rkey for %s", m.did)
}
// Verify CID is defined
if !crew.Cid.Defined() {
t.Errorf("Expected defined CID for %s", m.did)
}
}
}
// TestRemoveCrewMember tests deleting a crew member
func TestRemoveCrewMember(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:alice123"
_, err := pds.AddCrewMember(ctx, memberDID, "writer", []string{"blob:read", "blob:write"}, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// List to get the rkey
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
rkey := crewMembers[0].Rkey
// Remove crew member
err = pds.RemoveCrewMember(ctx, rkey)
if err != nil {
t.Fatalf("RemoveCrewMember failed: %v", err)
}
// Verify crew member is gone
crewMembers, err = pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed after removal: %v", err)
}
if len(crewMembers) != 0 {
t.Errorf("Expected 0 crew members after removal, got %d", len(crewMembers))
}
// Try to get removed crew member (should fail)
_, _, err = pds.GetCrewMember(ctx, rkey)
if err == nil {
t.Error("Expected error when getting removed crew member")
}
}
// TestRemoveCrewMember_NotFound tests removing non-existent crew member
func TestRemoveCrewMember_NotFound(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Try to remove non-existent crew member
err := pds.RemoveCrewMember(ctx, "nonexistent-rkey")
if err == nil {
t.Fatal("Expected error when removing non-existent crew member")
}
// Verify error message
errMsg := err.Error()
if !strings.Contains(errMsg, "failed to delete crew record") {
t.Errorf("Expected 'failed to delete crew record' in error, got: %s", errMsg)
}
}
// TestRemoveCrewMember_Multiple tests removing one crew member from many
func TestRemoveCrewMember_Multiple(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add multiple crew members
dids := []string{
"did:plc:alice123",
"did:plc:bob456",
"did:plc:charlie789",
}
for _, did := range dids {
_, err := pds.AddCrewMember(ctx, did, "writer", []string{"blob:read"}, "")
if err != nil {
t.Fatalf("AddCrewMember failed for %s: %v", did, err)
}
}
// List crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 3 {
t.Fatalf("Expected 3 crew members, got %d", len(crewMembers))
}
// Remove middle member
middleRkey := crewMembers[1].Rkey
middleDID := crewMembers[1].Record.Member
err = pds.RemoveCrewMember(ctx, middleRkey)
if err != nil {
t.Fatalf("RemoveCrewMember failed: %v", err)
}
// Verify only 2 remain
crewMembers, err = pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed after removal: %v", err)
}
if len(crewMembers) != 2 {
t.Fatalf("Expected 2 crew members after removal, got %d", len(crewMembers))
}
// Verify removed member is not in list
for _, cm := range crewMembers {
if cm.Record.Member == middleDID {
t.Errorf("Expected %s to be removed, but still found in list", middleDID)
}
}
}
// TestCrewRecord_CBORRoundtrip tests CBOR marshal/unmarshal integrity
func TestCrewRecord_CBORRoundtrip(t *testing.T) {
tests := []struct {
name string
record *atproto.CrewRecord
}{
{
name: "Basic crew member",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:alice123",
Role: "writer",
Permissions: []string{"blob:read", "blob:write"},
AddedAt: "2025-10-16T12:00:00Z",
},
},
{
name: "Admin crew member",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:bob456",
Role: "admin",
Permissions: []string{"blob:read", "blob:write", "crew:admin"},
AddedAt: "2025-10-16T13:00:00Z",
},
},
{
name: "Reader crew member",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:charlie789",
Role: "reader",
Permissions: []string{"blob:read"},
AddedAt: "2025-10-16T14:00:00Z",
},
},
{
name: "Crew member with empty permissions",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:dave012",
Role: "none",
Permissions: []string{},
AddedAt: "2025-10-16T15:00:00Z",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Marshal to CBOR
var buf bytes.Buffer
err := tt.record.MarshalCBOR(&buf)
if err != nil {
t.Fatalf("MarshalCBOR failed: %v", err)
}
cborBytes := buf.Bytes()
if len(cborBytes) == 0 {
t.Fatal("Expected non-empty CBOR bytes")
}
// Unmarshal from CBOR
var decoded atproto.CrewRecord
err = decoded.UnmarshalCBOR(bytes.NewReader(cborBytes))
if err != nil {
t.Fatalf("UnmarshalCBOR failed: %v", err)
}
// Verify all fields match
if decoded.Type != tt.record.Type {
t.Errorf("Type mismatch: expected %s, got %s", tt.record.Type, decoded.Type)
}
if decoded.Member != tt.record.Member {
t.Errorf("Member mismatch: expected %s, got %s", tt.record.Member, decoded.Member)
}
if decoded.Role != tt.record.Role {
t.Errorf("Role mismatch: expected %s, got %s", tt.record.Role, decoded.Role)
}
if decoded.AddedAt != tt.record.AddedAt {
t.Errorf("AddedAt mismatch: expected %s, got %s", tt.record.AddedAt, decoded.AddedAt)
}
// Verify permissions
if len(decoded.Permissions) != len(tt.record.Permissions) {
t.Fatalf("Permissions length mismatch: expected %d, got %d", len(tt.record.Permissions), len(decoded.Permissions))
}
for i, perm := range tt.record.Permissions {
if decoded.Permissions[i] != perm {
t.Errorf("Permission[%d] mismatch: expected %s, got %s", i, perm, decoded.Permissions[i])
}
}
})
}
}
// TestCrewMemberWithKey_Structure tests the CrewMemberWithKey struct
func TestCrewMemberWithKey_Structure(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:alice123"
_, err := pds.AddCrewMember(ctx, memberDID, "writer", []string{"blob:read"}, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// List crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
cm := crewMembers[0]
// Verify CrewMemberWithKey structure
if cm.Rkey == "" {
t.Error("Expected non-empty Rkey")
}
if !cm.Cid.Defined() {
t.Error("Expected defined Cid")
}
if cm.Record == nil {
t.Fatal("Expected non-nil Record")
}
if cm.Record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, cm.Record.Member)
}
}
// TestAddCrewMember_DidWeb tests adding crew members with did:web DIDs
func TestAddCrewMember_DidWeb(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member with did:web
memberDID := "did:web:alice.example.com"
role := "writer"
permissions := []string{"blob:read", "blob:write"}
recordCID, err := pds.AddCrewMember(ctx, memberDID, role, permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed with did:web: %v", err)
}
if !recordCID.Defined() {
t.Error("Expected defined CID")
}
// Verify crew member was added
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
crew := crewMembers[0]
if crew.Record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, crew.Record.Member)
}
// Verify we can get it by rkey
_, retrievedCrew, err := pds.GetCrewMember(ctx, crew.Rkey)
if err != nil {
t.Fatalf("GetCrewMember failed for did:web: %v", err)
}
if retrievedCrew.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, retrievedCrew.Member)
}
}
// TestListCrewMembers_MixedDIDs tests listing crew members with mixed DID types
func TestListCrewMembers_MixedDIDs(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew members with different DID types
members := []struct {
did string
role string
permissions []string
}{
{
did: "did:plc:alice123",
role: "admin",
permissions: []string{"blob:read", "blob:write", "crew:admin"},
},
{
did: "did:web:bob.example.com",
role: "writer",
permissions: []string{"blob:read", "blob:write"},
},
{
did: "did:web:charlie.example.org",
role: "reader",
permissions: []string{"blob:read"},
},
}
for _, m := range members {
_, err := pds.AddCrewMember(ctx, m.did, m.role, m.permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed for %s: %v", m.did, err)
}
}
// List all crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != len(members) {
t.Fatalf("Expected %d crew members, got %d", len(members), len(crewMembers))
}
// Verify each crew member exists (order may vary)
foundMembers := make(map[string]*CrewMemberWithKey)
for _, cm := range crewMembers {
foundMembers[cm.Record.Member] = cm
}
for _, m := range members {
crew, found := foundMembers[m.did]
if !found {
t.Errorf("Expected to find crew member %s", m.did)
continue
}
if crew.Record.Role != m.role {
t.Errorf("Expected role %s for %s, got %s", m.role, m.did, crew.Record.Role)
}
}
}