package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/sp4-debug-1770477266/pkg/logging" "git.threesix.ai/jordan/sp4-debug-1770477266/services/auth-svc/internal/domain" "git.threesix.ai/jordan/sp4-debug-1770477266/services/auth-svc/internal/port" "git.threesix.ai/jordan/sp4-debug-1770477266/services/auth-svc/internal/service" ) // mockExampleRepository implements port.ExampleRepository for testing. type mockExampleRepository struct { mu sync.RWMutex examples map[domain.ExampleID]*domain.Example } var _ port.ExampleRepository = (*mockExampleRepository)(nil) func newMockExampleRepository() *mockExampleRepository { return &mockExampleRepository{ examples: make(map[domain.ExampleID]*domain.Example), } } func (m *mockExampleRepository) List(ctx context.Context) ([]domain.Example, error) { m.mu.RLock() defer m.mu.RUnlock() result := make([]domain.Example, 0, len(m.examples)) for _, e := range m.examples { result = append(result, *e) } return result, nil } func (m *mockExampleRepository) Get(ctx context.Context, id domain.ExampleID) (*domain.Example, error) { m.mu.RLock() defer m.mu.RUnlock() e, ok := m.examples[id] if !ok { return nil, domain.ErrExampleNotFound } copy := *e return ©, nil } func (m *mockExampleRepository) Create(ctx context.Context, example *domain.Example) error { m.mu.Lock() defer m.mu.Unlock() copy := *example m.examples[example.ID] = © return nil } func (m *mockExampleRepository) Update(ctx context.Context, example *domain.Example) error { m.mu.Lock() defer m.mu.Unlock() if _, ok := m.examples[example.ID]; !ok { return domain.ErrExampleNotFound } copy := *example m.examples[example.ID] = © return nil } func (m *mockExampleRepository) Delete(ctx context.Context, id domain.ExampleID) error { m.mu.Lock() defer m.mu.Unlock() if _, ok := m.examples[id]; !ok { return domain.ErrExampleNotFound } delete(m.examples, id) return nil } func (m *mockExampleRepository) ExistsByName(ctx context.Context, name string) (bool, error) { m.mu.RLock() defer m.mu.RUnlock() for _, e := range m.examples { if e.Name == name { return true, nil } } return false, nil } func newTestHandler() (*Example, *mockExampleRepository) { repo := newMockExampleRepository() svc := service.NewExampleService(repo, logging.Nop()) handler := NewExample(svc, logging.Nop()) return handler, repo } func TestExample_List(t *testing.T) { handler, repo := newTestHandler() // Seed data ex, _ := domain.NewExample("test-id-1", "Test Example", "Description") _ = repo.Create(context.Background(), ex) r := chi.NewRouter() r.Get("/api/v1/examples", func(w http.ResponseWriter, r *http.Request) { if err := handler.List(w, r); err != nil { t.Fatalf("unexpected error: %v", err) } }) req := httptest.NewRequest(http.MethodGet, "/api/v1/examples", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var resp map[string]any if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data, ok := resp["data"] if !ok { t.Fatal("expected 'data' field in response") } items, ok := data.([]any) if !ok { t.Fatal("expected 'data' to be an array") } if len(items) != 1 { t.Errorf("expected 1 item, got %d", len(items)) } } func TestExample_Get(t *testing.T) { handler, repo := newTestHandler() // Seed data ex, _ := domain.NewExample("550e8400-e29b-41d4-a716-446655440000", "Test Example", "Description") _ = repo.Create(context.Background(), ex) tests := []struct { name string id string wantStatus int }{ { name: "valid uuid - found", id: "550e8400-e29b-41d4-a716-446655440000", wantStatus: http.StatusOK, }, { name: "valid uuid - not found", id: "550e8400-e29b-41d4-a716-446655440001", wantStatus: http.StatusNotFound, }, { name: "invalid uuid", id: "not-a-uuid", wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Get("/api/v1/examples/{id}", func(w http.ResponseWriter, r *http.Request) { if err := handler.Get(w, r); err != nil { // Map error to status for testing switch tt.wantStatus { case http.StatusNotFound: w.WriteHeader(http.StatusNotFound) case http.StatusBadRequest: w.WriteHeader(http.StatusBadRequest) default: w.WriteHeader(http.StatusInternalServerError) } return } }) req := httptest.NewRequest(http.MethodGet, "/api/v1/examples/"+tt.id, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code) } }) } } func TestExample_Create(t *testing.T) { handler, repo := newTestHandler() // Seed existing data for duplicate test ex, _ := domain.NewExample("existing-id", "Existing Name", "") _ = repo.Create(context.Background(), ex) tests := []struct { name string body any wantStatus int }{ { name: "valid request", body: CreateRequest{ Name: "New Example", Description: "A test description", }, wantStatus: http.StatusCreated, }, { name: "empty body", body: nil, wantStatus: http.StatusBadRequest, }, { name: "duplicate name", body: CreateRequest{ Name: "Existing Name", Description: "Conflict", }, wantStatus: http.StatusConflict, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Post("/api/v1/examples", func(w http.ResponseWriter, r *http.Request) { if err := handler.Create(w, r); err != nil { switch tt.wantStatus { case http.StatusBadRequest: w.WriteHeader(http.StatusBadRequest) case http.StatusConflict: w.WriteHeader(http.StatusConflict) default: w.WriteHeader(http.StatusInternalServerError) } return } }) var body []byte if tt.body != nil { var err error body, err = json.Marshal(tt.body) if err != nil { t.Fatalf("failed to marshal body: %v", err) } } req := httptest.NewRequest(http.MethodPost, "/api/v1/examples", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code) } }) } } func TestExample_Delete(t *testing.T) { handler, repo := newTestHandler() // Seed data ex, _ := domain.NewExample("550e8400-e29b-41d4-a716-446655440000", "To Delete", "") _ = repo.Create(context.Background(), ex) tests := []struct { name string id string wantStatus int }{ { name: "existing example", id: "550e8400-e29b-41d4-a716-446655440000", wantStatus: http.StatusNoContent, }, { name: "non-existent example", id: "550e8400-e29b-41d4-a716-446655440001", wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Delete("/api/v1/examples/{id}", func(w http.ResponseWriter, r *http.Request) { if err := handler.Delete(w, r); err != nil { if tt.wantStatus == http.StatusNotFound { w.WriteHeader(http.StatusNotFound) } else { w.WriteHeader(http.StatusBadRequest) } return } }) req := httptest.NewRequest(http.MethodDelete, "/api/v1/examples/"+tt.id, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code) } }) } } func TestExample_Update(t *testing.T) { handler, repo := newTestHandler() // Seed data ex1, _ := domain.NewExample("550e8400-e29b-41d4-a716-446655440000", "Example 1", "") _ = repo.Create(context.Background(), ex1) ex2, _ := domain.NewExample("550e8400-e29b-41d4-a716-446655440001", "Example 2", "") _ = repo.Create(context.Background(), ex2) tests := []struct { name string id string body UpdateRequest wantStatus int }{ { name: "valid update", id: "550e8400-e29b-41d4-a716-446655440000", body: UpdateRequest{ Name: "Updated Name", Description: "Updated", }, wantStatus: http.StatusOK, }, { name: "name conflict", id: "550e8400-e29b-41d4-a716-446655440000", body: UpdateRequest{ Name: "Example 2", Description: "Conflict", }, wantStatus: http.StatusConflict, }, { name: "not found", id: "550e8400-e29b-41d4-a716-446655440099", body: UpdateRequest{ Name: "Whatever", Description: "", }, wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Put("/api/v1/examples/{id}", func(w http.ResponseWriter, r *http.Request) { if err := handler.Update(w, r); err != nil { switch tt.wantStatus { case http.StatusNotFound: w.WriteHeader(http.StatusNotFound) case http.StatusConflict: w.WriteHeader(http.StatusConflict) default: w.WriteHeader(http.StatusBadRequest) } return } }) body, _ := json.Marshal(tt.body) req := httptest.NewRequest(http.MethodPut, "/api/v1/examples/"+tt.id, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code) } }) } }