package main import ( "bookhoard/internal/handlers" "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" ) // TestUserProfileEndpoints tests user profile management endpoints func TestUserProfileEndpoints(t *testing.T) { userID := uuid.New() t.Run("GET /api/auth/profile - Get profile without auth", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/auth/profile", nil) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" || !containsPrefix(authHeader, "Bearer ") { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) t.Run("GET /api/auth/profile - Get profile with valid auth", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/auth/profile", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { profile := handlers.UserProfile{ ID: userID.String(), Email: "user@example.com", Username: "testuser", Role: "user", } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(profile) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("PUT /api/auth/profile - Update profile with valid data", func(t *testing.T) { payload := map[string]interface{}{ "first_name": "Updated", "last_name": "Name", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"profile updated"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) } // TestUserUpdateEndpoints tests user field update endpoints func TestUserUpdateEndpoints(t *testing.T) { t.Run("PUT /api/auth/email - Update email to existing email", func(t *testing.T) { payload := handlers.UpdateEmailRequest{ Email: "existing@example.com", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req handlers.UpdateEmailRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } if req.Email == "existing@example.com" { w.WriteHeader(http.StatusConflict) w.Write([]byte(`{"error":"email already taken"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusConflict, rr.Code) }) t.Run("PUT /api/auth/email - Update email with invalid format", func(t *testing.T) { payload := handlers.UpdateEmailRequest{ Email: "invalid-email", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req handlers.UpdateEmailRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } email := req.Email if !contains(email, "@") || !contains(email, ".") { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"email is invalid"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("PUT /api/auth/email - Update email with empty value", func(t *testing.T) { payload := handlers.UpdateEmailRequest{ Email: "", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"email is required"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("PUT /api/auth/username - Update username to existing username", func(t *testing.T) { payload := handlers.UpdateUsernameRequest{ Username: "existinguser", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/username", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req handlers.UpdateUsernameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } if req.Username == "existinguser" { w.WriteHeader(http.StatusConflict) w.Write([]byte(`{"error":"username already taken"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusConflict, rr.Code) }) t.Run("PUT /api/auth/username - Update username with invalid length", func(t *testing.T) { testCases := []struct { name string username string }{ {"Username too short", "ab"}, {"Username too long", "thisusernameiswaytoolongandshouldfailvalidation"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { payload := map[string]interface{}{ "username": tc.username, } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/username", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"username must be between 3 and 50 characters"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) } }) t.Run("PUT /api/auth/password - Update password with wrong current password", func(t *testing.T) { payload := map[string]interface{}{ "current_password": "wrongpassword", "new_password": "newpassword123", "confirm_password": "newpassword123", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"error":"current password is incorrect"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) t.Run("PUT /api/auth/password - Update password with mismatched passwords", func(t *testing.T) { payload := map[string]interface{}{ "current_password": "correctpassword", "new_password": "newpassword123", "confirm_password": "differentpassword", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"new passwords do not match"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("PUT /api/auth/password - Update password with too short new password", func(t *testing.T) { payload := map[string]interface{}{ "current_password": "correctpassword", "new_password": "short", "confirm_password": "short", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"password must be at least 6 characters"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("PUT /api/auth/theme - Update theme", func(t *testing.T) { payload := map[string]interface{}{ "theme": "tokyo-night", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/theme", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"theme updated successfully"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("PUT /api/auth/theme - Update theme with empty value", func(t *testing.T) { payload := map[string]interface{}{ "theme": "", } jsonData, _ := json.Marshal(payload) req := httptest.NewRequest("PUT", "/api/auth/theme", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"theme is required"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) } // TestAccountDeletion tests account deletion scenarios func TestAccountDeletion(t *testing.T) { userID := uuid.New() t.Run("DELETE /api/auth/account - Delete without auth", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/auth/account", nil) rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") if authHeader == "" { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"message":"missing or malformed jwt"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) t.Run("DELETE /api/auth/account - Delete as last admin", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/auth/account", nil) req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") req.Header.Set("X-Admin-Count", "1") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { adminCount := r.Header.Get("X-Admin-Count") if adminCount == "1" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"cannot delete the last admin account"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) }) t.Run("DELETE /api/auth/account - Delete successfully", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/auth/account", nil) req.Header.Set("Authorization", "Bearer valid-token") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"account deleted successfully"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("DELETE /api/auth/account - Admin delete another user", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/auth/account?user_id="+userID.String(), nil) req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole := r.Header.Get("X-User-Role") if userRole != "admin" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"admin access required"}`)) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message":"user account deleted successfully"}`)) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) t.Run("DELETE /api/auth/account - Non-admin tries to delete another user", func(t *testing.T) { req := httptest.NewRequest("DELETE", "/api/auth/account?user_id="+userID.String(), nil) req.Header.Set("Authorization", "Bearer user-token") req.Header.Set("X-User-Role", "user") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole := r.Header.Get("X-User-Role") if userRole != "admin" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"admin access required"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) }) } // TestAdminOnlyEndpoints tests admin-only endpoints func TestAdminOnlyEndpoints(t *testing.T) { t.Run("GET /api/auth/users - List users without admin role", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/auth/users", nil) req.Header.Set("Authorization", "Bearer user-token") req.Header.Set("X-User-Role", "user") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userRole := r.Header.Get("X-User-Role") if userRole != "admin" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"admin access required"}`)) return } w.WriteHeader(http.StatusOK) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) }) t.Run("GET /api/auth/users - List users with admin role", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/auth/users", nil) req.Header.Set("Authorization", "Bearer admin-token") req.Header.Set("X-User-Role", "admin") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { users := []map[string]interface{}{ { "id": uuid.New().String(), "email": "user@example.com", "username": "testuser", "role": "user", }, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(users) }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) }) }