package services import ( "net/http" "net/http/httptest" "testing" "time" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/testutil" ) func TestAuditService_LogEvent_WritesToDatabase(t *testing.T) { db := testutil.SetupTestDB(t) svc := NewAuditService(db) defer svc.Stop() // Create a fake Echo context e := echo.New() req := httptest.NewRequest(http.MethodPost, "/api/auth/login/", nil) req.Header.Set("User-Agent", "TestAgent/1.0") req.RemoteAddr = "192.168.1.1:12345" rec := httptest.NewRecorder() c := e.NewContext(req, rec) userID := uint(42) svc.LogEvent(c, &userID, AuditEventLogin, map[string]interface{}{ "method": "password", }) // Stop flushes the channel svc.Stop() var entries []models.AuditLog err := db.Find(&entries).Error require.NoError(t, err) require.Len(t, entries, 1) entry := entries[0] assert.Equal(t, uint(42), *entry.UserID) assert.Equal(t, AuditEventLogin, entry.EventType) assert.Equal(t, "TestAgent/1.0", entry.UserAgent) assert.NotEmpty(t, entry.IPAddress) assert.Equal(t, "password", entry.Details["method"]) assert.False(t, entry.CreatedAt.IsZero()) } func TestAuditService_LogEvent_NilUserID(t *testing.T) { db := testutil.SetupTestDB(t) svc := NewAuditService(db) defer svc.Stop() e := echo.New() req := httptest.NewRequest(http.MethodPost, "/api/auth/login/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) svc.LogEvent(c, nil, AuditEventLoginFailed, map[string]interface{}{ "identifier": "unknown@test.com", }) svc.Stop() var entries []models.AuditLog err := db.Find(&entries).Error require.NoError(t, err) require.Len(t, entries, 1) assert.Nil(t, entries[0].UserID) assert.Equal(t, AuditEventLoginFailed, entries[0].EventType) } func TestAuditService_LogEvent_NilContext(t *testing.T) { db := testutil.SetupTestDB(t) svc := NewAuditService(db) defer svc.Stop() userID := uint(1) svc.LogEvent(nil, &userID, AuditEventLogout, nil) svc.Stop() var entries []models.AuditLog err := db.Find(&entries).Error require.NoError(t, err) require.Len(t, entries, 1) assert.Equal(t, AuditEventLogout, entries[0].EventType) assert.Empty(t, entries[0].IPAddress) assert.Empty(t, entries[0].UserAgent) } func TestAuditService_LogEvent_MultipleEvents(t *testing.T) { db := testutil.SetupTestDB(t) svc := NewAuditService(db) defer svc.Stop() e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) userID := uint(10) svc.LogEvent(c, &userID, AuditEventRegister, nil) svc.LogEvent(c, &userID, AuditEventLogin, nil) svc.LogEvent(c, &userID, AuditEventLogout, nil) svc.Stop() var count int64 err := db.Model(&models.AuditLog{}).Count(&count).Error require.NoError(t, err) assert.Equal(t, int64(3), count) } func TestAuditService_EventTypeConstants(t *testing.T) { // Verify all event constants have expected values assert.Equal(t, "auth.login", AuditEventLogin) assert.Equal(t, "auth.login_failed", AuditEventLoginFailed) assert.Equal(t, "auth.register", AuditEventRegister) assert.Equal(t, "auth.logout", AuditEventLogout) assert.Equal(t, "auth.password_reset", AuditEventPasswordReset) assert.Equal(t, "auth.password_changed", AuditEventPasswordChanged) assert.Equal(t, "auth.account_deleted", AuditEventAccountDeleted) } func TestAuditService_Stop_FlushesRemainingEntries(t *testing.T) { db := testutil.SetupTestDB(t) svc := NewAuditService(db) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) // Send many events quickly for i := 0; i < 50; i++ { uid := uint(i) svc.LogEvent(c, &uid, AuditEventLogin, nil) } // Stop should block until all entries are written svc.Stop() var count int64 err := db.Model(&models.AuditLog{}).Count(&count).Error require.NoError(t, err) assert.Equal(t, int64(50), count) } func TestAuditLog_TableName(t *testing.T) { log := models.AuditLog{} assert.Equal(t, "audit_log", log.TableName()) } func TestAuditLog_JSONMap_NilHandling(t *testing.T) { db := testutil.SetupTestDB(t) // Create entry with nil details entry := &models.AuditLog{ EventType: "test", CreatedAt: time.Now().UTC(), } err := db.Create(entry).Error require.NoError(t, err) // Read it back var found models.AuditLog err = db.First(&found, entry.ID).Error require.NoError(t, err) assert.Nil(t, found.Details) }