retention.audit_days and retention.revisions_keep (DDI_RETENTION_*) drive a cleanup five minutes after the start and then daily. The latest revision, revisions agents run, fall back to or target, and revisions with pending deployments are never deleted; deployment rows stay. A cleanup that deletes anything records retention.cleanup in the audit log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
7.3 KiB
Go
221 lines
7.3 KiB
Go
// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
|
|
|
|
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func env(m map[string]string) LookupFunc {
|
|
return func(k string) (string, bool) {
|
|
v, ok := m[k]
|
|
return v, ok
|
|
}
|
|
}
|
|
|
|
func writeFile(t *testing.T, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "server.yaml")
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestDefaults(t *testing.T) {
|
|
cfg, err := Load("", env(nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.HTTP.Listen != DefaultHTTPListen || cfg.ShutdownTimeout.Std() != DefaultShutdownTimeout {
|
|
t.Errorf("unexpected defaults: %+v", cfg)
|
|
}
|
|
if cfg.TLSEnabled() || !cfg.AutoMigrate() {
|
|
t.Error("TLS must default off, auto-migrate on")
|
|
}
|
|
if cfg.License.KeygenURL != DefaultKeygenURL || cfg.License.GracePeriod.Std() != 14*24*time.Hour {
|
|
t.Errorf("license defaults: %+v", cfg.License)
|
|
}
|
|
if !strings.HasSuffix(filepath.ToSlash(cfg.MasterKeyFile), "/master.key") {
|
|
t.Errorf("master key default %q", cfg.MasterKeyFile)
|
|
}
|
|
if cfg.Retention != (Retention{}) {
|
|
t.Errorf("retention must default to keeping everything: %+v", cfg.Retention)
|
|
}
|
|
}
|
|
|
|
func TestFileAndEnvPrecedence(t *testing.T) {
|
|
path := writeFile(t, `
|
|
http:
|
|
listen: "127.0.0.1:9000"
|
|
public_url: "https://ddi.example.test"
|
|
database:
|
|
url: "postgres://file"
|
|
auto_migrate: false
|
|
license:
|
|
grace_period: 7d
|
|
session:
|
|
idle_timeout: 30m
|
|
retention:
|
|
audit_days: 365
|
|
revisions_keep: 50
|
|
`)
|
|
cfg, err := Load(path, env(map[string]string{
|
|
"DDI_DATABASE_URL": "postgres://env",
|
|
"KEYGEN_PRODUCT_ID": "prod-1",
|
|
"DDI_LOG_LEVEL": "debug",
|
|
"DDI_RETENTION_REVISIONS_KEEP": "20",
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.HTTP.Listen != "127.0.0.1:9000" || cfg.HTTP.PublicURL != "https://ddi.example.test" {
|
|
t.Errorf("file values not applied: %+v", cfg.HTTP)
|
|
}
|
|
if cfg.Database.URL != "postgres://env" {
|
|
t.Errorf("env must override file, got %q", cfg.Database.URL)
|
|
}
|
|
if cfg.AutoMigrate() {
|
|
t.Error("auto_migrate: false not applied")
|
|
}
|
|
if cfg.License.GracePeriod.Std() != 7*24*time.Hour || cfg.License.ProductID != "prod-1" {
|
|
t.Errorf("license: %+v", cfg.License)
|
|
}
|
|
if cfg.Session.IdleTimeout.Std() != 30*time.Minute || cfg.Log.Level != "debug" {
|
|
t.Errorf("session/log: %+v %+v", cfg.Session, cfg.Log)
|
|
}
|
|
if cfg.Retention.AuditDays != 365 || cfg.Retention.RevisionsKeep != 20 {
|
|
t.Errorf("retention: %+v", cfg.Retention)
|
|
}
|
|
}
|
|
|
|
func TestDatabaseURLFile(t *testing.T) {
|
|
urlFile := filepath.Join(t.TempDir(), "db.url")
|
|
if err := os.WriteFile(urlFile, []byte("postgres://from-file\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := Load("", env(map[string]string{"DDI_DATABASE_URL_FILE": urlFile}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Database.URL != "postgres://from-file" {
|
|
t.Errorf("URL = %q", cfg.Database.URL)
|
|
}
|
|
}
|
|
|
|
func TestUnknownFieldsRejected(t *testing.T) {
|
|
path := writeFile(t, "htttp:\n listen: ':1'\n")
|
|
if _, err := Load(path, env(nil)); err == nil || !strings.Contains(err.Error(), "htttp") {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidation(t *testing.T) {
|
|
tests := map[string]struct {
|
|
env map[string]string
|
|
wantErr string
|
|
}{
|
|
"bad listen": {map[string]string{"DDI_HTTP_LISTEN": "nonsense"}, "http.listen"},
|
|
"cert without key": {map[string]string{"DDI_TLS_CERT_FILE": "/c.pem"}, "set together"},
|
|
"bad duration": {map[string]string{"DDI_SHUTDOWN_TIMEOUT": "soon"}, "SHUTDOWN_TIMEOUT"},
|
|
"negative timeout": {map[string]string{"DDI_SHUTDOWN_TIMEOUT": "-1s"}, "must be positive"},
|
|
"http keygen": {map[string]string{"KEYGEN_URL": "http://license.example.test"}, "keygen_url"},
|
|
"relative public": {map[string]string{"DDI_PUBLIC_URL": "/ddi"}, "public_url"},
|
|
"bad grace in days": {map[string]string{"DDI_LICENSE_GRACE_PERIOD": "xd"}, "LICENSE_GRACE_PERIOD"},
|
|
"audit days text": {map[string]string{"DDI_RETENTION_AUDIT_DAYS": "1y"}, "RETENTION_AUDIT_DAYS"},
|
|
"negative audit": {map[string]string{"DDI_RETENTION_AUDIT_DAYS": "-1"}, "retention.audit_days"},
|
|
"negative keep": {map[string]string{"DDI_RETENTION_REVISIONS_KEEP": "-3"}, "retention.revisions_keep"},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := Load("", env(tt.env))
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("err = %v, want containing %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAgentAPIDefaults(t *testing.T) {
|
|
orig := hostname
|
|
t.Cleanup(func() { hostname = orig })
|
|
hostname = func() (string, error) { return "Mgmt01", nil }
|
|
|
|
cfg, err := Load("", env(nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := cfg.AgentAPI
|
|
if a.Listen != DefaultAgentAPIListen || a.PublicAddress != "mgmt01:8444" || len(a.ServerNames) != 1 || a.ServerNames[0] != "mgmt01" {
|
|
t.Errorf("defaults from hostname: %+v", a)
|
|
}
|
|
|
|
cfg, err = Load("", env(map[string]string{"DDI_PUBLIC_URL": "https://ddi.example.test", "DDI_AGENT_API_LISTEN": "0.0.0.0:9444"}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.AgentAPI.PublicAddress != "ddi.example.test:9444" || cfg.AgentAPI.ServerNames[0] != "ddi.example.test" {
|
|
t.Errorf("defaults from public URL: %+v", cfg.AgentAPI)
|
|
}
|
|
|
|
cfg, err = Load("", env(map[string]string{"DDI_AGENT_API_LISTEN": "127.0.0.1:8444"}))
|
|
if err != nil || cfg.AgentAPI.PublicAddress != "127.0.0.1:8444" || cfg.AgentAPI.ServerNames[0] != "127.0.0.1" {
|
|
t.Errorf("defaults from listen host: %+v %v", cfg.AgentAPI, err)
|
|
}
|
|
|
|
cfg, err = Load("", env(map[string]string{"DDI_AGENT_API_PUBLIC_ADDRESS": "[2001:db8::1]:8444",
|
|
"DDI_AGENT_API_SERVER_NAMES": "agents.example.test, 2001:db8::1"}))
|
|
if err != nil || cfg.AgentAPI.PublicAddress != "[2001:db8::1]:8444" || len(cfg.AgentAPI.ServerNames) != 2 {
|
|
t.Errorf("explicit values: %+v %v", cfg.AgentAPI, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentAPIValidation(t *testing.T) {
|
|
for name, tt := range map[string]map[string]string{
|
|
"bad listen": {"DDI_AGENT_API_LISTEN": "nonsense"},
|
|
"public without port": {"DDI_AGENT_API_PUBLIC_ADDRESS": "agents.example.test"},
|
|
"public bad port": {"DDI_AGENT_API_PUBLIC_ADDRESS": "agents.example.test:99999"},
|
|
"bad server name": {"DDI_AGENT_API_SERVER_NAMES": "bad name"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := Load("", env(tt)); err == nil || !strings.Contains(err.Error(), "agent_api") {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoopbackKeygenAllowedOverHTTP(t *testing.T) {
|
|
if _, err := Load("", env(map[string]string{"KEYGEN_URL": "http://127.0.0.1:3000"})); err != nil {
|
|
t.Errorf("loopback http must be allowed for tests: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseDuration(t *testing.T) {
|
|
for in, want := range map[string]time.Duration{"14d": 14 * 24 * time.Hour, "0d": 0, "90m": 90 * time.Minute, " 2h ": 2 * time.Hour} {
|
|
got, err := ParseDuration(in)
|
|
if err != nil || got != want {
|
|
t.Errorf("ParseDuration(%q) = %v, %v", in, got, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "-1d", "1.5d", "d"} {
|
|
if _, err := ParseDuration(bad); err == nil {
|
|
t.Errorf("ParseDuration(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDevConfigIsValid(t *testing.T) {
|
|
cfg, err := Load(filepath.Join("..", "..", "..", "deploy", "config", "server.dev.yaml"), env(nil))
|
|
if err != nil {
|
|
t.Fatalf("deploy/config/server.dev.yaml: %v", err)
|
|
}
|
|
if cfg.HTTP.Listen != "127.0.0.1:8080" || cfg.License.AccountID == "" || cfg.Database.URL == "" {
|
|
t.Errorf("unexpected dev config: %+v", cfg)
|
|
}
|
|
}
|