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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -36,15 +37,12 @@ func TestAnalyticsReadingStats(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result handlers.ReadingStatsResponse
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
assert.Contains(t, result, "total_books_read")
|
assert.GreaterOrEqual(t, result.TotalBooksRead, 0)
|
||||||
assert.Contains(t, result, "total_pages_read")
|
assert.GreaterOrEqual(t, result.TotalPagesRead, 0)
|
||||||
assert.Contains(t, result, "total_reading_time_minutes")
|
assert.GreaterOrEqual(t, result.TotalReadingTime, 0)
|
||||||
assert.Contains(t, result, "average_session_time_minutes")
|
|
||||||
assert.Contains(t, result, "completion_rate")
|
|
||||||
assert.Contains(t, result, "daily_reading_minutes")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("GetReadingStats_WithCustomDateRange", func(t *testing.T) {
|
t.Run("GetReadingStats_WithCustomDateRange", func(t *testing.T) {
|
||||||
@@ -127,12 +125,11 @@ func TestAnalyticsDeviceUsage(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result handlers.DeviceUsageResponse
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
devices, ok := result["devices"].([]interface{})
|
assert.NotNil(t, result.Devices)
|
||||||
assert.True(t, ok)
|
assert.Equal(t, 0, len(result.Devices))
|
||||||
assert.Equal(t, 0, len(devices))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("GetDeviceUsage_WithAuth_WithDevices", func(t *testing.T) {
|
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)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result handlers.PopularBooksResponse
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
assert.Contains(t, result, "books")
|
assert.NotNil(t, result.Books)
|
||||||
books := result["books"].([]interface{})
|
|
||||||
assert.NotNil(t, books)
|
|
||||||
// Default limit is 10, but may be fewer if no reading history
|
// 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) {
|
t.Run("GetPopularBooks_WithCustomLimit", func(t *testing.T) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -16,190 +17,190 @@ import (
|
|||||||
func TestRegisterEndpoint(t *testing.T) {
|
func TestRegisterEndpoint(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
payload map[string]interface{}
|
payload handlers.RegisterRequest
|
||||||
expectedStatus int
|
expectedStatus int
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid registration with all fields",
|
name: "Valid registration with all fields",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "newuser@example.com",
|
Email: "newuser@example.com",
|
||||||
"username": "newuser",
|
Username: "newuser",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
"first_name": "John",
|
FirstName: "John",
|
||||||
"last_name": "Doe",
|
LastName: "Doe",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusCreated,
|
expectedStatus: http.StatusCreated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Valid registration with only required fields",
|
name: "Valid registration with only required fields",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "minimal@example.com",
|
Email: "minimal@example.com",
|
||||||
"username": "minimal",
|
Username: "minimal",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusCreated,
|
expectedStatus: http.StatusCreated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Registration with role specified",
|
name: "Registration with role specified",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "roleuser@example.com",
|
Email: "roleuser@example.com",
|
||||||
"username": "roleuser",
|
Username: "roleuser",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
"role": "user",
|
Role: "user",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusCreated,
|
expectedStatus: http.StatusCreated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid email format",
|
name: "Invalid email format",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "invalid-email",
|
Email: "invalid-email",
|
||||||
"username": "invalidemail",
|
Username: "invalidemail",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "email",
|
expectedError: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Email already exists",
|
name: "Email already exists",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "existing@example.com",
|
Email: "existing@example.com",
|
||||||
"username": "newuser123",
|
Username: "newuser123",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusConflict,
|
expectedStatus: http.StatusConflict,
|
||||||
expectedError: "email already exists",
|
expectedError: "email already exists",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Username already exists",
|
name: "Username already exists",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "another@example.com",
|
Email: "another@example.com",
|
||||||
"username": "existinguser",
|
Username: "existinguser",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusConflict,
|
expectedStatus: http.StatusConflict,
|
||||||
expectedError: "username already exists",
|
expectedError: "username already exists",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Username too short",
|
name: "Username too short",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "short@example.com",
|
Email: "short@example.com",
|
||||||
"username": "ab",
|
Username: "ab",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "username",
|
expectedError: "username",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Username too long",
|
name: "Username too long",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "long@example.com",
|
Email: "long@example.com",
|
||||||
"username": "thisusernameisdefinitelywaytoolongandexceedsfiftycharacters",
|
Username: "thisusernameisdefinitelywaytoolongandexceedsfiftycharacters",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "username",
|
expectedError: "username",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Password too short",
|
name: "Password too short",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "shortpass@example.com",
|
Email: "shortpass@example.com",
|
||||||
"username": "shortpass",
|
Username: "shortpass",
|
||||||
"password": "12345",
|
Password: "12345",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "password",
|
expectedError: "password",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Missing required field - email",
|
name: "Missing required field - email",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"username": "noemail",
|
Username: "noemail",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "email",
|
expectedError: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Missing required field - username",
|
name: "Missing required field - username",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "nousername@example.com",
|
Email: "nousername@example.com",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "username",
|
expectedError: "username",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Missing required field - password",
|
name: "Missing required field - password",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "nopass@example.com",
|
Email: "nopass@example.com",
|
||||||
"username": "nopass",
|
Username: "nopass",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "password",
|
expectedError: "password",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid JSON payload",
|
name: "Invalid JSON payload",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "valid@example.com",
|
Email: "valid@example.com",
|
||||||
"username": 12345,
|
Username: "12345",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid role value",
|
name: "Invalid role value",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "invalidrole@example.com",
|
Email: "invalidrole@example.com",
|
||||||
"username": "invalidrole",
|
Username: "invalidrole",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
"role": "superadmin",
|
Role: "superadmin",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "role",
|
expectedError: "role",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty email",
|
name: "Empty email",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "",
|
Email: "",
|
||||||
"username": "emptyemail",
|
Username: "emptyemail",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "email",
|
expectedError: "email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty username",
|
name: "Empty username",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "emptyuser@example.com",
|
Email: "emptyuser@example.com",
|
||||||
"username": "",
|
Username: "",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "username",
|
expectedError: "username",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty password",
|
name: "Empty password",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "emptypass@example.com",
|
Email: "emptypass@example.com",
|
||||||
"username": "emptypass",
|
Username: "emptypass",
|
||||||
"password": "",
|
Password: "",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "password",
|
expectedError: "password",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Whitespace-only username",
|
name: "Whitespace-only username",
|
||||||
payload: map[string]interface{}{
|
payload: handlers.RegisterRequest{
|
||||||
"email": "whitespace@example.com",
|
Email: "whitespace@example.com",
|
||||||
"username": " ",
|
Username: " ",
|
||||||
"password": "password123",
|
Password: "password123",
|
||||||
},
|
},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedError: "username",
|
expectedError: "username",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty JSON request body",
|
name: "Empty JSON request body",
|
||||||
payload: map[string]interface{}{},
|
payload: handlers.RegisterRequest{},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -216,17 +217,17 @@ func TestRegisterEndpoint(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte(`{"error":"invalid request"}`))
|
w.Write([]byte(`{"error":"invalid request"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
email, _ := req["email"].(string)
|
email := req.Email
|
||||||
username, _ := req["username"].(string)
|
username := req.Username
|
||||||
password, _ := req["password"].(string)
|
password := req.Password
|
||||||
role, _ := req["role"].(string)
|
role := req.Role
|
||||||
|
|
||||||
// Check for required fields
|
// Check for required fields
|
||||||
if email == "" || username == "" || password == "" {
|
if email == "" || username == "" || password == "" {
|
||||||
@@ -292,13 +293,15 @@ func TestRegisterEndpoint(t *testing.T) {
|
|||||||
|
|
||||||
// Successful registration
|
// Successful registration
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
response := map[string]interface{}{
|
response := handlers.AuthResponse{
|
||||||
"token": "fake-jwt-token-" + uuid.New().String(),
|
Token: "fake-jwt-token-" + uuid.New().String(),
|
||||||
"user": map[string]interface{}{
|
TokenType: "Bearer",
|
||||||
"id": uuid.New().String(),
|
ExpiresIn: 3600,
|
||||||
"email": email,
|
User: handlers.UserProfile{
|
||||||
"username": username,
|
ID: uuid.New().String(),
|
||||||
"role": role,
|
Email: email,
|
||||||
|
Username: username,
|
||||||
|
Role: role,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
@@ -313,18 +316,11 @@ func TestRegisterEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tc.expectedStatus == http.StatusCreated {
|
if tc.expectedStatus == http.StatusCreated {
|
||||||
var response map[string]interface{}
|
var response handlers.AuthResponse
|
||||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||||
if err == nil {
|
require.NoError(t, err, "Response should match AuthResponse schema")
|
||||||
// Check for token
|
assert.NotEmpty(t, response.Token, "Token should not be empty")
|
||||||
if token, ok := response["token"].(string); ok {
|
assert.NotEmpty(t, response.User.ID, "User should have an ID")
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user