veza/veza-backend-api/internal/services/audit_service_test.go
senke a6cf20e614 fix(tests): fix 2 skipped tests, add clear skip reasons to 11 others
INT-04: Fixed nil UserID panic in AuditService (re-enabled 2 tests).
Added INT-04 comments explaining skip reasons for tests requiring
PostgreSQL, real file headers, or external services.
2026-02-22 17:53:00 +01:00

489 lines
14 KiB
Go

package services
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"veza-backend-api/internal/database"
)
func setupTestAuditService(t *testing.T) (*AuditService, *gorm.DB, *database.Database) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
db.Exec("PRAGMA foreign_keys = ON")
// Create audit_logs table
err = db.Exec(`
CREATE TABLE audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT,
action TEXT NOT NULL,
resource TEXT NOT NULL,
resource_id TEXT,
ip_address TEXT NOT NULL,
user_agent TEXT NOT NULL,
metadata TEXT,
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`).Error
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
testDB := &database.Database{
DB: sqlDB,
}
logger := zap.NewNop()
service := NewAuditService(testDB, logger)
return service, db, testDB
}
func TestAuditService_NewAuditService(t *testing.T) {
logger := zap.NewNop()
testDB := &database.Database{}
service := NewAuditService(testDB, logger)
assert.NotNil(t, service)
assert.Equal(t, testDB, service.db)
assert.Equal(t, logger, service.logger)
}
func TestAuditService_LogAction_Success(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
req := &AuditLogCreateRequest{
UserID: &userID,
Action: "test_action",
Resource: "test_resource",
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: map[string]interface{}{"key": "value"},
}
err := service.LogAction(ctx, req)
assert.NoError(t, err)
// Verify log was created
var count int
err = testDB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM audit_logs WHERE action = $1 AND resource = $2
`, req.Action, req.Resource).Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestAuditService_LogAction_WithResourceID(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
resourceID := uuid.New()
req := &AuditLogCreateRequest{
UserID: &userID,
Action: "test_action",
Resource: "test_resource",
ResourceID: &resourceID,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: map[string]interface{}{},
}
err := service.LogAction(ctx, req)
assert.NoError(t, err)
// Verify log was created with resource_id
var storedResourceID string
err = testDB.QueryRowContext(ctx, `
SELECT resource_id FROM audit_logs WHERE action = $1
`, req.Action).Scan(&storedResourceID)
assert.NoError(t, err)
assert.Equal(t, resourceID.String(), storedResourceID)
}
func TestAuditService_LogAction_WithoutUserID(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
req := &AuditLogCreateRequest{
UserID: nil, // UserID can be nil for anonymous/system actions (e.g. account_locked)
Action: "account_locked",
Resource: "user",
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: map[string]interface{}{"reason": "too_many_attempts"},
}
err := service.LogAction(ctx, req)
assert.NoError(t, err)
var count int
err = testDB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM audit_logs WHERE action = $1 AND user_id IS NULL
`, req.Action).Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestAuditService_LogAction_ComplexMetadata(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
req := &AuditLogCreateRequest{
UserID: &userID,
Action: "test_action",
Resource: "test_resource",
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: map[string]interface{}{
"string": "value",
"number": 123,
"bool": true,
"nested": map[string]interface{}{
"key": "nested_value",
},
},
}
err := service.LogAction(ctx, req)
assert.NoError(t, err)
// Verify metadata was stored correctly
var metadataJSON string
err = testDB.QueryRowContext(ctx, `
SELECT metadata FROM audit_logs WHERE action = $1
`, req.Action).Scan(&metadataJSON)
assert.NoError(t, err)
var metadata map[string]interface{}
err = json.Unmarshal([]byte(metadataJSON), &metadata)
assert.NoError(t, err)
assert.Equal(t, "value", metadata["string"])
assert.Equal(t, float64(123), metadata["number"]) // JSON numbers are float64
assert.Equal(t, true, metadata["bool"])
}
func TestAuditService_LogLogin_Success(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
metadata := map[string]interface{}{"method": "password"}
err := service.LogLogin(ctx, &userID, true, "127.0.0.1", "test-agent", metadata)
assert.NoError(t, err)
// Verify login_success was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "login_success", action)
}
func TestAuditService_LogLogin_Failed(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
err := service.LogLogin(ctx, &userID, false, "127.0.0.1", "test-agent", nil)
assert.NoError(t, err)
// Verify login_failed was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "login_failed", action)
}
func TestAuditService_LogLogin_WithoutUserID(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
// LogLogin with nil userID (e.g. failed login before user lookup)
err := service.LogLogin(ctx, nil, false, "127.0.0.1", "test-agent", nil)
assert.NoError(t, err)
var count int
err = testDB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM audit_logs WHERE action = $1 AND user_id IS NULL
`, "login_failed").Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 1, count)
}
func TestAuditService_LogPasswordChange(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
err := service.LogPasswordChange(ctx, userID, "127.0.0.1", "test-agent")
assert.NoError(t, err)
// Verify password_change was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "password_change", action)
}
func TestAuditService_LogPasswordResetRequest(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
email := "test@example.com"
err := service.LogPasswordResetRequest(ctx, &userID, email, "127.0.0.1", "test-agent")
assert.NoError(t, err)
// Verify password_reset_request was logged
var action string
var metadataJSON string
err = testDB.QueryRowContext(ctx, `
SELECT action, metadata FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action, &metadataJSON)
assert.NoError(t, err)
assert.Equal(t, "password_reset_request", action)
// Verify email is in metadata
var metadata map[string]interface{}
err = json.Unmarshal([]byte(metadataJSON), &metadata)
assert.NoError(t, err)
assert.Equal(t, email, metadata["email"])
}
func TestAuditService_LogPasswordReset_Success(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
err := service.LogPasswordReset(ctx, userID, true, "127.0.0.1", "test-agent")
assert.NoError(t, err)
// Verify password_reset_success was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "password_reset_success", action)
}
func TestAuditService_LogPasswordReset_Failed(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
err := service.LogPasswordReset(ctx, userID, false, "127.0.0.1", "test-agent")
assert.NoError(t, err)
// Verify password_reset_failed was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "password_reset_failed", action)
}
func TestAuditService_LogTwoFactorEnabled(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
err := service.LogTwoFactorEnabled(ctx, userID, "127.0.0.1", "test-agent")
assert.NoError(t, err)
// Verify 2fa_enabled was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "2fa_enabled", action)
}
func TestAuditService_LogTwoFactorDisabled(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
err := service.LogTwoFactorDisabled(ctx, userID, "127.0.0.1", "test-agent")
assert.NoError(t, err)
// Verify 2fa_disabled was logged
var action string
err = testDB.QueryRowContext(ctx, `
SELECT action FROM audit_logs WHERE user_id = $1 ORDER BY timestamp DESC LIMIT 1
`, userID.String()).Scan(&action)
assert.NoError(t, err)
assert.Equal(t, "2fa_disabled", action)
}
func TestAuditService_LogAction_MultipleLogs(t *testing.T) {
service, _, testDB := setupTestAuditService(t)
ctx := context.Background()
userID := uuid.New()
// Create multiple logs
for i := 0; i < 5; i++ {
req := &AuditLogCreateRequest{
UserID: &userID,
Action: "test_action",
Resource: "test_resource",
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: map[string]interface{}{"index": i},
}
err := service.LogAction(ctx, req)
assert.NoError(t, err)
}
// Verify all logs were created
var count int
err := testDB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM audit_logs WHERE user_id = $1 AND action = $2
`, userID.String(), "test_action").Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 5, count)
}
func TestAuditService_AuditLog_Fields(t *testing.T) {
log := &AuditLog{
ID: uuid.New(),
UserID: func() *uuid.UUID { id := uuid.New(); return &id }(),
Action: "test_action",
Resource: "test_resource",
ResourceID: func() *uuid.UUID { id := uuid.New(); return &id }(),
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: json.RawMessage(`{"key":"value"}`),
Timestamp: time.Now(),
}
assert.NotZero(t, log.ID)
assert.NotNil(t, log.UserID)
assert.Equal(t, "test_action", log.Action)
assert.Equal(t, "test_resource", log.Resource)
assert.NotNil(t, log.ResourceID)
assert.Equal(t, "127.0.0.1", log.IPAddress)
assert.Equal(t, "test-agent", log.UserAgent)
assert.NotEmpty(t, log.Metadata)
assert.NotZero(t, log.Timestamp)
}
func TestAuditService_AuditLogCreateRequest_Fields(t *testing.T) {
userID := uuid.New()
resourceID := uuid.New()
req := &AuditLogCreateRequest{
UserID: &userID,
Action: "test_action",
Resource: "test_resource",
ResourceID: &resourceID,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
Metadata: map[string]interface{}{"key": "value"},
}
assert.Equal(t, &userID, req.UserID)
assert.Equal(t, "test_action", req.Action)
assert.Equal(t, "test_resource", req.Resource)
assert.Equal(t, &resourceID, req.ResourceID)
assert.Equal(t, "127.0.0.1", req.IPAddress)
assert.Equal(t, "test-agent", req.UserAgent)
assert.NotEmpty(t, req.Metadata)
}
func TestAuditService_AuditLogSearchRequest_Fields(t *testing.T) {
userID := uuid.New()
resourceID := uuid.New()
startDate := time.Now()
endDate := time.Now().Add(24 * time.Hour)
req := &AuditLogSearchRequest{
UserID: &userID,
Action: "test_action",
Resource: "test_resource",
ResourceID: &resourceID,
IPAddress: "127.0.0.1",
UserAgent: "test-agent",
StartDate: &startDate,
EndDate: &endDate,
Limit: 10,
Offset: 0,
Page: 1,
}
assert.Equal(t, &userID, req.UserID)
assert.Equal(t, "test_action", req.Action)
assert.Equal(t, "test_resource", req.Resource)
assert.Equal(t, &resourceID, req.ResourceID)
assert.Equal(t, "127.0.0.1", req.IPAddress)
assert.Equal(t, "test-agent", req.UserAgent)
assert.Equal(t, &startDate, req.StartDate)
assert.Equal(t, &endDate, req.EndDate)
assert.Equal(t, 10, req.Limit)
assert.Equal(t, 0, req.Offset)
assert.Equal(t, 1, req.Page)
}
func TestAuditService_AuditStats_Fields(t *testing.T) {
stats := &AuditStats{
Action: "test_action",
Resource: "test_resource",
ActionCount: 100,
UniqueUsers: 50,
UniqueIPs: 25,
}
assert.Equal(t, "test_action", stats.Action)
assert.Equal(t, "test_resource", stats.Resource)
assert.Equal(t, int64(100), stats.ActionCount)
assert.Equal(t, int64(50), stats.UniqueUsers)
assert.Equal(t, int64(25), stats.UniqueIPs)
}
func TestAuditService_SuspiciousActivity_Fields(t *testing.T) {
userID := uuid.New()
activity := &SuspiciousActivity{
UserID: &userID,
IPAddress: "127.0.0.1",
ActionCount: 100,
UniqueActions: 10,
RiskScore: 75,
}
assert.Equal(t, &userID, activity.UserID)
assert.Equal(t, "127.0.0.1", activity.IPAddress)
assert.Equal(t, int64(100), activity.ActionCount)
assert.Equal(t, int64(10), activity.UniqueActions)
assert.Equal(t, 75, activity.RiskScore)
}