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