package integration import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/casera-api/internal/config" "github.com/treytartt/casera-api/internal/handlers" "github.com/treytartt/casera-api/internal/middleware" "github.com/treytartt/casera-api/internal/repositories" "github.com/treytartt/casera-api/internal/services" "github.com/treytartt/casera-api/internal/testutil" "gorm.io/gorm" ) // TestApp holds all components for integration testing type TestApp struct { DB *gorm.DB Router *gin.Engine AuthHandler *handlers.AuthHandler ResidenceHandler *handlers.ResidenceHandler TaskHandler *handlers.TaskHandler ContractorHandler *handlers.ContractorHandler UserRepo *repositories.UserRepository ResidenceRepo *repositories.ResidenceRepository TaskRepo *repositories.TaskRepository ContractorRepo *repositories.ContractorRepository AuthService *services.AuthService } func setupIntegrationTest(t *testing.T) *TestApp { gin.SetMode(gin.TestMode) db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) // Create repositories userRepo := repositories.NewUserRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) // Create config cfg := &config.Config{ Security: config.SecurityConfig{ SecretKey: "test-secret-key-for-integration-tests", PasswordResetExpiry: 15 * time.Minute, ConfirmationExpiry: 24 * time.Hour, MaxPasswordResetRate: 3, }, } // Create services authService := services.NewAuthService(userRepo, cfg) residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) taskService := services.NewTaskService(taskRepo, residenceRepo) // Create handlers authHandler := handlers.NewAuthHandler(authService, nil, nil) residenceHandler := handlers.NewResidenceHandler(residenceService, nil, nil) taskHandler := handlers.NewTaskHandler(taskService, nil) // Create router with real middleware router := gin.New() // Public routes auth := router.Group("/api/auth") { auth.POST("/register", authHandler.Register) auth.POST("/login", authHandler.Login) } // Protected routes - use AuthMiddleware without Redis cache for testing authMiddleware := middleware.NewAuthMiddleware(db, nil) api := router.Group("/api") api.Use(authMiddleware.TokenAuth()) { api.GET("/auth/me", authHandler.CurrentUser) api.POST("/auth/logout", authHandler.Logout) residences := api.Group("/residences") { residences.GET("", residenceHandler.ListResidences) residences.POST("", residenceHandler.CreateResidence) residences.GET("/:id", residenceHandler.GetResidence) residences.PUT("/:id", residenceHandler.UpdateResidence) residences.DELETE("/:id", residenceHandler.DeleteResidence) residences.POST("/:id/generate-share-code", residenceHandler.GenerateShareCode) residences.GET("/:id/users", residenceHandler.GetResidenceUsers) residences.DELETE("/:id/users/:userId", residenceHandler.RemoveResidenceUser) } api.POST("/residences/join-with-code", residenceHandler.JoinWithCode) api.GET("/residence-types", residenceHandler.GetResidenceTypes) tasks := api.Group("/tasks") { tasks.GET("", taskHandler.ListTasks) tasks.POST("", taskHandler.CreateTask) tasks.GET("/:id", taskHandler.GetTask) tasks.PUT("/:id", taskHandler.UpdateTask) tasks.DELETE("/:id", taskHandler.DeleteTask) tasks.POST("/:id/cancel", taskHandler.CancelTask) tasks.POST("/:id/uncancel", taskHandler.UncancelTask) tasks.POST("/:id/archive", taskHandler.ArchiveTask) tasks.POST("/:id/unarchive", taskHandler.UnarchiveTask) tasks.POST("/:id/mark-in-progress", taskHandler.MarkInProgress) } api.GET("/tasks/by-residence/:residence_id", taskHandler.GetTasksByResidence) completions := api.Group("/completions") { completions.GET("", taskHandler.ListCompletions) completions.POST("", taskHandler.CreateCompletion) completions.GET("/:id", taskHandler.GetCompletion) completions.DELETE("/:id", taskHandler.DeleteCompletion) } api.GET("/task-categories", taskHandler.GetCategories) api.GET("/task-priorities", taskHandler.GetPriorities) api.GET("/task-statuses", taskHandler.GetStatuses) api.GET("/task-frequencies", taskHandler.GetFrequencies) } return &TestApp{ DB: db, Router: router, AuthHandler: authHandler, ResidenceHandler: residenceHandler, TaskHandler: taskHandler, UserRepo: userRepo, ResidenceRepo: residenceRepo, TaskRepo: taskRepo, AuthService: authService, } } // Helper to make authenticated requests func (app *TestApp) makeAuthenticatedRequest(t *testing.T, method, path string, body interface{}, token string) *httptest.ResponseRecorder { var reqBody []byte var err error if body != nil { reqBody, err = json.Marshal(body) require.NoError(t, err) } req := httptest.NewRequest(method, path, bytes.NewBuffer(reqBody)) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Token "+token) } w := httptest.NewRecorder() app.Router.ServeHTTP(w, req) return w } // Helper to register and login a user, returns token func (app *TestApp) registerAndLogin(t *testing.T, username, email, password string) string { // Register registerBody := map[string]string{ "username": username, "email": email, "password": password, } w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "") require.Equal(t, http.StatusCreated, w.Code) // Login loginBody := map[string]string{ "username": username, "password": password, } w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/login", loginBody, "") require.Equal(t, http.StatusOK, w.Code) var loginResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &loginResp) require.NoError(t, err) return loginResp["token"].(string) } // ============ Authentication Flow Tests ============ func TestIntegration_AuthenticationFlow(t *testing.T) { app := setupIntegrationTest(t) // 1. Register a new user registerBody := map[string]string{ "username": "testuser", "email": "test@example.com", "password": "SecurePass123!", } w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "") assert.Equal(t, http.StatusCreated, w.Code) var registerResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), ®isterResp) require.NoError(t, err) assert.NotEmpty(t, registerResp["token"]) assert.NotNil(t, registerResp["user"]) // 2. Login with the same credentials loginBody := map[string]string{ "username": "testuser", "password": "SecurePass123!", } w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/login", loginBody, "") assert.Equal(t, http.StatusOK, w.Code) var loginResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &loginResp) require.NoError(t, err) token := loginResp["token"].(string) assert.NotEmpty(t, token) // 3. Get current user with token w = app.makeAuthenticatedRequest(t, "GET", "/api/auth/me", nil, token) assert.Equal(t, http.StatusOK, w.Code) var meResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &meResp) require.NoError(t, err) assert.Equal(t, "testuser", meResp["username"]) assert.Equal(t, "test@example.com", meResp["email"]) // 4. Access protected route without token should fail w = app.makeAuthenticatedRequest(t, "GET", "/api/auth/me", nil, "") assert.Equal(t, http.StatusUnauthorized, w.Code) // 5. Access protected route with invalid token should fail w = app.makeAuthenticatedRequest(t, "GET", "/api/auth/me", nil, "invalid-token") assert.Equal(t, http.StatusUnauthorized, w.Code) // 6. Logout w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/logout", nil, token) assert.Equal(t, http.StatusOK, w.Code) } func TestIntegration_RegistrationValidation(t *testing.T) { app := setupIntegrationTest(t) tests := []struct { name string body map[string]string expectedStatus int }{ { name: "missing username", body: map[string]string{"email": "test@example.com", "password": "pass123"}, expectedStatus: http.StatusBadRequest, }, { name: "missing email", body: map[string]string{"username": "testuser", "password": "pass123"}, expectedStatus: http.StatusBadRequest, }, { name: "missing password", body: map[string]string{"username": "testuser", "email": "test@example.com"}, expectedStatus: http.StatusBadRequest, }, { name: "invalid email", body: map[string]string{"username": "testuser", "email": "invalid", "password": "pass123"}, expectedStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", tt.body, "") assert.Equal(t, tt.expectedStatus, w.Code) }) } } func TestIntegration_DuplicateRegistration(t *testing.T) { app := setupIntegrationTest(t) // Register first user (password must be >= 8 chars) registerBody := map[string]string{ "username": "testuser", "email": "test@example.com", "password": "password123", } w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "") assert.Equal(t, http.StatusCreated, w.Code) // Try to register with same username - returns 400 (BadRequest) registerBody2 := map[string]string{ "username": "testuser", "email": "different@example.com", "password": "password123", } w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody2, "") assert.Equal(t, http.StatusBadRequest, w.Code) // Try to register with same email - returns 400 (BadRequest) registerBody3 := map[string]string{ "username": "differentuser", "email": "test@example.com", "password": "password123", } w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody3, "") assert.Equal(t, http.StatusBadRequest, w.Code) } // ============ Residence Flow Tests ============ func TestIntegration_ResidenceFlow(t *testing.T) { app := setupIntegrationTest(t) token := app.registerAndLogin(t, "owner", "owner@test.com", "password123") // 1. Create a residence createBody := map[string]interface{}{ "name": "My House", "street_address": "123 Main St", "city": "Austin", "state_province": "TX", "postal_code": "78701", } w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", createBody, token) assert.Equal(t, http.StatusCreated, w.Code) var createResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &createResp) require.NoError(t, err) residenceID := createResp["id"].(float64) assert.NotZero(t, residenceID) assert.Equal(t, "My House", createResp["name"]) assert.True(t, createResp["is_primary"].(bool)) // 2. Get the residence w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, token) assert.Equal(t, http.StatusOK, w.Code) var getResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &getResp) require.NoError(t, err) assert.Equal(t, "My House", getResp["name"]) // 3. List residences w = app.makeAuthenticatedRequest(t, "GET", "/api/residences", nil, token) assert.Equal(t, http.StatusOK, w.Code) var listResp []map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &listResp) require.NoError(t, err) assert.Len(t, listResp, 1) // 4. Update the residence updateBody := map[string]interface{}{ "name": "My Updated House", "city": "Dallas", } w = app.makeAuthenticatedRequest(t, "PUT", "/api/residences/"+formatID(residenceID), updateBody, token) assert.Equal(t, http.StatusOK, w.Code) var updateResp map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &updateResp) require.NoError(t, err) assert.Equal(t, "My Updated House", updateResp["name"]) assert.Equal(t, "Dallas", updateResp["city"]) // 5. Delete the residence (returns 200 with message, not 204) w = app.makeAuthenticatedRequest(t, "DELETE", "/api/residences/"+formatID(residenceID), nil, token) assert.Equal(t, http.StatusOK, w.Code) // 6. Verify it's deleted (should return 403 - access denied since it doesn't exist/inactive) w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, token) assert.Equal(t, http.StatusForbidden, w.Code) } func TestIntegration_ResidenceSharingFlow(t *testing.T) { app := setupIntegrationTest(t) // Create owner and another user ownerToken := app.registerAndLogin(t, "owner", "owner@test.com", "password123") userToken := app.registerAndLogin(t, "shareduser", "shared@test.com", "password123") // Create residence as owner createBody := map[string]interface{}{ "name": "Shared House", } w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", createBody, ownerToken) require.Equal(t, http.StatusCreated, w.Code) var createResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &createResp) residenceID := createResp["id"].(float64) // Other user cannot access initially w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, userToken) assert.Equal(t, http.StatusForbidden, w.Code) // Generate share code w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/"+formatID(residenceID)+"/generate-share-code", nil, ownerToken) assert.Equal(t, http.StatusOK, w.Code) var shareResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &shareResp) require.NoError(t, err) shareCodeObj, ok := shareResp["share_code"].(map[string]interface{}) require.True(t, ok, "Expected share_code object in response") shareCode := shareCodeObj["code"].(string) assert.Len(t, shareCode, 6) // User joins with code joinBody := map[string]interface{}{ "code": shareCode, } w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/join-with-code", joinBody, userToken) assert.Equal(t, http.StatusOK, w.Code) // Now user can access w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, userToken) assert.Equal(t, http.StatusOK, w.Code) // Get users list - returns array directly, not wrapped in {"users": ...} w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID)+"/users", nil, ownerToken) assert.Equal(t, http.StatusOK, w.Code) var users []interface{} err = json.Unmarshal(w.Body.Bytes(), &users) require.NoError(t, err) assert.Len(t, users, 2) // owner + shared user } // ============ Task Flow Tests ============ func TestIntegration_TaskFlow(t *testing.T) { app := setupIntegrationTest(t) token := app.registerAndLogin(t, "owner", "owner@test.com", "password123") // Create residence first residenceBody := map[string]interface{}{"name": "Task House"} w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, token) require.Equal(t, http.StatusCreated, w.Code) var residenceResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &residenceResp) residenceID := uint(residenceResp["id"].(float64)) // 1. Create a task taskBody := map[string]interface{}{ "residence_id": residenceID, "title": "Fix leaky faucet", "description": "Kitchen faucet is dripping", } w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody, token) assert.Equal(t, http.StatusCreated, w.Code) var taskResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &taskResp) taskID := taskResp["id"].(float64) assert.NotZero(t, taskID) assert.Equal(t, "Fix leaky faucet", taskResp["title"]) // 2. Get the task w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/"+formatID(taskID), nil, token) assert.Equal(t, http.StatusOK, w.Code) // 3. Update the task updateBody := map[string]interface{}{ "title": "Fix kitchen faucet", "description": "Updated description", } w = app.makeAuthenticatedRequest(t, "PUT", "/api/tasks/"+formatID(taskID), updateBody, token) assert.Equal(t, http.StatusOK, w.Code) var updateResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &updateResp) assert.Equal(t, "Fix kitchen faucet", updateResp["title"]) // 4. Mark as in progress w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/mark-in-progress", nil, token) assert.Equal(t, http.StatusOK, w.Code) var progressResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &progressResp) task := progressResp["task"].(map[string]interface{}) status := task["status"].(map[string]interface{}) assert.Equal(t, "In Progress", status["name"]) // 5. Complete the task completionBody := map[string]interface{}{ "task_id": taskID, "notes": "Fixed the faucet", } w = app.makeAuthenticatedRequest(t, "POST", "/api/completions", completionBody, token) assert.Equal(t, http.StatusCreated, w.Code) var completionResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &completionResp) completionID := completionResp["id"].(float64) assert.NotZero(t, completionID) assert.Equal(t, "Fixed the faucet", completionResp["notes"]) // 6. List completions w = app.makeAuthenticatedRequest(t, "GET", "/api/completions?task_id="+formatID(taskID), nil, token) assert.Equal(t, http.StatusOK, w.Code) // 7. Archive the task w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/archive", nil, token) assert.Equal(t, http.StatusOK, w.Code) var archiveResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &archiveResp) archivedTask := archiveResp["task"].(map[string]interface{}) assert.True(t, archivedTask["is_archived"].(bool)) // 8. Unarchive the task w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/unarchive", nil, token) assert.Equal(t, http.StatusOK, w.Code) // 9. Cancel the task w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/cancel", nil, token) assert.Equal(t, http.StatusOK, w.Code) var cancelResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &cancelResp) cancelledTask := cancelResp["task"].(map[string]interface{}) assert.True(t, cancelledTask["is_cancelled"].(bool)) // 10. Delete the task (returns 200 with message, not 204) w = app.makeAuthenticatedRequest(t, "DELETE", "/api/tasks/"+formatID(taskID), nil, token) assert.Equal(t, http.StatusOK, w.Code) } func TestIntegration_TasksByResidenceKanban(t *testing.T) { app := setupIntegrationTest(t) token := app.registerAndLogin(t, "owner", "owner@test.com", "password123") // Create residence residenceBody := map[string]interface{}{"name": "Kanban House"} w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, token) require.Equal(t, http.StatusCreated, w.Code) var residenceResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &residenceResp) residenceID := uint(residenceResp["id"].(float64)) // Create multiple tasks for i := 1; i <= 3; i++ { taskBody := map[string]interface{}{ "residence_id": residenceID, "title": "Task " + formatID(float64(i)), } w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody, token) require.Equal(t, http.StatusCreated, w.Code) } // Get tasks by residence (kanban view) w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/by-residence/"+formatID(float64(residenceID)), nil, token) assert.Equal(t, http.StatusOK, w.Code) var kanbanResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &kanbanResp) columns := kanbanResp["columns"].([]interface{}) assert.Greater(t, len(columns), 0) // Check column structure for _, col := range columns { column := col.(map[string]interface{}) assert.NotEmpty(t, column["name"]) assert.NotEmpty(t, column["display_name"]) assert.NotNil(t, column["tasks"]) assert.NotNil(t, column["count"]) } } // ============ Lookup Data Tests ============ func TestIntegration_LookupEndpoints(t *testing.T) { app := setupIntegrationTest(t) token := app.registerAndLogin(t, "user", "user@test.com", "password123") tests := []struct { name string endpoint string }{ {"residence types", "/api/residence-types"}, {"task categories", "/api/task-categories"}, {"task priorities", "/api/task-priorities"}, {"task statuses", "/api/task-statuses"}, {"task frequencies", "/api/task-frequencies"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := app.makeAuthenticatedRequest(t, "GET", tt.endpoint, nil, token) assert.Equal(t, http.StatusOK, w.Code) // All lookup endpoints return arrays directly var items []interface{} err := json.Unmarshal(w.Body.Bytes(), &items) require.NoError(t, err) assert.Greater(t, len(items), 0) // Check item structure for _, item := range items { obj := item.(map[string]interface{}) assert.NotZero(t, obj["id"]) assert.NotEmpty(t, obj["name"]) } }) } } // ============ Access Control Tests ============ func TestIntegration_CrossUserAccessDenied(t *testing.T) { app := setupIntegrationTest(t) // Create two users with their own residences user1Token := app.registerAndLogin(t, "user1", "user1@test.com", "password123") user2Token := app.registerAndLogin(t, "user2", "user2@test.com", "password123") // User1 creates a residence residenceBody := map[string]interface{}{"name": "User1's House"} w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, user1Token) require.Equal(t, http.StatusCreated, w.Code) var residenceResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &residenceResp) residenceID := residenceResp["id"].(float64) // User1 creates a task taskBody := map[string]interface{}{ "residence_id": residenceID, "title": "User1's Task", } w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody, user1Token) require.Equal(t, http.StatusCreated, w.Code) var taskResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &taskResp) taskID := taskResp["id"].(float64) // User2 cannot access User1's residence w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, user2Token) assert.Equal(t, http.StatusForbidden, w.Code) // User2 cannot access User1's task w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/"+formatID(taskID), nil, user2Token) assert.Equal(t, http.StatusForbidden, w.Code) // User2 cannot update User1's residence updateBody := map[string]interface{}{"name": "Hacked!"} w = app.makeAuthenticatedRequest(t, "PUT", "/api/residences/"+formatID(residenceID), updateBody, user2Token) assert.Equal(t, http.StatusForbidden, w.Code) // User2 cannot delete User1's residence w = app.makeAuthenticatedRequest(t, "DELETE", "/api/residences/"+formatID(residenceID), nil, user2Token) assert.Equal(t, http.StatusForbidden, w.Code) // User2 cannot create task in User1's residence taskBody2 := map[string]interface{}{ "residence_id": residenceID, "title": "Malicious Task", } w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks", taskBody2, user2Token) assert.Equal(t, http.StatusForbidden, w.Code) } // ============ JSON Response Structure Tests ============ func TestIntegration_ResponseStructure(t *testing.T) { app := setupIntegrationTest(t) token := app.registerAndLogin(t, "user", "user@test.com", "password123") // Create residence residenceBody := map[string]interface{}{ "name": "Response Test House", "street_address": "123 Test St", "city": "Austin", "state_province": "TX", "postal_code": "78701", } w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, token) require.Equal(t, http.StatusCreated, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) // Verify all expected fields are present expectedFields := []string{ "id", "owner_id", "name", "street_address", "city", "state_province", "postal_code", "country", "is_primary", "is_active", "created_at", "updated_at", } for _, field := range expectedFields { _, exists := resp[field] assert.True(t, exists, "Expected field %s to be present", field) } // Check that nullable fields can be null assert.Nil(t, resp["bedrooms"]) assert.Nil(t, resp["bathrooms"]) } // ============ Helper Functions ============ func formatID(id float64) string { return fmt.Sprintf("%d", uint(id)) } // setupContractorTest sets up a test environment including contractor routes func setupContractorTest(t *testing.T) *TestApp { gin.SetMode(gin.TestMode) db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) // Create repositories userRepo := repositories.NewUserRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) // Create config cfg := &config.Config{ Security: config.SecurityConfig{ SecretKey: "test-secret-key-for-integration-tests", PasswordResetExpiry: 15 * time.Minute, ConfirmationExpiry: 24 * time.Hour, MaxPasswordResetRate: 3, }, } // Create services authService := services.NewAuthService(userRepo, cfg) residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) taskService := services.NewTaskService(taskRepo, residenceRepo) contractorService := services.NewContractorService(contractorRepo, residenceRepo) // Create handlers authHandler := handlers.NewAuthHandler(authService, nil, nil) residenceHandler := handlers.NewResidenceHandler(residenceService, nil, nil) taskHandler := handlers.NewTaskHandler(taskService, nil) contractorHandler := handlers.NewContractorHandler(contractorService) // Create router with real middleware router := gin.New() // Public routes auth := router.Group("/api/auth") { auth.POST("/register", authHandler.Register) auth.POST("/login", authHandler.Login) } // Protected routes authMiddleware := middleware.NewAuthMiddleware(db, nil) api := router.Group("/api") api.Use(authMiddleware.TokenAuth()) { api.GET("/auth/me", authHandler.CurrentUser) api.POST("/auth/logout", authHandler.Logout) residences := api.Group("/residences") { residences.GET("", residenceHandler.ListResidences) residences.POST("", residenceHandler.CreateResidence) residences.GET("/:id", residenceHandler.GetResidence) residences.PUT("/:id", residenceHandler.UpdateResidence) residences.DELETE("/:id", residenceHandler.DeleteResidence) residences.POST("/:id/generate-share-code", residenceHandler.GenerateShareCode) residences.GET("/:id/users", residenceHandler.GetResidenceUsers) residences.DELETE("/:id/users/:userId", residenceHandler.RemoveResidenceUser) } api.POST("/residences/join-with-code", residenceHandler.JoinWithCode) tasks := api.Group("/tasks") { tasks.GET("", taskHandler.ListTasks) tasks.POST("", taskHandler.CreateTask) tasks.GET("/:id", taskHandler.GetTask) tasks.PUT("/:id", taskHandler.UpdateTask) tasks.DELETE("/:id", taskHandler.DeleteTask) } contractors := api.Group("/contractors") { contractors.GET("", contractorHandler.ListContractors) contractors.POST("", contractorHandler.CreateContractor) contractors.GET("/:id", contractorHandler.GetContractor) contractors.PUT("/:id", contractorHandler.UpdateContractor) contractors.DELETE("/:id", contractorHandler.DeleteContractor) } } return &TestApp{ DB: db, Router: router, AuthHandler: authHandler, ResidenceHandler: residenceHandler, TaskHandler: taskHandler, ContractorHandler: contractorHandler, UserRepo: userRepo, ResidenceRepo: residenceRepo, TaskRepo: taskRepo, ContractorRepo: contractorRepo, AuthService: authService, } }