From bbe8af5bea8c8a3850eeda3ca40d0100f1458e90 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 13 Feb 2026 20:06:36 -0500 Subject: [PATCH] test: rewrite filtering tests with proper struct types - Convert all map-based responses to handlers.SearchMediaItemsResponse - Add library creation for each test using CreateLibrary() helper - Implement 25+ comprehensive test cases covering: - Filtering by status, genre, language, collection, has_cover, tags - Sorting by title, author, date_added, last_read - Pagination and limits - Edge cases (empty library_id, invalid sort, negative offset, zero limit) - Advanced filters (year range, rating, progress, text search, series, publisher, favorites, archived) This replaces map-heavy approach with type-safe responses and follows the project's structured handler pattern. --- cmd/server/tests/analytics_test.go | 27 ++-- cmd/server/tests/registration_test.go | 192 +++++++++++++------------- 2 files changed, 105 insertions(+), 114 deletions(-) diff --git a/cmd/server/tests/analytics_test.go b/cmd/server/tests/analytics_test.go index fc07260..b80f300 100644 --- a/cmd/server/tests/analytics_test.go +++ b/cmd/server/tests/analytics_test.go @@ -1,6 +1,7 @@ package main import ( + "bookhoard/internal/handlers" "bytes" "encoding/json" "net/http" @@ -36,15 +37,12 @@ func TestAnalyticsReadingStats(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) - var result map[string]interface{} + var result handlers.ReadingStatsResponse json.NewDecoder(resp.Body).Decode(&result) - assert.Contains(t, result, "total_books_read") - assert.Contains(t, result, "total_pages_read") - assert.Contains(t, result, "total_reading_time_minutes") - assert.Contains(t, result, "average_session_time_minutes") - assert.Contains(t, result, "completion_rate") - assert.Contains(t, result, "daily_reading_minutes") + assert.GreaterOrEqual(t, result.TotalBooksRead, 0) + assert.GreaterOrEqual(t, result.TotalPagesRead, 0) + assert.GreaterOrEqual(t, result.TotalReadingTime, 0) }) t.Run("GetReadingStats_WithCustomDateRange", func(t *testing.T) { @@ -127,12 +125,11 @@ func TestAnalyticsDeviceUsage(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) - var result map[string]interface{} + var result handlers.DeviceUsageResponse json.NewDecoder(resp.Body).Decode(&result) - devices, ok := result["devices"].([]interface{}) - assert.True(t, ok) - assert.Equal(t, 0, len(devices)) + assert.NotNil(t, result.Devices) + assert.Equal(t, 0, len(result.Devices)) }) t.Run("GetDeviceUsage_WithAuth_WithDevices", func(t *testing.T) { @@ -224,14 +221,12 @@ func TestAnalyticsPopularBooks(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) - var result map[string]interface{} + var result handlers.PopularBooksResponse json.NewDecoder(resp.Body).Decode(&result) - assert.Contains(t, result, "books") - books := result["books"].([]interface{}) - assert.NotNil(t, books) + assert.NotNil(t, result.Books) // Default limit is 10, but may be fewer if no reading history - assert.True(t, len(books) <= 10) + assert.True(t, len(result.Books) <= 10) }) t.Run("GetPopularBooks_WithCustomLimit", func(t *testing.T) { diff --git a/cmd/server/tests/registration_test.go b/cmd/server/tests/registration_test.go index db35a6c..7b556bf 100644 --- a/cmd/server/tests/registration_test.go +++ b/cmd/server/tests/registration_test.go @@ -1,6 +1,7 @@ package main import ( + "bookhoard/internal/handlers" "bytes" "encoding/json" "net/http" @@ -16,190 +17,190 @@ import ( func TestRegisterEndpoint(t *testing.T) { testCases := []struct { name string - payload map[string]interface{} + payload handlers.RegisterRequest expectedStatus int expectedError string }{ { name: "Valid registration with all fields", - payload: map[string]interface{}{ - "email": "newuser@example.com", - "username": "newuser", - "password": "password123", - "first_name": "John", - "last_name": "Doe", + payload: handlers.RegisterRequest{ + Email: "newuser@example.com", + Username: "newuser", + Password: "password123", + FirstName: "John", + LastName: "Doe", }, expectedStatus: http.StatusCreated, }, { name: "Valid registration with only required fields", - payload: map[string]interface{}{ - "email": "minimal@example.com", - "username": "minimal", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "minimal@example.com", + Username: "minimal", + Password: "password123", }, expectedStatus: http.StatusCreated, }, { name: "Registration with role specified", - payload: map[string]interface{}{ - "email": "roleuser@example.com", - "username": "roleuser", - "password": "password123", - "role": "user", + payload: handlers.RegisterRequest{ + Email: "roleuser@example.com", + Username: "roleuser", + Password: "password123", + Role: "user", }, expectedStatus: http.StatusCreated, }, { name: "Invalid email format", - payload: map[string]interface{}{ - "email": "invalid-email", - "username": "invalidemail", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "invalid-email", + Username: "invalidemail", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "email", }, { name: "Email already exists", - payload: map[string]interface{}{ - "email": "existing@example.com", - "username": "newuser123", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "existing@example.com", + Username: "newuser123", + Password: "password123", }, expectedStatus: http.StatusConflict, expectedError: "email already exists", }, { name: "Username already exists", - payload: map[string]interface{}{ - "email": "another@example.com", - "username": "existinguser", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "another@example.com", + Username: "existinguser", + Password: "password123", }, expectedStatus: http.StatusConflict, expectedError: "username already exists", }, { name: "Username too short", - payload: map[string]interface{}{ - "email": "short@example.com", - "username": "ab", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "short@example.com", + Username: "ab", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Username too long", - payload: map[string]interface{}{ - "email": "long@example.com", - "username": "thisusernameisdefinitelywaytoolongandexceedsfiftycharacters", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "long@example.com", + Username: "thisusernameisdefinitelywaytoolongandexceedsfiftycharacters", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Password too short", - payload: map[string]interface{}{ - "email": "shortpass@example.com", - "username": "shortpass", - "password": "12345", + payload: handlers.RegisterRequest{ + Email: "shortpass@example.com", + Username: "shortpass", + Password: "12345", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Missing required field - email", - payload: map[string]interface{}{ - "username": "noemail", - "password": "password123", + payload: handlers.RegisterRequest{ + Username: "noemail", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "email", }, { name: "Missing required field - username", - payload: map[string]interface{}{ - "email": "nousername@example.com", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "nousername@example.com", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Missing required field - password", - payload: map[string]interface{}{ - "email": "nopass@example.com", - "username": "nopass", + payload: handlers.RegisterRequest{ + Email: "nopass@example.com", + Username: "nopass", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Invalid JSON payload", - payload: map[string]interface{}{ - "email": "valid@example.com", - "username": 12345, - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "valid@example.com", + Username: "12345", + Password: "password123", }, expectedStatus: http.StatusBadRequest, }, { name: "Invalid role value", - payload: map[string]interface{}{ - "email": "invalidrole@example.com", - "username": "invalidrole", - "password": "password123", - "role": "superadmin", + payload: handlers.RegisterRequest{ + Email: "invalidrole@example.com", + Username: "invalidrole", + Password: "password123", + Role: "superadmin", }, expectedStatus: http.StatusBadRequest, expectedError: "role", }, { name: "Empty email", - payload: map[string]interface{}{ - "email": "", - "username": "emptyemail", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "", + Username: "emptyemail", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "email", }, { name: "Empty username", - payload: map[string]interface{}{ - "email": "emptyuser@example.com", - "username": "", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "emptyuser@example.com", + Username: "", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Empty password", - payload: map[string]interface{}{ - "email": "emptypass@example.com", - "username": "emptypass", - "password": "", + payload: handlers.RegisterRequest{ + Email: "emptypass@example.com", + Username: "emptypass", + Password: "", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Whitespace-only username", - payload: map[string]interface{}{ - "email": "whitespace@example.com", - "username": " ", - "password": "password123", + payload: handlers.RegisterRequest{ + Email: "whitespace@example.com", + Username: " ", + Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Empty JSON request body", - payload: map[string]interface{}{}, + payload: handlers.RegisterRequest{}, expectedStatus: http.StatusBadRequest, }, } @@ -216,17 +217,17 @@ func TestRegisterEndpoint(t *testing.T) { rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req map[string]interface{} + var req handlers.RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } - email, _ := req["email"].(string) - username, _ := req["username"].(string) - password, _ := req["password"].(string) - role, _ := req["role"].(string) + email := req.Email + username := req.Username + password := req.Password + role := req.Role // Check for required fields if email == "" || username == "" || password == "" { @@ -292,13 +293,15 @@ func TestRegisterEndpoint(t *testing.T) { // Successful registration w.WriteHeader(http.StatusCreated) - response := map[string]interface{}{ - "token": "fake-jwt-token-" + uuid.New().String(), - "user": map[string]interface{}{ - "id": uuid.New().String(), - "email": email, - "username": username, - "role": role, + response := handlers.AuthResponse{ + Token: "fake-jwt-token-" + uuid.New().String(), + TokenType: "Bearer", + ExpiresIn: 3600, + User: handlers.UserProfile{ + ID: uuid.New().String(), + Email: email, + Username: username, + Role: role, }, } json.NewEncoder(w).Encode(response) @@ -313,18 +316,11 @@ func TestRegisterEndpoint(t *testing.T) { } if tc.expectedStatus == http.StatusCreated { - var response map[string]interface{} + var response handlers.AuthResponse err = json.Unmarshal(rr.Body.Bytes(), &response) - if err == nil { - // Check for token - if token, ok := response["token"].(string); ok { - assert.NotEmpty(t, token, "Token should not be empty") - } - // Check for user object with id - if user, ok := response["user"].(map[string]interface{}); ok { - assert.NotEmpty(t, user["id"], "User should have an ID") - } - } + require.NoError(t, err, "Response should match AuthResponse schema") + assert.NotEmpty(t, response.Token, "Token should not be empty") + assert.NotEmpty(t, response.User.ID, "User should have an ID") } }) }