package handlers import ( "encoding/json" "fmt" "net/http" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" "github.com/treytartt/honeydue-api/internal/config" "github.com/treytartt/honeydue-api/internal/dto/requests" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/repositories" "github.com/treytartt/honeydue-api/internal/services" "github.com/treytartt/honeydue-api/internal/testutil" ) // ============================================================================= // Suggestion Handler Tests (previously zero coverage) // ============================================================================= func setupSuggestionHandler(t *testing.T) (*SuggestionHandler, *echo.Echo, *gorm.DB) { db := testutil.SetupTestDB(t) residenceRepo := repositories.NewResidenceRepository(db) suggestionService := services.NewSuggestionService(db, residenceRepo) handler := NewSuggestionHandler(suggestionService) e := testutil.SetupTestRouter() return handler, e, db } func TestSuggestionHandler_GetSuggestions(t *testing.T) { handler, e, db := setupSuggestionHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/suggestions/", handler.GetSuggestions) t.Run("successful suggestions with valid residence", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/suggestions/?residence_id=%d", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("missing residence_id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/suggestions/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("invalid residence_id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/suggestions/?residence_id=abc", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("access denied for other user residence", func(t *testing.T) { otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/suggestions/?residence_id=%d", otherResidence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestSuggestionHandler_NoAuth_Returns401(t *testing.T) { handler, _, _ := setupSuggestionHandler(t) e := testutil.SetupTestRouter() e.GET("/api/tasks/suggestions/", handler.GetSuggestions) w := testutil.MakeRequest(e, "GET", "/api/tasks/suggestions/?residence_id=1", nil, "") testutil.AssertStatusCode(t, w, http.StatusUnauthorized) } // ============================================================================= // Task Handler - Additional Error Path Tests // ============================================================================= func TestTaskHandler_GetTask_InvalidID(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/", handler.GetTask) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestTaskHandler_UpdateTask_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.PUT("/:id/", handler.UpdateTask) t.Run("invalid id returns 400", func(t *testing.T) { newTitle := "Updated" req := requests.UpdateTaskRequest{Title: &newTitle} w := testutil.MakeRequest(e, "PUT", "/api/tasks/invalid/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { newTitle := "Updated" req := requests.UpdateTaskRequest{Title: &newTitle} w := testutil.MakeRequest(e, "PUT", "/api/tasks/99999/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) t.Run("access denied for other user task", func(t *testing.T) { otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") otherTask := testutil.CreateTestTask(t, db, otherResidence.ID, otherUser.ID, "Other Task") newTitle := "Hacked" req := requests.UpdateTaskRequest{Title: &newTitle} w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/tasks/%d/", otherTask.ID), req, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestTaskHandler_DeleteTask_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.DELETE("/:id/", handler.DeleteTask) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", "/api/tasks/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", "/api/tasks/99999/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_QuickComplete(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Quick Complete Me") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/quick-complete/", handler.QuickComplete) t.Run("successful quick complete", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/quick-complete/", task.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/invalid/quick-complete/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/99999/quick-complete/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) t.Run("access denied for other user task", func(t *testing.T) { otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") otherTask := testutil.CreateTestTask(t, db, otherResidence.ID, otherUser.ID, "Other Task") w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/quick-complete/", otherTask.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestTaskHandler_GetTaskCompletions(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Completions Task") // Create a completion completion := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), Notes: "Done", } require.NoError(t, db.Create(completion).Error) authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/completions/", handler.GetTaskCompletions) t.Run("successful get completions", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/%d/completions/", task.ID), 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.Len(t, response, 1) }) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/invalid/completions/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/99999/completions/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_GetCompletion_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/task-completions") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/", handler.GetCompletion) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/task-completions/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/task-completions/99999/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_UpdateCompletion(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Update Completion Task") completion := &models.TaskCompletion{ TaskID: task.ID, CompletedByID: user.ID, CompletedAt: time.Now().UTC(), Notes: "Original notes", } require.NoError(t, db.Create(completion).Error) authGroup := e.Group("/api/task-completions") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.PUT("/:id/", handler.UpdateCompletion) t.Run("successful update", func(t *testing.T) { req := map[string]interface{}{ "notes": "Updated notes", } w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/task-completions/%d/", completion.ID), 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 notes", response["notes"]) }) t.Run("invalid id returns 400", func(t *testing.T) { req := map[string]interface{}{"notes": "test"} w := testutil.MakeRequest(e, "PUT", "/api/task-completions/invalid/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { req := map[string]interface{}{"notes": "test"} w := testutil.MakeRequest(e, "PUT", "/api/task-completions/99999/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_DeleteCompletion_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/task-completions") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.DELETE("/:id/", handler.DeleteCompletion) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", "/api/task-completions/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", "/api/task-completions/99999/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_GetTasksByResidence_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/by-residence/:residence_id/", handler.GetTasksByResidence) t.Run("invalid residence id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/by-residence/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("access denied for other user residence", func(t *testing.T) { otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", otherResidence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) t.Run("days param out of range returns 400", func(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/?days=5000", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestTaskHandler_ListTasks_DaysParam(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 1") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/", handler.ListTasks) t.Run("custom days param", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/?days=60", 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, float64(60), response["days_threshold"]) }) t.Run("days_threshold param (backward compat)", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/?days_threshold=45", 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, float64(45), response["days_threshold"]) }) t.Run("days out of range returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/?days=0", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("days too large returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/?days=4000", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestTaskHandler_CancelTask_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/cancel/", handler.CancelTask) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/invalid/cancel/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/99999/cancel/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_UncancelTask_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/uncancel/", handler.UncancelTask) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/invalid/uncancel/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not cancelled task returns error", func(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Not Cancelled") w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/uncancel/", task.ID), nil, "test-token") // Service does not validate that the task is actually cancelled before uncancelling; // it succeeds silently (sets is_cancelled=false on an already non-cancelled task) testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestTaskHandler_ArchiveTask_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/archive/", handler.ArchiveTask) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/invalid/archive/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/99999/archive/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_UnarchiveTask_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/unarchive/", handler.UnarchiveTask) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/invalid/unarchive/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not archived task returns error", func(t *testing.T) { residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Not Archived") w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/unarchive/", task.ID), nil, "test-token") // Service does not validate that the task is actually archived before unarchiving; // it succeeds silently (sets is_archived=false on an already non-archived task) testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestTaskHandler_MarkInProgress_ErrorPaths(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/tasks") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/mark-in-progress/", handler.MarkInProgress) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/invalid/mark-in-progress/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/tasks/99999/mark-in-progress/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestTaskHandler_CreateCompletion_NoTaskID(t *testing.T) { handler, e, db := setupTaskHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/task-completions") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/", handler.CreateCompletion) t.Run("missing task_id returns 400", func(t *testing.T) { req := map[string]interface{}{ "notes": "No task id", } w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("non-existent task returns 404", func(t *testing.T) { completedAt := time.Now().UTC() req := requests.CreateTaskCompletionRequest{ TaskID: 99999, CompletedAt: &completedAt, } w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } // ============================================================================= // Auth Handler - Additional Coverage // ============================================================================= func TestAuthHandler_AppleSignIn_NotConfigured(t *testing.T) { handler, e, _ := setupAuthHandler(t) e.POST("/api/auth/apple-sign-in/", handler.AppleSignIn) t.Run("returns 500 when apple auth not configured", func(t *testing.T) { req := map[string]interface{}{ "id_token": "fake-token", "user_id": "fake-user-id", } w := testutil.MakeRequest(e, "POST", "/api/auth/apple-sign-in/", req, "") testutil.AssertStatusCode(t, w, http.StatusInternalServerError) }) t.Run("missing identity_token returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/auth/apple-sign-in/", req, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestAuthHandler_GoogleSignIn_NotConfigured(t *testing.T) { handler, e, _ := setupAuthHandler(t) e.POST("/api/auth/google-sign-in/", handler.GoogleSignIn) t.Run("returns 500 when google auth not configured", func(t *testing.T) { req := map[string]interface{}{ "id_token": "fake-token", } w := testutil.MakeRequest(e, "POST", "/api/auth/google-sign-in/", req, "") testutil.AssertStatusCode(t, w, http.StatusInternalServerError) }) t.Run("missing id_token returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/auth/google-sign-in/", req, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } // setupAuthHandlerWithDB is like setupAuthHandler but also returns the underlying *gorm.DB // for tests that need to create records like ConfirmationCode directly. func setupAuthHandlerWithDB(t *testing.T) (*AuthHandler, *echo.Echo, *gorm.DB) { 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) e := testutil.SetupTestRouter() return handler, e, db } func TestAuthHandler_VerifyEmail(t *testing.T) { handler, e, db := setupAuthHandlerWithDB(t) user := testutil.CreateTestUser(t, db, "verifytest", "verify@test.com", "Password123") // Create confirmation code confirmCode := &models.ConfirmationCode{ UserID: user.ID, Code: "123456", ExpiresAt: time.Now().Add(24 * time.Hour), IsUsed: false, } require.NoError(t, db.Create(confirmCode).Error) authGroup := e.Group("/api/auth") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/verify-email/", handler.VerifyEmail) t.Run("successful verification", func(t *testing.T) { req := requests.VerifyEmailRequest{ Code: "123456", } w := testutil.MakeRequest(e, "POST", "/api/auth/verify-email/", 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, true, response["verified"]) }) t.Run("wrong code returns error", func(t *testing.T) { req := requests.VerifyEmailRequest{ Code: "999999", } w := testutil.MakeRequest(e, "POST", "/api/auth/verify-email/", req, "test-token") // Code already used or wrong code assert.True(t, w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound, "expected 400 or 404, got %d", w.Code) }) t.Run("missing code returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/auth/verify-email/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestAuthHandler_ResendVerification(t *testing.T) { handler, e, db := setupAuthHandlerWithDB(t) user := testutil.CreateTestUser(t, db, "resendtest", "resend@test.com", "Password123") authGroup := e.Group("/api/auth") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/resend-verification/", handler.ResendVerification) t.Run("successful resend", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/auth/resend-verification/", 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.Contains(t, response, "message") }) } func TestAuthHandler_RefreshToken(t *testing.T) { handler, e, db := setupAuthHandlerWithDB(t) user := testutil.CreateTestUser(t, db, "refreshtest", "refresh@test.com", "Password123") // Create auth token and use its actual key in the middleware authToken := testutil.CreateTestToken(t, db, user.ID) authGroup := e.Group("/api/auth") authGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { c.Set("auth_user", user) c.Set("auth_token", authToken.Key) return next(c) } }) authGroup.POST("/refresh/", handler.RefreshToken) t.Run("successful refresh", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/auth/refresh/", nil, authToken.Key) testutil.AssertStatusCode(t, w, http.StatusOK) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Contains(t, response, "token") }) } func TestAuthHandler_VerifyResetCode(t *testing.T) { handler, e, _ := setupAuthHandler(t) e.POST("/api/auth/register/", handler.Register) e.POST("/api/auth/verify-reset-code/", handler.VerifyResetCode) t.Run("invalid code returns error", func(t *testing.T) { req := requests.VerifyResetCodeRequest{ Email: "nonexistent@test.com", Code: "999999", } w := testutil.MakeRequest(e, "POST", "/api/auth/verify-reset-code/", req, "") // Should not be 200 since no valid code exists assert.NotEqual(t, http.StatusOK, w.Code) }) t.Run("missing fields returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/auth/verify-reset-code/", req, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestAuthHandler_ResetPassword(t *testing.T) { handler, e, _ := setupAuthHandler(t) e.POST("/api/auth/reset-password/", handler.ResetPassword) t.Run("invalid reset token returns error", func(t *testing.T) { req := requests.ResetPasswordRequest{ ResetToken: "invalid-token", NewPassword: "NewPassword123", } w := testutil.MakeRequest(e, "POST", "/api/auth/reset-password/", req, "") assert.NotEqual(t, http.StatusOK, w.Code) }) t.Run("missing fields returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/auth/reset-password/", req, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("short password returns 400", func(t *testing.T) { req := requests.ResetPasswordRequest{ ResetToken: "some-token", NewPassword: "short", } w := testutil.MakeRequest(e, "POST", "/api/auth/reset-password/", req, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestAuthHandler_ForgotPassword_MissingEmail(t *testing.T) { handler, e, _ := setupAuthHandler(t) e.POST("/api/auth/forgot-password/", handler.ForgotPassword) t.Run("missing email returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/auth/forgot-password/", req, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } // ============================================================================= // Residence Handler - Additional Error Paths // ============================================================================= func TestResidenceHandler_GenerateShareCode_ErrorPaths(t *testing.T) { handler, e, db := setupResidenceHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") // Share with user residenceRepo := repositories.NewResidenceRepository(db) residenceRepo.AddUser(residence.ID, otherUser.ID) authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/generate-share-code/", handler.GenerateShareCode) otherGroup := e.Group("/api/other-residences") otherGroup.Use(testutil.MockAuthMiddleware(otherUser)) otherGroup.POST("/:id/generate-share-code/", handler.GenerateShareCode) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/residences/invalid/generate-share-code/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("non-owner cannot generate share code", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/other-residences/%d/generate-share-code/", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) t.Run("non-existent residence returns error", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/residences/99999/generate-share-code/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestResidenceHandler_GetShareCode_ErrorPaths(t *testing.T) { handler, e, db := setupResidenceHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/share-code/", handler.GetShareCode) otherGroup := e.Group("/api/other-residences") otherGroup.Use(testutil.MockAuthMiddleware(otherUser)) otherGroup.GET("/:id/share-code/", handler.GetShareCode) t.Run("non-member cannot get share code", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/other-residences/%d/share-code/", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) t.Run("non-existent residence returns error", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/residences/99999/share-code/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestResidenceHandler_JoinWithCode_ValidationErrors(t *testing.T) { handler, e, db := setupResidenceHandler(t) testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") newUser := testutil.CreateTestUser(t, db, "newuser", "new@test.com", "Password123") authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(newUser)) authGroup.POST("/join-with-code/", handler.JoinWithCode) t.Run("empty code returns 400", func(t *testing.T) { req := map[string]interface{}{ "code": "", } w := testutil.MakeRequest(e, "POST", "/api/residences/join-with-code/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("missing code returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/residences/join-with-code/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestResidenceHandler_GetResidenceUsers_ErrorPaths(t *testing.T) { handler, e, db := setupResidenceHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") otherGroup := e.Group("/api/other-residences") otherGroup.Use(testutil.MockAuthMiddleware(otherUser)) otherGroup.GET("/:id/users/", handler.GetResidenceUsers) authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/users/", handler.GetResidenceUsers) t.Run("access denied for non-member", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/other-residences/%d/users/", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/residences/invalid/users/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestResidenceHandler_RemoveUser_ErrorPaths(t *testing.T) { handler, e, db := setupResidenceHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Remove Test") residenceRepo := repositories.NewResidenceRepository(db) residenceRepo.AddUser(residence.ID, sharedUser.ID) authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.DELETE("/:id/users/:user_id/", handler.RemoveResidenceUser) sharedGroup := e.Group("/api/shared-residences") sharedGroup.Use(testutil.MockAuthMiddleware(sharedUser)) sharedGroup.DELETE("/:id/users/:user_id/", handler.RemoveResidenceUser) t.Run("invalid residence id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/invalid/users/%d/", sharedUser.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("invalid user id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/users/invalid/", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("non-owner cannot remove users", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/shared-residences/%d/users/%d/", residence.ID, user.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) t.Run("remove non-existent user returns error", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/users/99999/", residence.ID), nil, "test-token") // Service does not verify that the user was actually a member before removing; // the repo delete affects 0 rows but does not return an error, so the handler returns 200 testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestResidenceHandler_GenerateSharePackage_ErrorPaths(t *testing.T) { handler, e, db := setupResidenceHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/generate-share-package/", handler.GenerateSharePackage) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/residences/invalid/generate-share-package/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("non-existent residence returns error", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/residences/99999/generate-share-package/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestResidenceHandler_GenerateTasksReport(t *testing.T) { handler, e, db := setupResidenceHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Report Test") authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/generate-tasks-report/", handler.GenerateTasksReport) t.Run("successful report generation", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/residences/%d/generate-tasks-report/", residence.ID), 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.Contains(t, response, "report") assert.Contains(t, response, "residence_name") }) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/residences/invalid/generate-tasks-report/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("non-existent residence returns error", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/residences/99999/generate-tasks-report/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestResidenceHandler_GenerateTasksReport_Disabled(t *testing.T) { db := testutil.SetupTestDB(t) residenceRepo := repositories.NewResidenceRepository(db) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{} residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) // Create handler with PDF reports DISABLED handler := NewResidenceHandler(residenceService, nil, nil, false) e := testutil.SetupTestRouter() user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Disabled Report Test") authGroup := e.Group("/api/residences") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/generate-tasks-report/", handler.GenerateTasksReport) t.Run("feature disabled returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/residences/%d/generate-tasks-report/", residence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } // ============================================================================= // Contractor Handler - Additional Error Paths // ============================================================================= func TestContractorHandler_ListContractorsByResidence_AccessDenied(t *testing.T) { handler, e, db := setupContractorHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") authGroup := e.Group("/api/contractors") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/by-residence/:residence_id/", handler.ListContractorsByResidence) t.Run("access denied for non-member residence", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/contractors/by-residence/%d/", otherResidence.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestContractorHandler_GetContractor_AccessDenied(t *testing.T) { handler, e, db := setupContractorHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") otherContractor := testutil.CreateTestContractor(t, db, otherResidence.ID, otherUser.ID, "Other Plumber") authGroup := e.Group("/api/contractors") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/", handler.GetContractor) t.Run("access denied for other user contractor", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/contractors/%d/", otherContractor.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestContractorHandler_UpdateContractor_AccessDenied(t *testing.T) { handler, e, db := setupContractorHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") otherContractor := testutil.CreateTestContractor(t, db, otherResidence.ID, otherUser.ID, "Other Plumber") authGroup := e.Group("/api/contractors") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.PUT("/:id/", handler.UpdateContractor) t.Run("access denied for other user contractor", func(t *testing.T) { newName := "Hacked" req := requests.UpdateContractorRequest{Name: &newName} w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/contractors/%d/", otherContractor.ID), req, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestContractorHandler_DeleteContractor_AccessDenied(t *testing.T) { handler, e, db := setupContractorHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") otherContractor := testutil.CreateTestContractor(t, db, otherResidence.ID, otherUser.ID, "Other Plumber") authGroup := e.Group("/api/contractors") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.DELETE("/:id/", handler.DeleteContractor) t.Run("access denied for other user contractor", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/contractors/%d/", otherContractor.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestContractorHandler_ToggleFavorite_AccessDenied(t *testing.T) { handler, e, db := setupContractorHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") otherResidence := testutil.CreateTestResidence(t, db, otherUser.ID, "Other House") otherContractor := testutil.CreateTestContractor(t, db, otherResidence.ID, otherUser.ID, "Other Plumber") authGroup := e.Group("/api/contractors") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/toggle-favorite/", handler.ToggleFavorite) t.Run("access denied for other user contractor", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/contractors/%d/toggle-favorite/", otherContractor.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } func TestContractorHandler_GetContractorTasks_NotFound(t *testing.T) { handler, e, db := setupContractorHandler(t) testutil.SeedLookupData(t, db) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/contractors") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/tasks/", handler.GetContractorTasks) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/contractors/99999/tasks/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } // ============================================================================= // Document Handler - Additional Error Paths // ============================================================================= func TestDocumentHandler_UploadDocumentImage_InvalidIDs(t *testing.T) { handler, e, db := setupDocumentHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/documents") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/:id/images/", handler.UploadDocumentImage) t.Run("invalid document id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/documents/invalid/images/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestDocumentHandler_DeleteDocumentImage_InvalidIDs(t *testing.T) { handler, e, db := setupDocumentHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/documents") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.DELETE("/:id/images/:imageId/", handler.DeleteDocumentImage) t.Run("invalid document id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", "/api/documents/invalid/images/1/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("invalid image id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", "/api/documents/1/images/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestDocumentHandler_ListDocuments_TypeFilter(t *testing.T) { handler, e, db := setupDocumentHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") doc := testutil.CreateTestDocument(t, db, residence.ID, user.ID, "Warranty Doc") require.NoError(t, db.Model(doc).Update("document_type", "warranty").Error) authGroup := e.Group("/api/documents") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/", handler.ListDocuments) t.Run("filter by document_type", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/documents/?document_type=warranty", 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.Len(t, response, 1) }) t.Run("filter by non-matching type returns empty", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/documents/?document_type=insurance", 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.Len(t, response, 0) }) t.Run("filter by expiring_soon valid", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/documents/?expiring_soon=30", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("negative expiring_soon out of range", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/documents/?expiring_soon=-1", nil, "test-token") // -1 parses successfully via Atoi, then fails the <1 range check, returning 400 testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestDocumentHandler_ActivateDeactivate_AccessDenied(t *testing.T) { handler, e, db := setupDocumentHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") doc := testutil.CreateTestDocument(t, db, residence.ID, user.ID, "Test Doc") otherGroup := e.Group("/api/documents") otherGroup.Use(testutil.MockAuthMiddleware(otherUser)) otherGroup.POST("/:id/activate/", handler.ActivateDocument) otherGroup.POST("/:id/deactivate/", handler.DeactivateDocument) t.Run("activate access denied", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/documents/%d/activate/", doc.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) t.Run("deactivate access denied", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/documents/%d/deactivate/", doc.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } // ============================================================================= // Notification Handler - Additional Coverage // ============================================================================= func TestNotificationHandler_RegisterDevice_Android(t *testing.T) { handler, e, db := setupNotificationHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/notifications") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/devices/", handler.RegisterDevice) t.Run("successful android device registration", func(t *testing.T) { req := map[string]interface{}{ "name": "Pixel 8", "device_id": "android-device-123", "registration_id": "android-reg-abc", "platform": "android", } w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusCreated) }) t.Run("duplicate registration updates existing", func(t *testing.T) { req := map[string]interface{}{ "name": "Pixel 8 Updated", "device_id": "android-device-123", "registration_id": "android-reg-abc", "platform": "android", } w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/", req, "test-token") // Should succeed (upsert behavior) assert.True(t, w.Code == http.StatusCreated || w.Code == http.StatusOK, "expected 201 or 200, got %d", w.Code) }) } func TestNotificationHandler_UnregisterDevice_Valid(t *testing.T) { handler, e, db := setupNotificationHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/notifications") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/devices/", handler.RegisterDevice) authGroup.POST("/devices/unregister/", handler.UnregisterDevice) // First register a device regReq := map[string]interface{}{ "name": "iPhone 15", "device_id": "test-device-unreg", "registration_id": "test-reg-unreg", "platform": "ios", } testutil.MakeRequest(e, "POST", "/api/notifications/devices/", regReq, "test-token") t.Run("successful unregister", func(t *testing.T) { req := map[string]interface{}{ "registration_id": "test-reg-unreg", "platform": "ios", } w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/unregister/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("unregister non-existent device", func(t *testing.T) { req := map[string]interface{}{ "registration_id": "nonexistent-reg-id", "platform": "ios", } w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/unregister/", req, "test-token") // Should be 404 or succeed silently assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusNotFound, "expected 200 or 404, got %d", w.Code) }) } func TestNotificationHandler_DeleteDevice_Valid(t *testing.T) { handler, e, db := setupNotificationHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") // Create a device directly apnsDevice := &models.APNSDevice{ UserID: &user.ID, Name: "Test iPhone", DeviceID: "delete-device-id", RegistrationID: "delete-reg-id", Active: true, } require.NoError(t, db.Create(apnsDevice).Error) authGroup := e.Group("/api/notifications") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.DELETE("/devices/:id/", handler.DeleteDevice) t.Run("successful delete with platform", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/notifications/devices/%d/?platform=ios", apnsDevice.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("not found after delete", func(t *testing.T) { w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/notifications/devices/%d/?platform=ios", apnsDevice.ID), nil, "test-token") // Device is deactivated (active=false) rather than deleted, so the second call // still finds the device and "deactivates" it again, returning 200 testutil.AssertStatusCode(t, w, http.StatusOK) }) } // ============================================================================= // User Handler Tests (previously zero functional tests) // ============================================================================= func setupUserHandler(t *testing.T) (*UserHandler, *echo.Echo, *gorm.DB) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) userService := services.NewUserService(userRepo) handler := NewUserHandler(userService) e := testutil.SetupTestRouter() return handler, e, db } func TestUserHandler_ListUsers(t *testing.T) { handler, e, db := setupUserHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123") // Create residence and share it residence := testutil.CreateTestResidence(t, db, user.ID, "Shared House") residenceRepo := repositories.NewResidenceRepository(db) residenceRepo.AddUser(residence.ID, sharedUser.ID) authGroup := e.Group("/api/users") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/", handler.ListUsers) t.Run("list users in shared residences", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/users/", 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.Contains(t, response, "count") assert.Contains(t, response, "results") }) } func TestUserHandler_GetUser(t *testing.T) { handler, e, db := setupUserHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "Password123") outsideUser := testutil.CreateTestUser(t, db, "outside", "outside@test.com", "Password123") // Create residence and share it residence := testutil.CreateTestResidence(t, db, user.ID, "Shared House") residenceRepo := repositories.NewResidenceRepository(db) residenceRepo.AddUser(residence.ID, sharedUser.ID) authGroup := e.Group("/api/users") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/:id/", handler.GetUser) t.Run("get shared user", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/users/%d/", sharedUser.ID), nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("access denied for non-shared user", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/users/%d/", outsideUser.ID), nil, "test-token") // Service returns "user not found" (404) rather than "forbidden" (403) for // non-shared users — this avoids revealing that a user exists testutil.AssertStatusCode(t, w, http.StatusNotFound) }) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/users/invalid/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("not found returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/users/99999/", nil, "test-token") assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusForbidden, "expected 404 or 403, got %d", w.Code) }) } func TestUserHandler_ListProfiles(t *testing.T) { handler, e, db := setupUserHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/users") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/profiles/", handler.ListProfiles) t.Run("successful list profiles", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/users/profiles/", 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.Contains(t, response, "count") assert.Contains(t, response, "results") }) } // ============================================================================= // Subscription Handler Tests (previously zero functional tests) // ============================================================================= func setupSubscriptionHandler(t *testing.T) (*SubscriptionHandler, *echo.Echo, *gorm.DB) { db := testutil.SetupTestDB(t) subscriptionRepo := repositories.NewSubscriptionRepository(db) residenceRepo := repositories.NewResidenceRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) documentRepo := repositories.NewDocumentRepository(db) subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo) handler := NewSubscriptionHandler(subscriptionService, nil) e := testutil.SetupTestRouter() return handler, e, db } func TestSubscriptionHandler_GetSubscription(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/", handler.GetSubscription) t.Run("get subscription for new user", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/subscription/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestSubscriptionHandler_GetSubscriptionStatus(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/status/", handler.GetSubscriptionStatus) t.Run("get subscription status", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/subscription/status/", 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.Contains(t, response, "tier") }) } func TestSubscriptionHandler_ProcessPurchase_ValidationErrors(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/purchase/", handler.ProcessPurchase) t.Run("missing fields returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/subscription/purchase/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("invalid platform returns 400", func(t *testing.T) { req := map[string]interface{}{ "platform": "windows", } w := testutil.MakeRequest(e, "POST", "/api/subscription/purchase/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("ios without receipt returns 400", func(t *testing.T) { req := map[string]interface{}{ "platform": "ios", } w := testutil.MakeRequest(e, "POST", "/api/subscription/purchase/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("android without purchase token returns 400", func(t *testing.T) { req := map[string]interface{}{ "platform": "android", } w := testutil.MakeRequest(e, "POST", "/api/subscription/purchase/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestSubscriptionHandler_RestoreSubscription_ValidationErrors(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/restore/", handler.RestoreSubscription) t.Run("missing fields returns 400", func(t *testing.T) { req := map[string]interface{}{} w := testutil.MakeRequest(e, "POST", "/api/subscription/restore/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("ios without receipt returns 400", func(t *testing.T) { req := map[string]interface{}{ "platform": "ios", } w := testutil.MakeRequest(e, "POST", "/api/subscription/restore/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestSubscriptionHandler_GetPromotions(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.GET("/promotions/", handler.GetPromotions) t.Run("get promotions for user", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/subscription/promotions/", nil, "test-token") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestSubscriptionHandler_GetFeatureBenefits(t *testing.T) { handler, e, _ := setupSubscriptionHandler(t) e.GET("/api/subscription/features/", handler.GetFeatureBenefits) t.Run("get feature benefits", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/subscription/features/", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestSubscriptionHandler_GetAllUpgradeTriggers(t *testing.T) { handler, e, _ := setupSubscriptionHandler(t) e.GET("/api/subscription/upgrade-triggers/", handler.GetAllUpgradeTriggers) t.Run("get all upgrade triggers", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/subscription/upgrade-triggers/", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestSubscriptionHandler_GetUpgradeTrigger(t *testing.T) { handler, e, _ := setupSubscriptionHandler(t) e.GET("/api/subscription/upgrade-trigger/:key/", handler.GetUpgradeTrigger) t.Run("non-existent trigger key returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/subscription/upgrade-trigger/nonexistent/", nil, "") testutil.AssertStatusCode(t, w, http.StatusNotFound) }) } func TestSubscriptionHandler_CancelSubscription(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/cancel/", handler.CancelSubscription) t.Run("cancel subscription for free user", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/subscription/cancel/", nil, "test-token") // Free user cancelling might return 400 or success assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusBadRequest, "expected 200 or 400, got %d", w.Code) }) } func TestSubscriptionHandler_CreateCheckoutSession_NotConfigured(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/checkout/", handler.CreateCheckoutSession) t.Run("stripe not configured returns 400", func(t *testing.T) { req := map[string]interface{}{ "price_id": "price_123", "success_url": "https://example.com/success", "cancel_url": "https://example.com/cancel", } w := testutil.MakeRequest(e, "POST", "/api/subscription/checkout/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestSubscriptionHandler_CreatePortalSession_NotConfigured(t *testing.T) { handler, e, db := setupSubscriptionHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123") authGroup := e.Group("/api/subscription") authGroup.Use(testutil.MockAuthMiddleware(user)) authGroup.POST("/portal/", handler.CreatePortalSession) t.Run("stripe not configured returns 400", func(t *testing.T) { req := map[string]interface{}{ "return_url": "https://example.com/return", } w := testutil.MakeRequest(e, "POST", "/api/subscription/portal/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } // ============================================================================= // Task Template Handler Tests (previously zero coverage) // ============================================================================= func setupTaskTemplateHandler(t *testing.T) (*TaskTemplateHandler, *echo.Echo, *gorm.DB) { db := testutil.SetupTestDB(t) templateRepo := repositories.NewTaskTemplateRepository(db) templateService := services.NewTaskTemplateService(templateRepo) handler := NewTaskTemplateHandler(templateService) e := testutil.SetupTestRouter() return handler, e, db } func TestTaskTemplateHandler_GetTemplates(t *testing.T) { handler, e, db := setupTaskTemplateHandler(t) testutil.SeedLookupData(t, db) e.GET("/api/tasks/templates/", handler.GetTemplates) t.Run("get all templates", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestTaskTemplateHandler_GetTemplatesGrouped(t *testing.T) { handler, e, db := setupTaskTemplateHandler(t) testutil.SeedLookupData(t, db) e.GET("/api/tasks/templates/grouped/", handler.GetTemplatesGrouped) t.Run("get grouped templates", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/grouped/", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestTaskTemplateHandler_SearchTemplates(t *testing.T) { handler, e, db := setupTaskTemplateHandler(t) testutil.SeedLookupData(t, db) e.GET("/api/tasks/templates/search/", handler.SearchTemplates) t.Run("missing query returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/search/", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("query too short returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/search/?q=a", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("valid query returns 200", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/search/?q=plumbing", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) } func TestTaskTemplateHandler_GetTemplatesByCategory(t *testing.T) { handler, e, db := setupTaskTemplateHandler(t) testutil.SeedLookupData(t, db) e.GET("/api/tasks/templates/by-category/:category_id/", handler.GetTemplatesByCategory) t.Run("valid category id", func(t *testing.T) { var cat models.TaskCategory db.First(&cat) w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/templates/by-category/%d/", cat.ID), nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) t.Run("invalid category id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/by-category/invalid/", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } // NOTE: TestTaskTemplateHandler_GetTemplatesByRegion was removed. The // /api/tasks/templates/by-region/ endpoint was deleted; climate-zone // affinity is now a JSON condition on each template and is scored by the // main /api/tasks/suggestions/ endpoint (see SuggestionService tests). func TestTaskTemplateHandler_GetTemplate(t *testing.T) { handler, e, db := setupTaskTemplateHandler(t) testutil.SeedLookupData(t, db) e.GET("/api/tasks/templates/:id/", handler.GetTemplate) t.Run("invalid id returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/invalid/", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) t.Run("non-existent id returns 404", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/99999/", nil, "") // Service does not wrap gorm.ErrRecordNotFound as a NotFound error, // so the raw error falls through to the global error handler as 500 testutil.AssertStatusCode(t, w, http.StatusInternalServerError) }) } // ============================================================================= // Tracking Handler Tests (previously zero coverage) // ============================================================================= func TestTrackingHandler_TrackEmailOpen(t *testing.T) { handler := NewTrackingHandler(nil) // nil service -- won't record, but returns pixel e := testutil.SetupTestRouter() e.GET("/api/track/open/:trackingID", handler.TrackEmailOpen) t.Run("returns transparent GIF", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/track/open/test-tracking-id", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) assert.Equal(t, "image/gif", w.Header().Get("Content-Type")) assert.Equal(t, "no-store, no-cache, must-revalidate, proxy-revalidate", w.Header().Get("Cache-Control")) assert.Greater(t, w.Body.Len(), 0) }) t.Run("empty tracking id still returns GIF", func(t *testing.T) { e2 := testutil.SetupTestRouter() e2.GET("/api/track/open/", handler.TrackEmailOpen) w := testutil.MakeRequest(e2, "GET", "/api/track/open/", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) }) } // ============================================================================= // Static Data Handler Tests (previously zero coverage) // ============================================================================= func setupStaticDataHandler(t *testing.T) (*StaticDataHandler, *echo.Echo, *gorm.DB) { db := testutil.SetupTestDB(t) testutil.SeedLookupData(t, db) residenceRepo := repositories.NewResidenceRepository(db) userRepo := repositories.NewUserRepository(db) taskRepo := repositories.NewTaskRepository(db) contractorRepo := repositories.NewContractorRepository(db) templateRepo := repositories.NewTaskTemplateRepository(db) cfg := &config.Config{} residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg) taskService := services.NewTaskService(taskRepo, residenceRepo) contractorService := services.NewContractorService(contractorRepo, residenceRepo) templateService := services.NewTaskTemplateService(templateRepo) handler := NewStaticDataHandler(residenceService, taskService, contractorService, templateService, nil) e := testutil.SetupTestRouter() return handler, e, db } func TestStaticDataHandler_GetStaticData(t *testing.T) { handler, e, _ := setupStaticDataHandler(t) e.GET("/api/static_data/", handler.GetStaticData) t.Run("returns all lookup data", func(t *testing.T) { w := testutil.MakeRequest(e, "GET", "/api/static_data/", nil, "") testutil.AssertStatusCode(t, w, http.StatusOK) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Contains(t, response, "residence_types") assert.Contains(t, response, "task_categories") assert.Contains(t, response, "task_priorities") assert.Contains(t, response, "task_frequencies") assert.Contains(t, response, "contractor_specialties") assert.Contains(t, response, "task_templates") }) } func TestStaticDataHandler_RefreshStaticData(t *testing.T) { handler, e, _ := setupStaticDataHandler(t) e.POST("/api/static_data/refresh/", handler.RefreshStaticData) t.Run("returns success", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/static_data/refresh/", nil, "") 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, "success", response["status"]) }) } // ============================================================================= // Upload Handler - Additional Error Paths // ============================================================================= func TestUploadHandler_UploadImage_NoFile(t *testing.T) { storageSvc := newTestStorageService("/var/uploads") handler := NewUploadHandler(storageSvc, nil) e := testutil.SetupTestRouter() e.POST("/api/uploads/image", handler.UploadImage) t.Run("no file returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/uploads/image", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestUploadHandler_UploadDocument_NoFile(t *testing.T) { storageSvc := newTestStorageService("/var/uploads") handler := NewUploadHandler(storageSvc, nil) e := testutil.SetupTestRouter() e.POST("/api/uploads/document", handler.UploadDocument) t.Run("no file returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/uploads/document", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestUploadHandler_UploadCompletion_NoFile(t *testing.T) { storageSvc := newTestStorageService("/var/uploads") handler := NewUploadHandler(storageSvc, nil) e := testutil.SetupTestRouter() e.POST("/api/uploads/completion", handler.UploadCompletion) t.Run("no file returns 400", func(t *testing.T) { w := testutil.MakeRequest(e, "POST", "/api/uploads/completion", nil, "") testutil.AssertStatusCode(t, w, http.StatusBadRequest) }) } func TestUploadHandler_DeleteFile_OwnershipDenied(t *testing.T) { storageSvc := newTestStorageService("/var/uploads") // Mock ownership checker that always denies checker := &mockOwnershipChecker{owned: false} handler := NewUploadHandler(storageSvc, checker) e := testutil.SetupTestRouter() testUser := &models.User{FirstName: "Test", Email: "test@test.com"} testUser.ID = 1 authGroup := e.Group("/api") authGroup.Use(testutil.MockAuthMiddleware(testUser)) authGroup.DELETE("/uploads/", handler.DeleteFile) t.Run("ownership denied returns 403", func(t *testing.T) { req := map[string]string{"url": "/uploads/images/test.jpg"} w := testutil.MakeRequest(e, "DELETE", "/api/uploads/", req, "test-token") testutil.AssertStatusCode(t, w, http.StatusForbidden) }) } // mockOwnershipChecker implements FileOwnershipChecker for testing type mockOwnershipChecker struct { owned bool } func (m *mockOwnershipChecker) IsFileOwnedByUser(fileURL string, userID uint) (bool, error) { return m.owned, nil }