internal/discovery registers three service types on the listen port: _vellum._tcp for native clients, and _jellyfin._tcp / _subsonic._tcp because those ecosystems already have clients that browse the LAN. That last part is the point: Infuse, Jellyfin Media Player and Symfonium can find Vellum with no client-side work, because they are already looking for those names. TXT records carry api, ver, proto, types, tls and (when set) pub, so a client that resolves one can tell what it found before connecting. Definitions is a pure function of config and Start only registers what it returns, so what is advertised is testable without a socket. Two behaviours worth knowing: - VELLUM_DISCOVERY=off stops advertising. It accepts off/no/false/0; strconv.ParseBool would have silently ignored "off". - A loopback-only listen address advertises nothing. That is the normal shape behind a reverse proxy, and advertising it would put a port in the LAN browse list that refuses every client that dials it. Verified red-green: defeating the guard fails TestDefinitionsSkipsLoopbackListen. zeroconf is the only new dependency. It logs to the stdlib logger, so a host with no multicast-capable interface prints to stderr outside slog. Not verified: real mDNS on a real LAN. There is no multicast here, so the wire behaviour of zeroconf is untested - only the definitions it is handed.
175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
package core
|
|
|
|
import (
|
|
"net"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadConfigDefaults(t *testing.T) {
|
|
// No environment set: every field should have a usable default.
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig: %v", err)
|
|
}
|
|
if cfg.Listen != ":7788" {
|
|
t.Errorf("Listen = %q, want :7788", cfg.Listen)
|
|
}
|
|
if !filepath.IsAbs(cfg.DataDir) {
|
|
t.Errorf("DataDir = %q, want absolute", cfg.DataDir)
|
|
}
|
|
if cfg.DBPath != filepath.Join(cfg.DataDir, "vellum.db") {
|
|
t.Errorf("DBPath = %q, want <DataDir>/vellum.db", cfg.DBPath)
|
|
}
|
|
if cfg.LogLevel != "info" {
|
|
t.Errorf("LogLevel = %q, want info", cfg.LogLevel)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigFromEnv(t *testing.T) {
|
|
t.Setenv("VELLUM_LISTEN", "127.0.0.1:9999")
|
|
t.Setenv("VELLUM_DATA_DIR", "/tmp/vellum-data")
|
|
t.Setenv("VELLUM_MEDIA_DIRS", "/media/a:/media/b")
|
|
t.Setenv("VELLUM_SESSION_TTL", "48h")
|
|
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig: %v", err)
|
|
}
|
|
if cfg.Listen != "127.0.0.1:9999" {
|
|
t.Errorf("Listen = %q", cfg.Listen)
|
|
}
|
|
if cfg.DataDir != "/tmp/vellum-data" {
|
|
t.Errorf("DataDir = %q", cfg.DataDir)
|
|
}
|
|
if len(cfg.MediaDirs) != 2 || cfg.MediaDirs[0] != "/media/a" {
|
|
t.Errorf("MediaDirs = %v", cfg.MediaDirs)
|
|
}
|
|
if cfg.SessionTTL.Hours() != 48 {
|
|
t.Errorf("SessionTTL = %v, want 48h", cfg.SessionTTL)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigTrustedProxies(t *testing.T) {
|
|
t.Setenv("VELLUM_TRUSTED_PROXIES", "172.16.0.0/12, 127.0.0.1/32")
|
|
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig: %v", err)
|
|
}
|
|
if len(cfg.TrustedProxies) != 2 {
|
|
t.Fatalf("TrustedProxies = %v, want 2 entries", cfg.TrustedProxies)
|
|
}
|
|
if !cfg.TrustedProxies[0].Contains(net.ParseIP("172.20.0.5")) {
|
|
t.Errorf("TrustedProxies[0] = %v, want it to contain 172.20.0.5", cfg.TrustedProxies[0])
|
|
}
|
|
if cfg.TrustedProxies[1].Contains(net.ParseIP("172.20.0.5")) {
|
|
t.Errorf("TrustedProxies[1] = %v, want it to contain only 127.0.0.1", cfg.TrustedProxies[1])
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigRejectsBadTrustedProxy(t *testing.T) {
|
|
// A malformed entry must fail the boot: silently dropping it would quietly
|
|
// narrow the trust boundary and the operator would never know.
|
|
t.Setenv("VELLUM_TRUSTED_PROXIES", "172.16.0.0/12, not-a-cidr")
|
|
|
|
_, err := LoadConfig()
|
|
if err == nil {
|
|
t.Fatal("expected error for malformed VELLUM_TRUSTED_PROXIES")
|
|
}
|
|
if !strings.Contains(err.Error(), "not-a-cidr") {
|
|
t.Errorf("error %q should name the offending entry", err)
|
|
}
|
|
}
|
|
|
|
func TestCookieSecureDefaultsFromPublicURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
public string
|
|
cookie string // empty means unset
|
|
want bool
|
|
}{
|
|
{name: "https origin secures the cookie", public: "https://media.example.com", want: true},
|
|
{name: "http origin leaves it off", public: "http://media.example.com", want: false},
|
|
{name: "unset origin leaves it off", want: false},
|
|
{name: "explicit false overrides https", public: "https://media.example.com", cookie: "false", want: false},
|
|
{name: "explicit true overrides http", public: "http://media.example.com", cookie: "true", want: true},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv("VELLUM_PUBLIC_URL", tc.public)
|
|
// An empty value reads as unset to the env helpers, and t.Setenv
|
|
// restores the ambient value either way.
|
|
t.Setenv("VELLUM_COOKIE_SECURE", tc.cookie)
|
|
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig: %v", err)
|
|
}
|
|
if cfg.PublicURL != tc.public {
|
|
t.Errorf("PublicURL = %q, want %q", cfg.PublicURL, tc.public)
|
|
}
|
|
if cfg.CookieSecure != tc.want {
|
|
t.Errorf("CookieSecure = %v, want %v", cfg.CookieSecure, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsMissingMediaDir(t *testing.T) {
|
|
cfg := Config{
|
|
DataDir: t.TempDir(),
|
|
ConfigDir: t.TempDir(),
|
|
MediaDirs: []string{"/definitely/not/a/real/path"},
|
|
}
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected error for missing media dir")
|
|
}
|
|
if !strings.Contains(err.Error(), "/definitely/not/a/real/path") {
|
|
t.Errorf("error %q should name the offending path", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateCreatesDirs(t *testing.T) {
|
|
base := t.TempDir()
|
|
cfg := Config{
|
|
DataDir: filepath.Join(base, "data"),
|
|
ConfigDir: filepath.Join(base, "config"),
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("Validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoveryDefaultAndOff(t *testing.T) {
|
|
// Advertising is on by default: a server nobody can find on the LAN is the
|
|
// common case this feature exists to remove.
|
|
tests := []struct {
|
|
value string // empty means unset
|
|
want bool
|
|
}{
|
|
{value: "", want: true},
|
|
{value: "off", want: false},
|
|
{value: "OFF", want: false},
|
|
{value: "false", want: false},
|
|
{value: "0", want: false},
|
|
{value: "no", want: false},
|
|
{value: "on", want: true},
|
|
{value: "garbage", want: true}, // unrecognised values leave it on
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run("VELLUM_DISCOVERY="+tc.value, func(t *testing.T) {
|
|
t.Setenv("VELLUM_DISCOVERY", tc.value)
|
|
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
t.Fatalf("LoadConfig: %v", err)
|
|
}
|
|
if cfg.Discovery != tc.want {
|
|
t.Errorf("Discovery = %v, want %v", cfg.Discovery, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|