arche / internal/archesrv/db_test.go

commit 154431fd
  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}