The agent ignored its config.toml method preference for web-driven downloads (only the local `unarr download` command read it), and resolveMethod tried torrent first in auto mode — so a 'debrid only' user still got torrent tasks. - config: preferred_methods (ordered list, e.g. ["debrid","usenet"]) with MethodOrder() resolution; back-compat with the singular preferred_method. Methods absent from the list are disabled (debrid-only never torrents). - resolveMethod/tryFallback honor the config order (gating, no fallback to a method outside the list) over the per-task preference. - report preferred_methods on register so the web honors it (resolves debrid, gates the P2P stream fallback). - enable the usenet downloader when usenet is listed (it was never enabled). - config_menu: ordered presets (debrid-only, debrid→torrent, debrid→usenet…). Tests: resolveMethod gating + fallback within/outside the list.
216 lines
7.4 KiB
Go
216 lines
7.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
// mockDownloader implements Downloader for testing.
|
|
type mockDownloader struct {
|
|
method DownloadMethod
|
|
available bool
|
|
err error
|
|
}
|
|
|
|
func (m *mockDownloader) Method() DownloadMethod { return m.method }
|
|
func (m *mockDownloader) Available(_ context.Context, _ *Task) (bool, error) {
|
|
return m.available, m.err
|
|
}
|
|
func (m *mockDownloader) Download(_ context.Context, _ *Task, _ string, _ chan<- Progress) (*Result, error) {
|
|
return &Result{Method: m.method, FileName: "test.mkv", FilePath: "/tmp/test.mkv"}, nil
|
|
}
|
|
func (m *mockDownloader) Pause(_ string) error { return nil }
|
|
func (m *mockDownloader) Cancel(_ string) error { return nil }
|
|
func (m *mockDownloader) Shutdown(_ context.Context) error { return nil }
|
|
|
|
func TestResolveMethodAuto(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
|
|
task := &Task{PreferredMethod: "auto"}
|
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Torrent is first in auto order
|
|
if method != MethodTorrent {
|
|
t.Errorf("method = %q, want torrent (first in auto order)", method)
|
|
}
|
|
}
|
|
|
|
func TestResolveMethodSpecific(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
|
|
task := &Task{PreferredMethod: "debrid"}
|
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if method != MethodDebrid {
|
|
t.Errorf("method = %q, want debrid", method)
|
|
}
|
|
}
|
|
|
|
func TestResolveMethodSkipsTried(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
|
|
task := &Task{
|
|
PreferredMethod: "auto",
|
|
TriedMethods: []DownloadMethod{MethodTorrent},
|
|
}
|
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if method != MethodDebrid {
|
|
t.Errorf("method = %q, want debrid (torrent already tried)", method)
|
|
}
|
|
}
|
|
|
|
func TestResolveMethodNoneAvailable(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: false},
|
|
}
|
|
|
|
task := &Task{PreferredMethod: "auto"}
|
|
_, err := resolveMethod(context.Background(), task, downloaders, nil)
|
|
if err == nil {
|
|
t.Error("expected error when no method available")
|
|
}
|
|
}
|
|
|
|
func TestResolveMethodAvailabilityError(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: false, err: fmt.Errorf("network error")},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
|
|
task := &Task{ID: "test-resolve-err", PreferredMethod: "auto"}
|
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Should fallback to debrid when torrent has error
|
|
if method != MethodDebrid {
|
|
t.Errorf("method = %q, want debrid (torrent errored)", method)
|
|
}
|
|
}
|
|
|
|
func TestTryFallbackAutoMode(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
|
|
task := &Task{
|
|
PreferredMethod: "auto",
|
|
ResolvedMethod: MethodTorrent,
|
|
}
|
|
|
|
if !tryFallback(task, downloaders, nil) {
|
|
t.Error("should have fallback available")
|
|
}
|
|
if len(task.TriedMethods) != 1 || task.TriedMethods[0] != MethodTorrent {
|
|
t.Error("torrent should be in tried methods")
|
|
}
|
|
}
|
|
|
|
func TestTryFallbackSpecificMode(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
|
|
task := &Task{
|
|
PreferredMethod: "torrent",
|
|
ResolvedMethod: MethodTorrent,
|
|
}
|
|
|
|
if tryFallback(task, downloaders, nil) {
|
|
t.Error("should not fallback in specific mode")
|
|
}
|
|
}
|
|
|
|
// ── config.toml preferred_methods gating ──────────────────────────────────
|
|
|
|
// Config list wins over the per-task preference: a "debrid only" agent picks
|
|
// debrid even when the task says auto AND torrent is available (the old bug:
|
|
// torrent-first auto stole every download from debrid-only users).
|
|
func TestResolveMethodConfigListWins(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
task := &Task{PreferredMethod: "auto"} // web said auto
|
|
method, err := resolveMethod(context.Background(), task, downloaders, []string{"debrid"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if method != MethodDebrid {
|
|
t.Errorf("method = %q, want debrid (config list debrid-only wins)", method)
|
|
}
|
|
}
|
|
|
|
// A method not in the config list is NEVER used, even if it's the only one
|
|
// available — failing closed is the whole point of "debrid only".
|
|
func TestResolveMethodConfigListGatesOutTorrent(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: false},
|
|
}
|
|
task := &Task{PreferredMethod: "auto"}
|
|
_, err := resolveMethod(context.Background(), task, downloaders, []string{"debrid"})
|
|
if err == nil {
|
|
t.Error("expected error: torrent is available but not in the debrid-only list")
|
|
}
|
|
}
|
|
|
|
// Ordered list honours order: debrid first, then usenet.
|
|
func TestResolveMethodConfigListOrder(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: false},
|
|
MethodUsenet: &mockDownloader{method: MethodUsenet, available: true},
|
|
}
|
|
task := &Task{PreferredMethod: "auto"}
|
|
method, err := resolveMethod(context.Background(), task, downloaders, []string{"debrid", "usenet"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if method != MethodUsenet {
|
|
t.Errorf("method = %q, want usenet (debrid unavailable, next in list)", method)
|
|
}
|
|
}
|
|
|
|
// A multi-method config list allows fallback within the list...
|
|
func TestTryFallbackConfigListMulti(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
task := &Task{PreferredMethod: "auto", ResolvedMethod: MethodDebrid}
|
|
if !tryFallback(task, downloaders, []string{"debrid", "torrent"}) {
|
|
t.Error("should fall back to torrent (in the list)")
|
|
}
|
|
}
|
|
|
|
// ...but a single-method config list has no fallback (debrid-only never torrents).
|
|
func TestTryFallbackConfigListSingle(t *testing.T) {
|
|
downloaders := map[DownloadMethod]Downloader{
|
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
|
}
|
|
task := &Task{PreferredMethod: "auto", ResolvedMethod: MethodDebrid}
|
|
if tryFallback(task, downloaders, []string{"debrid"}) {
|
|
t.Error("debrid-only must not fall back to torrent")
|
|
}
|
|
}
|