package clans import ( "context" "errors" "testing" "time" "github.com/jackc/pgx/v5/pgconn" ) type memoryStore struct { clans map[string]Clan } func newMemoryStore() *memoryStore { return &memoryStore{clans: make(map[string]Clan)} } func (m *memoryStore) Exec(ctx context.Context, sql string) (int64, error) { return 0, nil } func (m *memoryStore) Create(ctx context.Context, c Clan) error { for _, existing := range m.clans { if existing.Tag == c.Tag { return &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint"} } } m.clans[c.ID] = c return nil } func (m *memoryStore) Get(ctx context.Context, id string) (Clan, error) { c, ok := m.clans[id] if !ok { return Clan{}, ErrNotFound } return c, nil } func (m *memoryStore) GetByTag(ctx context.Context, tag string) (Clan, error) { for _, c := range m.clans { if c.Tag == tag { return c, nil } } return Clan{}, ErrNotFound } func (m *memoryStore) List(ctx context.Context, limit, offset int) ([]Clan, error) { out := make([]Clan, 0) for _, c := range m.clans { out = append(out, c) } return out, nil } func (m *memoryStore) Delete(ctx context.Context, id string) error { if _, ok := m.clans[id]; !ok { return ErrNotFound } delete(m.clans, id) return nil } func (m *memoryStore) Update(ctx context.Context, c Clan) error { if _, ok := m.clans[c.ID]; !ok { return ErrNotFound } m.clans[c.ID] = c return nil } func (m *memoryStore) Count(ctx context.Context) (int, error) { return len(m.clans), nil } func TestClansService(t *testing.T) { store := newMemoryStore() svc := NewService(store) svc.now = func() time.Time { return time.Unix(1700000000, 0) } ctx := context.Background() // 1. Create a clan with valid inputs c1, err := svc.Create(ctx, CreateInput{ ID: "clan-1", Tag: "SPD", Name: "Speed Demons", AvatarURL: "https://example.com/spd.png", }) if err != nil { t.Fatalf("Create failed: %v", err) } if c1.ID != "clan-1" || c1.Tag != "SPD" || c1.Name != "Speed Demons" { t.Errorf("Unexpected created clan: %+v", c1) } // 2. Validate Tag must be 3 uppercase ASCII letters _, err = svc.Create(ctx, CreateInput{Tag: "SPEED", Name: "toolongtag"}) if !errors.Is(err, ErrInvalidInput) { t.Errorf("Expected ErrInvalidInput for tag 'SPEED', got: %v", err) } _, err = svc.Create(ctx, CreateInput{Tag: "S1D", Name: "withnumbertag"}) if !errors.Is(err, ErrInvalidInput) { t.Errorf("Expected ErrInvalidInput for tag 'S1D', got: %v", err) } // 3. Validate empty Name is rejected _, err = svc.Create(ctx, CreateInput{Tag: "AAA", Name: ""}) if !errors.Is(err, ErrInvalidInput) { t.Errorf("Expected ErrInvalidInput for empty name, got: %v", err) } // 4. Duplicate tag check _, err = svc.Create(ctx, CreateInput{ ID: "clan-2", Tag: "SPD", Name: "Duplicate SPD Tag", }) if !errors.Is(err, ErrAlreadyExists) { t.Errorf("Expected ErrAlreadyExists for duplicate tag, got: %v", err) } // 5. Get clan got, err := svc.Get(ctx, "clan-1") if err != nil { t.Fatalf("Get failed: %v", err) } if got.Tag != "SPD" { t.Errorf("Expected SPD, got %s", got.Tag) } // 6. Get by tag gotByTag, err := svc.GetByTag(ctx, "spd") // should normalize to SPD if err != nil { t.Fatalf("GetByTag failed: %v", err) } if gotByTag.ID != "clan-1" { t.Errorf("Expected clan-1, got %s", gotByTag.ID) } // 7. Update clan updated, err := svc.Update(ctx, "clan-1", UpdateInput{ Name: "Super Speed Demons", AvatarURL: "https://example.com/super-spd.png", }) if err != nil { t.Fatalf("Update failed: %v", err) } if updated.Name != "Super Speed Demons" || updated.AvatarURL != "https://example.com/super-spd.png" { t.Errorf("Unexpected updated clan fields: %+v", updated) } // 8. Delete clan err = svc.Delete(ctx, "clan-1") if err != nil { t.Fatalf("Delete failed: %v", err) } _, err = svc.Get(ctx, "clan-1") if !errors.Is(err, ErrNotFound) { t.Errorf("Expected ErrNotFound after deletion, got: %v", err) } }