Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- 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>
This commit is contained in:
@@ -86,3 +86,323 @@ func TestNotificationHandler_ListNotifications_LimitCappedAt200(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user