mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 23:42:00 +00:00
8526314a44
All 7 handler files now have test coverage: jobs (14 tests), notifications (11), policies (15), issuers (15), targets (14). Negative-path integration tests cover nonexistent resources, invalid payloads, malformed CSR, expired cert lifecycle, and method-not-allowed errors. CI now enforces coverage thresholds (service 60%+, handler 50%+) and includes connector tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
284 lines
7.9 KiB
Go
284 lines
7.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// MockNotificationService is a mock implementation of NotificationService interface.
|
|
type MockNotificationService struct {
|
|
ListNotificationsFn func(page, perPage int) ([]domain.NotificationEvent, int64, error)
|
|
GetNotificationFn func(id string) (*domain.NotificationEvent, error)
|
|
MarkAsReadFn func(id string) error
|
|
}
|
|
|
|
func (m *MockNotificationService) ListNotifications(page, perPage int) ([]domain.NotificationEvent, int64, error) {
|
|
if m.ListNotificationsFn != nil {
|
|
return m.ListNotificationsFn(page, perPage)
|
|
}
|
|
return nil, 0, nil
|
|
}
|
|
|
|
func (m *MockNotificationService) GetNotification(id string) (*domain.NotificationEvent, error) {
|
|
if m.GetNotificationFn != nil {
|
|
return m.GetNotificationFn(id)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockNotificationService) MarkAsRead(id string) error {
|
|
if m.MarkAsReadFn != nil {
|
|
return m.MarkAsReadFn(id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestListNotifications_Success(t *testing.T) {
|
|
now := time.Now()
|
|
certID := "mc-prod-001"
|
|
n1 := domain.NotificationEvent{
|
|
ID: "notif-001",
|
|
Type: domain.NotificationTypeExpirationWarning,
|
|
CertificateID: &certID,
|
|
Channel: domain.NotificationChannelEmail,
|
|
Recipient: "admin@example.com",
|
|
Message: "Certificate expiring in 30 days",
|
|
Status: "sent",
|
|
CreatedAt: now,
|
|
}
|
|
n2 := domain.NotificationEvent{
|
|
ID: "notif-002",
|
|
Type: domain.NotificationTypeRenewalSuccess,
|
|
CertificateID: &certID,
|
|
Channel: domain.NotificationChannelWebhook,
|
|
Recipient: "https://hooks.example.com/cert",
|
|
Message: "Certificate renewed successfully",
|
|
Status: "sent",
|
|
CreatedAt: now,
|
|
}
|
|
|
|
mock := &MockNotificationService{
|
|
ListNotificationsFn: func(page, perPage int) ([]domain.NotificationEvent, int64, error) {
|
|
return []domain.NotificationEvent{n1, n2}, 2, nil
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ListNotifications(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp PagedResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp.Total != 2 {
|
|
t.Errorf("expected total 2, got %d", resp.Total)
|
|
}
|
|
}
|
|
|
|
func TestListNotifications_Pagination(t *testing.T) {
|
|
var capturedPage, capturedPerPage int
|
|
mock := &MockNotificationService{
|
|
ListNotificationsFn: func(page, perPage int) ([]domain.NotificationEvent, int64, error) {
|
|
capturedPage = page
|
|
capturedPerPage = perPage
|
|
return []domain.NotificationEvent{}, 0, nil
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications?page=2&per_page=10", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ListNotifications(w, req)
|
|
|
|
if capturedPage != 2 {
|
|
t.Errorf("expected page 2, got %d", capturedPage)
|
|
}
|
|
if capturedPerPage != 10 {
|
|
t.Errorf("expected per_page 10, got %d", capturedPerPage)
|
|
}
|
|
}
|
|
|
|
func TestListNotifications_ServiceError(t *testing.T) {
|
|
mock := &MockNotificationService{
|
|
ListNotificationsFn: func(page, perPage int) ([]domain.NotificationEvent, int64, error) {
|
|
return nil, 0, ErrMockServiceFailed
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ListNotifications(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected status 500, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestListNotifications_MethodNotAllowed(t *testing.T) {
|
|
handler := NewNotificationHandler(&MockNotificationService{})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ListNotifications(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status 405, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGetNotification_Success(t *testing.T) {
|
|
now := time.Now()
|
|
certID := "mc-prod-001"
|
|
mock := &MockNotificationService{
|
|
GetNotificationFn: func(id string) (*domain.NotificationEvent, error) {
|
|
return &domain.NotificationEvent{
|
|
ID: id,
|
|
Type: domain.NotificationTypeExpirationWarning,
|
|
CertificateID: &certID,
|
|
Channel: domain.NotificationChannelEmail,
|
|
Recipient: "admin@example.com",
|
|
Message: "Certificate expiring",
|
|
Status: "sent",
|
|
CreatedAt: now,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/notif-001", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.GetNotification(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGetNotification_NotFound(t *testing.T) {
|
|
mock := &MockNotificationService{
|
|
GetNotificationFn: func(id string) (*domain.NotificationEvent, error) {
|
|
return nil, ErrMockNotFound
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/nonexistent", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.GetNotification(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected status 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestGetNotification_EmptyID(t *testing.T) {
|
|
handler := NewNotificationHandler(&MockNotificationService{})
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.GetNotification(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestMarkAsRead_Success(t *testing.T) {
|
|
var markedID string
|
|
mock := &MockNotificationService{
|
|
MarkAsReadFn: func(id string) error {
|
|
markedID = id
|
|
return nil
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/notif-001/read", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.MarkAsRead(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d", w.Code)
|
|
}
|
|
if markedID != "notif-001" {
|
|
t.Errorf("expected marked ID 'notif-001', got '%s'", markedID)
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if resp["status"] != "marked_as_read" {
|
|
t.Errorf("expected status 'marked_as_read', got '%s'", resp["status"])
|
|
}
|
|
}
|
|
|
|
func TestMarkAsRead_ServiceError(t *testing.T) {
|
|
mock := &MockNotificationService{
|
|
MarkAsReadFn: func(id string) error {
|
|
return ErrMockServiceFailed
|
|
},
|
|
}
|
|
|
|
handler := NewNotificationHandler(mock)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/notif-001/read", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.MarkAsRead(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected status 500, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestMarkAsRead_MethodNotAllowed(t *testing.T) {
|
|
handler := NewNotificationHandler(&MockNotificationService{})
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/notif-001/read", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.MarkAsRead(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected status 405, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestMarkAsRead_EmptyID(t *testing.T) {
|
|
handler := NewNotificationHandler(&MockNotificationService{})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications//read", nil)
|
|
req = req.WithContext(contextWithRequestID())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.MarkAsRead(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected status 400, got %d", w.Code)
|
|
}
|
|
}
|