- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests) - Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests) - Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests) - Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests) - Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests) - Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1870 lines
73 KiB
Go
1870 lines
73 KiB
Go
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)
|
|
})
|
|
}
|
|
|
|
func TestTaskTemplateHandler_GetTemplatesByRegion(t *testing.T) {
|
|
handler, e, db := setupTaskTemplateHandler(t)
|
|
testutil.SeedLookupData(t, db)
|
|
|
|
e.GET("/api/tasks/templates/by-region/", handler.GetTemplatesByRegion)
|
|
|
|
t.Run("missing both state and zip returns 400", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/by-region/", nil, "")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("with state param returns 200", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/by-region/?state=TX", nil, "")
|
|
testutil.AssertStatusCode(t, w, http.StatusOK)
|
|
})
|
|
|
|
t.Run("with zip param returns 200", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/tasks/templates/by-region/?zip=78701", nil, "")
|
|
testutil.AssertStatusCode(t, w, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|