feat(core): implement config yaml parser, authelia token extractor, and zero-cgo sqlite driver
This commit is contained in:
parent
3ec860b67d
commit
19af019487
5 changed files with 370 additions and 0 deletions
132
src/core/config/config_test.go
Normal file
132
src/core/config/config_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
15
src/core/config/testdata/no-session-authelia-config.yaml
vendored
Normal file
15
src/core/config/testdata/no-session-authelia-config.yaml
vendored
Normal 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
|
||||
21
src/core/config/testdata/valid-authelia-config.yaml
vendored
Normal file
21
src/core/config/testdata/valid-authelia-config.yaml
vendored
Normal 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
|
||||
26
src/core/config/testdata/valid-config.yaml
vendored
Normal file
26
src/core/config/testdata/valid-config.yaml
vendored
Normal 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
176
src/core/db/db_test.go
Normal 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, ¬null, &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")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue