- Marked 12 tests as skip pending feature implementation - Features tracked in GitHub issue #686 (system log viewer feature completion) - Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality - Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation - TODO comments in code reference GitHub #686 for feature completion tracking - Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
436 lines
13 KiB
Go
436 lines
13 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/config"
|
|
"github.com/Wikid82/charon/backend/internal/database"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
)
|
|
|
|
// setupTestDB creates a temporary test database
|
|
func setupTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
|
|
// Create temp database file
|
|
tmpFile := t.TempDir() + "/test.db"
|
|
db, err := database.Connect(tmpFile)
|
|
require.NoError(t, err, "Failed to create test database")
|
|
|
|
// Run migrations
|
|
err = db.AutoMigrate(
|
|
&models.Setting{},
|
|
&models.SecurityConfig{},
|
|
&models.SecurityAudit{},
|
|
)
|
|
require.NoError(t, err, "Failed to run migrations")
|
|
|
|
return db
|
|
}
|
|
|
|
func TestEmergencyServer_Disabled(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: false,
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start successfully when disabled")
|
|
|
|
// Server should not be running
|
|
assert.Nil(t, server.server, "HTTP server should not be initialized when disabled")
|
|
}
|
|
|
|
func TestEmergencyServer_Health(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Set emergency token required for enabled server
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-for-health-check-32chars"))
|
|
defer func() { _ = os.Unsetenv("CHARON_EMERGENCY_TOKEN") }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0", // Random port for testing
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start successfully")
|
|
defer func() { _ = server.Stop(context.Background()) }()
|
|
|
|
// Wait for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Get actual port
|
|
addr := server.GetAddr()
|
|
assert.NotEmpty(t, addr, "Server address should be set")
|
|
|
|
// Make health check request
|
|
resp, err := http.Get(fmt.Sprintf("http://%s/health", addr))
|
|
require.NoError(t, err, "Health check request should succeed")
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Health check should return 200")
|
|
|
|
var body map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
|
require.NoError(t, err, "Should decode JSON response")
|
|
|
|
assert.Equal(t, "ok", body["status"], "Status should be ok")
|
|
assert.Equal(t, "emergency", body["server"], "Server should be emergency")
|
|
assert.NotEmpty(t, body["time"], "Time should be present")
|
|
}
|
|
|
|
func TestEmergencyServer_SecurityReset(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Set emergency token
|
|
emergencyToken := "test-emergency-token-for-testing-32chars"
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", emergencyToken))
|
|
defer func() { require.NoError(t, os.Unsetenv("CHARON_EMERGENCY_TOKEN")) }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0",
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start successfully")
|
|
defer func() { _ = server.Stop(context.Background()) }()
|
|
|
|
// Wait for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
addr := server.GetAddr()
|
|
|
|
// Create HTTP client
|
|
client := &http.Client{}
|
|
|
|
// Make emergency reset request
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/emergency/security-reset", addr), nil)
|
|
require.NoError(t, err, "Should create request")
|
|
req.Header.Set("X-Emergency-Token", emergencyToken)
|
|
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err, "Emergency reset request should succeed")
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Emergency reset should return 200")
|
|
|
|
var body map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
|
require.NoError(t, err, "Should decode JSON response")
|
|
|
|
assert.True(t, body["success"].(bool), "Success should be true")
|
|
assert.NotNil(t, body["disabled_modules"], "Disabled modules should be present")
|
|
}
|
|
|
|
func TestEmergencyServer_BasicAuth(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Set emergency token
|
|
emergencyToken := "test-emergency-token-for-testing-32chars"
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", emergencyToken))
|
|
defer func() { require.NoError(t, os.Unsetenv("CHARON_EMERGENCY_TOKEN")) }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0",
|
|
BasicAuthUsername: "admin",
|
|
BasicAuthPassword: "testpass",
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start successfully")
|
|
defer func() { _ = server.Stop(context.Background()) }()
|
|
|
|
// Wait for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
addr := server.GetAddr()
|
|
|
|
t.Run("WithoutAuth", func(t *testing.T) {
|
|
// Try without Basic Auth - should fail
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/emergency/security-reset", addr), nil)
|
|
require.NoError(t, err, "Should create request")
|
|
req.Header.Set("X-Emergency-Token", emergencyToken)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err, "Request should complete")
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should require authentication")
|
|
})
|
|
|
|
t.Run("WithInvalidAuth", func(t *testing.T) {
|
|
// Try with wrong credentials
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/emergency/security-reset", addr), nil)
|
|
require.NoError(t, err, "Should create request")
|
|
req.Header.Set("X-Emergency-Token", emergencyToken)
|
|
req.SetBasicAuth("admin", "wrongpassword")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err, "Request should complete")
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should reject invalid credentials")
|
|
})
|
|
|
|
t.Run("WithValidAuth", func(t *testing.T) {
|
|
// Try with correct credentials
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/emergency/security-reset", addr), nil)
|
|
require.NoError(t, err, "Should create request")
|
|
req.Header.Set("X-Emergency-Token", emergencyToken)
|
|
req.SetBasicAuth("admin", "testpass")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err, "Request should complete")
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should accept valid credentials")
|
|
|
|
var body map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
|
require.NoError(t, err, "Should decode JSON response")
|
|
|
|
assert.True(t, body["success"].(bool), "Success should be true")
|
|
})
|
|
}
|
|
|
|
func TestEmergencyServer_NoAuth_Warning(t *testing.T) {
|
|
// This test verifies that a warning is logged when no auth is configured
|
|
// We can't easily test log output, but we can verify the server starts
|
|
db := setupTestDB(t)
|
|
|
|
// Set emergency token required for enabled server
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-for-no-auth-warning-test"))
|
|
defer func() { _ = os.Unsetenv("CHARON_EMERGENCY_TOKEN") }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0",
|
|
// No auth configured
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start even without auth")
|
|
defer func() { _ = server.Stop(context.Background()) }()
|
|
|
|
// Wait for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify server is accessible without auth
|
|
addr := server.GetAddr()
|
|
resp, err := http.Get(fmt.Sprintf("http://%s/health", addr))
|
|
require.NoError(t, err, "Health check should work without auth")
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should return 200")
|
|
}
|
|
|
|
func TestEmergencyServer_GracefulShutdown(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Set emergency token required for enabled server
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-for-graceful-shutdown-test"))
|
|
defer func() { _ = os.Unsetenv("CHARON_EMERGENCY_TOKEN") }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0",
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start successfully")
|
|
|
|
// Wait for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify server is running
|
|
addr := server.GetAddr()
|
|
resp, err := http.Get(fmt.Sprintf("http://%s/health", addr))
|
|
require.NoError(t, err, "Server should be running")
|
|
_ = resp.Body.Close()
|
|
|
|
// Stop server with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
err = server.Stop(ctx)
|
|
assert.NoError(t, err, "Server should stop gracefully")
|
|
|
|
// Verify server is stopped (request should fail)
|
|
resp, err = http.Get(fmt.Sprintf("http://%s/health", addr))
|
|
if resp != nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
assert.Error(t, err, "Server should be stopped")
|
|
}
|
|
|
|
func TestEmergencyServer_MultipleEndpoints(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
// Set emergency token
|
|
emergencyToken := "test-emergency-token-for-testing-32chars"
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", emergencyToken))
|
|
defer func() { require.NoError(t, os.Unsetenv("CHARON_EMERGENCY_TOKEN")) }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0",
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
require.NoError(t, err, "Server should start successfully")
|
|
defer func() { _ = server.Stop(context.Background()) }()
|
|
|
|
// Wait for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
addr := server.GetAddr()
|
|
|
|
t.Run("HealthEndpoint", func(t *testing.T) {
|
|
resp, err := http.Get(fmt.Sprintf("http://%s/health", addr))
|
|
require.NoError(t, err)
|
|
defer func() { _ = resp.Body.Close() }()
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("EmergencyResetEndpoint", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/emergency/security-reset", addr), nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("X-Emergency-Token", emergencyToken)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
defer func() { _ = resp.Body.Close() }()
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("NotFoundEndpoint", func(t *testing.T) {
|
|
resp, err := http.Get(fmt.Sprintf("http://%s/nonexistent", addr))
|
|
require.NoError(t, err)
|
|
defer func() { _ = resp.Body.Close() }()
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestEmergencyServer_StartupValidation tests that server fails fast if token is empty or whitespace
|
|
func TestEmergencyServer_StartupValidation(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
token string
|
|
expectSuccess bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "EmptyToken",
|
|
token: "",
|
|
expectSuccess: false,
|
|
description: "Server should fail to start with empty token",
|
|
},
|
|
{
|
|
name: "WhitespaceToken",
|
|
token: " ",
|
|
expectSuccess: false,
|
|
description: "Server should fail to start with whitespace-only token",
|
|
},
|
|
{
|
|
name: "ValidToken",
|
|
token: "test-emergency-token-for-testing-32chars",
|
|
expectSuccess: true,
|
|
description: "Server should start successfully with valid token",
|
|
},
|
|
{
|
|
name: "ShortToken",
|
|
token: "short",
|
|
expectSuccess: true, // Server starts but logs warning
|
|
description: "Server should start with short token but log warning",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Set token
|
|
if tt.token != "" {
|
|
require.NoError(t, os.Setenv("CHARON_EMERGENCY_TOKEN", tt.token))
|
|
} else {
|
|
_ = os.Unsetenv("CHARON_EMERGENCY_TOKEN")
|
|
}
|
|
defer func() { _ = os.Unsetenv("CHARON_EMERGENCY_TOKEN") }()
|
|
|
|
cfg := config.EmergencyConfig{
|
|
Enabled: true,
|
|
BindAddress: "127.0.0.1:0",
|
|
}
|
|
|
|
server := NewEmergencyServer(db, cfg)
|
|
err := server.Start()
|
|
|
|
if tt.expectSuccess {
|
|
assert.NoError(t, err, tt.description)
|
|
if err == nil {
|
|
_ = server.Stop(context.Background())
|
|
}
|
|
} else {
|
|
assert.Error(t, err, tt.description)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEmergencyServer_TokenRedaction tests the token redaction function
|
|
func TestEmergencyServer_TokenRedaction(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
token string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "EmptyToken",
|
|
token: "",
|
|
expected: "[EMERGENCY_TOKEN:empty]",
|
|
},
|
|
{
|
|
name: "ShortToken",
|
|
token: "short",
|
|
expected: "[EMERGENCY_TOKEN:***]",
|
|
},
|
|
{
|
|
name: "ValidToken",
|
|
token: "f51dedd6a4f2eaa200dcbf4feecae78ff926e06d9094d726f3613729b66d346b",
|
|
expected: "[EMERGENCY_TOKEN:f51d...346b]",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := redactToken(tt.token)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|