- 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>
409 lines
15 KiB
Go
409 lines
15 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
|
|
"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"
|
|
)
|
|
|
|
func setupNotificationHandler(t *testing.T) (*NotificationHandler, *echo.Echo, *gorm.DB) {
|
|
db := testutil.SetupTestDB(t)
|
|
notifRepo := repositories.NewNotificationRepository(db)
|
|
notifService := services.NewNotificationService(notifRepo, nil)
|
|
handler := NewNotificationHandler(notifService)
|
|
e := testutil.SetupTestRouter()
|
|
return handler, e, db
|
|
}
|
|
|
|
func createTestNotifications(t *testing.T, db *gorm.DB, userID uint, count int) {
|
|
for i := 0; i < count; i++ {
|
|
notif := &models.Notification{
|
|
UserID: userID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: fmt.Sprintf("Test Notification %d", i+1),
|
|
Body: fmt.Sprintf("Body %d", i+1),
|
|
}
|
|
err := db.Create(notif).Error
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestNotificationHandler_ListNotifications_LimitCappedAt200(t *testing.T) {
|
|
handler, e, db := setupNotificationHandler(t)
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
|
|
|
// Create 210 notifications to exceed the cap
|
|
createTestNotifications(t, db, user.ID, 210)
|
|
|
|
authGroup := e.Group("/api/notifications")
|
|
authGroup.Use(testutil.MockAuthMiddleware(user))
|
|
authGroup.GET("/", handler.ListNotifications)
|
|
|
|
t.Run("limit is capped at 200 when user requests more", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/?limit=999", 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)
|
|
|
|
count := int(response["count"].(float64))
|
|
assert.Equal(t, 200, count, "response should contain at most 200 notifications when limit exceeds cap")
|
|
})
|
|
|
|
t.Run("limit below cap is respected", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/?limit=10", 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)
|
|
|
|
count := int(response["count"].(float64))
|
|
assert.Equal(t, 10, count, "response should respect limit when below cap")
|
|
})
|
|
|
|
t.Run("default limit is used when no limit param", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/", 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)
|
|
|
|
count := int(response["count"].(float64))
|
|
assert.Equal(t, 50, count, "response should use default limit of 50")
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_ListNotifications_Pagination(t *testing.T) {
|
|
handler, e, db := setupNotificationHandler(t)
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
createTestNotifications(t, db, user.ID, 20)
|
|
|
|
authGroup := e.Group("/api/notifications")
|
|
authGroup.Use(testutil.MockAuthMiddleware(user))
|
|
authGroup.GET("/", handler.ListNotifications)
|
|
|
|
t.Run("offset skips notifications", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/?limit=5&offset=15", 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)
|
|
|
|
count := int(response["count"].(float64))
|
|
assert.Equal(t, 5, count, "should return remaining 5 after offset 15")
|
|
})
|
|
|
|
t.Run("response has results array", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/?limit=3", 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, "results")
|
|
assert.Contains(t, response, "count")
|
|
results := response["results"].([]interface{})
|
|
assert.Len(t, results, 3)
|
|
})
|
|
|
|
t.Run("negative limit ignored", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/?limit=-5", 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)
|
|
|
|
// Negative limit should default to 50 (since -5 > 0 is false)
|
|
count := int(response["count"].(float64))
|
|
assert.Equal(t, 20, count, "should return all 20 with default limit of 50")
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_GetUnreadCount(t *testing.T) {
|
|
handler, e, db := setupNotificationHandler(t)
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Create some unread notifications
|
|
createTestNotifications(t, db, user.ID, 5)
|
|
|
|
authGroup := e.Group("/api/notifications")
|
|
authGroup.Use(testutil.MockAuthMiddleware(user))
|
|
authGroup.GET("/unread-count/", handler.GetUnreadCount)
|
|
|
|
t.Run("successful unread count", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/unread-count/", 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, "unread_count")
|
|
unreadCount := int(response["unread_count"].(float64))
|
|
assert.Equal(t, 5, unreadCount)
|
|
})
|
|
|
|
t.Run("user with no notifications returns zero", func(t *testing.T) {
|
|
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "Password123")
|
|
|
|
e2 := testutil.SetupTestRouter()
|
|
authGroup2 := e2.Group("/api/notifications")
|
|
authGroup2.Use(testutil.MockAuthMiddleware(otherUser))
|
|
authGroup2.GET("/unread-count/", handler.GetUnreadCount)
|
|
|
|
w := testutil.MakeRequest(e2, "GET", "/api/notifications/unread-count/", 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(0), response["unread_count"])
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_MarkAsRead(t *testing.T) {
|
|
handler, e, db := setupNotificationHandler(t)
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
// Create a notification
|
|
notif := &models.Notification{
|
|
UserID: user.ID,
|
|
NotificationType: models.NotificationTaskDueSoon,
|
|
Title: "Test Notification",
|
|
Body: "Test Body",
|
|
}
|
|
require.NoError(t, db.Create(notif).Error)
|
|
|
|
authGroup := e.Group("/api/notifications")
|
|
authGroup.Use(testutil.MockAuthMiddleware(user))
|
|
authGroup.POST("/:id/read/", handler.MarkAsRead)
|
|
|
|
t.Run("successful mark as read", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/notifications/%d/read/", notif.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, "message")
|
|
})
|
|
|
|
t.Run("invalid id returns 400", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/invalid/read/", 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/notifications/99999/read/", nil, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusNotFound)
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_MarkAllAsRead(t *testing.T) {
|
|
handler, e, db := setupNotificationHandler(t)
|
|
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
|
|
|
createTestNotifications(t, db, user.ID, 5)
|
|
|
|
authGroup := e.Group("/api/notifications")
|
|
authGroup.Use(testutil.MockAuthMiddleware(user))
|
|
authGroup.POST("/mark-all-read/", handler.MarkAllAsRead)
|
|
authGroup.GET("/unread-count/", handler.GetUnreadCount)
|
|
|
|
t.Run("successful mark all as read", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/mark-all-read/", 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")
|
|
})
|
|
|
|
t.Run("unread count is zero after mark all", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/unread-count/", 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(0), response["unread_count"])
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_GetPreferences(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.GET("/preferences/", handler.GetPreferences)
|
|
|
|
t.Run("successful get preferences", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/preferences/", 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)
|
|
|
|
// Default preferences should have standard fields
|
|
assert.Contains(t, response, "task_due_soon")
|
|
assert.Contains(t, response, "task_overdue")
|
|
assert.Contains(t, response, "task_completed")
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_UpdatePreferences(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.PUT("/preferences/", handler.UpdatePreferences)
|
|
|
|
t.Run("successful update preferences", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"task_due_soon": false,
|
|
"task_overdue": true,
|
|
}
|
|
w := testutil.MakeRequest(e, "PUT", "/api/notifications/preferences/", 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, false, response["task_due_soon"])
|
|
assert.Equal(t, true, response["task_overdue"])
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_RegisterDevice(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 device registration", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"name": "iPhone 15",
|
|
"device_id": "test-device-id-123",
|
|
"registration_id": "test-registration-id-abc",
|
|
"platform": "ios",
|
|
}
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/", req, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
|
})
|
|
|
|
t.Run("missing required fields returns 400", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"name": "iPhone 15",
|
|
// Missing device_id, registration_id, platform
|
|
}
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/", req, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("invalid platform returns 400", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"device_id": "test-device-id-456",
|
|
"registration_id": "test-registration-id-def",
|
|
"platform": "windows", // invalid
|
|
}
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/", req, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_ListDevices(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.GET("/devices/", handler.ListDevices)
|
|
|
|
t.Run("successful list devices", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "GET", "/api/notifications/devices/", nil, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_UnregisterDevice(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/unregister/", handler.UnregisterDevice)
|
|
|
|
t.Run("missing registration_id returns 400", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"platform": "ios",
|
|
}
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/unregister/", req, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("missing platform returns 400", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"registration_id": "test-id",
|
|
}
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/unregister/", req, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("invalid platform returns 400", func(t *testing.T) {
|
|
req := map[string]interface{}{
|
|
"registration_id": "test-id",
|
|
"platform": "windows",
|
|
}
|
|
w := testutil.MakeRequest(e, "POST", "/api/notifications/devices/unregister/", req, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
}
|
|
|
|
func TestNotificationHandler_DeleteDevice(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.DELETE("/devices/:id/", handler.DeleteDevice)
|
|
|
|
t.Run("missing platform query param returns 400", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "DELETE", "/api/notifications/devices/1/", nil, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("invalid platform returns 400", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "DELETE", "/api/notifications/devices/1/?platform=windows", nil, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("invalid id returns 400", func(t *testing.T) {
|
|
w := testutil.MakeRequest(e, "DELETE", "/api/notifications/devices/invalid/?platform=ios", nil, "test-token")
|
|
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
|
})
|
|
}
|