queue_test.go
⎇
Raw
1package server
2
3import (
4 "database/sql"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strconv"
9 "strings"
10 "testing"
11)
12
13// insertDownload writes a queue row in a chosen status, which is the only way to
14// reach the finished states an HTTP test can't produce on its own.
15func insertDownload(t *testing.T, db *sql.DB, status, errMsg string) int64 {
16 t.Helper()
17 res, err := db.Exec(
18 `INSERT INTO downloads (url, status, error_message, format_override, custom_flags)
19 VALUES (?, ?, ?, '', '')`,
20 "https://example.com/"+status, status, sql.NullString{String: errMsg, Valid: errMsg != ""},
21 )
22 if err != nil {
23 t.Fatalf("insert %s download: %v", status, err)
24 }
25 id, err := res.LastInsertId()
26 if err != nil {
27 t.Fatalf("insert id: %v", err)
28 }
29 return id
30}
31
32func downloadStatus(t *testing.T, db *sql.DB, id int64) (string, sql.NullString) {
33 t.Helper()
34 var status string
35 var errMsg sql.NullString
36 err := db.QueryRow(`SELECT status, error_message FROM downloads WHERE id = ?`, id).Scan(&status, &errMsg)
37 if err != nil {
38 t.Fatalf("read download %d: %v", id, err)
39 }
40 return status, errMsg
41}
42
43func TestRetryRequeuesFailedDownload(t *testing.T) {
44 srv, _, db, cleanup := setupTestServerDB(t)
45 defer cleanup()
46 router := srv.Router()
47
48 failed := insertDownload(t, db, "error", "boom")
49 w := postForm(router, "/queue/"+strconv.FormatInt(failed, 10)+"/retry", nil)
50 if w.Code != http.StatusSeeOther {
51 t.Fatalf("retry = %d, want 303", w.Code)
52 }
53 if k, _, ok := flash(w); !ok || k != "success" {
54 t.Error("retry did not flash success")
55 }
56
57 // A worker may claim the re-queued row on its next tick, so accept either
58 // state; what matters is that it left the terminal error state.
59 status, errMsg := downloadStatus(t, db, failed)
60 if status != "queued" && status != "downloading" {
61 t.Errorf("status after retry = %q, want queued", status)
62 }
63 if errMsg.Valid {
64 t.Errorf("error message %q survived the retry", errMsg.String)
65 }
66
67 // A completed download is not retryable: re-running it would duplicate work.
68 done := insertDownload(t, db, "completed", "")
69 w = postForm(router, "/queue/"+strconv.FormatInt(done, 10)+"/retry", nil)
70 if k, _, ok := flash(w); !ok || k != "error" {
71 t.Error("retrying a completed download did not flash an error")
72 }
73 if status, _ := downloadStatus(t, db, done); status != "completed" {
74 t.Errorf("completed download moved to %q", status)
75 }
76}
77
78func TestCancelKeepsTheRow(t *testing.T) {
79 srv, _, db, cleanup := setupTestServerDB(t)
80 defer cleanup()
81
82 id := insertDownload(t, db, "queued", "")
83 path := "/queue/" + strconv.FormatInt(id, 10)
84
85 detail := getWith(srv.Router(), path, nil).Body.String()
86 if !strings.Contains(detail, path+"/cancel") {
87 t.Error("detail page of a queued download offers no cancel button")
88 }
89
90 w := postForm(srv.Router(), path+"/cancel", nil)
91 if w.Code != http.StatusSeeOther {
92 t.Fatalf("cancel = %d, want 303", w.Code)
93 }
94 if status, _ := downloadStatus(t, db, id); status != "cancelled" {
95 t.Errorf("status after cancel = %q, want cancelled", status)
96 }
97
98 // The row survives the cancel, and now offers a retry instead.
99 detail = getWith(srv.Router(), path, nil).Body.String()
100 if !strings.Contains(detail, path+"/retry") {
101 t.Error("cancelled download offers no retry button")
102 }
103}
104
105func TestClearByStatus(t *testing.T) {
106 srv, _, db, cleanup := setupTestServerDB(t)
107 defer cleanup()
108 router := srv.Router()
109
110 done := insertDownload(t, db, "completed", "")
111 failed := insertDownload(t, db, "error", "boom")
112
113 postForm(router, "/queue/clear", url.Values{"status": {"completed"}})
114 if err := db.QueryRow(`SELECT status FROM downloads WHERE id = ?`, done).Scan(new(string)); err != sql.ErrNoRows {
115 t.Errorf("completed download survived the clear: %v", err)
116 }
117 if status, _ := downloadStatus(t, db, failed); status != "error" {
118 t.Errorf("failed download was cleared too, status = %q", status)
119 }
120
121 postForm(router, "/queue/clear", url.Values{"status": {"error"}})
122 if err := db.QueryRow(`SELECT status FROM downloads WHERE id = ?`, failed).Scan(new(string)); err != sql.ErrNoRows {
123 t.Errorf("failed download survived the clear: %v", err)
124 }
125
126 // Running downloads are not clearable, so a hand-made form can't kill them.
127 running := insertDownload(t, db, "downloading", "")
128 w := postForm(router, "/queue/clear", url.Values{"status": {"downloading"}})
129 if w.Code != http.StatusBadRequest {
130 t.Errorf("clearing downloading = %d, want 400", w.Code)
131 }
132 if status, _ := downloadStatus(t, db, running); status != "downloading" {
133 t.Errorf("running download was cleared, status = %q", status)
134 }
135}
136
137func TestQueuePagination(t *testing.T) {
138 srv, _, db, cleanup := setupTestServerDB(t)
139 defer cleanup()
140 router := srv.Router()
141
142 // Completed rows, so no worker picks them up mid-test.
143 for i := 0; i < 55; i++ {
144 insertDownload(t, db, "completed", "")
145 }
146
147 first := getWith(router, "/queue?status=completed", nil).Body.String()
148 if got := strings.Count(first, `class="queue-item`); got != 50 {
149 t.Errorf("page 1 rendered %d items, want 50", got)
150 }
151 if !strings.Contains(first, "page=2") {
152 t.Error("page 1 has no next link")
153 }
154
155 second := getWith(router, "/queue?status=completed&page=2", nil).Body.String()
156 if got := strings.Count(second, `class="queue-item`); got != 5 {
157 t.Errorf("page 2 rendered %d items, want 5", got)
158 }
159 if !strings.Contains(second, "page=1") {
160 t.Error("page 2 has no previous link")
161 }
162 // The link has to carry the filter, otherwise paging resets it.
163 if !strings.Contains(second, "status=completed&amp;page=1") {
164 t.Error("previous link dropped the status filter")
165 }
166 if strings.Contains(second, "page=3") {
167 t.Error("page 2 offers a next page that does not exist")
168 }
169}
170
171func TestLibraryPagination(t *testing.T) {
172 srv, cfg, cleanup := setupTestServer(t)
173 defer cleanup()
174
175 for i := 0; i < 52; i++ {
176 name := "item-" + strconv.Itoa(i)
177 createItem(t, cfg.LibraryDir, name, name, map[string]string{"video.mp4": "dummy"})
178 }
179
180 first := getWith(srv.Router(), "/library", nil).Body.String()
181 if got := strings.Count(first, `class="media-item`); got != 50 {
182 t.Errorf("page 1 rendered %d items, want 50", got)
183 }
184
185 second := getWith(srv.Router(), "/library?page=2", nil).Body.String()
186 if got := strings.Count(second, `class="media-item`); got != 2 {
187 t.Errorf("page 2 rendered %d items, want 2", got)
188 }
189
190 // A page past the end is empty, not a crash or a wrapped-around first page.
191 last := getWith(srv.Router(), "/library?page=9", nil)
192 if last.Code != http.StatusOK {
193 t.Fatalf("page 9 = %d, want 200", last.Code)
194 }
195 if got := strings.Count(last.Body.String(), `class="media-item`); got != 0 {
196 t.Errorf("page 9 rendered %d items, want 0", got)
197 }
198}
199
200// postFrom is postForm with a Referer, which is how the queue actions know
201// which page to send the user back to.
202func postFrom(router http.Handler, path, referer string, form url.Values) *httptest.ResponseRecorder {
203 req := httptest.NewRequest("POST", path, strings.NewReader(form.Encode()))
204 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
205 req.Header.Set("Referer", "http://example.test"+referer)
206 w := httptest.NewRecorder()
207 router.ServeHTTP(w, req)
208 return w
209}
210
211func TestQueueActionsKeepFilterAndPage(t *testing.T) {
212 srv, _, db, cleanup := setupTestServerDB(t)
213 defer cleanup()
214 router := srv.Router()
215
216 from := "/queue?status=error&page=2"
217 id := insertDownload(t, db, "error", "boom")
218 w := postFrom(router, "/queue/"+strconv.FormatInt(id, 10)+"/retry", from, nil)
219 if got := w.Header().Get("Location"); got != from {
220 t.Errorf("retry redirected to %q, wanted back to %q", got, from)
221 }
222
223 // An off-site Referer must not be honoured as a redirect target.
224 req := httptest.NewRequest("POST", "/queue/clear", nil)
225 req.Header.Set("Referer", "https://evil.example/queue?x=1")
226 rec := httptest.NewRecorder()
227 router.ServeHTTP(rec, req)
228 if got := rec.Header().Get("Location"); got != "/queue?x=1" {
229 t.Errorf("clear redirected to %q, want the local path only", got)
230 }
231}
232
233func TestPaginationRejectsOverflowingPage(t *testing.T) {
234 srv, cfg, db, cleanup := setupTestServerDB(t)
235 defer cleanup()
236 router := srv.Router()
237
238 createItem(t, cfg.LibraryDir, "item", "Item", map[string]string{"video.mp4": "dummy"})
239 insertDownload(t, db, "completed", "")
240
241 // A page number large enough to overflow page*pageSize used to slice the
242 // library listing with a negative index, which panicked into a 500.
243 for _, page := range []string{"9223372036854775807", "184467440737095518", "99999999999999999999", "-1", "abc"} {
244 for _, path := range []string{"/library?page=", "/queue?page="} {
245 if got := getWith(router, path+page, nil); got.Code != http.StatusOK {
246 t.Errorf("GET %s%s = %d, want 200", path, page, got.Code)
247 }
248 }
249 }
250}
251