Drops the custom WebRTC DataChannel pipeline + pion deps + WSS signaling client + wire framing. Every in-browser playback now uses HLS over HTTP from the daemon (Tailscale/LAN/UPnP). Browser P2P never re-enabled. Wire renames (incompatible with web < 2026-05-26): agent.WebRTCSession => agent.StreamSession, SyncResponse.WebRTCSessions (JSON: webrtcSessions) => StreamSessions (JSON: streamSessions). MIN_AGENT_VERSION is bumped to 0.9.4 on the web side so older agents see an upgrade card. Also fixes the libx264 'VBV bitrate > level limit' abort by clamping the encoder bitrate to the effective output height instead of the requested label (carried over from the prior 0.9.3 unreleased work). The seed_file vertical (mode=seed_file handler + engine.SeedFile) was retired with the in-browser P2P player. [downloads.webrtc] config block deleted; existing TOML files with the section still parse fine.
255 lines
6.7 KiB
Go
255 lines
6.7 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestDefault(t *testing.T) {
|
|
cfg := Default()
|
|
|
|
if cfg.Auth.APIURL != "https://torrentclaw.com" {
|
|
t.Errorf("default APIURL = %q, want https://torrentclaw.com", cfg.Auth.APIURL)
|
|
}
|
|
if cfg.Download.PreferredMethod != "auto" {
|
|
t.Errorf("default PreferredMethod = %q, want auto", cfg.Download.PreferredMethod)
|
|
}
|
|
if cfg.Download.MaxConcurrent != 3 {
|
|
t.Errorf("default MaxConcurrent = %d, want 3", cfg.Download.MaxConcurrent)
|
|
}
|
|
if cfg.General.Country != "US" {
|
|
t.Errorf("default Country = %q, want US", cfg.General.Country)
|
|
}
|
|
if cfg.Daemon.StatusInterval != "" {
|
|
t.Errorf("default StatusInterval = %q, want empty", cfg.Daemon.StatusInterval)
|
|
}
|
|
}
|
|
|
|
func TestLoadMissingFile(t *testing.T) {
|
|
cfg, err := Load("/nonexistent/path/config.toml")
|
|
if err != nil {
|
|
t.Fatalf("Load nonexistent should return defaults, got err: %v", err)
|
|
}
|
|
if cfg.Auth.APIURL != "https://torrentclaw.com" {
|
|
t.Errorf("missing file should return default APIURL, got %q", cfg.Auth.APIURL)
|
|
}
|
|
}
|
|
|
|
func TestSaveAndLoad(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "config.toml")
|
|
|
|
cfg := Default()
|
|
cfg.Auth.APIKey = "tc_test123"
|
|
cfg.Auth.APIURL = "https://custom.example.com"
|
|
cfg.General.Country = "ES"
|
|
cfg.Download.Dir = "/media/downloads"
|
|
cfg.Agent.ID = "agent-uuid-123"
|
|
cfg.Agent.Name = "Test Machine"
|
|
|
|
if err := Save(cfg, path); err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
// File should exist
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
t.Fatal("config file was not created")
|
|
}
|
|
|
|
// No .tmp file left behind
|
|
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
|
|
t.Error("temp file was not cleaned up")
|
|
}
|
|
|
|
// Load it back
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
|
|
if loaded.Auth.APIKey != "tc_test123" {
|
|
t.Errorf("APIKey = %q, want tc_test123", loaded.Auth.APIKey)
|
|
}
|
|
if loaded.Auth.APIURL != "https://custom.example.com" {
|
|
t.Errorf("APIURL = %q, want https://custom.example.com", loaded.Auth.APIURL)
|
|
}
|
|
if loaded.General.Country != "ES" {
|
|
t.Errorf("Country = %q, want ES", loaded.General.Country)
|
|
}
|
|
if loaded.Download.Dir != "/media/downloads" {
|
|
t.Errorf("Dir = %q, want /media/downloads", loaded.Download.Dir)
|
|
}
|
|
if loaded.Agent.ID != "agent-uuid-123" {
|
|
t.Errorf("AgentID = %q, want agent-uuid-123", loaded.Agent.ID)
|
|
}
|
|
if loaded.Agent.Name != "Test Machine" {
|
|
t.Errorf("AgentName = %q, want Test Machine", loaded.Agent.Name)
|
|
}
|
|
}
|
|
|
|
func TestLoadPreservesDefaults(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "config.toml")
|
|
|
|
// Write partial config (only auth section)
|
|
os.WriteFile(path, []byte(`[auth]
|
|
api_key = "tc_partial"
|
|
`), 0o644)
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
|
|
if cfg.Auth.APIKey != "tc_partial" {
|
|
t.Errorf("APIKey = %q, want tc_partial", cfg.Auth.APIKey)
|
|
}
|
|
// Defaults should be preserved for missing sections
|
|
if cfg.Auth.APIURL != "https://torrentclaw.com" {
|
|
t.Errorf("APIURL should default, got %q", cfg.Auth.APIURL)
|
|
}
|
|
if cfg.Download.MaxConcurrent != 3 {
|
|
t.Errorf("MaxConcurrent should default to 3, got %d", cfg.Download.MaxConcurrent)
|
|
}
|
|
if cfg.General.Country != "US" {
|
|
t.Errorf("Country should default to US, got %q", cfg.General.Country)
|
|
}
|
|
}
|
|
|
|
func TestApplyEnvOverrides(t *testing.T) {
|
|
cfg := Default()
|
|
|
|
t.Setenv("UNARR_API_KEY", "tc_env_key")
|
|
t.Setenv("UNARR_API_URL", "https://env.example.com")
|
|
t.Setenv("UNARR_COUNTRY", "DE")
|
|
t.Setenv("UNARR_DOWNLOAD_DIR", "/env/downloads")
|
|
|
|
cfg.ApplyEnvOverrides()
|
|
|
|
if cfg.Auth.APIKey != "tc_env_key" {
|
|
t.Errorf("APIKey = %q, want tc_env_key", cfg.Auth.APIKey)
|
|
}
|
|
if cfg.Auth.APIURL != "https://env.example.com" {
|
|
t.Errorf("APIURL = %q, want https://env.example.com", cfg.Auth.APIURL)
|
|
}
|
|
if cfg.General.Country != "DE" {
|
|
t.Errorf("Country = %q, want DE", cfg.General.Country)
|
|
}
|
|
if cfg.Download.Dir != "/env/downloads" {
|
|
t.Errorf("Dir = %q, want /env/downloads", cfg.Download.Dir)
|
|
}
|
|
}
|
|
|
|
func TestSaveCreatesDirectory(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "nested", "deep", "config.toml")
|
|
|
|
cfg := Default()
|
|
if err := Save(cfg, path); err != nil {
|
|
t.Fatalf("Save with nested dir failed: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
t.Error("config file was not created in nested dir")
|
|
}
|
|
}
|
|
|
|
func TestParseSpeed(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want int64
|
|
}{
|
|
{"0", 0},
|
|
{"", 0},
|
|
{"10MB", 10 * 1024 * 1024},
|
|
{"500KB", 500 * 1024},
|
|
{"1GB", 1024 * 1024 * 1024},
|
|
{"1.5MB", int64(1.5 * 1024 * 1024)},
|
|
{"10mb", 10 * 1024 * 1024},
|
|
{"1024", 1024},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got, err := ParseSpeed(tt.input)
|
|
if err != nil {
|
|
t.Errorf("ParseSpeed(%q) error: %v", tt.input, err)
|
|
continue
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("ParseSpeed(%q) = %d, want %d", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
|
|
// Error cases
|
|
if _, err := ParseSpeed("abc"); err == nil {
|
|
t.Error("ParseSpeed(\"abc\") should error")
|
|
}
|
|
if _, err := ParseSpeed("-5MB"); err == nil {
|
|
t.Error("ParseSpeed(\"-5MB\") should error")
|
|
}
|
|
}
|
|
|
|
func TestLoadMinimalTOMLAppliesStreamingDefaults(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "config.toml")
|
|
|
|
// Minimal config — only auth + agent. Nothing about webrtc / transcode.
|
|
os.WriteFile(path, []byte(`[auth]
|
|
api_key = "tc_minimal"
|
|
|
|
[agent]
|
|
id = "agent-uuid"
|
|
name = "Test"
|
|
`), 0o644)
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
|
|
// Transcode should be on by default.
|
|
if !cfg.Download.Transcode.Enabled {
|
|
t.Error("Transcode.Enabled should default to true when [downloads.transcode] is absent")
|
|
}
|
|
if cfg.Download.Transcode.HWAccel != "auto" {
|
|
t.Errorf("Transcode.HWAccel = %q, want auto", cfg.Download.Transcode.HWAccel)
|
|
}
|
|
if cfg.Download.Transcode.Preset != "veryfast" {
|
|
t.Errorf("Transcode.Preset = %q, want veryfast", cfg.Download.Transcode.Preset)
|
|
}
|
|
if cfg.Download.Transcode.MaxConcurrent != 2 {
|
|
t.Errorf("Transcode.MaxConcurrent = %d, want 2", cfg.Download.Transcode.MaxConcurrent)
|
|
}
|
|
}
|
|
|
|
func TestLoadRespectsExplicitlyDisabledStreaming(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "config.toml")
|
|
|
|
// User explicitly opted out of transcode. Defaults must NOT override
|
|
// it — that would silently re-enable a feature the user disabled.
|
|
os.WriteFile(path, []byte(`[downloads.transcode]
|
|
enabled = false
|
|
`), 0o644)
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load failed: %v", err)
|
|
}
|
|
|
|
if cfg.Download.Transcode.Enabled {
|
|
t.Error("Transcode.Enabled = true, want false (user explicitly disabled)")
|
|
}
|
|
}
|
|
|
|
func TestLoadInvalidTOML(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "config.toml")
|
|
os.WriteFile(path, []byte(`not valid toml [[[`), 0o644)
|
|
|
|
_, err := Load(path)
|
|
if err == nil {
|
|
t.Error("expected error for invalid TOML, got nil")
|
|
}
|
|
}
|