Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
405 lines
11 KiB
Go
Executable File
405 lines
11 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/database"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
)
|
|
|
|
func TestResetPasswordCommand_Succeeds(t *testing.T) {
|
|
if os.Getenv("CHARON_TEST_RUN_MAIN") == "1" {
|
|
// Child process: emulate CLI args and run main().
|
|
email := os.Getenv("CHARON_TEST_EMAIL")
|
|
newPassword := os.Getenv("CHARON_TEST_NEW_PASSWORD")
|
|
os.Args = []string{"charon", "reset-password", email, newPassword}
|
|
main()
|
|
return
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
// #nosec G301 -- Test fixture directory with standard permissions
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
db, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("connect db: %v", err)
|
|
}
|
|
if err = db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("automigrate: %v", err)
|
|
}
|
|
|
|
email := "user@example.com"
|
|
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: models.RoleAdmin, Enabled: true}
|
|
user.PasswordHash = "$2a$10$example_hashed_password"
|
|
if err = db.Create(&user).Error; err != nil {
|
|
t.Fatalf("seed user: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestResetPasswordCommand_Succeeds") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe
|
|
cmd.Dir = tmp
|
|
cmd.Env = append(os.Environ(),
|
|
"CHARON_TEST_RUN_MAIN=1",
|
|
"CHARON_TEST_EMAIL="+email,
|
|
"CHARON_TEST_NEW_PASSWORD=new-password",
|
|
"CHARON_DB_PATH="+dbPath,
|
|
"CHARON_CADDY_CONFIG_DIR="+filepath.Join(tmp, "caddy"),
|
|
"CHARON_IMPORT_DIR="+filepath.Join(tmp, "imports"),
|
|
)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("expected exit 0; err=%v; output=%s", err, string(out))
|
|
}
|
|
}
|
|
|
|
func TestMigrateCommand_Succeeds(t *testing.T) {
|
|
if os.Getenv("CHARON_TEST_RUN_MAIN") == "1" {
|
|
// Child process: emulate CLI args and run main().
|
|
os.Args = []string{"charon", "migrate"}
|
|
main()
|
|
return
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
// #nosec G301 -- Test fixture directory with standard permissions
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
// Create database without security tables
|
|
db, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("connect db: %v", err)
|
|
}
|
|
// Only migrate User table to simulate old database
|
|
if err = db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("automigrate user: %v", err)
|
|
}
|
|
|
|
// Verify security tables don't exist
|
|
if db.Migrator().HasTable(&models.SecurityConfig{}) {
|
|
t.Fatal("SecurityConfig table should not exist yet")
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestMigrateCommand_Succeeds") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe
|
|
cmd.Dir = tmp
|
|
cmd.Env = append(os.Environ(),
|
|
"CHARON_TEST_RUN_MAIN=1",
|
|
"CHARON_DB_PATH="+dbPath,
|
|
"CHARON_CADDY_CONFIG_DIR="+filepath.Join(tmp, "caddy"),
|
|
"CHARON_IMPORT_DIR="+filepath.Join(tmp, "imports"),
|
|
)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("expected exit 0; err=%v; output=%s", err, string(out))
|
|
}
|
|
|
|
// Reconnect and verify security tables were created
|
|
db2, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("reconnect db: %v", err)
|
|
}
|
|
|
|
securityModels := []any{
|
|
&models.SecurityConfig{},
|
|
&models.SecurityDecision{},
|
|
&models.SecurityAudit{},
|
|
&models.SecurityRuleSet{},
|
|
&models.CrowdsecPresetEvent{},
|
|
&models.CrowdsecConsoleEnrollment{},
|
|
}
|
|
|
|
for _, model := range securityModels {
|
|
if !db2.Migrator().HasTable(model) {
|
|
t.Errorf("Table for %T was not created by migrate command", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStartupVerification_MissingTables(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0o750); err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
// Create database without security tables
|
|
db, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("connect db: %v", err)
|
|
}
|
|
// Only migrate User table to simulate old database
|
|
if err = db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("automigrate user: %v", err)
|
|
}
|
|
|
|
// Verify security tables don't exist
|
|
if db.Migrator().HasTable(&models.SecurityConfig{}) {
|
|
t.Fatal("SecurityConfig table should not exist yet")
|
|
}
|
|
|
|
// Close and reopen to simulate startup scenario
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
|
|
db, err = database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("reconnect db: %v", err)
|
|
}
|
|
|
|
// Simulate startup verification logic from main.go
|
|
securityModels := []any{
|
|
&models.SecurityConfig{},
|
|
&models.SecurityDecision{},
|
|
&models.SecurityAudit{},
|
|
&models.SecurityRuleSet{},
|
|
&models.CrowdsecPresetEvent{},
|
|
&models.CrowdsecConsoleEnrollment{},
|
|
}
|
|
|
|
missingTables := false
|
|
for _, model := range securityModels {
|
|
if !db.Migrator().HasTable(model) {
|
|
missingTables = true
|
|
t.Logf("Missing table for model %T", model)
|
|
}
|
|
}
|
|
|
|
if !missingTables {
|
|
t.Fatal("Expected to find missing tables but all were present")
|
|
}
|
|
|
|
// Run auto-migration (simulating startup verification logic)
|
|
if err := db.AutoMigrate(securityModels...); err != nil {
|
|
t.Fatalf("failed to migrate security tables: %v", err)
|
|
}
|
|
|
|
// Verify all tables now exist
|
|
for _, model := range securityModels {
|
|
if !db.Migrator().HasTable(model) {
|
|
t.Errorf("Table for %T was not created by auto-migration", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMain_MigrateCommand_InProcess(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0o750); err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
db, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("connect db: %v", err)
|
|
}
|
|
if err = db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("automigrate user: %v", err)
|
|
}
|
|
|
|
originalArgs := os.Args
|
|
t.Cleanup(func() { os.Args = originalArgs })
|
|
|
|
t.Setenv("CHARON_DB_PATH", dbPath)
|
|
t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tmp, "caddy"))
|
|
t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tmp, "imports"))
|
|
os.Args = []string{"charon", "migrate"}
|
|
|
|
main()
|
|
|
|
db2, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("reconnect db: %v", err)
|
|
}
|
|
|
|
securityModels := []any{
|
|
&models.SecurityConfig{},
|
|
&models.SecurityDecision{},
|
|
&models.SecurityAudit{},
|
|
&models.SecurityRuleSet{},
|
|
&models.CrowdsecPresetEvent{},
|
|
&models.CrowdsecConsoleEnrollment{},
|
|
}
|
|
|
|
for _, model := range securityModels {
|
|
if !db2.Migrator().HasTable(model) {
|
|
t.Errorf("Table for %T was not created by migrate command", model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMain_ResetPasswordCommand_InProcess(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0o750); err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
db, err := database.Connect(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("connect db: %v", err)
|
|
}
|
|
if err = db.AutoMigrate(&models.User{}); err != nil {
|
|
t.Fatalf("automigrate: %v", err)
|
|
}
|
|
|
|
email := "user@example.com"
|
|
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: models.RoleAdmin, Enabled: true}
|
|
user.PasswordHash = "$2a$10$example_hashed_password"
|
|
user.FailedLoginAttempts = 3
|
|
if err = db.Create(&user).Error; err != nil {
|
|
t.Fatalf("seed user: %v", err)
|
|
}
|
|
|
|
originalArgs := os.Args
|
|
t.Cleanup(func() { os.Args = originalArgs })
|
|
|
|
t.Setenv("CHARON_DB_PATH", dbPath)
|
|
t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tmp, "caddy"))
|
|
t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tmp, "imports"))
|
|
os.Args = []string{"charon", "reset-password", email, "new-password"}
|
|
|
|
main()
|
|
|
|
var updated models.User
|
|
if err := db.Where("email = ?", email).First(&updated).Error; err != nil {
|
|
t.Fatalf("fetch updated user: %v", err)
|
|
}
|
|
if updated.PasswordHash == "$2a$10$example_hashed_password" {
|
|
t.Fatal("expected password hash to be updated")
|
|
}
|
|
if updated.FailedLoginAttempts != 0 {
|
|
t.Fatalf("expected failed login attempts reset to 0, got %d", updated.FailedLoginAttempts)
|
|
}
|
|
}
|
|
|
|
func TestMain_DefaultStartupGracefulShutdown_Subprocess(t *testing.T) {
|
|
if os.Getenv("CHARON_TEST_RUN_MAIN_SERVER") == "1" {
|
|
os.Args = []string{"charon"}
|
|
signalPort := os.Getenv("CHARON_TEST_SIGNAL_PORT")
|
|
|
|
go func() {
|
|
if signalPort != "" {
|
|
_ = waitForTCPReady("127.0.0.1:"+signalPort, 10*time.Second)
|
|
}
|
|
process, err := os.FindProcess(os.Getpid())
|
|
if err == nil {
|
|
_ = process.Signal(syscall.SIGTERM)
|
|
}
|
|
}()
|
|
|
|
main()
|
|
return
|
|
}
|
|
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
httpPort, err := findFreeTCPPort()
|
|
if err != nil {
|
|
t.Fatalf("find free http port: %v", err)
|
|
}
|
|
err = os.MkdirAll(filepath.Dir(dbPath), 0o750)
|
|
if err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestMain_DefaultStartupGracefulShutdown_Subprocess") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe
|
|
cmd.Dir = tmp
|
|
cmd.Env = append(os.Environ(),
|
|
"CHARON_TEST_RUN_MAIN_SERVER=1",
|
|
"CHARON_DB_PATH="+dbPath,
|
|
"CHARON_HTTP_PORT="+httpPort,
|
|
"CHARON_TEST_SIGNAL_PORT="+httpPort,
|
|
"CHARON_EMERGENCY_SERVER_ENABLED=false",
|
|
"CHARON_CADDY_CONFIG_DIR="+filepath.Join(tmp, "caddy"),
|
|
"CHARON_IMPORT_DIR="+filepath.Join(tmp, "imports"),
|
|
"CHARON_IMPORT_CADDYFILE="+filepath.Join(tmp, "imports", "does-not-exist", "Caddyfile"),
|
|
"CHARON_FRONTEND_DIR="+filepath.Join(tmp, "frontend", "dist"),
|
|
)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("expected startup/shutdown to exit 0; err=%v; output=%s", err, string(out))
|
|
}
|
|
}
|
|
|
|
func TestMain_DefaultStartupGracefulShutdown_InProcess(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
dbPath := filepath.Join(tmp, "data", "test.db")
|
|
httpPort, err := findFreeTCPPort()
|
|
if err != nil {
|
|
t.Fatalf("find free http port: %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dbPath), 0o750); err != nil {
|
|
t.Fatalf("mkdir db dir: %v", err)
|
|
}
|
|
|
|
originalArgs := os.Args
|
|
t.Cleanup(func() { os.Args = originalArgs })
|
|
|
|
t.Setenv("CHARON_DB_PATH", dbPath)
|
|
t.Setenv("CHARON_HTTP_PORT", httpPort)
|
|
t.Setenv("CHARON_EMERGENCY_SERVER_ENABLED", "false")
|
|
t.Setenv("CHARON_CADDY_CONFIG_DIR", filepath.Join(tmp, "caddy"))
|
|
t.Setenv("CHARON_IMPORT_DIR", filepath.Join(tmp, "imports"))
|
|
t.Setenv("CHARON_IMPORT_CADDYFILE", filepath.Join(tmp, "imports", "does-not-exist", "Caddyfile"))
|
|
t.Setenv("CHARON_FRONTEND_DIR", filepath.Join(tmp, "frontend", "dist"))
|
|
os.Args = []string{"charon"}
|
|
|
|
go func() {
|
|
_ = waitForTCPReady("127.0.0.1:"+httpPort, 10*time.Second)
|
|
process, err := os.FindProcess(os.Getpid())
|
|
if err == nil {
|
|
_ = process.Signal(syscall.SIGTERM)
|
|
}
|
|
}()
|
|
|
|
main()
|
|
}
|
|
|
|
func findFreeTCPPort() (string, error) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return "", fmt.Errorf("listen free port: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = listener.Close()
|
|
}()
|
|
|
|
addr, ok := listener.Addr().(*net.TCPAddr)
|
|
if !ok {
|
|
return "", fmt.Errorf("unexpected listener addr type: %T", listener.Addr())
|
|
}
|
|
|
|
return fmt.Sprintf("%d", addr.Port), nil
|
|
}
|
|
|
|
func waitForTCPReady(address string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
conn, err := net.DialTimeout("tcp", address, 100*time.Millisecond)
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
return nil
|
|
}
|
|
|
|
time.Sleep(25 * time.Millisecond)
|
|
}
|
|
|
|
return fmt.Errorf("timed out waiting for TCP readiness at %s", address)
|
|
}
|