//go:build test package testutils import ( "context" "crypto/sha256" "fmt" "sync" "time" "crussell/clock" "crussell/internal/square" ) // NewTestSquareClient returns an in-memory SquareClient for tests that must // compile under BOTH the dev ("test,dev") and prod ("test,!dev") build tags. // The real dev mock (internal/square.NewDevClient) is only compiled under the // dev tag, so prod-tag tests cannot reference it. This lightweight stand-in // implements just the surface the GDPR erasure / cache-invalidation tests // exercise (CreateCustomer, CreateCardOnFile, DeleteCustomer, // DeleteCardOnFile); any other method panics (never called by those tests). // // CreateCustomer is deterministic per email: re-provisioning the SAME email // returns the same id, while a different email (e.g. the anonymized // anon-{id}@anon.invalid address) mints a different id — the property the // cache-invalidation tests assert on. func NewTestSquareClient() square.SquareClient { return &testSquareClient{ customersByEmail: map[string]*square.CustomerResult{}, } } // testSquareClient is a minimal in-memory SquareClient for test-only use. // It embeds the square.SquareClient interface so it satisfies the full // interface without implementing every method; only the methods below are // overridden and actually called by the erasure/cache tests. type testSquareClient struct { square.SquareClient // embedded interface — satisfies SquareClient; unoverridden methods panic if called mu sync.Mutex seq int customersByEmail map[string]*square.CustomerResult } func (c *testSquareClient) CreateCustomer(ctx context.Context, name, email string) (*square.CustomerResult, error) { c.mu.Lock() defer c.mu.Unlock() if existing, ok := c.customersByEmail[email]; ok { return existing, nil } sum := sha256.Sum256([]byte(email)) customer := &square.CustomerResult{ ID: "cus_test_" + fmt.Sprintf("%x", sum)[:12], Email: email, CreatedAt: clock.Now().UTC().Format(time.RFC3339), } c.customersByEmail[email] = customer return customer, nil } func (c *testSquareClient) CreateCardOnFile(ctx context.Context, userID, cardToken, customerID string) (*square.CardOnFile, error) { c.mu.Lock() defer c.mu.Unlock() c.seq++ return &square.CardOnFile{ ID: fmt.Sprintf("test_card_%d", c.seq), CardID: fmt.Sprintf("ccof:test_%d", c.seq), Brand: "VISA", Last4: "4242", ExpMonth: 12, ExpYear: 2030, ReferenceID: userID, Enabled: true, }, nil } func (c *testSquareClient) DeleteCardOnFile(ctx context.Context, cardID string) error { return nil } func (c *testSquareClient) DeleteCustomer(ctx context.Context, customerID string) error { c.mu.Lock() defer c.mu.Unlock() for email, customer := range c.customersByEmail { if customer.ID == customerID { delete(c.customersByEmail, email) return nil } } return nil }