Foundational libraries: deterministic resource naming, the RCON client, the Postgres store with embedded SQL migrations, configuration loading, and container image-build helpers.
214 lines
5.7 KiB
Go
214 lines
5.7 KiB
Go
package config_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"felis.lolicon.best/internal/config"
|
|
)
|
|
|
|
// writeTOML writes content to a temp felis.toml and returns its path.
|
|
func writeTOML(t *testing.T, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "felis.toml")
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("write toml: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// Note: tests use the neutral example domain mc.example.net, never a real
|
|
// deployment domain, to keep the source tree clean of domain literals.
|
|
const validTOML = `
|
|
[server]
|
|
listen = "0.0.0.0:9090"
|
|
root_domain = "mc.example.net"
|
|
|
|
[database]
|
|
url = "postgres://felis:secret@db:5432/felis"
|
|
|
|
[velocity]
|
|
public_ip = "203.0.113.4"
|
|
service_token_ref = "felis-velocity-token"
|
|
|
|
[auth]
|
|
admin_hostname = "admin.example.net"
|
|
panel_hostname = "panel.example.net"
|
|
access_jwt_aud = "felis-panel"
|
|
|
|
[k8s]
|
|
namespace = "minecraft"
|
|
egress_mode = "loadbalancer"
|
|
metallb_pool = "192.0.2.200-250"
|
|
|
|
[registry]
|
|
url = "registry.felis.svc:5000"
|
|
build_namespace = "felis-build"
|
|
|
|
[archive]
|
|
store = "tarLocal"
|
|
local_path = "backup-pvc"
|
|
retention = "3mo"
|
|
warn_before = ["3d", "1d"]
|
|
max_local_bytes = "200Gi"
|
|
[archive.s3]
|
|
endpoint = ""
|
|
bucket = "felis-backups"
|
|
access_key_ref = ""
|
|
secret_key_ref = ""
|
|
`
|
|
|
|
func TestLoadValid(t *testing.T) {
|
|
cfg, err := config.Load(writeTOML(t, validTOML))
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Server.Listen != "0.0.0.0:9090" {
|
|
t.Errorf("listen = %q", cfg.Server.Listen)
|
|
}
|
|
if cfg.Server.RootDomain != "mc.example.net" {
|
|
t.Errorf("root_domain = %q", cfg.Server.RootDomain)
|
|
}
|
|
if cfg.Database.URL == "" {
|
|
t.Error("database url empty")
|
|
}
|
|
if cfg.Archive.Store != "tarLocal" {
|
|
t.Errorf("archive store = %q", cfg.Archive.Store)
|
|
}
|
|
if len(cfg.Archive.WarnBefore) != 2 || cfg.Archive.WarnBefore[0] != "3d" {
|
|
t.Errorf("warn_before = %v", cfg.Archive.WarnBefore)
|
|
}
|
|
if cfg.Archive.S3.Bucket != "felis-backups" {
|
|
t.Errorf("s3 bucket = %q", cfg.Archive.S3.Bucket)
|
|
}
|
|
}
|
|
|
|
func TestLoadAppliesDefaults(t *testing.T) {
|
|
cfg, err := config.Load(writeTOML(t, `
|
|
[server]
|
|
root_domain = "mc.example.net"
|
|
[database]
|
|
url = "postgres://felis@db/felis"
|
|
`))
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Server.Listen != "0.0.0.0:8080" {
|
|
t.Errorf("default listen = %q, want 0.0.0.0:8080", cfg.Server.Listen)
|
|
}
|
|
if cfg.K8s.Namespace != "minecraft" {
|
|
t.Errorf("default namespace = %q, want minecraft", cfg.K8s.Namespace)
|
|
}
|
|
if cfg.K8s.EgressMode != "loadbalancer" {
|
|
t.Errorf("default egress_mode = %q", cfg.K8s.EgressMode)
|
|
}
|
|
if cfg.Archive.Store != "tarLocal" {
|
|
t.Errorf("default archive store = %q", cfg.Archive.Store)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsMissingDatabaseURL(t *testing.T) {
|
|
_, err := config.Load(writeTOML(t, `
|
|
[server]
|
|
root_domain = "mc.example.net"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected error when database.url is missing")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsMissingRootDomain(t *testing.T) {
|
|
_, err := config.Load(writeTOML(t, `
|
|
[database]
|
|
url = "postgres://felis@db/felis"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected error when root_domain is missing")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsUnknownArchiveStore(t *testing.T) {
|
|
_, err := config.Load(writeTOML(t, `
|
|
[server]
|
|
root_domain = "mc.example.net"
|
|
[database]
|
|
url = "postgres://felis@db/felis"
|
|
[archive]
|
|
store = "magicbox"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown archive store")
|
|
}
|
|
}
|
|
|
|
// TestLoadRejectsUnimplementedArchiveStore guards the §19/build-reality gap:
|
|
// tarS3, volumeSnapshot and longhorn are recognized store names but only
|
|
// tarLocal is implemented in this build. A config naming one of them must be
|
|
// rejected at load — otherwise felis-api boots green while the reaper CronJob
|
|
// fails every run and restore silently 503s. The error must point the operator
|
|
// at the fix (tarLocal), distinct from the "unknown store" message.
|
|
func TestLoadRejectsUnimplementedArchiveStore(t *testing.T) {
|
|
for _, store := range []string{"tarS3", "volumeSnapshot", "longhorn"} {
|
|
t.Run(store, func(t *testing.T) {
|
|
_, err := config.Load(writeTOML(t, `
|
|
[server]
|
|
root_domain = "mc.example.net"
|
|
[database]
|
|
url = "postgres://felis@db/felis"
|
|
[archive]
|
|
store = "`+store+`"
|
|
`))
|
|
if err == nil {
|
|
t.Fatalf("expected error for recognized-but-unimplemented store %q", store)
|
|
}
|
|
if !strings.Contains(err.Error(), "tarLocal") {
|
|
t.Errorf("error for %q should point at the tarLocal remediation, got: %v", store, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLoadRejectsSchemeQualifiedRegistryURL guards the §24 split-brain: the
|
|
// registry url is a bare host[:port], read scheme-tolerantly by the admin build
|
|
// path (registryHost strips the scheme) but scheme-INtolerantly by the
|
|
// user-modpack lane (deriveImageRef concatenates raw). A scheme-qualified url
|
|
// would boot felis-api green and 500 every approve while admin builds keep
|
|
// working, so it must be rejected at load with the bare-host contract spelled
|
|
// out. Both http:// and https:// are caught (the check is on "://").
|
|
func TestLoadRejectsSchemeQualifiedRegistryURL(t *testing.T) {
|
|
for _, url := range []string{"http://registry.felis.svc:5000", "https://registry.felis.svc:5000"} {
|
|
t.Run(url, func(t *testing.T) {
|
|
_, err := config.Load(writeTOML(t, `
|
|
[server]
|
|
root_domain = "mc.example.net"
|
|
[database]
|
|
url = "postgres://felis@db/felis"
|
|
[registry]
|
|
url = "`+url+`"
|
|
`))
|
|
if err == nil {
|
|
t.Fatalf("expected error for scheme-qualified registry url %q", url)
|
|
}
|
|
if !strings.Contains(err.Error(), "scheme") {
|
|
t.Errorf("error for %q should explain the bare-host contract, got: %v", url, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsUnknownKeys(t *testing.T) {
|
|
_, err := config.Load(writeTOML(t, `
|
|
[server]
|
|
root_domain = "mc.example.net"
|
|
typo_field = "oops"
|
|
[database]
|
|
url = "postgres://felis@db/felis"
|
|
`))
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown key")
|
|
}
|
|
}
|