internal/handler/data_change_log_handler_test.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/johnzastrow/actalog/internal/domain"
"github.com/johnzastrow/actalog/internal/service"
)
func createTestDataChangeLogService() *service.DataChangeLogService {
mockDataChangeLogRepo := NewMockDataChangeLogRepository()
return service.NewDataChangeLogService(mockDataChangeLogRepo)
}
func createTestDataChangeLogServiceWithData() (*service.DataChangeLogService, *MockDataChangeLogRepository) {
mockRepo := NewMockDataChangeLogRepository()
now := time.Now()
afterVal1 := `{"email":"test@example.com"}`
mockRepo.AddLog(&domain.DataChangeLog{
ID: 1,
EntityType: "user",
EntityID: 1,
EntityName: "test@example.com",
Operation: "update",
UserID: 1,
UserEmail: "admin@example.com",
AfterValues: &afterVal1,
CreatedAt: now,
})
beforeVal2 := `{"name":"Old Name"}`
afterVal2 := `{"name":"New Name"}`
mockRepo.AddLog(&domain.DataChangeLog{
ID: 2,
EntityType: "workout",
EntityID: 1,
EntityName: "Test Workout",
Operation: "update",
UserID: 1,
UserEmail: "admin@example.com",
BeforeValues: &beforeVal2,
AfterValues: &afterVal2,
CreatedAt: now,
})
return service.NewDataChangeLogService(mockRepo), mockRepo
}
func TestDataChangeLogHandler_GetDataChangeLog_NonAdmin(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/1", "", 1, "user@example.com", "user")
rr := httptest.NewRecorder()
handler.GetDataChangeLog(rr, req)
assertStatusCode(t, rr, http.StatusForbidden)
assertBodyContains(t, rr, "Forbidden")
}
func TestDataChangeLogHandler_GetDataChangeLog_InvalidID(t *testing.T) {
handler := &DataChangeLogHandler{}
// chi.URLParam returns empty string without router context
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/abc", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.GetDataChangeLog(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid ID")
}
func TestDataChangeLogHandler_ListDataChangeLogs_NonAdmin(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs", "", 1, "user@example.com", "user")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusForbidden)
assertBodyContains(t, rr, "Forbidden")
}
func TestDataChangeLogHandler_ListDataChangeLogs_InvalidEntityID(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs?entity_id=abc", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid entity_id")
}
func TestDataChangeLogHandler_ListDataChangeLogs_InvalidUserID(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs?user_id=abc", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid user_id")
}
func TestDataChangeLogHandler_ListDataChangeLogs_InvalidStartDate(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs?start_date=invalid", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid start_date format")
}
func TestDataChangeLogHandler_ListDataChangeLogs_InvalidEndDate(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs?end_date=invalid", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid end_date format")
}
func TestDataChangeLogHandler_GetEntityHistory_NonAdmin(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/entity/user/1", "", 1, "user@example.com", "user")
rr := httptest.NewRecorder()
handler.GetEntityHistory(rr, req)
assertStatusCode(t, rr, http.StatusForbidden)
assertBodyContains(t, rr, "Forbidden")
}
func TestDataChangeLogHandler_GetEntityHistory_InvalidEntityID(t *testing.T) {
handler := &DataChangeLogHandler{}
// chi.URLParam returns empty string without router context
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/entity/user/abc", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.GetEntityHistory(rr, req)
assertStatusCode(t, rr, http.StatusBadRequest)
assertBodyContains(t, rr, "Invalid entity_id")
}
func TestDataChangeLogHandler_CleanupOldLogs_NonAdmin(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/data-change-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 TestDataChangeLogHandler_CleanupOldLogs_InvalidJSON(t *testing.T) {
handler := &DataChangeLogHandler{}
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/data-change-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 TestDataChangeLogHandler_CleanupOldLogs_InvalidRetentionDays(t *testing.T) {
handler := &DataChangeLogHandler{}
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/data-change-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 TestNewDataChangeLogHandler(t *testing.T) {
handler := NewDataChangeLogHandler(nil, nil)
if handler == nil {
t.Error("NewDataChangeLogHandler should return a non-nil handler")
}
}
// Removed 11 panic-expectation tests:
// - TestDataChangeLogHandler_GetDataChangeLog_ValidIDNilService
// - TestDataChangeLogHandler_ListDataChangeLogs_WithFilters (7 subtests)
// - TestDataChangeLogHandler_ListDataChangeLogs_ValidDateFormats (5 subtests)
// - TestDataChangeLogHandler_ListDataChangeLogs_ValidEntityID
// - TestDataChangeLogHandler_ListDataChangeLogs_ValidUserID
// - TestDataChangeLogHandler_GetEntityHistory_ValidInputNilService
// - TestDataChangeLogHandler_GetEntityHistory_WithPagination (4 subtests)
// - TestDataChangeLogHandler_GetEntityHistory_DifferentEntityTypes (4 subtests)
// - TestDataChangeLogHandler_CleanupOldLogs_ValidInputNilService
// - TestDataChangeLogHandler_CleanupOldLogs_DifferentRetentionDays (4 subtests)
// - TestDataChangeLogHandler_GetDataChangeLog_DifferentIDs (3 subtests)
// These tests verified nil pointer panics, not business logic.
// Test for GetDataChangeLog with logger
func TestDataChangeLogHandler_GetDataChangeLog_WithLogger(t *testing.T) {
handler := &DataChangeLogHandler{
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/1", "", 1, "user@example.com", "user")
rr := httptest.NewRecorder()
handler.GetDataChangeLog(rr, req)
assertStatusCode(t, rr, http.StatusForbidden)
}
// Test for ListDataChangeLogs with logger
func TestDataChangeLogHandler_ListDataChangeLogs_WithLogger(t *testing.T) {
handler := &DataChangeLogHandler{
logger: createTestLogger(),
}
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs", "", 1, "user@example.com", "user")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusForbidden)
}
// Success path tests
func TestDataChangeLogHandler_GetDataChangeLog_Success(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/1", "", 1, "admin@example.com", "admin")
req = addChiURLParam(req, "id", "1")
rr := httptest.NewRecorder()
handler.GetDataChangeLog(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertContentType(t, rr, "application/json")
body := rr.Body.String()
if !strings.Contains(body, "user") {
t.Error("Response should contain entity_type 'user'")
}
if !strings.Contains(body, "create") {
t.Error("Response should contain operation 'create'")
}
}
func TestDataChangeLogHandler_GetDataChangeLog_NotFound(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/999", "", 1, "admin@example.com", "admin")
req = addChiURLParam(req, "id", "999")
rr := httptest.NewRecorder()
handler.GetDataChangeLog(rr, req)
assertStatusCode(t, rr, http.StatusNotFound)
assertBodyContains(t, rr, "not found")
}
func TestDataChangeLogHandler_ListDataChangeLogs_Success(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs", "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertContentType(t, rr, "application/json")
body := rr.Body.String()
if !strings.Contains(body, "logs") {
t.Error("Response should contain 'logs' field")
}
if !strings.Contains(body, "total") {
t.Error("Response should contain 'total' field")
}
}
func TestDataChangeLogHandler_ListDataChangeLogs_WithFilters(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
tests := []struct {
name string
query string
}{
{"entity_type filter", "?entity_type=user"},
{"entity_id filter", "?entity_id=1"},
{"operation filter", "?operation=create"},
{"user_id filter", "?user_id=1"},
{"user_email filter", "?user_email=test@example.com"},
{"pagination", "?limit=10&offset=0"},
{"combined filters", "?entity_type=user&operation=create&limit=5"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs"+tt.query, "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
})
}
}
func TestDataChangeLogHandler_ListDataChangeLogs_ValidDateFormats(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
tests := []struct {
name string
query string
}{
{"RFC3339 start_date", "?start_date=2025-01-01T00:00:00Z"},
{"simple start_date", "?start_date=2025-01-01"},
{"RFC3339 end_date", "?end_date=2025-12-31T23:59:59Z"},
{"simple end_date", "?end_date=2025-12-31"},
{"date range", "?start_date=2025-01-01&end_date=2025-12-31"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs"+tt.query, "", 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.ListDataChangeLogs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
})
}
}
func TestDataChangeLogHandler_GetEntityHistory_Success(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/entity/user/1", "", 1, "admin@example.com", "admin")
req = addChiURLParam(req, "entity_type", "user")
req = addChiURLParam(req, "entity_id", "1")
rr := httptest.NewRecorder()
handler.GetEntityHistory(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertContentType(t, rr, "application/json")
body := rr.Body.String()
if !strings.Contains(body, "entity_type") {
t.Error("Response should contain 'entity_type' field")
}
if !strings.Contains(body, "entity_id") {
t.Error("Response should contain 'entity_id' field")
}
if !strings.Contains(body, "logs") {
t.Error("Response should contain 'logs' field")
}
}
func TestDataChangeLogHandler_GetEntityHistory_WithPagination(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
tests := []struct {
name string
query string
}{
{"with limit", "?limit=10"},
{"with offset", "?offset=0"},
{"with pagination", "?limit=10&offset=0"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/entity/user/1"+tt.query, "", 1, "admin@example.com", "admin")
req = addChiURLParam(req, "entity_type", "user")
req = addChiURLParam(req, "entity_id", "1")
rr := httptest.NewRecorder()
handler.GetEntityHistory(rr, req)
assertStatusCode(t, rr, http.StatusOK)
})
}
}
func TestDataChangeLogHandler_GetEntityHistory_DifferentEntityTypes(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
entityTypes := []string{"user", "workout", "wod", "movement"}
for _, entityType := range entityTypes {
t.Run(entityType, func(t *testing.T) {
req := createAuthenticatedRequest(http.MethodGet, "/api/data-change-logs/entity/"+entityType+"/1", "", 1, "admin@example.com", "admin")
req = addChiURLParam(req, "entity_type", entityType)
req = addChiURLParam(req, "entity_id", "1")
rr := httptest.NewRecorder()
handler.GetEntityHistory(rr, req)
assertStatusCode(t, rr, http.StatusOK)
})
}
}
func TestDataChangeLogHandler_CleanupOldLogs_Success(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/data-change-logs/cleanup", `{"retention_days": 30}`, 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CleanupOldLogs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
assertContentType(t, rr, "application/json")
body := rr.Body.String()
if !strings.Contains(body, "deleted_count") {
t.Error("Response should contain 'deleted_count' field")
}
if !strings.Contains(body, "message") {
t.Error("Response should contain 'message' field")
}
}
func TestDataChangeLogHandler_CleanupOldLogs_DifferentRetentionDays(t *testing.T) {
svc, _ := createTestDataChangeLogServiceWithData()
handler := NewDataChangeLogHandler(svc, createTestLogger())
tests := []struct {
days int
name string
}{
{7, "7_days"},
{30, "30_days"},
{60, "60_days"},
{90, "90_days"},
{365, "365_days"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := `{"retention_days": ` + strconv.Itoa(tt.days) + `}`
req := createAuthenticatedRequest(http.MethodPost, "/api/admin/data-change-logs/cleanup", body, 1, "admin@example.com", "admin")
rr := httptest.NewRecorder()
handler.CleanupOldLogs(rr, req)
assertStatusCode(t, rr, http.StatusOK)
})
}
}