package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slack-auth-1770277926/pkg/auth" "git.threesix.ai/jordan/slack-auth-1770277926/pkg/logging" "git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/domain" "git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/port" "git.threesix.ai/jordan/slack-auth-1770277926/services/auth-api/internal/service" ) // mockUserRepository implements port.UserRepository for testing. type mockUserRepository struct { mu sync.RWMutex users map[domain.UserID]*domain.User } var _ port.UserRepository = (*mockUserRepository)(nil) func newMockUserRepository() *mockUserRepository { return &mockUserRepository{ users: make(map[domain.UserID]*domain.User), } } func (m *mockUserRepository) Get(ctx context.Context, id domain.UserID) (*domain.User, error) { m.mu.RLock() defer m.mu.RUnlock() u, ok := m.users[id] if !ok { return nil, domain.ErrUserNotFound } cpy := *u return &cpy, nil } func (m *mockUserRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) { m.mu.RLock() defer m.mu.RUnlock() for _, u := range m.users { if u.Email == email { cpy := *u return &cpy, nil } } return nil, domain.ErrUserNotFound } func (m *mockUserRepository) Create(ctx context.Context, user *domain.User) error { m.mu.Lock() defer m.mu.Unlock() cpy := *user m.users[user.ID] = &cpy return nil } func (m *mockUserRepository) ExistsByEmail(ctx context.Context, email string) (bool, error) { m.mu.RLock() defer m.mu.RUnlock() for _, u := range m.users { if u.Email == email { return true, nil } } return false, nil } var testJWTSecret = []byte("test-secret-key-for-testing-only") func newTestUserHandler() (*User, *mockUserRepository) { repo := newMockUserRepository() svc := service.NewUserService(repo, testJWTSecret, "slack-auth-1770277926", logging.Nop()) handler := NewUser(svc, logging.Nop()) return handler, repo } func TestUser_Register(t *testing.T) { tests := []struct { name string body any setup func(*mockUserRepository) wantStatus int }{ { name: "valid registration", body: RegisterRequest{ Email: "newuser@example.com", Password: "password123", }, wantStatus: http.StatusCreated, }, { name: "empty body", body: nil, wantStatus: http.StatusBadRequest, }, { name: "invalid email", body: RegisterRequest{ Email: "not-an-email", Password: "password123", }, wantStatus: http.StatusBadRequest, }, { name: "short password", body: RegisterRequest{ Email: "user@example.com", Password: "short", }, wantStatus: http.StatusBadRequest, }, { name: "duplicate email", body: RegisterRequest{ Email: "existing@example.com", Password: "password123", }, setup: func(repo *mockUserRepository) { user, _ := domain.NewUser("existing-id", "existing@example.com", "password123") _ = repo.Create(context.Background(), user) }, wantStatus: http.StatusConflict, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler, repo := newTestUserHandler() if tt.setup != nil { tt.setup(repo) } r := chi.NewRouter() r.Post("/api/auth-api/register", func(w http.ResponseWriter, r *http.Request) { if err := handler.Register(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/auth-api/register", 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) } if tt.wantStatus == http.StatusCreated { 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"].(map[string]any) if !ok { t.Fatal("expected 'data' field in response") } if _, ok := data["token"].(string); !ok { t.Error("expected 'token' in response") } if _, ok := data["user"].(map[string]any); !ok { t.Error("expected 'user' in response") } } }) } } func TestUser_Login(t *testing.T) { tests := []struct { name string body any setup func(*mockUserRepository) wantStatus int }{ { name: "valid login", body: LoginRequest{ Email: "user@example.com", Password: "correctpassword", }, setup: func(repo *mockUserRepository) { user, _ := domain.NewUser("user-id", "user@example.com", "correctpassword") _ = repo.Create(context.Background(), user) }, wantStatus: http.StatusOK, }, { name: "wrong password", body: LoginRequest{ Email: "user@example.com", Password: "wrongpassword", }, setup: func(repo *mockUserRepository) { user, _ := domain.NewUser("user-id", "user@example.com", "correctpassword") _ = repo.Create(context.Background(), user) }, wantStatus: http.StatusUnauthorized, }, { name: "user not found", body: LoginRequest{ Email: "nonexistent@example.com", Password: "password123", }, wantStatus: http.StatusUnauthorized, }, { name: "empty body", body: nil, wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler, repo := newTestUserHandler() if tt.setup != nil { tt.setup(repo) } r := chi.NewRouter() r.Post("/api/auth-api/login", func(w http.ResponseWriter, r *http.Request) { if err := handler.Login(w, r); err != nil { switch tt.wantStatus { case http.StatusBadRequest: w.WriteHeader(http.StatusBadRequest) case http.StatusUnauthorized: w.WriteHeader(http.StatusUnauthorized) 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/auth-api/login", 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) } if tt.wantStatus == http.StatusOK { 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"].(map[string]any) if !ok { t.Fatal("expected 'data' field in response") } if _, ok := data["token"].(string); !ok { t.Error("expected 'token' in response") } } }) } } func TestUser_Me(t *testing.T) { tests := []struct { name string setup func(*mockUserRepository) withAuth bool authUserID string wantStatus int }{ { name: "authenticated user", setup: func(repo *mockUserRepository) { user, _ := domain.NewUser("user-123", "user@example.com", "password123") _ = repo.Create(context.Background(), user) }, withAuth: true, authUserID: "user-123", wantStatus: http.StatusOK, }, { name: "no auth", wantStatus: http.StatusUnauthorized, }, { name: "user not found in db", setup: func(repo *mockUserRepository) { // Don't seed the user }, withAuth: true, authUserID: "nonexistent-user", wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler, repo := newTestUserHandler() if tt.setup != nil { tt.setup(repo) } r := chi.NewRouter() r.Get("/api/auth-api/me", func(w http.ResponseWriter, r *http.Request) { if err := handler.Me(w, r); err != nil { switch tt.wantStatus { case http.StatusUnauthorized: w.WriteHeader(http.StatusUnauthorized) case http.StatusNotFound: w.WriteHeader(http.StatusNotFound) default: w.WriteHeader(http.StatusInternalServerError) } return } }) req := httptest.NewRequest(http.MethodGet, "/api/auth-api/me", nil) // Add authenticated user to context if needed if tt.withAuth { authUser := &auth.User{ ID: tt.authUserID, Email: "user@example.com", } ctx := auth.SetUser(req.Context(), authUser) req = req.WithContext(ctx) } w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code) } if tt.wantStatus == http.StatusOK { 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"].(map[string]any) if !ok { t.Fatal("expected 'data' field in response") } if id, ok := data["id"].(string); !ok || id != tt.authUserID { t.Errorf("expected id %s, got %v", tt.authUserID, data["id"]) } } }) } }