1package archesrv
2
3import (
4 "path/filepath"
5 "testing"
6
7 _ "github.com/mattn/go-sqlite3"
8)
9
10func newTestDB(t *testing.T) *DB {
11 t.Helper()
12 db, err := openDB(filepath.Join(t.TempDir(), "test.db"))
13 if err != nil {
14 t.Fatalf("openDB: %v", err)
15 }
16 t.Cleanup(func() { db.Close() })
17 return db
18}
19
20func TestDB_CreateUser_Roundtrip(t *testing.T) {
21 db := newTestDB(t)
22 u, err := db.CreateUser("alice", "password123", false)
23 if err != nil {
24 t.Fatalf("CreateUser: %v", err)
25 }
26 if u.Username != "alice" {
27 t.Errorf("Username: want alice, got %q", u.Username)
28 }
29 if u.IsAdmin {
30 t.Error("should not be admin")
31 }
32
33 got, _, err := db.GetUserByName("alice")
34 if err != nil {
35 t.Fatalf("GetUserByName: %v", err)
36 }
37 if got.ID != u.ID {
38 t.Errorf("ID mismatch: want %d, got %d", u.ID, got.ID)
39 }
40}
41
42func TestDB_CreateUser_DuplicateIsError(t *testing.T) {
43 db := newTestDB(t)
44 if _, err := db.CreateUser("alice", "pass", false); err != nil {
45 t.Fatalf("first CreateUser: %v", err)
46 }
47 if _, err := db.CreateUser("alice", "pass2", false); err == nil {
48 t.Error("expected error for duplicate username, got nil")
49 }
50}
51
52func TestDB_HasAnyUser(t *testing.T) {
53 db := newTestDB(t)
54 has, err := db.HasAnyUser()
55 if err != nil {
56 t.Fatalf("HasAnyUser: %v", err)
57 }
58 if has {
59 t.Error("fresh DB should have no users")
60 }
61
62 db.CreateUser("alice", "pass", false) //nolint:errcheck
63 has, err = db.HasAnyUser()
64 if err != nil {
65 t.Fatalf("HasAnyUser after create: %v", err)
66 }
67 if !has {
68 t.Error("should have a user after CreateUser")
69 }
70}
71
72func TestDB_DeleteUser(t *testing.T) {
73 db := newTestDB(t)
74 u, _ := db.CreateUser("alice", "pass", false)
75
76 if err := db.DeleteUser(u.ID); err != nil {
77 t.Fatalf("DeleteUser: %v", err)
78 }
79
80 got, _, err := db.GetUserByName("alice")
81 if err != nil {
82 t.Fatalf("GetUserByName after delete: %v", err)
83 }
84 if got != nil {
85 t.Error("user should be nil after delete")
86 }
87}
88
89func TestDB_Password_HashAndCheck(t *testing.T) {
90 hash, err := hashPassword("secret")
91 if err != nil {
92 t.Fatalf("hashPassword: %v", err)
93 }
94 if !checkPassword(hash, "secret") {
95 t.Error("checkPassword: correct password should return true")
96 }
97 if checkPassword(hash, "wrongpassword") {
98 t.Error("checkPassword: wrong password should return false")
99 }
100}
101
102func TestDB_Session_CreateAndLookup(t *testing.T) {
103 db := newTestDB(t)
104 u, _ := db.CreateUser("alice", "pass", false)
105
106 tok, err := db.CreateSession(u.ID)
107 if err != nil {
108 t.Fatalf("CreateSession: %v", err)
109 }
110 if tok == "" {
111 t.Fatal("session token is empty")
112 }
113
114 got, err := db.GetSessionUser(tok)
115 if err != nil {
116 t.Fatalf("GetSessionUser: %v", err)
117 }
118 if got == nil || got.ID != u.ID {
119 t.Errorf("GetSessionUser: want user %d, got %v", u.ID, got)
120 }
121}
122
123func TestDB_Session_DeleteInvalidates(t *testing.T) {
124 db := newTestDB(t)
125 u, _ := db.CreateUser("alice", "pass", false)
126 tok, _ := db.CreateSession(u.ID)
127
128 if err := db.DeleteSession(tok); err != nil {
129 t.Fatalf("DeleteSession: %v", err)
130 }
131
132 got, _ := db.GetSessionUser(tok)
133 if got != nil {
134 t.Error("session should be gone after DeleteSession")
135 }
136}
137
138func TestDB_APIToken_CreateAndLookup(t *testing.T) {
139 db := newTestDB(t)
140 u, _ := db.CreateUser("alice", "pass", false)
141
142 tok, err := db.CreateAPIToken(u.ID, "laptop")
143 if err != nil {
144 t.Fatalf("CreateAPIToken: %v", err)
145 }
146
147 looked, err := db.lookupAPIToken(tok)
148 if err != nil {
149 t.Fatalf("lookupAPIToken: %v", err)
150 }
151 if looked == nil || looked.ID != u.ID {
152 t.Errorf("lookupAPIToken: want user %d, got %v", u.ID, looked)
153 }
154}
155
156func TestDB_APIToken_WrongTokenReturnsNil(t *testing.T) {
157 db := newTestDB(t)
158 u, _ := db.CreateUser("alice", "pass", false)
159 db.CreateAPIToken(u.ID, "test") //nolint:errcheck
160
161 looked, err := db.lookupAPIToken("completelyinvalidtoken")
162 if err != nil {
163 t.Fatalf("lookupAPIToken: %v", err)
164 }
165 if looked != nil {
166 t.Error("wrong token should return nil user")
167 }
168}
169
170func TestDB_APIToken_DeleteRemovesAccess(t *testing.T) {
171 db := newTestDB(t)
172 u, _ := db.CreateUser("alice", "pass", false)
173 tok, _ := db.CreateAPIToken(u.ID, "test")
174
175 tokens, _ := db.ListAPITokens(u.ID)
176 if len(tokens) != 1 {
177 t.Fatalf("want 1 token, got %d", len(tokens))
178 }
179
180 if err := db.DeleteAPIToken(tokens[0].ID, u.ID); err != nil {
181 t.Fatalf("DeleteAPIToken: %v", err)
182 }
183
184 looked, _ := db.lookupAPIToken(tok)
185 if looked != nil {
186 t.Error("token should be invalid after delete")
187 }
188}
189
190func TestDB_CreateRepo_Roundtrip(t *testing.T) {
191 db := newTestDB(t)
192 rec, err := db.CreateRepo("myrepo", "a repo", "private")
193 if err != nil {
194 t.Fatalf("CreateRepo: %v", err)
195 }
196 if rec.Name != "myrepo" {
197 t.Errorf("Name: want myrepo, got %q", rec.Name)
198 }
199 if rec.Visibility != "private" {
200 t.Errorf("Visibility: want private, got %q", rec.Visibility)
201 }
202
203 got, err := db.GetRepo("myrepo")
204 if err != nil || got == nil {
205 t.Fatalf("GetRepo: %v", err)
206 }
207 if got.ID != rec.ID {
208 t.Errorf("ID mismatch")
209 }
210}
211
212func TestDB_ListRepos(t *testing.T) {
213 db := newTestDB(t)
214 db.CreateRepo("alpha", "", "public") //nolint:errcheck
215 db.CreateRepo("beta", "", "private") //nolint:errcheck
216 db.CreateRepo("gamma", "", "private") //nolint:errcheck
217
218 repos, err := db.ListRepos()
219 if err != nil {
220 t.Fatalf("ListRepos: %v", err)
221 }
222 if len(repos) != 3 {
223 t.Errorf("want 3 repos, got %d", len(repos))
224 }
225}
226
227func TestDB_DeleteRepo(t *testing.T) {
228 db := newTestDB(t)
229 db.CreateRepo("gone", "", "private") //nolint:errcheck
230
231 if err := db.DeleteRepo("gone"); err != nil {
232 t.Fatalf("DeleteRepo: %v", err)
233 }
234
235 got, _ := db.GetRepo("gone")
236 if got != nil {
237 t.Error("repo should be nil after delete")
238 }
239}
240
241func TestDB_CanRead_Public(t *testing.T) {
242 db := newTestDB(t)
243 rec, _ := db.CreateRepo("pub", "", "public")
244 if !db.CanRead(rec, nil) {
245 t.Error("public repo should be readable by anonymous")
246 }
247}
248
249func TestDB_CanRead_PrivateAnonymous(t *testing.T) {
250 db := newTestDB(t)
251 rec, _ := db.CreateRepo("priv", "", "private")
252 if db.CanRead(rec, nil) {
253 t.Error("private repo should not be readable by anonymous")
254 }
255}
256
257func TestDB_CanRead_AdminAlwaysCanRead(t *testing.T) {
258 db := newTestDB(t)
259 rec, _ := db.CreateRepo("priv", "", "private")
260 admin, _ := db.CreateUser("admin", "pass", true)
261 if !db.CanRead(rec, admin) {
262 t.Error("admin should be able to read any repo")
263 }
264}
265
266func TestDB_CanWrite_AdminAlwaysCanWrite(t *testing.T) {
267 db := newTestDB(t)
268 rec, _ := db.CreateRepo("priv", "", "private")
269 admin, _ := db.CreateUser("admin", "pass", true)
270 if !db.CanWrite(rec, admin) {
271 t.Error("admin should be able to write any repo")
272 }
273}
274
275func TestDB_CanWrite_NormalUserCannotWithoutPermission(t *testing.T) {
276 db := newTestDB(t)
277 rec, _ := db.CreateRepo("priv", "", "private")
278 u, _ := db.CreateUser("alice", "pass", false)
279 if db.CanWrite(rec, u) {
280 t.Error("normal user without permission should not be able to write")
281 }
282}
283
284func TestDB_Permissions_SetAndCheck(t *testing.T) {
285 db := newTestDB(t)
286 rec, _ := db.CreateRepo("priv", "", "private")
287 u, _ := db.CreateUser("alice", "pass", false)
288
289 if err := db.SetPermission(rec.ID, u.ID, "write"); err != nil {
290 t.Fatalf("SetPermission: %v", err)
291 }
292
293 if !db.CanWrite(rec, u) {
294 t.Error("user with write permission should be able to write")
295 }
296 if !db.CanRead(rec, u) {
297 t.Error("user with write permission should be able to read")
298 }
299}
300
301func TestDB_Permissions_ReadOnly(t *testing.T) {
302 db := newTestDB(t)
303 rec, _ := db.CreateRepo("priv", "", "private")
304 u, _ := db.CreateUser("alice", "pass", false)
305
306 db.SetPermission(rec.ID, u.ID, "read") //nolint:errcheck
307
308 if !db.CanRead(rec, u) {
309 t.Error("user with read permission should be able to read")
310 }
311 if db.CanWrite(rec, u) {
312 t.Error("user with only read permission should not be able to write")
313 }
314}
315
316func TestDB_Permissions_RemoveRevokesAccess(t *testing.T) {
317 db := newTestDB(t)
318 rec, _ := db.CreateRepo("priv", "", "private")
319 u, _ := db.CreateUser("alice", "pass", false)
320
321 db.SetPermission(rec.ID, u.ID, "write") //nolint:errcheck
322 db.RemovePermission(rec.ID, u.ID) //nolint:errcheck
323
324 if db.CanWrite(rec, u) {
325 t.Error("user should lose write after RemovePermission")
326 }
327 if db.CanRead(rec, u) {
328 t.Error("user should lose read after RemovePermission")
329 }
330}
331
332func TestDB_Invite_CreateAndGet(t *testing.T) {
333 db := newTestDB(t)
334 admin, _ := db.CreateUser("admin", "pass", true)
335
336 inv, err := db.CreateInvite(admin.ID)
337 if err != nil {
338 t.Fatalf("CreateInvite: %v", err)
339 }
340 if inv.Token == "" {
341 t.Error("token should not be empty")
342 }
343
344 got, err := db.GetInvite(inv.Token)
345 if err != nil || got == nil {
346 t.Fatalf("GetInvite: %v", err)
347 }
348 if got.UsedBy != nil {
349 t.Error("invite should not be used yet")
350 }
351}
352
353func TestDB_Invite_UseOnce(t *testing.T) {
354 db := newTestDB(t)
355 admin, _ := db.CreateUser("admin", "pass", true)
356 inv, _ := db.CreateInvite(admin.ID)
357 user, _ := db.CreateUser("bob", "pass", false)
358
359 if err := db.UseInvite(inv.Token, user.ID); err != nil {
360 t.Fatalf("UseInvite: %v", err)
361 }
362
363 user2, _ := db.CreateUser("carol", "pass", false)
364 if err := db.UseInvite(inv.Token, user2.ID); err == nil {
365 t.Error("using an already-used invite should fail")
366 }
367}
368
369func TestDB_Invite_InvalidTokenReturnsNil(t *testing.T) {
370 db := newTestDB(t)
371 got, err := db.GetInvite("nosuchtoken")
372 if err != nil {
373 t.Fatalf("GetInvite: %v", err)
374 }
375 if got != nil {
376 t.Error("non-existent invite should return nil")
377 }
378}
379
380func TestDB_Invite_DeleteRemoves(t *testing.T) {
381 db := newTestDB(t)
382 admin, _ := db.CreateUser("admin", "pass", true)
383 inv, _ := db.CreateInvite(admin.ID)
384
385 if err := db.DeleteInvite(inv.ID, admin.ID); err != nil {
386 t.Fatalf("DeleteInvite: %v", err)
387 }
388
389 list, _ := db.ListInvites(admin.ID)
390 if len(list) != 0 {
391 t.Errorf("want 0 invites after delete, got %d", len(list))
392 }
393}
394
395func TestDB_Webhook_CreateAndList(t *testing.T) {
396 db := newTestDB(t)
397 repo, _ := db.CreateRepo("myrepo", "", "private")
398
399 wh, err := db.CreateWebhook(repo.ID, "http://example.com/hook", "secret", "push")
400 if err != nil {
401 t.Fatalf("CreateWebhook: %v", err)
402 }
403 if wh.URL != "http://example.com/hook" {
404 t.Errorf("URL mismatch: %q", wh.URL)
405 }
406
407 hooks, err := db.ListWebhooks(repo.ID)
408 if err != nil {
409 t.Fatalf("ListWebhooks: %v", err)
410 }
411 if len(hooks) != 1 {
412 t.Errorf("want 1 webhook, got %d", len(hooks))
413 }
414}
415
416func TestDB_Webhook_Delete(t *testing.T) {
417 db := newTestDB(t)
418 repo, _ := db.CreateRepo("myrepo", "", "private")
419 wh, _ := db.CreateWebhook(repo.ID, "http://example.com/hook", "", "push")
420
421 if err := db.DeleteWebhook(wh.ID); err != nil {
422 t.Fatalf("DeleteWebhook: %v", err)
423 }
424
425 hooks, _ := db.ListWebhooks(repo.ID)
426 if len(hooks) != 0 {
427 t.Errorf("want 0 webhooks after delete, got %d", len(hooks))
428 }
429}
430
431func TestDB_Webhook_HMAC(t *testing.T) {
432 payload := []byte(`{"event":"push"}`)
433 sig1 := computeHMAC("secret", payload)
434 sig2 := computeHMAC("secret", payload)
435 if sig1 != sig2 {
436 t.Error("HMAC should be deterministic")
437 }
438 sig3 := computeHMAC("differentsecret", payload)
439 if sig1 == sig3 {
440 t.Error("different secrets should produce different HMACs")
441 }
442 sigEmpty := computeHMAC("", payload)
443 if sigEmpty == sig1 {
444 t.Error("empty secret should produce different HMAC")
445 }
446}
447
448func TestDB_UpdateRepo(t *testing.T) {
449 db := newTestDB(t)
450 db.CreateRepo("myrepo", "old desc", "private") //nolint:errcheck
451
452 if err := db.UpdateRepo("myrepo", "new desc", "public"); err != nil {
453 t.Fatalf("UpdateRepo: %v", err)
454 }
455
456 rec, _ := db.GetRepo("myrepo")
457 if rec.Description != "new desc" {
458 t.Errorf("Description: want %q, got %q", "new desc", rec.Description)
459 }
460 if rec.Visibility != "public" {
461 t.Errorf("Visibility: want public, got %q", rec.Visibility)
462 }
463}