handler_test.go
⎇
Raw
1package server
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "regexp"
8 "strconv"
9 "strings"
10 "testing"
11)
12
13// getWith issues a GET carrying the given cookies (so a flash set by a prior
14// POST is delivered to the page that renders it).
15func getWith(router http.Handler, path string, cookies []*http.Cookie) *httptest.ResponseRecorder {
16 req := httptest.NewRequest("GET", path, nil)
17 for _, c := range cookies {
18 req.AddCookie(c)
19 }
20 w := httptest.NewRecorder()
21 router.ServeHTTP(w, req)
22 return w
23}
24
25func flash(w *httptest.ResponseRecorder) (kind, message string, ok bool) {
26 for _, c := range w.Result().Cookies() {
27 if c.Name == "flash" && c.Value != "" {
28 if v, err := url.QueryUnescape(c.Value); err == nil {
29 if k, m, found := strings.Cut(v, "|"); found {
30 return k, m, true
31 }
32 }
33 }
34 }
35 return "", "", false
36}
37
38var presetIDRe = regexp.MustCompile(`/settings/presets/(\d+)"`)
39
40// newestPresetID returns the highest preset id referenced on the settings page,
41// which (ids autoincrement) is the most recently created preset.
42func newestPresetID(t *testing.T, router http.Handler) string {
43 t.Helper()
44 body := getWith(router, "/settings", nil).Body.String()
45 max := int64(-1)
46 for _, m := range presetIDRe.FindAllStringSubmatch(body, -1) {
47 if id, err := strconv.ParseInt(m[1], 10, 64); err == nil && id > max {
48 max = id
49 }
50 }
51 if max < 0 {
52 t.Fatal("no preset id found on settings page")
53 }
54 return strconv.FormatInt(max, 10)
55}
56
57func validPresetForm(name string) url.Values {
58 return url.Values{
59 "name": {name},
60 "format_mode": {"default"},
61 }
62}
63
64// A created preset shows a success flash exactly once, then the message is gone.
65func TestPresetCreateFlashShownOnce(t *testing.T) {
66 srv, _, cleanup := setupTestServer(t)
67 defer cleanup()
68 router := srv.Router()
69
70 w := postForm(router, "/settings/presets", validPresetForm("Archival"))
71 if w.Code != http.StatusSeeOther {
72 t.Fatalf("create preset: expected 303, got %d", w.Code)
73 }
74 kind, msg, ok := flash(w)
75 if !ok || kind != "success" || !strings.Contains(msg, "created") {
76 t.Fatalf("expected success flash, got kind=%q msg=%q ok=%v", kind, msg, ok)
77 }
78
79 // The redirected page carries the flash cookie and shows the message.
80 page := getWith(router, "/settings", w.Result().Cookies())
81 if !strings.Contains(page.Body.String(), msg) {
82 t.Error("settings page did not render the flash message")
83 }
84 if !strings.Contains(page.Body.String(), "Archival") {
85 t.Error("created preset not shown on settings page")
86 }
87 // ...and it clears the cookie, so a subsequent load is clean.
88 if _, _, stillSet := flash(page); stillSet {
89 t.Error("flash cookie should have been cleared after being shown")
90 }
91 clean := getWith(router, "/settings", page.Result().Cookies())
92 if strings.Contains(clean.Body.String(), msg) {
93 t.Error("flash message should not persist across loads")
94 }
95}
96
97func TestPresetUpdateAndDelete(t *testing.T) {
98 srv, _, cleanup := setupTestServer(t)
99 defer cleanup()
100 router := srv.Router()
101
102 if w := postForm(router, "/settings/presets", validPresetForm("Original")); w.Code != http.StatusSeeOther {
103 t.Fatalf("create: got %d", w.Code)
104 }
105 id := newestPresetID(t, router)
106
107 if w := postForm(router, "/settings/presets/"+id, validPresetForm("Renamed")); w.Code != http.StatusSeeOther {
108 t.Fatalf("update: got %d", w.Code)
109 }
110 if body := getWith(router, "/settings", nil).Body.String(); !strings.Contains(body, "Renamed") {
111 t.Error("updated preset name not reflected")
112 }
113
114 w := postForm(router, "/settings/presets/"+id+"/delete", nil)
115 if w.Code != http.StatusSeeOther {
116 t.Fatalf("delete: got %d", w.Code)
117 }
118 if k, _, ok := flash(w); !ok || k != "success" {
119 t.Error("expected success flash on delete")
120 }
121 if body := getWith(router, "/settings", nil).Body.String(); strings.Contains(body, "Renamed") {
122 t.Error("deleted preset still present")
123 }
124}
125
126// The create and edit preset forms share one partial; both must render.
127func TestSettingsRendersSharedPresetForm(t *testing.T) {
128 srv, _, cleanup := setupTestServer(t)
129 defer cleanup()
130 router := srv.Router()
131
132 if w := postForm(router, "/settings/presets", validPresetForm("Editable")); w.Code != http.StatusSeeOther {
133 t.Fatalf("create: got %d", w.Code)
134 }
135 body := getWith(router, "/settings", nil).Body.String()
136 for _, want := range []string{"Create Preset", "Update Preset", `name="format_mode"`} {
137 if !strings.Contains(body, want) {
138 t.Errorf("settings page missing %q", want)
139 }
140 }
141}
142
143func TestCreateDownloadRequiresURL(t *testing.T) {
144 srv, _, cleanup := setupTestServer(t)
145 defer cleanup()
146 router := srv.Router()
147
148 w := postForm(router, "/download", url.Values{"url": {""}})
149 if w.Code != http.StatusSeeOther {
150 t.Fatalf("expected 303 redirect, got %d", w.Code)
151 }
152 if loc := w.Header().Get("Location"); loc != "/download" {
153 t.Errorf("expected redirect to /download, got %q", loc)
154 }
155 if k, _, ok := flash(w); !ok || k != "error" {
156 t.Error("expected an error flash for the missing URL")
157 }
158}
159
160func TestCreateDownloadQueues(t *testing.T) {
161 srv, _, cleanup := setupTestServer(t)
162 defer cleanup()
163 router := srv.Router()
164
165 w := postForm(router, "/download", url.Values{"url": {"https://example.com/clip"}})
166 if w.Code != http.StatusSeeOther {
167 t.Fatalf("expected 303, got %d", w.Code)
168 }
169 if loc := w.Header().Get("Location"); loc != "/queue" {
170 t.Errorf("expected redirect to /queue, got %q", loc)
171 }
172 if k, _, ok := flash(w); !ok || k != "success" {
173 t.Error("expected a success flash")
174 }
175 if body := getWith(router, "/queue", nil).Body.String(); !strings.Contains(body, "example.com/clip") {
176 t.Error("queued download not shown on the queue page")
177 }
178}
179
180func TestUpdateSettingsFlash(t *testing.T) {
181 srv, _, cleanup := setupTestServer(t)
182 defer cleanup()
183 router := srv.Router()
184
185 w := postForm(router, "/settings", url.Values{
186 "refresh_interval": {"7"},
187 "auto_refresh_library": {"1"},
188 "auto_refresh_downloads": {"1"},
189 })
190 if w.Code != http.StatusSeeOther {
191 t.Fatalf("expected 303, got %d", w.Code)
192 }
193 k, msg, ok := flash(w)
194 if !ok || k != "success" || !strings.Contains(msg, "saved") {
195 t.Errorf("expected success flash, got kind=%q msg=%q ok=%v", k, msg, ok)
196 }
197 // The saved interval is reflected in the form.
198 if body := getWith(router, "/settings", nil).Body.String(); !strings.Contains(body, `value="7"`) {
199 t.Error("saved refresh interval not reflected in settings form")
200 }
201}
202
203// A subscription's edit form is rendered by the shared partial with its values
204// populated.
205func TestSubscriptionEditFormRenders(t *testing.T) {
206 srv, _, cleanup := setupTestServer(t)
207 defer cleanup()
208 router := srv.Router()
209
210 if w := postForm(router, "/subscriptions", url.Values{
211 "name": {"News"},
212 "url": {"https://example.com/news"},
213 "output_dir": {"subscriptions/news"},
214 "refresh_mode": {"skip"},
215 "schedule_kind": {"weekly"},
216 }); w.Code != http.StatusSeeOther {
217 t.Fatalf("create sub: got %d", w.Code)
218 }
219
220 body := getWith(router, "/subscriptions", nil).Body.String()
221 for _, want := range []string{
222 "Update Subscription", // edit form present
223 `value="https://example.com/news"`, // URL populated in edit form
224 `value="subscriptions/news"`, // folder populated
225 } {
226 if !strings.Contains(body, want) {
227 t.Errorf("subscriptions page missing %q", want)
228 }
229 }
230}
231
232func findCookie(w *httptest.ResponseRecorder, name string) (string, bool) {
233 for _, c := range w.Result().Cookies() {
234 if c.Name == name {
235 return c.Value, true
236 }
237 }
238 return "", false
239}
240
241// An explicit ?sort= is remembered, and a later request without one restores it.
242// The pages auto-refresh, so a sort order that did not persist would be reset
243// every few seconds.
244func TestSortOrderPersistsInCookie(t *testing.T) {
245 srv, _, cleanup := setupTestServer(t)
246 defer cleanup()
247 router := srv.Router()
248
249 cases := []struct {
250 name, path, cookie, sort, selected string
251 }{
252 {"queue", "/queue", "queue_sort", "status", `<option value="status" selected>`},
253 {"library", "/library", "library_sort", "title", `<option value="title" selected>`},
254 }
255
256 for _, tc := range cases {
257 t.Run(tc.name, func(t *testing.T) {
258 w := getWith(router, tc.path+"?sort="+tc.sort, nil)
259 if w.Code != http.StatusOK {
260 t.Fatalf("GET %s: %d", tc.path, w.Code)
261 }
262 got, ok := findCookie(w, tc.cookie)
263 if !ok || got != tc.sort {
264 t.Fatalf("cookie %s = %q (set=%v), want %q", tc.cookie, got, ok, tc.sort)
265 }
266
267 // No ?sort= this time: the remembered order must still apply.
268 page := getWith(router, tc.path, w.Result().Cookies())
269 if !strings.Contains(page.Body.String(), tc.selected) {
270 t.Errorf("remembered sort not applied: %s not found in the rendered page", tc.selected)
271 }
272 // Nothing to remember, so the page must not rewrite the cookie.
273 if _, rewritten := findCookie(page, tc.cookie); rewritten {
274 t.Errorf("cookie %s was rewritten on a request without ?sort=", tc.cookie)
275 }
276 })
277 }
278}
279
280// The cancelled status has its own queue filter, so a stopped download stays
281// reachable instead of disappearing from every view.
282func TestQueueFilterOffersCancelled(t *testing.T) {
283 srv, _, cleanup := setupTestServer(t)
284 defer cleanup()
285
286 body := getWith(srv.Router(), "/queue", nil).Body.String()
287 if !strings.Contains(body, `<option value="cancelled"`) {
288 t.Error("queue filter has no cancelled option")
289 }
290
291 w := getWith(srv.Router(), "/queue?status=cancelled", nil)
292 if w.Code != http.StatusOK {
293 t.Errorf("filtering by cancelled: %d, want 200", w.Code)
294 }
295 if !strings.Contains(w.Body.String(), `<option value="cancelled" selected>`) {
296 t.Error("cancelled filter not marked selected after being applied")
297 }
298}
299