459 lines
14 KiB
Go
459 lines
14 KiB
Go
package audit
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func newTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "test_audit.db")
|
|
db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
|
require.NoError(t, err)
|
|
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
actor TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
resource_type TEXT NOT NULL DEFAULT '',
|
|
resource_id TEXT NOT NULL DEFAULT '',
|
|
details TEXT NOT NULL DEFAULT '{}',
|
|
ip_address TEXT NOT NULL DEFAULT ''
|
|
)
|
|
`)
|
|
require.NoError(t, err)
|
|
return db
|
|
}
|
|
|
|
func TestLog(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{
|
|
Actor: "admin",
|
|
Action: "user.create",
|
|
ResourceType: "user",
|
|
ResourceID: "testuser",
|
|
Details: map[string]string{"role": "admin"},
|
|
IPAddress: "10.0.0.1",
|
|
})
|
|
|
|
entries, total, err := logger.Query("", "", "", "", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Len(t, entries, 1)
|
|
assert.Equal(t, "admin", entries[0].Actor)
|
|
assert.Equal(t, "user.create", entries[0].Action)
|
|
assert.Equal(t, "user", entries[0].ResourceType)
|
|
assert.Equal(t, "testuser", entries[0].ResourceID)
|
|
assert.Contains(t, entries[0].Details, "admin")
|
|
assert.Equal(t, "10.0.0.1", entries[0].IPAddress)
|
|
}
|
|
|
|
func TestLog_NilLogger(t *testing.T) {
|
|
var logger *Logger
|
|
// should not panic
|
|
logger.Log(Entry{Actor: "test", Action: "test"})
|
|
}
|
|
|
|
func TestLog_NilDB(t *testing.T) {
|
|
logger := &Logger{db: nil}
|
|
// should not panic
|
|
logger.Log(Entry{Actor: "test", Action: "test"})
|
|
}
|
|
|
|
func TestLogWithUser(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.LogWithUser("admin", "client.delete", "client", "xyz123", "192.168.1.1", nil)
|
|
|
|
entries, total, err := logger.Query("", "", "", "", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Equal(t, "admin", entries[0].Actor)
|
|
assert.Equal(t, "client.delete", entries[0].Action)
|
|
}
|
|
|
|
func TestQuery_Filtering(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "user.create", ResourceType: "user", ResourceID: "u1", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "client.create", ResourceType: "client", ResourceID: "c1", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "manager", Action: "client.update", ResourceType: "client", ResourceID: "c1", IPAddress: "10.0.0.2"})
|
|
|
|
// filter by actor
|
|
entries, total, err := logger.Query("", "", "admin", "", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, total)
|
|
assert.Len(t, entries, 2)
|
|
|
|
// filter by action
|
|
entries, total, err = logger.Query("", "", "", "client.create", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Equal(t, "client.create", entries[0].Action)
|
|
|
|
// filter by both
|
|
_, total, err = logger.Query("", "", "manager", "client.update", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
}
|
|
|
|
func TestQuery_Pagination(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
}
|
|
|
|
entries, total, err := logger.Query("", "", "", "", "", 1, 3)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 10, total)
|
|
assert.Len(t, entries, 3)
|
|
|
|
entries2, _, err := logger.Query("", "", "", "", "", 2, 3)
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries2, 3)
|
|
// ensure different page
|
|
assert.NotEqual(t, entries[0].ID, entries2[0].ID)
|
|
}
|
|
|
|
func TestQuery_DefaultPagination(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
|
|
entries, _, err := logger.Query("", "", "", "", "", 0, 0)
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 1)
|
|
}
|
|
|
|
func TestQuery_DateRange(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
|
|
// future date range should return nothing
|
|
future := time.Now().Add(24 * time.Hour).Format("2006-01-02")
|
|
entries, total, err := logger.Query(future, "", "", "", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, total)
|
|
assert.Len(t, entries, 0)
|
|
|
|
// past date range should return the entry
|
|
past := time.Now().Add(-24 * time.Hour).Format("2006-01-02")
|
|
_, total, err = logger.Query(past, "", "", "", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
}
|
|
|
|
func TestQueryAll(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
}
|
|
|
|
entries, err := logger.QueryAll("", "", "", "", "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 5)
|
|
}
|
|
|
|
func TestQueryAll_Filtering(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "user.create", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "user1", Action: "client.create", IPAddress: "10.0.0.2"})
|
|
|
|
entries, err := logger.QueryAll("", "", "admin", "", "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 1)
|
|
}
|
|
|
|
func TestQueryAll_DateRange(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test.action", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "test.action2", IPAddress: "10.0.0.1"})
|
|
|
|
// past date - should include entries
|
|
past := time.Now().Add(-24 * time.Hour).Format("2006-01-02")
|
|
entries, err := logger.QueryAll(past, "", "", "", "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 2)
|
|
|
|
// future date range should return nothing
|
|
future := time.Now().Add(24 * time.Hour).Format("2006-01-02")
|
|
entries, err = logger.QueryAll(future, "", "", "", "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 0)
|
|
|
|
// with end date in the past
|
|
pastEnd := time.Now().Add(-1 * time.Hour).Format("2006-01-02T15:04:05")
|
|
entries, err = logger.QueryAll("", pastEnd, "", "", "")
|
|
require.NoError(t, err)
|
|
// Entries were created "now" which is after pastEnd, so may return 0
|
|
// The exact result depends on timing - just verify no error
|
|
assert.NotNil(t, entries)
|
|
}
|
|
|
|
func TestQueryAll_ByAction(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "client.create", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "client.delete", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "user.create", IPAddress: "10.0.0.1"})
|
|
|
|
entries, err := logger.QueryAll("", "", "", "client.create", "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 1)
|
|
assert.Equal(t, "client.create", entries[0].Action)
|
|
}
|
|
|
|
func TestLog_NilDetails(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{
|
|
Actor: "admin",
|
|
Action: "test.nil.details",
|
|
ResourceType: "test",
|
|
ResourceID: "1",
|
|
Details: nil,
|
|
IPAddress: "10.0.0.1",
|
|
})
|
|
|
|
entries, total, err := logger.Query("", "", "", "test.nil.details", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Equal(t, "{}", entries[0].Details)
|
|
}
|
|
|
|
func TestQuery_CombinedActorAndDateRange(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "combined.test", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "user1", Action: "combined.test", IPAddress: "10.0.0.2"})
|
|
|
|
past := time.Now().Add(-24 * time.Hour).Format("2006-01-02")
|
|
futureEnd := time.Now().Add(24 * time.Hour).Format("2006-01-02")
|
|
|
|
entries, total, err := logger.Query(past, futureEnd, "admin", "combined.test", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Len(t, entries, 1)
|
|
assert.Equal(t, "admin", entries[0].Actor)
|
|
}
|
|
|
|
// --- DistinctFilters Tests ---
|
|
|
|
func TestDistinctFilters_Empty(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
actors, actions, err := logger.DistinctFilters()
|
|
require.NoError(t, err)
|
|
assert.Empty(t, actors)
|
|
assert.Empty(t, actions)
|
|
}
|
|
|
|
func TestDistinctFilters_WithData(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "user.create", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "client.create", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "manager", Action: "client.update", IPAddress: "10.0.0.2"})
|
|
logger.Log(Entry{Actor: "manager", Action: "user.create", IPAddress: "10.0.0.2"})
|
|
|
|
actors, actions, err := logger.DistinctFilters()
|
|
require.NoError(t, err)
|
|
assert.Len(t, actors, 2)
|
|
assert.Contains(t, actors, "admin")
|
|
assert.Contains(t, actors, "manager")
|
|
assert.Len(t, actions, 3)
|
|
assert.Contains(t, actions, "user.create")
|
|
assert.Contains(t, actions, "client.create")
|
|
assert.Contains(t, actions, "client.update")
|
|
}
|
|
|
|
func TestDistinctFilters_SingleActor(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "a1", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "a2", IPAddress: "10.0.0.1"})
|
|
|
|
actors, actions, err := logger.DistinctFilters()
|
|
require.NoError(t, err)
|
|
assert.Len(t, actors, 1)
|
|
assert.Equal(t, "admin", actors[0])
|
|
assert.Len(t, actions, 2)
|
|
}
|
|
|
|
// --- buildWhereClause with search parameter ---
|
|
|
|
func TestQuery_SearchFilter(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "user.create", ResourceType: "user", ResourceID: "user-abc", Details: map[string]string{"role": "admin"}, IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "manager", Action: "client.create", ResourceType: "client", ResourceID: "client-xyz", Details: map[string]string{"name": "test"}, IPAddress: "10.0.0.2"})
|
|
|
|
// search by resource_id
|
|
entries, total, err := logger.Query("", "", "", "", "user-abc", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Len(t, entries, 1)
|
|
assert.Equal(t, "user-abc", entries[0].ResourceID)
|
|
|
|
// search by details content
|
|
entries, total, err = logger.Query("", "", "", "", "test", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Equal(t, "client-xyz", entries[0].ResourceID)
|
|
|
|
// search by actor name
|
|
entries, total, err = logger.Query("", "", "", "", "manager", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Equal(t, "manager", entries[0].Actor)
|
|
|
|
// search with no matches
|
|
entries, total, err = logger.Query("", "", "", "", "nonexistent", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, total)
|
|
assert.Empty(t, entries)
|
|
}
|
|
|
|
func TestQueryAll_SearchFilter(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test", ResourceID: "res-123", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "test", ResourceID: "res-456", IPAddress: "10.0.0.1"})
|
|
|
|
entries, err := logger.QueryAll("", "", "", "", "res-123")
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 1)
|
|
assert.Equal(t, "res-123", entries[0].ResourceID)
|
|
}
|
|
|
|
// --- Query edge cases ---
|
|
|
|
func TestQuery_MaxPerPage(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
|
|
// perPage > maxPerPage should be clamped
|
|
entries, _, err := logger.Query("", "", "", "", "", 1, 999)
|
|
require.NoError(t, err)
|
|
assert.Len(t, entries, 1)
|
|
}
|
|
|
|
func TestQuery_AllFiltersCombined(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "user.create", ResourceType: "user", ResourceID: "match-this", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "admin", Action: "client.create", ResourceType: "client", ResourceID: "other", IPAddress: "10.0.0.1"})
|
|
logger.Log(Entry{Actor: "manager", Action: "user.create", ResourceType: "user", ResourceID: "match-this", IPAddress: "10.0.0.2"})
|
|
|
|
past := time.Now().Add(-24 * time.Hour).Format("2006-01-02")
|
|
futureEnd := time.Now().Add(24 * time.Hour).Format("2006-01-02")
|
|
|
|
// Combine all filters: from, to, actor, action, search
|
|
entries, total, err := logger.Query(past, futureEnd, "admin", "user.create", "match", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Len(t, entries, 1)
|
|
assert.Equal(t, "admin", entries[0].Actor)
|
|
assert.Equal(t, "user.create", entries[0].Action)
|
|
assert.Equal(t, "match-this", entries[0].ResourceID)
|
|
}
|
|
|
|
func TestQuery_ToDateFilter(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
|
|
// With end date far in the future should include the entry
|
|
futureEnd := time.Now().Add(24 * time.Hour).Format("2006-01-02")
|
|
entries, total, err := logger.Query("", futureEnd, "", "", "", 1, 50)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, total)
|
|
assert.Len(t, entries, 1)
|
|
}
|
|
|
|
// --- Error path tests ---
|
|
|
|
func TestQuery_ClosedDB(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
|
|
db.Close()
|
|
|
|
_, _, err := logger.Query("", "", "", "", "", 1, 50)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestQueryAll_ClosedDB(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
db.Close()
|
|
|
|
_, err := logger.QueryAll("", "", "", "", "")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestDistinctFilters_ClosedDB(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
db.Close()
|
|
|
|
_, _, err := logger.DistinctFilters()
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestLog_ClosedDB(t *testing.T) {
|
|
db := newTestDB(t)
|
|
logger := NewLogger(db)
|
|
|
|
db.Close()
|
|
|
|
// Should not panic, just log the error internally
|
|
logger.Log(Entry{Actor: "admin", Action: "test", IPAddress: "10.0.0.1"})
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
os.Exit(m.Run())
|
|
}
|