Add a PostgreSQL psql interactive terminal shell with backslash meta-commands, SQL statement handling with multi-line buffering, and canned responses for common queries. Add username-based shell routing via [shell.username_routes] config (second priority after credential- specific shell, before random selection). Bump version to 0.13.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
360 lines
8.9 KiB
Go
360 lines
8.9 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 TestLoadShellDefaults(t *testing.T) {
|
|
path := writeTemp(t, "")
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.Shell.Hostname != "ubuntu-server" {
|
|
t.Errorf("default hostname = %q, want %q", cfg.Shell.Hostname, "ubuntu-server")
|
|
}
|
|
if cfg.Shell.Banner == "" {
|
|
t.Error("default banner should not be empty")
|
|
}
|
|
if cfg.Shell.FakeUser != "" {
|
|
t.Errorf("default fake_user = %q, want empty", cfg.Shell.FakeUser)
|
|
}
|
|
}
|
|
|
|
func TestLoadShellConfig(t *testing.T) {
|
|
content := `
|
|
[shell]
|
|
hostname = "myhost"
|
|
banner = "Custom banner\r\n"
|
|
fake_user = "admin"
|
|
|
|
[shell.bash]
|
|
custom_key = "value"
|
|
`
|
|
path := writeTemp(t, content)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.Shell.Hostname != "myhost" {
|
|
t.Errorf("hostname = %q, want %q", cfg.Shell.Hostname, "myhost")
|
|
}
|
|
if cfg.Shell.Banner != "Custom banner\r\n" {
|
|
t.Errorf("banner = %q, want %q", cfg.Shell.Banner, "Custom banner\r\n")
|
|
}
|
|
if cfg.Shell.FakeUser != "admin" {
|
|
t.Errorf("fake_user = %q, want %q", cfg.Shell.FakeUser, "admin")
|
|
}
|
|
if cfg.Shell.Shells == nil {
|
|
t.Fatal("Shells map should not be nil")
|
|
}
|
|
bashCfg, ok := cfg.Shell.Shells["bash"]
|
|
if !ok {
|
|
t.Fatal("Shells[\"bash\"] not found")
|
|
}
|
|
if bashCfg["custom_key"] != "value" {
|
|
t.Errorf("Shells[\"bash\"][\"custom_key\"] = %v, want %q", bashCfg["custom_key"], "value")
|
|
}
|
|
}
|
|
|
|
func TestLoadWebDefaults(t *testing.T) {
|
|
path := writeTemp(t, "")
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.Web.Enabled {
|
|
t.Error("web should be disabled by default")
|
|
}
|
|
if cfg.Web.ListenAddr != ":8080" {
|
|
t.Errorf("default web listen_addr = %q, want %q", cfg.Web.ListenAddr, ":8080")
|
|
}
|
|
}
|
|
|
|
func TestLoadWebConfig(t *testing.T) {
|
|
content := `
|
|
[web]
|
|
enabled = true
|
|
listen_addr = ":9090"
|
|
`
|
|
path := writeTemp(t, content)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !cfg.Web.Enabled {
|
|
t.Error("web should be enabled")
|
|
}
|
|
if cfg.Web.ListenAddr != ":9090" {
|
|
t.Errorf("web listen_addr = %q, want %q", cfg.Web.ListenAddr, ":9090")
|
|
}
|
|
}
|
|
|
|
func TestLoadCredentialWithShell(t *testing.T) {
|
|
content := `
|
|
[[auth.static_credentials]]
|
|
username = "samsung"
|
|
password = "fridge"
|
|
shell = "fridge"
|
|
|
|
[[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 len(cfg.Auth.StaticCredentials) != 2 {
|
|
t.Fatalf("static_credentials len = %d, want 2", len(cfg.Auth.StaticCredentials))
|
|
}
|
|
if cfg.Auth.StaticCredentials[0].Shell != "fridge" {
|
|
t.Errorf("cred[0].Shell = %q, want %q", cfg.Auth.StaticCredentials[0].Shell, "fridge")
|
|
}
|
|
if cfg.Auth.StaticCredentials[1].Shell != "" {
|
|
t.Errorf("cred[1].Shell = %q, want empty", cfg.Auth.StaticCredentials[1].Shell)
|
|
}
|
|
}
|
|
|
|
func TestLoadMetricsToken(t *testing.T) {
|
|
content := `
|
|
[web]
|
|
enabled = true
|
|
metrics_token = "my-secret-token"
|
|
`
|
|
path := writeTemp(t, content)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.Web.MetricsToken != "my-secret-token" {
|
|
t.Errorf("metrics_token = %q, want %q", cfg.Web.MetricsToken, "my-secret-token")
|
|
}
|
|
}
|
|
|
|
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 TestLoadUsernameRoutes(t *testing.T) {
|
|
content := `
|
|
[shell]
|
|
hostname = "myhost"
|
|
|
|
[shell.username_routes]
|
|
postgres = "psql"
|
|
admin = "bash"
|
|
|
|
[shell.bash]
|
|
custom_key = "value"
|
|
`
|
|
path := writeTemp(t, content)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.Shell.UsernameRoutes == nil {
|
|
t.Fatal("UsernameRoutes should not be nil")
|
|
}
|
|
if cfg.Shell.UsernameRoutes["postgres"] != "psql" {
|
|
t.Errorf("UsernameRoutes[\"postgres\"] = %q, want %q", cfg.Shell.UsernameRoutes["postgres"], "psql")
|
|
}
|
|
if cfg.Shell.UsernameRoutes["admin"] != "bash" {
|
|
t.Errorf("UsernameRoutes[\"admin\"] = %q, want %q", cfg.Shell.UsernameRoutes["admin"], "bash")
|
|
}
|
|
// username_routes should NOT appear in the Shells map.
|
|
if _, ok := cfg.Shell.Shells["username_routes"]; ok {
|
|
t.Error("username_routes should not appear in Shells map")
|
|
}
|
|
// bash should still appear in Shells map.
|
|
if _, ok := cfg.Shell.Shells["bash"]; !ok {
|
|
t.Error("Shells[\"bash\"] should still be present")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|