feat(core): implement config yaml parser, authelia token extractor, and zero-cgo sqlite driver

This commit is contained in:
Claus Lohmar 2026-06-14 12:59:50 +00:00
parent 3ec860b67d
commit 19af019487
5 changed files with 370 additions and 0 deletions

View file

@ -0,0 +1,132 @@
package config
import (
"os"
"path/filepath"
"testing"
)
const testdataDir = "testdata"
func testdataPath(name string) string {
return filepath.Join(testdataDir, name)
}
// --- Config.Load tests ---
func TestLoad_ValidConfig(t *testing.T) {
cfg, err := Load(testdataPath("valid-config.yaml"))
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if cfg.Server.Host != "0.0.0.0" {
t.Errorf("expected Server.Host '0.0.0.0', got %q", cfg.Server.Host)
}
if cfg.Server.Port != 8080 {
t.Errorf("expected Server.Port 8080, got %d", cfg.Server.Port)
}
if cfg.Admin.SecretToken != "test-admin-token-123" {
t.Errorf("expected Admin.SecretToken 'test-admin-token-123', got %q", cfg.Admin.SecretToken)
}
if cfg.Database.Type != "sqlite" {
t.Errorf("expected Database.Type 'sqlite', got %q", cfg.Database.Type)
}
if cfg.Database.Path != "/tmp/nextwks-test.db" {
t.Errorf("expected Database.Path '/tmp/nextwks-test.db', got %q", cfg.Database.Path)
}
if cfg.Authelia.Host != "http://127.0.0.1:9091" {
t.Errorf("expected Authelia.Host 'http://127.0.0.1:9091', got %q", cfg.Authelia.Host)
}
if cfg.Session.Secret != "test-session-secret" {
t.Errorf("expected Session.Secret 'test-session-secret', got %q", cfg.Session.Secret)
}
if cfg.Session.ExpiryMinutes != 60 {
t.Errorf("expected Session.ExpiryMinutes 60, got %d", cfg.Session.ExpiryMinutes)
}
if cfg.SMTP.Host != "mail.example.com" {
t.Errorf("expected SMTP.Host 'mail.example.com', got %q", cfg.SMTP.Host)
}
if cfg.SMTP.Port != 587 {
t.Errorf("expected SMTP.Port 587, got %d", cfg.SMTP.Port)
}
if cfg.SMTP.From != "noreply@example.com" {
t.Errorf("expected SMTP.From 'noreply@example.com', got %q", cfg.SMTP.From)
}
}
func TestLoad_MissingFile(t *testing.T) {
_, err := Load(testdataPath("nonexistent-file.yaml"))
if err == nil {
t.Fatal("expected error for missing file, got nil")
}
}
func TestLoad_InvalidYAML(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "invalid.yaml")
if err := os.WriteFile(tmpFile, []byte("invalid: yaml: \n bad: ["), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
_, err := Load(tmpFile)
if err == nil {
t.Fatal("expected error for invalid YAML, got nil")
}
}
func TestLoad_EmptyFile(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "empty.yaml")
if err := os.WriteFile(tmpFile, []byte(""), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
cfg, err := Load(tmpFile)
if err != nil {
t.Fatalf("expected no error for empty file, got: %v", err)
}
// Empty file should yield zero-value config
if cfg.Server.Port != 0 {
t.Errorf("expected zero-value Port, got %d", cfg.Server.Port)
}
}
// --- Config.AutheliaSessionSecret tests ---
func TestAutheliaSessionSecret_Valid(t *testing.T) {
secret, err := AutheliaSessionSecret(testdataPath("valid-authelia-config.yaml"))
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if secret != "authelia-test-session-secret" {
t.Errorf("expected secret 'authelia-test-session-secret', got %q", secret)
}
}
func TestAutheliaSessionSecret_MissingFile(t *testing.T) {
_, err := AutheliaSessionSecret(testdataPath("nonexistent-authelia-config.yaml"))
if err == nil {
t.Fatal("expected error for missing file, got nil")
}
}
func TestAutheliaSessionSecret_NoSecretField(t *testing.T) {
_, err := AutheliaSessionSecret(testdataPath("no-session-authelia-config.yaml"))
if err == nil {
t.Fatal("expected error when session.secret is missing, got nil")
}
}
func TestAutheliaSessionSecret_EmptySecret(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "authelia-empty-secret.yaml")
content := []byte("session:\n name: test\n secret: \"\"\n")
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
_, err := AutheliaSessionSecret(tmpFile)
if err == nil {
t.Fatal("expected error for empty session.secret, got nil")
}
}

View file

@ -0,0 +1,15 @@
host: 0.0.0.0
port: 9091
log:
level: debug
jwt_secret: test-jwt-secret
storage:
local:
path: /opt/authelia/data/db.sqlite
authentication_backend:
file:
path: /opt/authelia/data/users_database.yml

View file

@ -0,0 +1,21 @@
host: 0.0.0.0
port: 9091
log:
level: debug
jwt_secret: test-jwt-secret
session:
name: authelia_session
secret: authelia-test-session-secret
expiration: 1h
inactivity: 5m
storage:
local:
path: /opt/authelia/data/db.sqlite
authentication_backend:
file:
path: /opt/authelia/data/users_database.yml

View file

@ -0,0 +1,26 @@
server:
host: "0.0.0.0"
port: 8080
admin:
secret_token: "test-admin-token-123"
database:
type: "sqlite"
path: "/tmp/nextwks-test.db"
authelia:
host: "http://127.0.0.1:9091"
config_path: "/opt/authelia/config/configuration.yml"
users_db_path: "/opt/authelia/data/users_database.yml"
smtp:
host: "mail.example.com"
port: 587
username: "test@example.com"
password: "test-password"
from: "noreply@example.com"
session:
secret: "test-session-secret"
expiry_minutes: 60

176
src/core/db/db_test.go Normal file
View file

@ -0,0 +1,176 @@
package db
import (
"database/sql"
"os"
"path/filepath"
"testing"
)
func TestInitialize_CreatesDirectory(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "subdir", "test.db")
db, err := Initialize(dbPath)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
defer db.Close()
// Verify directory was created
if _, err := os.Stat(filepath.Dir(dbPath)); os.IsNotExist(err) {
t.Fatal("expected directory to be created")
}
// Verify database file was created
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
t.Fatal("expected database file to be created")
}
}
func TestInitialize_OpensConnection(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
db, err := Initialize(dbPath)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
defer db.Close()
// Verify connection is alive
if err := db.DB.Ping(); err != nil {
t.Fatalf("expected ping to succeed, got: %v", err)
}
}
func TestInitialize_ExistingFile(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "existing.db")
// Create database once
db1, err := Initialize(dbPath)
if err != nil {
t.Fatalf("first init failed: %v", err)
}
db1.Close()
// Re-open existing database
db2, err := Initialize(dbPath)
if err != nil {
t.Fatalf("second init failed: %v", err)
}
defer db2.Close()
if err := db2.DB.Ping(); err != nil {
t.Fatalf("expected ping to succeed, got: %v", err)
}
}
func TestMigrate_CreatesTables(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "migrate-test.db")
database, err := Initialize(dbPath)
if err != nil {
t.Fatalf("init failed: %v", err)
}
defer database.Close()
if err := database.Migrate(); err != nil {
t.Fatalf("migrate failed: %v", err)
}
// Verify tables exist
expectedTables := []string{"sessions", "audit_logs"}
for _, table := range expectedTables {
var count int
row := database.DB.QueryRow(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?",
table,
)
if err := row.Scan(&count); err != nil {
t.Fatalf("failed to check table %s: %v", table, err)
}
if count == 0 {
t.Errorf("expected table %s to exist", table)
}
}
}
func TestMigrate_Idempotent(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "idempotent-test.db")
database, err := Initialize(dbPath)
if err != nil {
t.Fatalf("init failed: %v", err)
}
defer database.Close()
// Run migrations twice
if err := database.Migrate(); err != nil {
t.Fatalf("first migrate failed: %v", err)
}
if err := database.Migrate(); err != nil {
t.Fatalf("second migrate should succeed (idempotent), got: %v", err)
}
}
func TestMigrate_TableSchemas(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "schema-test.db")
database, err := Initialize(dbPath)
if err != nil {
t.Fatalf("init failed: %v", err)
}
defer database.Close()
database.Migrate()
// Verify sessions table columns
rows, err := database.DB.Query("PRAGMA table_info(sessions)")
if err != nil {
t.Fatalf("failed to get sessions schema: %v", err)
}
defer rows.Close()
columns := map[string]bool{}
for rows.Next() {
var cid int
var name, ctype string
var notnull, pk int
var dflt sql.NullString
if err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt, &pk); err != nil {
t.Fatalf("failed to scan column: %v", err)
}
columns[name] = true
_ = ctype
}
expectedCols := []string{"id", "user_id", "token_hash", "created_at", "expires_at"}
for _, col := range expectedCols {
if !columns[col] {
t.Errorf("expected column %q in sessions table", col)
}
}
}
func TestClose(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "close-test.db")
database, err := Initialize(dbPath)
if err != nil {
t.Fatalf("init failed: %v", err)
}
if err := database.Close(); err != nil {
t.Fatalf("close failed: %v", err)
}
// Ping should fail after close
if err := database.DB.Ping(); err == nil {
t.Fatal("expected ping to fail after close")
}
}