download_queue_test.go
⎇
Raw
1package repository
2
3import (
4 "context"
5 "database/sql"
6 "testing"
7
8 "vidarchive/internal/models"
9)
10
11func queueDownload(t *testing.T, repo *DownloadRepository, url, status string, subID *int64) *models.Download {
12 t.Helper()
13 d := &models.Download{URL: url, Status: status}
14 if subID != nil {
15 d.SubscriptionID = sql.NullInt64{Int64: *subID, Valid: true}
16 }
17 if err := repo.Create(d); err != nil {
18 t.Fatalf("create %s: %v", url, err)
19 }
20 return d
21}
22
23// The queue checker asks for the oldest queued rows up to the worker buffer
24// size. Anything else, in any other status, must stay out of the result.
25func TestGetQueuedOldestFirstWithinLimit(t *testing.T) {
26 db := setupTestDB(t)
27 defer db.Close()
28 repo := NewDownloadRepository(db)
29
30 first := queueDownload(t, repo, "first", "queued", nil)
31 second := queueDownload(t, repo, "second", "queued", nil)
32 queueDownload(t, repo, "third", "queued", nil)
33 queueDownload(t, repo, "running", "downloading", nil)
34 queueDownload(t, repo, "done", "completed", nil)
35
36 got, err := repo.GetQueued(2)
37 if err != nil {
38 t.Fatalf("get queued: %v", err)
39 }
40 if len(got) != 2 {
41 t.Fatalf("got %d rows, want 2 (the limit)", len(got))
42 }
43 if got[0].ID != first.ID || got[1].ID != second.ID {
44 t.Errorf("got ids %d,%d, want the two oldest %d,%d", got[0].ID, got[1].ID, first.ID, second.ID)
45 }
46}
47
48// The queue page filters by status; "" and "all" mean no filter.
49func TestGetAllStatusFilter(t *testing.T) {
50 db := setupTestDB(t)
51 defer db.Close()
52 repo := NewDownloadRepository(db)
53
54 queueDownload(t, repo, "a", "queued", nil)
55 queueDownload(t, repo, "b", "error", nil)
56 queueDownload(t, repo, "c", "completed", nil)
57
58 for _, tc := range []struct {
59 status string
60 want int
61 }{
62 {"", 3},
63 {"all", 3},
64 {"error", 1},
65 {"cancelled", 0},
66 } {
67 got, err := repo.GetAll(tc.status, "date")
68 if err != nil {
69 t.Fatalf("get all %q: %v", tc.status, err)
70 }
71 if len(got) != tc.want {
72 t.Errorf("status %q returned %d rows, want %d", tc.status, len(got), tc.want)
73 }
74 }
75
76 // Sorting by status must not drop rows.
77 got, err := repo.GetAll("", "status")
78 if err != nil {
79 t.Fatalf("get all sorted by status: %v", err)
80 }
81 if len(got) != 3 {
82 t.Errorf("status sort returned %d rows, want 3", len(got))
83 }
84}
85
86// The health endpoint only needs to know the database answers at all.
87func TestPing(t *testing.T) {
88 db := setupTestDB(t)
89 defer db.Close()
90 repo := NewDownloadRepository(db)
91
92 if err := repo.Ping(context.Background()); err != nil {
93 t.Fatalf("ping: %v", err)
94 }
95
96 db.Close()
97 if err := repo.Ping(context.Background()); err == nil {
98 t.Error("ping on a closed database should fail")
99 }
100}
101
102// Restart recovery: the stalled downloads are found by status and moved back to
103// queued in one statement.
104func TestIDsByStatusAndUpdateStatusWhere(t *testing.T) {
105 db := setupTestDB(t)
106 defer db.Close()
107 repo := NewDownloadRepository(db)
108
109 stalled := queueDownload(t, repo, "stalled", "downloading", nil)
110 queueDownload(t, repo, "done", "completed", nil)
111
112 ids, err := repo.IDsByStatus("downloading")
113 if err != nil {
114 t.Fatalf("ids by status: %v", err)
115 }
116 if len(ids) != 1 || ids[0] != stalled.ID {
117 t.Fatalf("ids = %v, want [%d]", ids, stalled.ID)
118 }
119
120 if err := repo.UpdateStatusWhere("downloading", "queued"); err != nil {
121 t.Fatalf("update status: %v", err)
122 }
123 if got, _ := repo.GetByID(stalled.ID); got.Status != "queued" {
124 t.Errorf("status = %q, want queued", got.Status)
125 }
126 if left, _ := repo.IDsByStatus("downloading"); len(left) != 0 {
127 t.Errorf("%d rows still downloading", len(left))
128 }
129}
130
131// Logs are flushed in slices as yt-dlp produces them, so appends must
132// accumulate rather than replace — including the first one, onto a NULL column.
133func TestAppendLogsAccumulates(t *testing.T) {
134 db := setupTestDB(t)
135 defer db.Close()
136 repo := NewDownloadRepository(db)
137
138 d := queueDownload(t, repo, "a", "downloading", nil)
139 for _, chunk := range []string{"first\n", "second\n"} {
140 if err := repo.AppendLogs(d.ID, chunk); err != nil {
141 t.Fatalf("append %q: %v", chunk, err)
142 }
143 }
144
145 got, err := repo.GetByID(d.ID)
146 if err != nil {
147 t.Fatal(err)
148 }
149 if got.Logs.String != "first\nsecond\n" {
150 t.Errorf("logs = %q, want both chunks in order", got.Logs.String)
151 }
152}
153
154func TestMarkErrorAndCompleted(t *testing.T) {
155 db := setupTestDB(t)
156 defer db.Close()
157 repo := NewDownloadRepository(db)
158
159 failed := queueDownload(t, repo, "bad", "downloading", nil)
160 if err := repo.MarkError(failed.ID, "yt-dlp exploded"); err != nil {
161 t.Fatalf("mark error: %v", err)
162 }
163 got, _ := repo.GetByID(failed.ID)
164 if got.Status != "error" || got.ErrorMessage.String != "yt-dlp exploded" {
165 t.Errorf("got status=%q message=%q", got.Status, got.ErrorMessage.String)
166 }
167
168 ok := queueDownload(t, repo, "good", "downloading", nil)
169 if err := repo.MarkCompleted(ok.ID, "cancelled"); err != nil {
170 t.Fatalf("mark completed: %v", err)
171 }
172 got, _ = repo.GetByID(ok.ID)
173 if got.Status != "cancelled" || !got.CompletedAt.Valid {
174 t.Errorf("got status=%q completed_at valid=%v", got.Status, got.CompletedAt.Valid)
175 }
176}
177
178// The scheduler and the manual run button both use this to avoid stacking a
179// second run on a subscription that is still working.
180func TestHasActiveForSubscription(t *testing.T) {
181 db := setupTestDB(t)
182 defer db.Close()
183 subRepo := NewSubscriptionRepository(db)
184 repo := NewDownloadRepository(db)
185
186 sub := &models.Subscription{
187 Name: "s", URL: "u", Enabled: true, RefreshMode: "overwrite",
188 ScheduleKind: "daily", CronExpr: "0 3 * * *", OutputDir: "dir",
189 }
190 if err := subRepo.Create(sub); err != nil {
191 t.Fatal(err)
192 }
193
194 if active, err := repo.HasActiveForSubscription(sub.ID); err != nil || active {
195 t.Fatalf("active = %v (err %v), want false with no downloads", active, err)
196 }
197
198 for _, status := range []string{"queued", "downloading"} {
199 d := queueDownload(t, repo, "u", status, &sub.ID)
200 active, err := repo.HasActiveForSubscription(sub.ID)
201 if err != nil {
202 t.Fatal(err)
203 }
204 if !active {
205 t.Errorf("status %q should count as active", status)
206 }
207 if err := repo.Delete(d.ID); err != nil {
208 t.Fatal(err)
209 }
210 }
211
212 // A finished run no longer blocks the next one.
213 queueDownload(t, repo, "u", "completed", &sub.ID)
214 if active, _ := repo.HasActiveForSubscription(sub.ID); active {
215 t.Error("a completed run must not count as active")
216 }
217}
218
219func TestDeleteAllClearsQueue(t *testing.T) {
220 db := setupTestDB(t)
221 defer db.Close()
222 repo := NewDownloadRepository(db)
223
224 queueDownload(t, repo, "a", "queued", nil)
225 queueDownload(t, repo, "b", "completed", nil)
226
227 if err := repo.DeleteAll(); err != nil {
228 t.Fatalf("delete all: %v", err)
229 }
230 got, err := repo.GetAll("", "date")
231 if err != nil {
232 t.Fatal(err)
233 }
234 if len(got) != 0 {
235 t.Errorf("%d rows left after DeleteAll", len(got))
236 }
237}
238