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.
489 lines
14 KiB
Go
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)
|
|
}
|