Files
Charon/backend/internal/services/log_service_test.go
2026-03-04 18:34:49 +00:00

215 lines
7.1 KiB
Go

package services
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogService(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "cpm-log-service-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
dataDir := filepath.Join(tmpDir, "data")
logsDir := filepath.Join(dataDir, "logs")
err = os.MkdirAll(logsDir, 0o750) // #nosec G301 -- test directory
require.NoError(t, err)
// Create sample JSON logs
logEntry1 := models.CaddyAccessLog{
Level: "info",
Ts: 1600000000,
Msg: "request handled",
Status: 200,
}
logEntry1.Request.Method = "GET"
logEntry1.Request.Host = "example.com"
logEntry1.Request.URI = "/"
logEntry1.Request.RemoteIP = "1.2.3.4"
logEntry2 := models.CaddyAccessLog{
Level: "error",
Ts: 1600000060,
Msg: "error handled",
Status: 500,
}
logEntry2.Request.Method = "POST"
logEntry2.Request.Host = "api.example.com"
logEntry2.Request.URI = "/submit"
logEntry2.Request.RemoteIP = "5.6.7.8"
line1, _ := json.Marshal(logEntry1)
line2, _ := json.Marshal(logEntry2)
content := string(line1) + "\n" + string(line2) + "\n"
err = os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o600) // #nosec G306 -- test fixture
require.NoError(t, err)
err = os.WriteFile(filepath.Join(logsDir, "other.txt"), []byte("ignore me"), 0o600) // #nosec G306 -- test fixture
require.NoError(t, err)
cfg := &config.Config{DatabasePath: filepath.Join(dataDir, "charon.db")}
service := NewLogService(cfg)
// Test List
logs, err := service.ListLogs()
require.NoError(t, err)
assert.Len(t, logs, 1)
assert.Equal(t, "access.log", logs[0].Name)
// Test QueryLogs - All
results, total, err := service.QueryLogs("access.log", models.LogFilter{Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(2), total)
assert.Len(t, results, 2)
// Should be reversed (newest first)
assert.Equal(t, 500, results[0].Status)
assert.Equal(t, 200, results[1].Status)
// Test QueryLogs - Filter Status
results, total, err = service.QueryLogs("access.log", models.LogFilter{Status: "5xx", Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, results, 1)
assert.Equal(t, 500, results[0].Status)
// Test QueryLogs - Filter Host
results, total, err = service.QueryLogs("access.log", models.LogFilter{Host: "api.example.com", Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, results, 1)
assert.Equal(t, "api.example.com", results[0].Request.Host)
// Test QueryLogs - Search
results, total, err = service.QueryLogs("access.log", models.LogFilter{Search: "submit", Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, results, 1)
assert.Equal(t, "/submit", results[0].Request.URI)
// Test GetLogPath
path, err := service.GetLogPath("access.log")
require.NoError(t, err)
assert.Equal(t, filepath.Join(logsDir, "access.log"), path)
// Test GetLogPath non-existent
_, err = service.GetLogPath("missing.log")
assert.Error(t, err)
// Test GetLogPath - Invalid
_, err = service.GetLogPath("nonexistent.log")
assert.Error(t, err)
// Test GetLogPath - Traversal
_, err = service.GetLogPath("../../etc/passwd")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid filename")
// Test ListLogs - Directory Not Exist
nonExistService := NewLogService(&config.Config{DatabasePath: filepath.Join(t.TempDir(), "missing", "charon.db")})
logs, err = nonExistService.ListLogs()
require.NoError(t, err)
assert.Empty(t, logs)
// Test QueryLogs - Non-JSON Logs
plainContent := "2023/10/27 10:00:00 Application started\nJust a plain line\n"
err = os.WriteFile(filepath.Join(logsDir, "app.log"), []byte(plainContent), 0o600) // #nosec G306 -- test fixture
require.NoError(t, err)
results, total, err = service.QueryLogs("app.log", models.LogFilter{Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(2), total)
// Reverse order check
assert.Equal(t, "Just a plain line", results[0].Msg)
assert.Equal(t, "Application started", results[1].Msg)
assert.Equal(t, "INFO", results[1].Level)
// Test QueryLogs - Pagination
// We have 2 logs in access.log
results, _, err = service.QueryLogs("access.log", models.LogFilter{Limit: 1, Offset: 0})
require.NoError(t, err)
assert.Len(t, results, 1)
assert.Equal(t, 500, results[0].Status) // Newest first
results, _, err = service.QueryLogs("access.log", models.LogFilter{Limit: 1, Offset: 1})
require.NoError(t, err)
assert.Len(t, results, 1)
assert.Equal(t, 200, results[0].Status) // Second newest
results, _, err = service.QueryLogs("access.log", models.LogFilter{Limit: 10, Offset: 5})
require.NoError(t, err)
assert.Empty(t, results)
// Test QueryLogs - Exact Status Match
results, total, err = service.QueryLogs("access.log", models.LogFilter{Status: "200", Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, 200, results[0].Status)
// Test QueryLogs - Search Fields
// Search Method
results, total, err = service.QueryLogs("access.log", models.LogFilter{Search: "POST", Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, "POST", results[0].Request.Method)
// Search RemoteIP
results, total, err = service.QueryLogs("access.log", models.LogFilter{Search: "5.6.7.8", Limit: 10})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, "5.6.7.8", results[0].Request.RemoteIP)
}
func TestLogService_logDirsAndSymlinkDedup(t *testing.T) {
tmpDir := t.TempDir()
dataDir := filepath.Join(tmpDir, "data")
logsDir := filepath.Join(dataDir, "logs")
caddyLogsDir := filepath.Join(dataDir, "caddy-logs")
require.NoError(t, os.MkdirAll(logsDir, 0o750))
require.NoError(t, os.MkdirAll(caddyLogsDir, 0o750))
cfg := &config.Config{DatabasePath: filepath.Join(dataDir, "charon.db"), CaddyLogDir: caddyLogsDir}
service := NewLogService(cfg)
accessPath := filepath.Join(logsDir, "access.log")
require.NoError(t, os.WriteFile(accessPath, []byte("{}\n"), 0o600))
require.NoError(t, os.Symlink(accessPath, filepath.Join(logsDir, "cpmp.log")))
t.Setenv("CHARON_CADDY_ACCESS_LOG", filepath.Join(caddyLogsDir, "access-caddy.log"))
dirs := service.logDirs()
assert.Contains(t, dirs, logsDir)
assert.Contains(t, dirs, caddyLogsDir)
logs, err := service.ListLogs()
require.NoError(t, err)
assert.Len(t, logs, 1)
assert.Equal(t, "access.log", logs[0].Name)
}
func TestLogService_logDirs_SkipsDotAndEmpty(t *testing.T) {
t.Setenv("CHARON_CADDY_ACCESS_LOG", filepath.Join(t.TempDir(), "caddy", "access.log"))
service := &LogService{LogDir: ".", CaddyLogDir: ""}
dirs := service.logDirs()
require.Len(t, dirs, 1)
assert.NotEqual(t, ".", dirs[0])
}
func TestLogService_ListLogs_ReadDirError(t *testing.T) {
tmpDir := t.TempDir()
notDir := filepath.Join(tmpDir, "not-a-dir")
require.NoError(t, os.WriteFile(notDir, []byte("x"), 0o600))
service := &LogService{LogDir: notDir}
_, err := service.ListLogs()
require.Error(t, err)
}