configs/server-dockers/jellyfin-news/main_test.go
server-dockers/jellyfin-news/main_test.goRaw
1package main
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10 "time"
11)
12
13type fakeSource struct {
14 start time.Time
15 end time.Time
16 items []Item
17 err error
18}
19
20func (f *fakeSource) AddedBetween(_ context.Context, start, end time.Time) ([]Item, error) {
21 f.start, f.end = start, end
22 return f.items, f.err
23}
24
25func testApp(source *fakeSource) *app {
26 return &app{
27 jellyfin: source,
28 location: time.UTC,
29 labels: DefaultLabels(),
30 now: func() time.Time {
31 return time.Date(2026, 9, 2, 12, 0, 0, 0, time.UTC)
32 },
33 }
34}
35
36func TestCurrentWeek(t *testing.T) {
37 source := &fakeSource{}
38 request := httptest.NewRequest(http.MethodGet, "/api/current-week", nil)
39 response := httptest.NewRecorder()
40 testApp(source).routes().ServeHTTP(response, request)
41
42 if response.Code != http.StatusOK {
43 t.Fatalf("status = %d, body = %q", response.Code, response.Body.String())
44 }
45 if got := response.Body.String(); !strings.HasPrefix(got, "2026-08-31 - 2026-09-02 jellyfin weekly update:") {
46 t.Fatalf("body = %q", got)
47 }
48 wantStart := time.Date(2026, 8, 31, 0, 0, 0, 0, time.UTC)
49 if !source.start.Equal(wantStart) || !source.end.Equal(testApp(source).now()) {
50 t.Fatalf("range = %v to %v", source.start, source.end)
51 }
52}
53
54func TestStartOfWeekUsesMondayMidnight(t *testing.T) {
55 location := time.FixedZone("test", 2*60*60)
56 now := time.Date(2026, 9, 6, 23, 45, 0, 0, location) // Sunday
57 want := time.Date(2026, 8, 31, 0, 0, 0, 0, location)
58 if got := startOfWeek(now); !got.Equal(want) {
59 t.Fatalf("startOfWeek() = %v, want %v", got, want)
60 }
61}
62
63func TestSelectedWeek(t *testing.T) {
64 source := &fakeSource{}
65 request := httptest.NewRequest(http.MethodGet, "/api/week?offset=-2", nil)
66 response := httptest.NewRecorder()
67 testApp(source).routes().ServeHTTP(response, request)
68
69 if response.Code != http.StatusOK {
70 t.Fatalf("status = %d, body = %q", response.Code, response.Body.String())
71 }
72 if got := response.Body.String(); !strings.HasPrefix(got, "2026-08-17 - 2026-08-24 jellyfin weekly update:") {
73 t.Fatalf("body = %q", got)
74 }
75}
76
77func TestSelectedWeekRejectsInvalidOffset(t *testing.T) {
78 request := httptest.NewRequest(http.MethodGet, "/api/week?offset=0", nil)
79 response := httptest.NewRecorder()
80 testApp(&fakeSource{}).routes().ServeHTTP(response, request)
81 if response.Code != http.StatusBadRequest {
82 t.Fatalf("status = %d", response.Code)
83 }
84}
85
86func TestSelectedMonth(t *testing.T) {
87 source := &fakeSource{}
88 request := httptest.NewRequest(http.MethodGet, "/api/month?offset=-2", nil)
89 response := httptest.NewRecorder()
90 testApp(source).routes().ServeHTTP(response, request)
91
92 if response.Code != http.StatusOK {
93 t.Fatalf("status = %d, body = %q", response.Code, response.Body.String())
94 }
95 if got := response.Body.String(); !strings.HasPrefix(got, "2026-07 jellyfin monthly update:") {
96 t.Fatalf("body = %q", got)
97 }
98 if source.start.Month() != time.July || source.end.Month() != time.August {
99 t.Fatalf("range = %v to %v", source.start, source.end)
100 }
101}
102
103func TestSelectedMonthRejectsInvalidOffset(t *testing.T) {
104 request := httptest.NewRequest(http.MethodGet, "/api/month?offset=0", nil)
105 response := httptest.NewRecorder()
106 testApp(&fakeSource{}).routes().ServeHTTP(response, request)
107 if response.Code != http.StatusBadRequest {
108 t.Fatalf("status = %d", response.Code)
109 }
110}
111
112func TestIndex(t *testing.T) {
113 request := httptest.NewRequest(http.MethodGet, "/", nil)
114 response := httptest.NewRecorder()
115 testApp(&fakeSource{}).routes().ServeHTTP(response, request)
116 if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Jellyfin updates") {
117 t.Fatalf("status = %d, body = %q", response.Code, response.Body.String())
118 }
119}
120
121type fakeWatched struct {
122 start, end time.Time
123 err error
124}
125
126func (f *fakeWatched) WatchedBetween(_ context.Context, start, end time.Time) ([]WatchedTitle, error) {
127 f.start, f.end = start, end
128 return []WatchedTitle{{ID: "movie", Name: "Example", Type: "Movie", Users: 2}}, f.err
129}
130
131func TestWatchedPeriods(t *testing.T) {
132 for _, tc := range []struct{ query, start, end string }{
133 {"period=current-week", "2026-08-31T00:00:00Z", "2026-09-02T12:00:00Z"},
134 {"period=week&offset=-2", "2026-08-17T00:00:00Z", "2026-08-24T00:00:00Z"},
135 {"period=month&offset=-1", "2026-08-01T00:00:00Z", "2026-09-01T00:00:00Z"},
136 } {
137 t.Run(tc.query, func(t *testing.T) {
138 a := testApp(&fakeSource{})
139 source := &fakeWatched{}
140 a.jellystat = source
141 response := httptest.NewRecorder()
142 a.routes().ServeHTTP(response, httptest.NewRequest("GET", "/api/watched?"+tc.query, nil))
143 if response.Code != 200 || !strings.Contains(response.Body.String(), "Example — 2 users") {
144 t.Fatalf("response: %d %s", response.Code, response.Body.String())
145 }
146 if source.start.Format(time.RFC3339) != tc.start || source.end.Format(time.RFC3339) != tc.end {
147 t.Fatalf("range: %v to %v", source.start, source.end)
148 }
149 if response.Header().Get("Cache-Control") != "no-store" {
150 t.Fatal("missing no-store")
151 }
152 })
153 }
154}
155
156func TestWatchedErrors(t *testing.T) {
157 for _, tc := range []struct {
158 query string
159 source watchedSource
160 status int
161 }{
162 {"period=current-week", nil, 503},
163 {"period=week&offset=0", nil, 400},
164 {"period=invalid", nil, 400},
165 {"period=current-week", &fakeWatched{err: fmt.Errorf("upstream failure")}, 502},
166 } {
167 a := testApp(&fakeSource{})
168 a.jellystat = tc.source
169 response := httptest.NewRecorder()
170 a.routes().ServeHTTP(response, httptest.NewRequest("GET", "/api/watched?"+tc.query, nil))
171 if response.Code != tc.status {
172 t.Fatalf("status = %d, want %d", response.Code, tc.status)
173 }
174 }
175}
176
177func TestWatchedButtonEscapesTranslation(t *testing.T) {
178 a := testApp(&fakeSource{})
179 a.labels.WatchedButton = "<Watch & copy>"
180 response := httptest.NewRecorder()
181 a.routes().ServeHTTP(response, httptest.NewRequest("GET", "/", nil))
182 if !strings.Contains(response.Body.String(), "<Watch & copy>") {
183 t.Fatal("button translation not escaped")
184 }
185}