2 files changed,
93 insertions(+),
139 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-05-23 15:55:44 +0300
Authored at:
2026-05-23 15:51:15 +0300
Change ID:
vlovrwlosknrznutqnotuxyqlkuzqlsr
Parent:
71f9578
M
sources/moviefeed/moviefeed.go
··· 13 13 ) 14 14 15 15 type moviefeed struct { 16 - api *TMDBAPI 16 + api episodeFetcher 17 17 shows []string 18 +} 19 + 20 +type episodeFetcher interface { 21 + FetchEpisodesForShow(showID string) ([]TMDBEpisode, error) 18 22 } 19 23 20 24 func Register(a *app.App) error { ··· 63 67 64 68 func generateFeed(episodes []TMDBEpisode) *atom.Feed { 65 69 feed := atom.NewFeed("moviefeed", "moviefeed") 66 - 67 70 for i := len(episodes) - 1; i >= 0; i-- { 68 71 ep := episodes[i] 69 72 airDate, _ := time.Parse(dateFormat, ep.AirDate)
M
sources/moviefeed/moviefeed_test.go
··· 2 2 3 3 import ( 4 4 "encoding/xml" 5 - "fmt" 5 + "errors" 6 6 "net/http" 7 7 "net/http/httptest" 8 - "net/url" 9 8 "strings" 10 9 "testing" 11 10 "time" ··· 14 13 "olexsmir.xyz/x/is" 15 14 ) 16 15 16 +type fakeEpisodeAPI struct { 17 + episodes map[string][]TMDBEpisode 18 + errs map[string]error 19 +} 20 + 21 +func (f fakeEpisodeAPI) FetchEpisodesForShow(showID string) ([]TMDBEpisode, error) { 22 + if err, ok := f.errs[showID]; ok { 23 + return nil, err 24 + } 25 + if episodes, ok := f.episodes[showID]; ok { 26 + return episodes, nil 27 + } 28 + return nil, nil 29 +} 30 + 17 31 func TestHandleMoviesRendersFeedFromConfiguredShows(t *testing.T) { 18 - server, client := newTMDBStub(t) 19 - defer server.Close() 32 + episodes := []TMDBEpisode{ 33 + { 34 + ID: 1001, 35 + Name: "Episode 1", 36 + Overview: "E1", 37 + AirDate: "2026-04-20", 38 + EpisodeNumber: 1, 39 + SeasonNumber: 1, 40 + StillPath: "/e1.jpg", 41 + ShowName: "Test Show", 42 + ShowID: "101", 43 + }, 44 + { 45 + ID: 1002, 46 + Name: "Episode 2", 47 + Overview: "E2", 48 + AirDate: "2026-04-21", 49 + EpisodeNumber: 2, 50 + SeasonNumber: 1, 51 + StillPath: "", 52 + ShowName: "Test Show", 53 + ShowID: "101", 54 + }, 55 + } 20 56 21 57 mf := &moviefeed{ 22 - api: NewTMDBAPI("test-key", client), 58 + api: fakeEpisodeAPI{ 59 + episodes: map[string][]TMDBEpisode{ 60 + "tt123": episodes, 61 + }, 62 + }, 23 63 shows: []string{"tt123"}, 24 64 } 25 65 ··· 38 78 var feed atom.Feed 39 79 is.Err(t, xml.NewDecoder(rr.Body).Decode(&feed), nil) 40 80 is.Equal(t, feed.Title, "moviefeed") 41 - is.Equal(t, feed.Subtitle, "Latest episodes from followed shows") 42 81 is.Equal(t, len(feed.Entry), 2) 43 82 is.Equal(t, strings.Contains(feed.Entry[0].Title, "S1E2"), true) 44 83 is.Equal(t, feed.Entry[0].Content.Type, "text") ··· 51 90 } 52 91 53 92 func TestHandleMoviesContinuesWhenOneShowFails(t *testing.T) { 54 - server, client := newTMDBStub(t) 55 - defer server.Close() 93 + episodes := []TMDBEpisode{ 94 + { 95 + ID: 1001, 96 + Name: "Episode 1", 97 + Overview: "E1", 98 + AirDate: "2026-04-20", 99 + EpisodeNumber: 1, 100 + SeasonNumber: 1, 101 + StillPath: "/e1.jpg", 102 + ShowName: "Test Show", 103 + ShowID: "101", 104 + }, 105 + { 106 + ID: 1002, 107 + Name: "Episode 2", 108 + Overview: "E2", 109 + AirDate: "2026-04-21", 110 + EpisodeNumber: 2, 111 + SeasonNumber: 1, 112 + StillPath: "", 113 + ShowName: "Test Show", 114 + ShowID: "101", 115 + }, 116 + } 56 117 57 118 mf := &moviefeed{ 58 - api: NewTMDBAPI("test-key", client), 119 + api: fakeEpisodeAPI{ 120 + episodes: map[string][]TMDBEpisode{ 121 + "tt123": episodes, 122 + }, 123 + errs: map[string]error{ 124 + "bad-show": errors.New("boom"), 125 + }, 126 + }, 59 127 shows: []string{"bad-show", "tt123"}, 60 128 } 61 129 ··· 73 141 is.Equal(t, len(feed.Entry), 2) 74 142 } 75 143 76 -func TestFetchEpisodesForShowFiltersRecentAndMapsFields(t *testing.T) { 77 - server, client := newTMDBStub(t) 78 - defer server.Close() 144 +func TestFilterRecentEpisodes(t *testing.T) { 145 + now := time.Now() 146 + recent := now.AddDate(0, 0, -5).Format(dateFormat) 147 + old := now.AddDate(0, 0, -40).Format(dateFormat) 79 148 80 - api := NewTMDBAPI("test-key", client) 81 - episodes, err := api.FetchEpisodesForShow("tt123") 82 - is.Err(t, err, nil) 149 + episodes := filterRecentEpisodes([]TMDBEpisode{ 150 + {AirDate: recent, Name: "recent"}, 151 + {AirDate: old, Name: "old"}, 152 + {AirDate: "", Name: "missing"}, 153 + }) 83 154 84 - is.Equal(t, len(episodes), 2) 85 - is.Equal(t, episodes[0].ShowID, "101") 86 - is.Equal(t, episodes[0].ShowName, "Test Show") 87 -} 88 - 89 -func TestFetchEpisodesForShowSeasonErrorDoesNotFailShow(t *testing.T) { 90 - server, client := newTMDBStubWithSeasonError(t) 91 - defer server.Close() 92 - 93 - api := NewTMDBAPI("test-key", client) 94 - episodes, err := api.FetchEpisodesForShow("tt123") 95 - is.Err(t, err, nil) 96 - is.Equal(t, len(episodes), 0) 155 + is.Equal(t, 1, len(episodes)) 156 + is.Equal(t, "recent", episodes[0].Name) 97 157 } 98 158 99 159 func TestEpisodeContentIncludesImageInBody(t *testing.T) { ··· 108 168 is.Equal(t, strings.Contains(content, `<img src="https://image.tmdb.org/t/p/w500/e1.jpg" alt="Episode 1"`), true) 109 169 is.Equal(t, strings.Contains(content, "</body>"), true) 110 170 } 111 - 112 -func newTMDBStub(t *testing.T) (*httptest.Server, *http.Client) { 113 - t.Helper() 114 - 115 - recentDay := time.Now().AddDate(0, 0, -7).Format(dateFormat) 116 - newestDay := time.Now().AddDate(0, 0, -1).Format(dateFormat) 117 - oldDay := time.Now().AddDate(0, 0, -45).Format(dateFormat) 118 - 119 - mux := http.NewServeMux() 120 - mux.HandleFunc("/3/find/tt123", func(w http.ResponseWriter, r *http.Request) { 121 - if got := r.URL.Query().Get("external_source"); got != "imdb_id" { 122 - t.Fatalf("unexpected external_source query: %q", got) 123 - } 124 - if got := r.URL.Query().Get("api_key"); got != "test-key" { 125 - t.Fatalf("unexpected api_key query: %q", got) 126 - } 127 - _, _ = w.Write([]byte(`{"tv_results":[{"id":101}]}`)) 128 - }) 129 - 130 - mux.HandleFunc("/3/find/bad-show", func(w http.ResponseWriter, r *http.Request) { 131 - if got := r.URL.Query().Get("external_source"); got != "imdb_id" { 132 - t.Fatalf("unexpected external_source query: %q", got) 133 - } 134 - if got := r.URL.Query().Get("api_key"); got != "test-key" { 135 - t.Fatalf("unexpected api_key query: %q", got) 136 - } 137 - _, _ = w.Write([]byte(`{"tv_results":[]}`)) 138 - }) 139 - 140 - mux.HandleFunc("/3/tv/101", func(w http.ResponseWriter, r *http.Request) { 141 - if got := r.URL.Query().Get("api_key"); got != "test-key" { 142 - t.Fatalf("unexpected api_key query: %q", got) 143 - } 144 - _, _ = w.Write([]byte(`{"id":101,"name":"Test Show","number_of_seasons":1}`)) 145 - }) 146 - 147 - mux.HandleFunc("/3/tv/101/season/1", func(w http.ResponseWriter, r *http.Request) { 148 - if got := r.URL.Query().Get("api_key"); got != "test-key" { 149 - t.Fatalf("unexpected api_key query: %q", got) 150 - } 151 - body := fmt.Sprintf(`{ 152 - "episodes": [ 153 - {"id": 1001, "name": "Episode 1", "overview": "E1", "air_date": %q, "episode_number": 1, "season_number": 1, "still_path": "/e1.jpg"}, 154 - {"id": 1002, "name": "Episode 2", "overview": "E2", "air_date": %q, "episode_number": 2, "season_number": 1, "still_path": ""}, 155 - {"id": 1003, "name": "Episode old", "overview": "old", "air_date": %q, "episode_number": 3, "season_number": 1, "still_path": ""} 156 - ] 157 - }`, recentDay, newestDay, oldDay) 158 - _, _ = w.Write([]byte(body)) 159 - }) 160 - 161 - server := httptest.NewServer(mux) 162 - target, err := url.Parse(server.URL) 163 - is.Err(t, err, nil) 164 - 165 - client := &http.Client{ 166 - Transport: rewriteTransport{target: target}, 167 - } 168 - return server, client 169 -} 170 - 171 -func newTMDBStubWithSeasonError(t *testing.T) (*httptest.Server, *http.Client) { 172 - t.Helper() 173 - 174 - mux := http.NewServeMux() 175 - mux.HandleFunc("/3/find/tt123", func(w http.ResponseWriter, r *http.Request) { 176 - if got := r.URL.Query().Get("external_source"); got != "imdb_id" { 177 - t.Fatalf("unexpected external_source query: %q", got) 178 - } 179 - if got := r.URL.Query().Get("api_key"); got != "test-key" { 180 - t.Fatalf("unexpected api_key query: %q", got) 181 - } 182 - _, _ = w.Write([]byte(`{"tv_results":[{"id":101}]}`)) 183 - }) 184 - 185 - mux.HandleFunc("/3/tv/101", func(w http.ResponseWriter, r *http.Request) { 186 - if got := r.URL.Query().Get("api_key"); got != "test-key" { 187 - t.Fatalf("unexpected api_key query: %q", got) 188 - } 189 - _, _ = w.Write([]byte(`{"id":101,"name":"Test Show","number_of_seasons":1}`)) 190 - }) 191 - 192 - mux.HandleFunc("/3/tv/101/season/1", func(w http.ResponseWriter, r *http.Request) { 193 - w.WriteHeader(http.StatusInternalServerError) 194 - _, _ = w.Write([]byte(`bad gateway`)) 195 - }) 196 - 197 - server := httptest.NewServer(mux) 198 - target, err := url.Parse(server.URL) 199 - is.Err(t, err, nil) 200 - 201 - client := &http.Client{ 202 - Transport: rewriteTransport{target: target}, 203 - } 204 - return server, client 205 -} 206 - 207 -type rewriteTransport struct { 208 - target *url.URL 209 -} 210 - 211 -func (t rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) { 212 - clone := req.Clone(req.Context()) 213 - copiedURL := *clone.URL 214 - copiedURL.Scheme = t.target.Scheme 215 - copiedURL.Host = t.target.Host 216 - clone.URL = &copiedURL 217 - clone.Host = t.target.Host 218 - return http.DefaultTransport.RoundTrip(clone) 219 -}