Ctrl K
ComunidadGuías…

internal/handler/audit_log_handler_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/johnzastrow/actalog/internal/domain"
)

// Note: GetAuditLog_NonAdmin test skipped because handler uses h.logger.Warn() without nil check
// which causes panic when logger is nil. Would require initializing logger for test.

func TestAuditLogHandler_GetAuditLog_InvalidID(t *testing.T) {
	handler := &AuditLogHandler{}

	// chi.URLParam returns empty string without router context
	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs/abc", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.GetAuditLog(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid ID")
}

// Note: ListAuditLogs_NonAdmin test skipped because handler uses h.logger.Warn() without nil check

func TestAuditLogHandler_ListAuditLogs_InvalidUserID(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs?user_id=abc", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.ListAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid user_id")
}

func TestAuditLogHandler_ListAuditLogs_InvalidTargetUserID(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs?target_user_id=abc", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.ListAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid target_user_id")
}

func TestAuditLogHandler_ListAuditLogs_InvalidStartDate(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs?start_date=invalid", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.ListAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid start_date format")
}

func TestAuditLogHandler_ListAuditLogs_InvalidEndDate(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs?end_date=invalid", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.ListAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid end_date format")
}

func TestAuditLogHandler_GetMyAuditLogs_Unauthorized(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createTestRequest(http.MethodGet, "/api/users/me/audit-logs", "")
	rr := httptest.NewRecorder()

	handler.GetMyAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestAuditLogHandler_CleanupOldLogs_NonAdmin(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createAuthenticatedRequest(http.MethodPost, "/api/admin/audit-logs/cleanup", `{"retention_days": 30}`, 1, "user@example.com", "user")
	rr := httptest.NewRecorder()

	handler.CleanupOldLogs(rr, req)

	assertStatusCode(t, rr, http.StatusForbidden)
	assertBodyContains(t, rr, "Forbidden")
}

func TestAuditLogHandler_CleanupOldLogs_InvalidJSON(t *testing.T) {
	handler := &AuditLogHandler{}

	req := createAuthenticatedRequest(http.MethodPost, "/api/admin/audit-logs/cleanup", "{bad json", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.CleanupOldLogs(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid request body")
}

func TestAuditLogHandler_CleanupOldLogs_InvalidRetentionDays(t *testing.T) {
	handler := &AuditLogHandler{}

	tests := []struct {
		name      string
		body      string
		wantError string
	}{
		{
			name:      "zero days",
			body:      `{"retention_days": 0}`,
			wantError: "retention_days must be positive",
		},
		{
			name:      "negative days",
			body:      `{"retention_days": -1}`,
			wantError: "retention_days must be positive",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := createAuthenticatedRequest(http.MethodPost, "/api/admin/audit-logs/cleanup", tt.body, 1, "admin@example.com", "admin")
			rr := httptest.NewRecorder()

			handler.CleanupOldLogs(rr, req)

			assertStatusCode(t, rr, http.StatusBadRequest)
			assertBodyContains(t, rr, tt.wantError)
		})
	}
}

func TestNewAuditLogHandler(t *testing.T) {
	handler := NewAuditLogHandler(nil, nil)
	if handler == nil {
		t.Error("NewAuditLogHandler should return a non-nil handler")
	}
}

// Removed 4 panic-expectation tests:
// - TestAuditLogHandler_GetMyAuditLogs_WithPaginationParams (8 subtests)
// - TestAuditLogHandler_GetAuditLog_ValidID
// - TestAuditLogHandler_ListAuditLogs_WithAllParams (7 subtests)
// - TestAuditLogHandler_CleanupOldLogs_ValidRetentionDays
// These tests verified nil pointer panics, not business logic.

// Tests with mock service

func TestAuditLogHandler_GetAuditLog_Success(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	// First, create an audit log entry via the mock
	_ = mockService.Log(&domain.AuditLog{
		EventType: "test_event",
		Details:   stringPtr("Test audit log"),
	})

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs/1", "", 1, "admin@example.com", "admin")
	req = addChiURLParam(req, "id", "1")
	rr := httptest.NewRecorder()

	handler.GetAuditLog(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestAuditLogHandler_GetAuditLog_NotFound(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs/999", "", 1, "admin@example.com", "admin")
	req = addChiURLParam(req, "id", "999")
	rr := httptest.NewRecorder()

	handler.GetAuditLog(rr, req)

	assertStatusCode(t, rr, http.StatusNotFound)
}

func TestAuditLogHandler_GetAuditLog_NonAdmin(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs/1", "", 1, "user@example.com", "user")
	req = addChiURLParam(req, "id", "1")
	rr := httptest.NewRecorder()

	handler.GetAuditLog(rr, req)

	assertStatusCode(t, rr, http.StatusForbidden)
}

func TestAuditLogHandler_ListAuditLogs_Success(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.ListAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertBodyContains(t, rr, "logs")
	assertBodyContains(t, rr, "total")
}

func TestAuditLogHandler_ListAuditLogs_NonAdmin(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/audit-logs", "", 1, "user@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ListAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusForbidden)
}

func TestAuditLogHandler_ListAuditLogs_WithFilters(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	tests := []struct {
		name  string
		query string
	}{
		{"with limit", "/api/audit-logs?limit=10"},
		{"with offset", "/api/audit-logs?offset=5"},
		{"with user_id", "/api/audit-logs?user_id=1"},
		{"with event_type", "/api/audit-logs?event_type=login"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := createAuthenticatedRequest(http.MethodGet, tt.query, "", 1, "admin@example.com", "admin")
			rr := httptest.NewRecorder()

			handler.ListAuditLogs(rr, req)

			assertStatusCode(t, rr, http.StatusOK)
		})
	}
}

func TestAuditLogHandler_GetMyAuditLogs_Success(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/users/me/audit-logs", "", 1, "user@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetMyAuditLogs(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertBodyContains(t, rr, "logs")
}

func TestAuditLogHandler_GetMyAuditLogs_WithPagination(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, 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},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := createAuthenticatedRequest(http.MethodGet, "/api/users/me/audit-logs"+tt.query, "", 1, "user@example.com", "user")
			rr := httptest.NewRecorder()

			handler.GetMyAuditLogs(rr, req)

			assertStatusCode(t, rr, tt.wantCode)
		})
	}
}

func TestAuditLogHandler_CleanupOldLogs_Success(t *testing.T) {
	mockService := createTestAuditLogService()
	handler := NewAuditLogHandler(mockService, createTestLogger())

	req := createAuthenticatedRequest(http.MethodPost, "/api/admin/audit-logs/cleanup", `{"retention_days": 30}`, 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.CleanupOldLogs(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertBodyContains(t, rr, "deleted")
}