package acquire import ( "context" "errors" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/downloader" "git.fabledsword.com/bvandeusen/minstrel/internal/exchange" "git.fabledsword.com/bvandeusen/minstrel/internal/indexer" "git.fabledsword.com/bvandeusen/minstrel/internal/prowlarr" ) func validUpdate() Update { return Update{Label: "Prowlarr", BaseURL: "http://prowlarr:9696", TimeoutMs: 30000, LocalPath: "/downloads", SearchesPerHour: 120} } func TestUpdateValidate(t *testing.T) { if err := validUpdate().validate(); err != nil { t.Fatalf("a valid form: %v", err) } for name, tc := range map[string]struct { edit func(*Update) field string }{ "no name": {func(u *Update) { u.Label = "" }, "label"}, "switched on with no address": {func(u *Update) { u.Enabled, u.BaseURL = true, "" }, "base_url"}, "an address with no scheme": {func(u *Update) { u.BaseURL = "prowlarr:9696" }, "base_url"}, "a timeout too short": {func(u *Update) { u.TimeoutMs = 10 }, "timeout_ms"}, "a relative downloads folder": {func(u *Update) { u.LocalPath = "downloads" }, "local_path"}, "a relative client folder": {func(u *Update) { u.RemotePath = "data" }, "remote_path"}, "too many searches": {func(u *Update) { u.SearchesPerHour = 4000 }, "searches_per_hour"}, } { u := validUpdate() tc.edit(&u) var invalid *InvalidError if err := u.validate(); !errors.As(err, &invalid) || invalid.Field != tc.field { t.Errorf("%s: err = %v, want an InvalidError on %s", name, err, tc.field) } } // Switched off, an empty address is a row still being set up. u := validUpdate() u.BaseURL = "" if err := u.validate(); err != nil { t.Errorf("switched off with no address: %v", err) } } func TestSourceFor_BuildsTheClientTheProtocolNames(t *testing.T) { for protocol, check := range map[string]func(Source) bool{ ProtocolProwlarr: func(s Source) bool { _, ok := s.Search.(*prowlarr.Client); return ok && s.Fetch != nil }, ProtocolTorznab: func(s Source) bool { _, ok := s.Search.(*indexer.Torznab); return ok && s.Fetch != nil }, // Exchange's results are all magnets: nothing to fetch through it. ProtocolExchange: func(s Source) bool { _, ok := s.Search.(*exchange.Client); return ok && s.Fetch == nil }, } { src := SourceFor(dbq.AcquisitionService{Code: protocol, Protocol: protocol, SearchesPerHour: 7}) if !check(src) || src.Code != protocol || src.PerHour != 7 { t.Errorf("%s: source %+v", protocol, src) } } } func TestJobs_AnExchangeDoesWhatItsSwitchesAllow(t *testing.T) { ex := dbq.AcquisitionService{Kind: KindDownload, Protocol: ProtocolExchange, ServesSearch: true} if !Searches(ex) || Fetches(ex) { t.Error("an exchange with only search on should search and not fetch") } // A qBittorrent's switches mean nothing: it fetches, whatever they say. qb := dbq.AcquisitionService{Kind: KindDownload, Protocol: ProtocolQBittorrent, ServesSearch: true} if Searches(qb) || !Fetches(qb) { t.Error("a qBittorrent fetches and never searches") } } func TestTest_NoAddressIsNothingToReach(t *testing.T) { r := Test(context.Background(), dbq.AcquisitionService{Protocol: ProtocolProwlarr, Kind: KindSearch}) if r.OK || !strings.Contains(r.Detail, "No address") { t.Fatalf("result = %+v", r) } } func TestTest_SearchReportsResultsAndARefusedKey(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Api-Key") != "good" { w.WriteHeader(http.StatusUnauthorized) return } _, _ = w.Write([]byte(`[{"title": "Radiohead - OK Computer"}]`)) })) defer srv.Close() row := dbq.AcquisitionService{Kind: KindSearch, Protocol: ProtocolProwlarr, BaseUrl: srv.URL, ApiKey: "good", TimeoutMs: 5000} if r := Test(context.Background(), row); !r.OK || !strings.Contains(r.Detail, "1 results") { t.Errorf("a working key: %+v", r) } row.ApiKey = "bad" if r := Test(context.Background(), row); r.OK || !strings.Contains(r.Detail, "refused") { t.Errorf("a refused key: %+v", r) } } // fakeExchange answers the three native APIs. func fakeExchange(t *testing.T) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/downloads": _, _ = w.Write([]byte(`{"enabled": true, "running": true, "inbound_conns": 3, "downloads": []}`)) case r.URL.Path == "/api/search": _, _ = w.Write([]byte(`{"total": 0, "results": []}`)) case strings.HasPrefix(r.URL.Path, "/api/torrents/"): w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error": "unknown torrent"}`)) default: w.WriteHeader(http.StatusNotFound) } })) t.Cleanup(srv.Close) return srv } func TestTest_AnExchangeReportsEachJobSwitchedOn(t *testing.T) { srv := fakeExchange(t) row := dbq.AcquisitionService{ Kind: KindDownload, Protocol: ProtocolExchange, BaseUrl: srv.URL, TimeoutMs: 5000, ServesDownloads: true, ServesSearch: true, ServesEnrichment: true, LocalPath: t.TempDir(), } r := Test(context.Background(), row) if !r.OK { t.Fatalf("all three jobs: %+v", r) } for _, job := range []string{"Downloads:", "Search:", "Release details:"} { if !strings.Contains(r.Detail, job) { t.Errorf("detail %q does not report %s", r.Detail, job) } } row.ServesDownloads, row.ServesEnrichment = false, false if r := Test(context.Background(), row); strings.Contains(r.Detail, "Downloads:") || !strings.Contains(r.Detail, "Search:") { t.Errorf("only search on: %+v", r) } row.ServesSearch = false if r := Test(context.Background(), row); r.OK || !strings.Contains(r.Detail, "switched off") { t.Errorf("every job off: %+v", r) } } func TestTest_ADownloadClientNeedsAFolderMinstrelCanSee(t *testing.T) { srv := fakeExchange(t) row := dbq.AcquisitionService{ Kind: KindDownload, Protocol: ProtocolExchange, BaseUrl: srv.URL, TimeoutMs: 5000, ServesDownloads: true, Category: DefaultCategory, LocalPath: filepath.Join(t.TempDir(), "not-mounted"), } r := Test(context.Background(), row) if r.OK || !strings.Contains(r.Detail, "cannot see its downloads folder") { t.Fatalf("a missing folder: %+v", r) } row.LocalPath = t.TempDir() if r := Test(context.Background(), row); !r.OK || !strings.Contains(r.Detail, `"minstrel" category`) { t.Errorf("a visible, writable folder: %+v", r) } } func TestDownloadClient_WrapsThePathMapping(t *testing.T) { d, err := DownloadClient(dbq.AcquisitionService{Protocol: ProtocolQBittorrent, LocalPath: "/downloads", RemotePath: "/data"}) if err != nil { t.Fatal(err) } if _, plain := d.(*downloader.Client); plain { t.Error("a row naming two folders should come back wrapped in its mapping") } if _, err := DownloadClient(dbq.AcquisitionService{Protocol: ProtocolProwlarr}); err == nil { t.Error("a search protocol is not a download client") } }