Files
bookhoard/cmd/server/tests/registration_test.go
T

482 lines
12 KiB
Go

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