Files
honeyDueAPI/internal/handlers/auth_handler_test.go
Trey t 469f21a833 Add PDF reports, file uploads, admin auth, and comprehensive tests
Features:
- PDF service for generating task reports with ReportLab-style formatting
- Storage service for file uploads (local and S3-compatible)
- Admin authentication middleware with JWT support
- Admin user model and repository

Infrastructure:
- Updated Docker configuration for admin panel builds
- Email service enhancements for task notifications
- Updated router with admin and file upload routes
- Environment configuration updates

Tests:
- Unit tests for handlers (auth, residence, task)
- Unit tests for models (user, residence, task)
- Unit tests for repositories (user, residence, task)
- Unit tests for services (residence, task)
- Integration test setup
- Test utilities for mocking database and services

Database:
- Admin user seed data
- Updated test data seeds

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 23:36:20 -06:00

408 lines
12 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/treytartt/mycrib-api/internal/config"
"github.com/treytartt/mycrib-api/internal/dto/requests"
"github.com/treytartt/mycrib-api/internal/repositories"
"github.com/treytartt/mycrib-api/internal/services"
"github.com/treytartt/mycrib-api/internal/testutil"
)
func setupAuthHandler(t *testing.T) (*AuthHandler, *gin.Engine, *repositories.UserRepository) {
db := testutil.SetupTestDB(t)
userRepo := repositories.NewUserRepository(db)
cfg := &config.Config{
Security: config.SecurityConfig{
SecretKey: "test-secret-key",
PasswordResetExpiry: 15 * time.Minute,
ConfirmationExpiry: 24 * time.Hour,
MaxPasswordResetRate: 3,
},
}
authService := services.NewAuthService(userRepo, cfg)
handler := NewAuthHandler(authService, nil, nil) // No email or cache for tests
router := testutil.SetupTestRouter()
return handler, router, userRepo
}
func TestAuthHandler_Register(t *testing.T) {
handler, router, _ := setupAuthHandler(t)
router.POST("/api/auth/register/", handler.Register)
t.Run("successful registration", func(t *testing.T) {
req := requests.RegisterRequest{
Username: "newuser",
Email: "new@test.com",
Password: "password123",
FirstName: "New",
LastName: "User",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusCreated)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
testutil.AssertJSONFieldExists(t, response, "token")
testutil.AssertJSONFieldExists(t, response, "user")
testutil.AssertJSONFieldExists(t, response, "message")
user := response["user"].(map[string]interface{})
assert.Equal(t, "newuser", user["username"])
assert.Equal(t, "new@test.com", user["email"])
assert.Equal(t, "New", user["first_name"])
assert.Equal(t, "User", user["last_name"])
})
t.Run("registration with missing fields", func(t *testing.T) {
req := map[string]string{
"username": "test",
// Missing email and password
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
response := testutil.ParseJSON(t, w.Body.Bytes())
testutil.AssertJSONFieldExists(t, response, "error")
})
t.Run("registration with short password", func(t *testing.T) {
req := requests.RegisterRequest{
Username: "testuser",
Email: "test@test.com",
Password: "short", // Less than 8 chars
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("registration with duplicate username", func(t *testing.T) {
// First registration
req := requests.RegisterRequest{
Username: "duplicate",
Email: "unique1@test.com",
Password: "password123",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusCreated)
// Try to register again with same username
req.Email = "unique2@test.com"
w = testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
response := testutil.ParseJSON(t, w.Body.Bytes())
assert.Contains(t, response["error"], "Username already taken")
})
t.Run("registration with duplicate email", func(t *testing.T) {
// First registration
req := requests.RegisterRequest{
Username: "user1",
Email: "duplicate@test.com",
Password: "password123",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusCreated)
// Try to register again with same email
req.Username = "user2"
w = testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
response := testutil.ParseJSON(t, w.Body.Bytes())
assert.Contains(t, response["error"], "Email already registered")
})
}
func TestAuthHandler_Login(t *testing.T) {
handler, router, _ := setupAuthHandler(t)
router.POST("/api/auth/register/", handler.Register)
router.POST("/api/auth/login/", handler.Login)
// Create a test user
registerReq := requests.RegisterRequest{
Username: "logintest",
Email: "login@test.com",
Password: "password123",
FirstName: "Test",
LastName: "User",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", registerReq, "")
testutil.AssertStatusCode(t, w, http.StatusCreated)
t.Run("successful login with username", func(t *testing.T) {
req := requests.LoginRequest{
Username: "logintest",
Password: "password123",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
testutil.AssertJSONFieldExists(t, response, "token")
testutil.AssertJSONFieldExists(t, response, "user")
user := response["user"].(map[string]interface{})
assert.Equal(t, "logintest", user["username"])
assert.Equal(t, "login@test.com", user["email"])
})
t.Run("successful login with email", func(t *testing.T) {
req := requests.LoginRequest{
Username: "login@test.com", // Using email as username
Password: "password123",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
testutil.AssertStatusCode(t, w, http.StatusOK)
})
t.Run("login with wrong password", func(t *testing.T) {
req := requests.LoginRequest{
Username: "logintest",
Password: "wrongpassword",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
response := testutil.ParseJSON(t, w.Body.Bytes())
assert.Contains(t, response["error"], "Invalid credentials")
})
t.Run("login with non-existent user", func(t *testing.T) {
req := requests.LoginRequest{
Username: "nonexistent",
Password: "password123",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
})
t.Run("login with missing fields", func(t *testing.T) {
req := map[string]string{
"username": "logintest",
// Missing password
}
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
}
func TestAuthHandler_CurrentUser(t *testing.T) {
handler, router, userRepo := setupAuthHandler(t)
db := testutil.SetupTestDB(t)
user := testutil.CreateTestUser(t, db, "metest", "me@test.com", "password123")
user.FirstName = "Test"
user.LastName = "User"
userRepo.Update(user)
// Set up route with mock auth middleware
authGroup := router.Group("/api/auth")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/me/", handler.CurrentUser)
t.Run("get current user", func(t *testing.T) {
w := testutil.MakeRequest(router, "GET", "/api/auth/me/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "metest", response["username"])
assert.Equal(t, "me@test.com", response["email"])
})
}
func TestAuthHandler_UpdateProfile(t *testing.T) {
handler, router, userRepo := setupAuthHandler(t)
db := testutil.SetupTestDB(t)
user := testutil.CreateTestUser(t, db, "updatetest", "update@test.com", "password123")
userRepo.Update(user)
authGroup := router.Group("/api/auth")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.PUT("/profile/", handler.UpdateProfile)
t.Run("update profile", func(t *testing.T) {
firstName := "Updated"
lastName := "Name"
req := requests.UpdateProfileRequest{
FirstName: &firstName,
LastName: &lastName,
}
w := testutil.MakeRequest(router, "PUT", "/api/auth/profile/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "Updated", response["first_name"])
assert.Equal(t, "Name", response["last_name"])
})
}
func TestAuthHandler_ForgotPassword(t *testing.T) {
handler, router, _ := setupAuthHandler(t)
router.POST("/api/auth/register/", handler.Register)
router.POST("/api/auth/forgot-password/", handler.ForgotPassword)
// Create a test user
registerReq := requests.RegisterRequest{
Username: "forgottest",
Email: "forgot@test.com",
Password: "password123",
}
testutil.MakeRequest(router, "POST", "/api/auth/register/", registerReq, "")
t.Run("forgot password with valid email", func(t *testing.T) {
req := requests.ForgotPasswordRequest{
Email: "forgot@test.com",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/forgot-password/", req, "")
// Always returns 200 to prevent email enumeration
testutil.AssertStatusCode(t, w, http.StatusOK)
response := testutil.ParseJSON(t, w.Body.Bytes())
testutil.AssertJSONFieldExists(t, response, "message")
})
t.Run("forgot password with invalid email", func(t *testing.T) {
req := requests.ForgotPasswordRequest{
Email: "nonexistent@test.com",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/forgot-password/", req, "")
// Still returns 200 to prevent email enumeration
testutil.AssertStatusCode(t, w, http.StatusOK)
})
}
func TestAuthHandler_Logout(t *testing.T) {
handler, router, userRepo := setupAuthHandler(t)
db := testutil.SetupTestDB(t)
user := testutil.CreateTestUser(t, db, "logouttest", "logout@test.com", "password123")
userRepo.Update(user)
authGroup := router.Group("/api/auth")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/logout/", handler.Logout)
t.Run("successful logout", func(t *testing.T) {
w := testutil.MakeRequest(router, "POST", "/api/auth/logout/", nil, "test-token")
testutil.AssertStatusCode(t, w, http.StatusOK)
response := testutil.ParseJSON(t, w.Body.Bytes())
assert.Contains(t, response["message"], "Logged out successfully")
})
}
func TestAuthHandler_JSONResponses(t *testing.T) {
handler, router, _ := setupAuthHandler(t)
router.POST("/api/auth/register/", handler.Register)
router.POST("/api/auth/login/", handler.Login)
t.Run("register response has correct JSON structure", func(t *testing.T) {
req := requests.RegisterRequest{
Username: "jsontest",
Email: "json@test.com",
Password: "password123",
FirstName: "JSON",
LastName: "Test",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusCreated)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Verify top-level structure
assert.Contains(t, response, "token")
assert.Contains(t, response, "user")
assert.Contains(t, response, "message")
// Verify token is not empty
assert.NotEmpty(t, response["token"])
// Verify user structure
user := response["user"].(map[string]interface{})
assert.Contains(t, user, "id")
assert.Contains(t, user, "username")
assert.Contains(t, user, "email")
assert.Contains(t, user, "first_name")
assert.Contains(t, user, "last_name")
assert.Contains(t, user, "is_active")
assert.Contains(t, user, "date_joined")
// Verify types
assert.IsType(t, float64(0), user["id"]) // JSON numbers are float64
assert.IsType(t, "", user["username"])
assert.IsType(t, "", user["email"])
assert.IsType(t, true, user["is_active"])
})
t.Run("error response has correct JSON structure", func(t *testing.T) {
req := map[string]string{
"username": "test",
}
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "error")
assert.IsType(t, "", response["error"])
})
}