package main import ( "bookhoard/internal/handlers" "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestRegisterEndpoint tests the registration endpoint comprehensively func TestRegisterEndpoint(t *testing.T) { testCases := []struct { name string payload handlers.RegisterRequest expectedStatus int expectedError string }{ { name: "Valid registration with all fields", 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: handlers.RegisterRequest{ Email: "minimal@example.com", Username: "minimal", Password: "password123", }, expectedStatus: http.StatusCreated, }, { name: "Registration with role specified", payload: handlers.RegisterRequest{ Email: "roleuser@example.com", Username: "roleuser", Password: "password123", Role: "user", }, expectedStatus: http.StatusCreated, }, { name: "Invalid email format", payload: handlers.RegisterRequest{ Email: "invalid-email", Username: "invalidemail", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "email", }, { name: "Email already exists", payload: handlers.RegisterRequest{ Email: "existing@example.com", Username: "newuser123", Password: "password123", }, expectedStatus: http.StatusConflict, expectedError: "email already exists", }, { name: "Username already exists", payload: handlers.RegisterRequest{ Email: "another@example.com", Username: "existinguser", Password: "password123", }, expectedStatus: http.StatusConflict, expectedError: "username already exists", }, { name: "Username too short", payload: handlers.RegisterRequest{ Email: "short@example.com", Username: "ab", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Username too long", payload: handlers.RegisterRequest{ Email: "long@example.com", Username: "thisusernameisdefinitelywaytoolongandexceedsfiftycharacters", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Password too short", payload: handlers.RegisterRequest{ Email: "shortpass@example.com", Username: "shortpass", Password: "12345", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Missing required field - email", payload: handlers.RegisterRequest{ Username: "noemail", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "email", }, { name: "Missing required field - username", payload: handlers.RegisterRequest{ Email: "nousername@example.com", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Missing required field - password", payload: handlers.RegisterRequest{ Email: "nopass@example.com", Username: "nopass", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Invalid role value", payload: handlers.RegisterRequest{ Email: "invalidrole@example.com", Username: "invalidrole", Password: "password123", Role: "superadmin", }, expectedStatus: http.StatusBadRequest, expectedError: "role", }, { name: "Empty email", payload: handlers.RegisterRequest{ Email: "", Username: "emptyemail", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "email", }, { name: "Empty username", payload: handlers.RegisterRequest{ Email: "emptyuser@example.com", Username: "", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Empty password", payload: handlers.RegisterRequest{ Email: "emptypass@example.com", Username: "emptypass", Password: "", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Whitespace-only username", payload: handlers.RegisterRequest{ Email: "whitespace@example.com", Username: " ", Password: "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "username", }, { name: "Empty JSON request body", payload: handlers.RegisterRequest{}, expectedStatus: http.StatusBadRequest, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { jsonData, err := json.Marshal(tc.payload) require.NoError(t, err) req, err := http.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(jsonData)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 username := req.Username password := req.Password role := req.Role // Check for required fields if email == "" || username == "" || password == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"email, username, and password are required"}`)) return } // Validate email format (basic check) if !contains(email, "@") || !contains(email, ".") { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"email is invalid"}`)) return } // Check for duplicate email if email == "existing@example.com" { w.WriteHeader(http.StatusConflict) w.Write([]byte(`{"error":"email already exists"}`)) return } // Check for duplicate username if username == "existinguser" { w.WriteHeader(http.StatusConflict) w.Write([]byte(`{"error":"username already exists"}`)) return } // Validate username length if len(username) < 3 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"username must be at least 3 characters"}`)) return } if len(username) > 50 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"username must be at most 50 characters"}`)) return } // Check for whitespace-only username if len(trimSpace(username)) == 0 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"username cannot be empty or whitespace"}`)) return } // Validate password length if len(password) < 6 { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"password must be at least 6 characters"}`)) return } // Validate role if provided if role != "" && role != "user" && role != "admin" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid role. must be 'user' or 'admin'"}`)) return } // Successful registration w.WriteHeader(http.StatusCreated) 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) }) handler.ServeHTTP(rr, req) assert.Equal(t, tc.expectedStatus, rr.Code, "Expected status %d, got %d", tc.expectedStatus, rr.Code) if tc.expectedError != "" { assert.Contains(t, rr.Body.String(), tc.expectedError, "Expected error message to contain '%s'", tc.expectedError) } if tc.expectedStatus == http.StatusCreated { var response handlers.AuthResponse err = json.Unmarshal(rr.Body.Bytes(), &response) 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") } }) } } // TestLoginEndpoint tests the login endpoint comprehensively func TestLoginEndpoint(t *testing.T) { testCases := []struct { name string payload map[string]interface{} expectedStatus int expectedError string }{ { name: "Valid login with email", payload: map[string]interface{}{ "login": "user@example.com", "password": "password123", }, expectedStatus: http.StatusOK, }, { name: "Valid login with username", payload: map[string]interface{}{ "login": "testuser", "password": "password123", }, expectedStatus: http.StatusOK, }, { name: "Invalid password", payload: map[string]interface{}{ "login": "user@example.com", "password": "wrongpassword", }, expectedStatus: http.StatusUnauthorized, expectedError: "invalid credentials", }, { name: "User not found", payload: map[string]interface{}{ "login": "nonexistent@example.com", "password": "password123", }, expectedStatus: http.StatusUnauthorized, expectedError: "invalid credentials", }, { name: "Missing login field", payload: map[string]interface{}{ "password": "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "login", }, { name: "Missing password field", payload: map[string]interface{}{ "login": "user@example.com", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Empty login", payload: map[string]interface{}{ "login": "", "password": "password123", }, expectedStatus: http.StatusBadRequest, expectedError: "login", }, { name: "Empty password", payload: map[string]interface{}{ "login": "user@example.com", "password": "", }, expectedStatus: http.StatusBadRequest, expectedError: "password", }, { name: "Invalid JSON payload", payload: map[string]interface{}{ "login": 12345, "password": "password123", }, expectedStatus: http.StatusBadRequest, }, { name: "Empty request body", payload: map[string]interface{}{}, expectedStatus: http.StatusBadRequest, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { jsonData, err := json.Marshal(tc.payload) require.NoError(t, err) req, err := http.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(jsonData)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid request"}`)) return } login, _ := req["login"].(string) password, _ := req["password"].(string) if login == "" || password == "" { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"login and password are required"}`)) return } if login != "user@example.com" && login != "testuser" { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"error":"invalid credentials"}`)) return } if password != "password123" { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"error":"invalid credentials"}`)) return } w.WriteHeader(http.StatusOK) response := map[string]interface{}{ "token": "fake-jwt-token", "user": map[string]interface{}{ "id": uuid.New().String(), "email": login, "username": "testuser", "role": "user", }, } json.NewEncoder(w).Encode(response) }) handler.ServeHTTP(rr, req) assert.Equal(t, tc.expectedStatus, rr.Code, "Expected status %d, got %d", tc.expectedStatus, rr.Code) if tc.expectedError != "" { assert.Contains(t, rr.Body.String(), tc.expectedError, "Expected error message to contain '%s'", tc.expectedError) } if tc.expectedStatus == http.StatusOK { var response map[string]interface{} err = json.Unmarshal(rr.Body.Bytes(), &response) require.NoError(t, err) assert.NotEmpty(t, response["token"]) assert.NotNil(t, response["user"]) } }) } }