Ctrl K
ComunidadGuías…

internal/handler/data_change_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"
	"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)
		})
	}
}