internal/handler/notification_handler_test.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/johnzastrow/actalog/internal/service"
)
func TestNotificationHandler_ListNotifications_Unauthorized(t *testing.T) {
handler := &NotificationHandler{}
req := createTestRequest(http.MethodGet, "/api/notifications", "")
rr := httptest.NewRecorder()
handler.ListNotifications(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestNotificationHandler_ListUnreadNotifications_Unauthorized(t *testing.T) {
handler := &NotificationHandler{}
req := createTestRequest(http.MethodGet, "/api/notifications/unread", "")
rr := httptest.NewRecorder()
handler.ListUnreadNotifications(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestNotificationHandler_GetUnreadCount_Unauthorized(t *testing.T) {
handler := &NotificationHandler{}
req := createTestRequest(http.MethodGet, "/api/notifications/count", "")
rr := httptest.NewRecorder()
handler.GetUnreadCount(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestNotificationHandler_MarkAsRead_Unauthorized(t *testing.T) {
handler := &NotificationHandler{}
req := createTestRequest(http.MethodPut, "/api/notifications/1/read", "")
rr := httptest.NewRecorder()
handler.MarkAsRead(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestNotificationHandler_MarkAsRead_InvalidID(t *testing.T) {
handler := &NotificationHandler{}
// chi.URLParam returns empty string without router context
req := createAuthenticatedRequest(http.MethodPut, "/api/notifications/abc/read", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.MarkAsRead(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid notification ID")
}
func TestNotificationHandler_MarkAllAsRead_Unauthorized(t *testing.T) {
handler := &NotificationHandler{}
req := createTestRequest(http.MethodPut, "/api/notifications/read-all", "")
rr := httptest.NewRecorder()
handler.MarkAllAsRead(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestNotificationHandler_DeleteNotification_Unauthorized(t *testing.T) {
handler := &NotificationHandler{}
req := createTestRequest(http.MethodDelete, "/api/notifications/1", "")
rr := httptest.NewRecorder()
handler.DeleteNotification(rr, req)
assertStatusCode(t, rr, http.StatusUnauthorized)
assertBodyContains(t, rr, "Unauthorized")
}
func TestNotificationHandler_DeleteNotification_InvalidID(t *testing.T) {
handler := &NotificationHandler{}
// chi.URLParam returns empty string without router context
req := createAuthenticatedRequest(http.MethodDelete, "/api/notifications/abc", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.DeleteNotification(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid notification ID")
}
func TestNotificationHandler_CreateAnnouncement_NonAdmin(t *testing.T) {
handler := &NotificationHandler{}
// Regular user trying to create announcement
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce", `{"title": "Test", "message": "Test message", "target_type": "all"}`, 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusForbidden)
assertBodyContains(t, rr, "Admin access required")
}
func TestNotificationHandler_CreateAnnouncement_InvalidJSON(t *testing.T) {
handler := &NotificationHandler{}
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce", "{bad json", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid request body")
}
func TestNotificationHandler_CreateAnnouncement_MissingFields(t *testing.T) {
handler := &NotificationHandler{}
tests := []struct {
name string
body string
wantError string
}{
{
name: "missing title",
body: `{"message": "Test", "target_type": "all"}`,
wantError: "Title is required",
},
{
name: "missing message",
body: `{"title": "Test", "target_type": "all"}`,
wantError: "Message is required",
},
{
name: "missing target_type",
body: `{"title": "Test", "message": "Test message"}`,
wantError: "Target type is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce", tt.body, 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, tt.wantError)
})
}
}
func TestNotificationHandler_CreateAnnouncement_InvalidTargetType(t *testing.T) {
handler := &NotificationHandler{}
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce", `{"title": "Test", "message": "Test message", "target_type": "invalid"}`, 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid target type")
}
func TestNotificationHandler_CreateAnnouncement_MissingTargetIDs(t *testing.T) {
handler := &NotificationHandler{}
tests := []struct {
name string
targetType string
}{
{name: "organization without IDs", targetType: "organization"},
{name: "users without IDs", targetType: "users"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := `{"title": "Test", "message": "Test message", "target_type": "` + tt.targetType + `"}`
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce", body, 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Target IDs required")
})
}
}
func TestNewNotificationHandler(t *testing.T) {
handler := NewNotificationHandler(nil, nil)
if handler == nil {
t.Error("NewNotificationHandler should return a non-nil handler")
}
}
// Removed 16 panic-expectation tests:
// - TestNotificationHandler_ListNotifications_NilService
// - TestNotificationHandler_GetUnreadCount_NilService
// - TestNotificationHandler_MarkAsRead_ValidIDNilService
// - TestNotificationHandler_MarkAllAsRead_NilService
// - TestNotificationHandler_ListNotifications_WithPaginationParams (9 subtests)
// - TestNotificationHandler_ListUnreadNotifications_WithPaginationParams (9 subtests)
// - TestNotificationHandler_DeleteNotification_ValidIDNilService
// - TestNotificationHandler_CreateAnnouncement_ValidInputAllUsers
// - TestNotificationHandler_CreateAnnouncement_ValidInputOrganization
// - TestNotificationHandler_CreateAnnouncement_ValidInputUsers
// - TestNotificationHandler_GetUnreadCount_DifferentUserIDs (3 subtests)
// - TestNotificationHandler_MarkAllAsRead_DifferentUserIDs (3 subtests)
// - TestNotificationHandler_MarkAsRead_DifferentIDs (4 subtests)
// - TestNotificationHandler_DeleteNotification_DifferentIDs (3 subtests)
// - TestNotificationHandler_ListUnreadNotifications_DifferentUserIDs (3 subtests)
// - TestNotificationHandler_ListNotifications_DifferentUserIDs (3 subtests)
// These tests verified nil pointer panics, not business logic.
// Helper to create a test notification service with mocks
func createTestNotificationService() *service.NotificationService {
mockNotificationRepo := NewMockNotificationRepository()
mockOrgRepo := NewMockOrganizationRepository()
mockUserRepo := NewMockUserRepository()
mockSettingsRepo := NewMockUserSettingsRepository()
mockEmailService := NewMockEmailService()
return service.NewNotificationService(
mockNotificationRepo,
mockOrgRepo,
mockUserRepo,
mockSettingsRepo,
mockEmailService,
)
}
// Tests using real NotificationService with mock repositories
func TestNotificationHandler_ListNotifications_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListNotifications(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "notifications")
}
func TestNotificationHandler_ListNotifications_WithPagination(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications?limit=10&offset=0", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListNotifications(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "notifications")
assertBodyContains(t, rr, `"limit":10`)
}
func TestNotificationHandler_ListUnreadNotifications_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/unread", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListUnreadNotifications(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "notifications")
}
func TestNotificationHandler_GetUnreadCount_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/count", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.GetUnreadCount(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "count")
}
func TestNotificationHandler_MarkAsRead_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodPut, "/api/notifications/1/read", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.MarkAsRead(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "Notification marked as read")
}
func TestNotificationHandler_MarkAllAsRead_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodPut, "/api/notifications/read-all", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.MarkAllAsRead(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "All notifications marked as read")
}
func TestNotificationHandler_DeleteNotification_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodDelete, "/api/notifications/1", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.DeleteNotification(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertBodyContains(t, rr, "Notification deleted")
}
func TestNotificationHandler_CreateAnnouncement_Success(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce",
`{"title": "Test Announcement", "message": "Test message", "target_type": "all"}`,
1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusCreated)
assertBodyContains(t, rr, "Announcement created successfully")
}
func TestNotificationHandler_ListNotifications_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListNotifications(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestNotificationHandler_GetUnreadCount_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/count", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.GetUnreadCount(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestNotificationHandler_ListUnreadNotifications_WithPagination(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
tests := []struct {
name string
query string
wantCode int
}{
{"with valid limit", "?limit=10", http.StatusOK},
{"with valid offset", "?offset=5", http.StatusOK},
{"with both params", "?limit=10&offset=5", http.StatusOK},
{"with invalid limit", "?limit=abc", http.StatusOK},
{"with negative limit", "?limit=-1", http.StatusOK},
{"with invalid offset", "?offset=abc", http.StatusOK},
{"with negative offset", "?offset=-1", http.StatusOK},
{"with zero limit", "?limit=0", http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/unread"+tt.query, "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListUnreadNotifications(rr, req)
assertStatusCode(t, rr, tt.wantCode)
})
}
}
func TestNotificationHandler_ListUnreadNotifications_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/unread", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.ListUnreadNotifications(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestNotificationHandler_MarkAllAsRead_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodPut, "/api/notifications/read-all", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.MarkAllAsRead(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestNotificationHandler_MarkAsRead_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodPut, "/api/notifications/1/read", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.MarkAsRead(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestNotificationHandler_DeleteNotification_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodDelete, "/api/notifications/1", "", 1, "test@example.com", "user")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.DeleteNotification(rr, req)
assertStatusCode(t, rr, http.StatusOK)
}
func TestNotificationHandler_CreateAnnouncement_NoLogger(t *testing.T) {
notificationService := createTestNotificationService()
handler := &NotificationHandler{
notificationService: notificationService,
}
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/notifications/announce",
`{"title": "Test", "message": "Test message", "target_type": "all"}`,
1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CreateAnnouncement(rr, req)
assertStatusCode(t, rr, http.StatusCreated)
}
// Helper to create a notification service that returns errors
func createTestNotificationServiceWithError() *service.NotificationService {
mockNotificationRepo := NewMockNotificationRepository()
mockNotificationRepo.SetError(ErrMockInternalError)
mockOrgRepo := NewMockOrganizationRepository()
mockUserRepo := NewMockUserRepository()
mockSettingsRepo := NewMockUserSettingsRepository()
mockEmailService := NewMockEmailService()
return service.NewNotificationService(
mockNotificationRepo,
mockOrgRepo,
mockUserRepo,
mockSettingsRepo,
mockEmailService,
)
}
func TestNotificationHandler_GetUnreadCount_ServiceError(t *testing.T) {
notificationService := createTestNotificationServiceWithError()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/count", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.GetUnreadCount(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "Failed to get unread count")
}
func TestNotificationHandler_GetUnreadCount_ServiceErrorNoLogger(t *testing.T) {
notificationService := createTestNotificationServiceWithError()
handler := &NotificationHandler{
notificationService: notificationService,
logger: nil,
}
req := createAuthenticatedRequest(http.MethodGet, "/api/notifications/count", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.GetUnreadCount(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
}
func TestNotificationHandler_MarkAllAsRead_ServiceError(t *testing.T) {
notificationService := createTestNotificationServiceWithError()
handler := &NotificationHandler{
notificationService: notificationService,
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodPut, "/api/notifications/read-all", "", 1, "test@example.com", "user")
rr := httptest.NewRecorder()
handler.MarkAllAsRead(rr, req)
assertStatusCode(t, rr, http.StatusInternalServerError)
assertBodyContains(t, rr, "Failed to mark all notifications as read")
}