unarr/internal/config/config_test.go
Deivid Soto 0f4ad67827 fix(transcode): make preset libx264-only + restore quality opt-in
Two issues with the 0.9.9 preset retune:

1. applyDefaults was filling Preset="veryfast" before
   ResolveEncoderProfile got to pick the latency-biased default, so the
   "superfast" change never reached users with a freshly-generated
   config.toml — only those who left the field empty saw it.

2. The configured preset was being passed through to every encoder.
   That's only valid for libx264 (ultrafast…veryslow); NVENC uses p1-p7
   and rejects anything else, QSV uses its own subset. A user with NVENC
   + preset="veryfast" would have ffmpeg reject the argv.

Now:
- TranscodeConfig.Preset documented as libx264-only with the full
  range + advice on quality vs first-start latency.
- Default in applyDefaults is empty (was "veryfast") so the engine
  fills in "superfast" on libx264.
- ResolveEncoderProfile ignores configuredPreset for vendor encoders
  (NVENC sticks to p3, QSV to veryfast, VideoToolbox has no preset
  knob). Test cases updated to lock in this behaviour.

Users who want better quality at slower first-play should set
download.transcode.preset = "veryfast" (previous default) / "faster" /
"fast" / "medium" in their config.toml.
2026-05-27 10:46:03 +02:00

258 lines
6.9 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 != "" {
// Default is now empty — engine.ResolveEncoderProfile picks
// "superfast" on libx264 for first-start latency. Users
// wanting better quality override in config.toml.
t.Errorf("Transcode.Preset = %q, want empty", 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")
}
}