Adds persistent storage using modernc.org/sqlite (pure Go). Login attempts are deduplicated by (username, password, ip) with counts. Sessions and session logs are tracked with UUID IDs. Includes embedded SQL migrations, configurable retention with background pruning, and an in-memory store for tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
4.8 KiB
Go
195 lines
4.8 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadValidConfig(t *testing.T) {
|
|
content := `
|
|
log_level = "debug"
|
|
|
|
[ssh]
|
|
listen_addr = ":3333"
|
|
host_key_path = "/tmp/test_key"
|
|
|
|
[auth]
|
|
accept_after = 5
|
|
credential_ttl = "1h"
|
|
|
|
[[auth.static_credentials]]
|
|
username = "root"
|
|
password = "toor"
|
|
`
|
|
path := writeTemp(t, content)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if cfg.LogLevel != "debug" {
|
|
t.Errorf("log_level = %q, want %q", cfg.LogLevel, "debug")
|
|
}
|
|
if cfg.SSH.ListenAddr != ":3333" {
|
|
t.Errorf("listen_addr = %q, want %q", cfg.SSH.ListenAddr, ":3333")
|
|
}
|
|
if cfg.SSH.HostKeyPath != "/tmp/test_key" {
|
|
t.Errorf("host_key_path = %q, want %q", cfg.SSH.HostKeyPath, "/tmp/test_key")
|
|
}
|
|
if cfg.Auth.AcceptAfter != 5 {
|
|
t.Errorf("accept_after = %d, want %d", cfg.Auth.AcceptAfter, 5)
|
|
}
|
|
if cfg.Auth.CredentialTTLDuration != time.Hour {
|
|
t.Errorf("credential_ttl_duration = %v, want %v", cfg.Auth.CredentialTTLDuration, time.Hour)
|
|
}
|
|
if len(cfg.Auth.StaticCredentials) != 1 {
|
|
t.Fatalf("static_credentials len = %d, want 1", len(cfg.Auth.StaticCredentials))
|
|
}
|
|
if cfg.Auth.StaticCredentials[0].Username != "root" {
|
|
t.Errorf("username = %q, want %q", cfg.Auth.StaticCredentials[0].Username, "root")
|
|
}
|
|
}
|
|
|
|
func TestLoadDefaults(t *testing.T) {
|
|
path := writeTemp(t, "")
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if cfg.SSH.ListenAddr != ":2222" {
|
|
t.Errorf("default listen_addr = %q, want %q", cfg.SSH.ListenAddr, ":2222")
|
|
}
|
|
if cfg.SSH.HostKeyPath != "oubliette_host_key" {
|
|
t.Errorf("default host_key_path = %q, want %q", cfg.SSH.HostKeyPath, "oubliette_host_key")
|
|
}
|
|
if cfg.Auth.AcceptAfter != 10 {
|
|
t.Errorf("default accept_after = %d, want %d", cfg.Auth.AcceptAfter, 10)
|
|
}
|
|
if cfg.Auth.CredentialTTLDuration != 24*time.Hour {
|
|
t.Errorf("default credential_ttl = %v, want %v", cfg.Auth.CredentialTTLDuration, 24*time.Hour)
|
|
}
|
|
if cfg.LogLevel != "info" {
|
|
t.Errorf("default log_level = %q, want %q", cfg.LogLevel, "info")
|
|
}
|
|
if cfg.Storage.DBPath != "oubliette.db" {
|
|
t.Errorf("default db_path = %q, want %q", cfg.Storage.DBPath, "oubliette.db")
|
|
}
|
|
if cfg.Storage.RetentionDays != 90 {
|
|
t.Errorf("default retention_days = %d, want %d", cfg.Storage.RetentionDays, 90)
|
|
}
|
|
if cfg.Storage.RetentionIntervalDuration != time.Hour {
|
|
t.Errorf("default retention_interval = %v, want %v", cfg.Storage.RetentionIntervalDuration, time.Hour)
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidTTL(t *testing.T) {
|
|
content := `
|
|
[auth]
|
|
credential_ttl = "notaduration"
|
|
`
|
|
path := writeTemp(t, content)
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid credential_ttl")
|
|
}
|
|
}
|
|
|
|
func TestLoadNegativeTTL(t *testing.T) {
|
|
content := `
|
|
[auth]
|
|
credential_ttl = "-1h"
|
|
`
|
|
path := writeTemp(t, content)
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for negative credential_ttl")
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidStaticCredential(t *testing.T) {
|
|
content := `
|
|
[[auth.static_credentials]]
|
|
username = ""
|
|
password = "test"
|
|
`
|
|
path := writeTemp(t, content)
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty username")
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidRetentionInterval(t *testing.T) {
|
|
content := `
|
|
[storage]
|
|
retention_interval = "notaduration"
|
|
`
|
|
path := writeTemp(t, content)
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid retention_interval")
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidRetentionDays(t *testing.T) {
|
|
content := `
|
|
[storage]
|
|
retention_days = -1
|
|
`
|
|
path := writeTemp(t, content)
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for negative retention_days")
|
|
}
|
|
}
|
|
|
|
func TestLoadStorageConfig(t *testing.T) {
|
|
content := `
|
|
[storage]
|
|
db_path = "/tmp/test.db"
|
|
retention_days = 30
|
|
retention_interval = "2h"
|
|
`
|
|
path := writeTemp(t, content)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.Storage.DBPath != "/tmp/test.db" {
|
|
t.Errorf("db_path = %q, want %q", cfg.Storage.DBPath, "/tmp/test.db")
|
|
}
|
|
if cfg.Storage.RetentionDays != 30 {
|
|
t.Errorf("retention_days = %d, want 30", cfg.Storage.RetentionDays)
|
|
}
|
|
if cfg.Storage.RetentionIntervalDuration != 2*time.Hour {
|
|
t.Errorf("retention_interval = %v, want 2h", cfg.Storage.RetentionIntervalDuration)
|
|
}
|
|
}
|
|
|
|
func TestLoadMissingFile(t *testing.T) {
|
|
_, err := Load("/nonexistent/path/config.toml")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidTOML(t *testing.T) {
|
|
path := writeTemp(t, "{{{{invalid toml")
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid TOML")
|
|
}
|
|
}
|
|
|
|
func writeTemp(t *testing.T, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "config.toml")
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatalf("writing temp config: %v", err)
|
|
}
|
|
return path
|
|
}
|