slack-auth-1770277926/services/auth-api/internal/api/handlers/user_test.go
rdev-worker fd9bf961bb
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature auth-system --requirements 'User model with email/...
2026-02-05 07:59:55 +00:00

403 lines
9.5 KiB
Go

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"])
}
}
})
}
}