package cmd import ( "bytes" "io" "os" "strings" "testing" "gitea.dcglab.co.uk/steve/arrman/sonarr" ) type mockSonarr struct { series []sonarr.Series added *sonarr.Series profiles []sonarr.QualityProfile folders []sonarr.RootFolder lookupFn func(term string) ([]sonarr.Series, error) addErr error } func (m *mockSonarr) List() ([]sonarr.Series, error) { return m.series, nil } func (m *mockSonarr) Lookup(term string) ([]sonarr.Series, error) { if m.lookupFn != nil { return m.lookupFn(term) } return m.series, nil } func (m *mockSonarr) Get(id int) (*sonarr.Series, error) { for _, s := range m.series { if s.ID == id { return &s, nil } } return nil, nil } func (m *mockSonarr) QualityProfiles() ([]sonarr.QualityProfile, error) { return m.profiles, nil } func (m *mockSonarr) RootFolders() ([]sonarr.RootFolder, error) { return m.folders, nil } func (m *mockSonarr) Add(s sonarr.Series) (*sonarr.Series, error) { if m.addErr != nil { return nil, m.addErr } m.added = &s s.ID = 1 return &s, nil } func captureOutput(fn func()) string { old := os.Stdout r, w, _ := os.Pipe() os.Stdout = w fn() w.Close() os.Stdout = old var buf bytes.Buffer io.Copy(&buf, r) return buf.String() } func TestTVListEmpty(t *testing.T) { mock := &mockSonarr{} out := captureOutput(func() { TVList(mock, "", false, false) }) if !strings.Contains(out, "No series found") { t.Errorf("expected 'No series found', got: %s", out) } } func TestTVListWithItems(t *testing.T) { mock := &mockSonarr{ series: []sonarr.Series{ { Title: "Breaking Bad", Year: 2008, Status: "ended", Statistics: &sonarr.Statistics{ EpisodeFileCount: 62, EpisodeCount: 62, }, }, }, } out := captureOutput(func() { TVList(mock, "", false, false) }) if !strings.Contains(out, "Breaking Bad (2008)") { t.Errorf("expected series in output, got: %s", out) } if !strings.Contains(out, "62/62") { t.Errorf("expected episode count, got: %s", out) } } func TestTVAddByID(t *testing.T) { mock := &mockSonarr{ lookupFn: func(term string) ([]sonarr.Series, error) { if term == "tvdb:448176" { return []sonarr.Series{{Title: "Test Show", Year: 2024, TvdbID: 448176}}, nil } return nil, nil }, } out := captureOutput(func() { err := TVAdd(mock, []string{"448176", "--profile", "1", "--rootfolder", "/tv"}, false) if err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Added: Test Show") { t.Errorf("expected confirmation, got: %s", out) } if mock.added == nil { t.Fatal("expected Add to be called") } if mock.added.QualityProfileID != 1 { t.Errorf("expected profile 1, got %d", mock.added.QualityProfileID) } if mock.added.RootFolderPath != "/tv" { t.Errorf("expected rootfolder /tv, got %s", mock.added.RootFolderPath) } } func TestTVAddMissingFlags(t *testing.T) { mock := &mockSonarr{} err := TVAdd(mock, []string{"448176"}, false) if err == nil { t.Fatal("expected error for missing flags") } } func TestTVAddNoResult(t *testing.T) { mock := &mockSonarr{ lookupFn: func(term string) ([]sonarr.Series, error) { return nil, nil }, } err := TVAdd(mock, []string{"999999", "--profile", "1", "--rootfolder", "/tv"}, false) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "No series found with TVDB ID 999999") { t.Errorf("unexpected error: %v", err) } } func TestTVAddByTitle(t *testing.T) { mock := &mockSonarr{ lookupFn: func(term string) ([]sonarr.Series, error) { return []sonarr.Series{ {Title: "Breaking Bad", Year: 2008, TvdbID: 81189}, }, nil }, } out := captureOutput(func() { err := TVAdd(mock, []string{"Breaking Bad", "--profile", "1", "--rootfolder", "/tv"}, false) if err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Added: Breaking Bad") { t.Errorf("expected confirmation, got: %s", out) } } func TestTVAddFuzzyMatch(t *testing.T) { mock := &mockSonarr{ lookupFn: func(term string) ([]sonarr.Series, error) { return []sonarr.Series{ {Title: "Breaking Bad", Year: 2008, TvdbID: 81189}, {Title: "Better Call Saul", Year: 2015, TvdbID: 273181}, }, nil }, } out := captureOutput(func() { err := TVAdd(mock, []string{"Breaking", "Bad", "--profile", "1", "--rootfolder", "/tv"}, false) if err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Added: Breaking Bad") { t.Errorf("expected fuzzy match to Breaking Bad, got: %s", out) } } func TestTVAddNoMatch(t *testing.T) { mock := &mockSonarr{ lookupFn: func(term string) ([]sonarr.Series, error) { return []sonarr.Series{ {Title: "Something Completely Different", Year: 2020, TvdbID: 99999}, }, nil }, } err := TVAdd(mock, []string{"xyz", "--profile", "1", "--rootfolder", "/tv"}, false) if err == nil { t.Fatal("expected error for no match") } } func TestTVProfiles(t *testing.T) { mock := &mockSonarr{ profiles: []sonarr.QualityProfile{ {ID: 1, Name: "HD-1080p"}, {ID: 4, Name: "Ultra-HD"}, }, } out := captureOutput(func() { TVProfiles(mock, false) }) if !strings.Contains(out, "ID: 1 — Name: HD-1080p") { t.Errorf("expected profile output, got: %s", out) } } func TestTVRootFolders(t *testing.T) { mock := &mockSonarr{ folders: []sonarr.RootFolder{ {ID: 1, Path: "/tv", FreeSpace: 107374182400}, }, } out := captureOutput(func() { TVRootFolders(mock, false) }) if !strings.Contains(out, "Path: /tv") { t.Errorf("expected folder output, got: %s", out) } }