db_test.go
⎇
Raw
1package db
2
3import (
4 "context"
5 "path/filepath"
6 "testing"
7)
8
9// npmHash was produced by the `argon2` npm package for "testpass123". It pins
10// the on-disk hash format, so existing accounts keep working.
11const npmHash = "$argon2id$v=19$m=65536,t=3,p=4$tM0uDIIEKAgByj9/dHJJvg$yrk+8WacG/bYr2106JCWtxaSv7uPFiVyO0ikDR4o6Kk"
12
13func TestInitAdminAndPasswords(t *testing.T) {
14 ctx := context.Background()
15 d, err := Open(filepath.Join(t.TempDir(), "hearthforge.db"))
16 if err != nil {
17 t.Fatal(err)
18 }
19 defer d.Close()
20
21 created, err := d.InitAdmin(ctx, "hunter2")
22 if err != nil {
23 t.Fatal(err)
24 }
25 if !created {
26 t.Fatal("expected admin to be created")
27 }
28 // A second call must be a no-op.
29 created, err = d.InitAdmin(ctx, "other")
30 if err != nil {
31 t.Fatal(err)
32 }
33 if created {
34 t.Fatal("admin was created twice")
35 }
36
37 user, err := d.UserByName(ctx, AdminUsername)
38 if err != nil {
39 t.Fatal(err)
40 }
41 if user == nil || user.PasswordHash == nil {
42 t.Fatal("admin has no password hash")
43 }
44 ok, err := VerifyPassword(*user.PasswordHash, "hunter2")
45 if err != nil || !ok {
46 t.Fatalf("password did not round-trip: ok=%v err=%v", ok, err)
47 }
48 ok, err = VerifyPassword(*user.PasswordHash, "wrong")
49 if err != nil {
50 t.Fatal(err)
51 }
52 if ok {
53 t.Fatal("wrong password verified")
54 }
55
56 ok, err = VerifyPassword(npmHash, "testpass123")
57 if err != nil || !ok {
58 t.Fatalf("npm argon2 hash did not verify: ok=%v err=%v", ok, err)
59 }
60
61 missing, err := d.UserByName(ctx, "nobody")
62 if err != nil {
63 t.Fatal(err)
64 }
65 if missing != nil {
66 t.Fatal("expected nil for unknown user")
67 }
68}
69
70// TestQueryLayer exercises one write and one read per table group.
71func TestQueryLayer(t *testing.T) {
72 ctx := context.Background()
73 d, err := Open(filepath.Join(t.TempDir(), "h.db"))
74 if err != nil {
75 t.Fatal(err)
76 }
77 defer d.Close()
78 if _, err := d.InitAdmin(ctx, "pw"); err != nil {
79 t.Fatal(err)
80 }
81 u, _ := d.UserByName(ctx, "admin")
82 repo, err := d.CreateRepo(ctx, "demo", nil, false, "main", "2026-01-01T00:00:00.000Z")
83 if err != nil {
84 t.Fatal(err)
85 }
86 if err := d.CreateLabel(ctx, repo.ID, "bug", "#f00", "2026-01-01T00:00:00.000Z"); err != nil {
87 t.Fatal(err)
88 }
89 labels, _ := d.ListLabels(ctx, repo.ID)
90 n, err := d.CreateIssue(ctx, repo.ID, &u.ID, "t", "b", "2026-01-01T00:00:00.000Z", []int64{labels[0].ID})
91 if err != nil {
92 t.Fatal(err)
93 }
94 if n != 1 {
95 t.Fatalf("number %d", n)
96 }
97 iss, err := d.IssueByNumber(ctx, repo.ID, 1)
98 if err != nil || iss == nil || *iss.AuthorUsername != "admin" {
99 t.Fatalf("%v %v", iss, err)
100 }
101 counts, err := d.IssueCounts(ctx, repo.ID, []int64{labels[0].ID})
102 if err != nil || counts["open"] != 1 {
103 t.Fatalf("%v %v", counts, err)
104 }
105 list, err := d.ListIssues(ctx, repo.ID, "open", []int64{labels[0].ID}, 20, 0)
106 if err != nil || len(list) != 1 {
107 t.Fatalf("%v %v", list, err)
108 }
109 if err := d.ToggleIssueReaction(ctx, iss.ID, nil, u.ID, "👍"); err != nil {
110 t.Fatal(err)
111 }
112 if err := d.ToggleIssueReaction(ctx, iss.ID, nil, u.ID, "🚀"); err != nil {
113 t.Fatal(err)
114 }
115 rs, _ := d.ListIssueReactions(ctx, iss.ID)
116 if len(rs) != 1 || rs[0].Emoji != "🚀" {
117 t.Fatalf("%v", rs)
118 }
119 num, pid, err := d.CreatePatch(ctx, repo.ID, &u.ID, "p", "", "diff", "a", "a@b", "v1", "2026-01-01T00:00:00.000Z", nil)
120 if err != nil || num != 1 || pid == 0 {
121 t.Fatalf("%v %v", num, err)
122 }
123 okMerge, err := d.ClaimPatchMerge(ctx, pid, "v1", "now")
124 if err != nil || !okMerge {
125 t.Fatalf("%v %v", okMerge, err)
126 }
127 // The CI runner inserts runs with raw SQL, so the test does too.
128 res, err := d.ExecContext(ctx,
129 `INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, status)
130 VALUES (?, ?, 'manual', 'pending')`, repo.ID, u.ID)
131 if err != nil {
132 t.Fatal(err)
133 }
134 runID, err := res.LastInsertId()
135 if err != nil {
136 t.Fatal(err)
137 }
138 run, err := d.CiRunInRepo(ctx, runID, repo.ID)
139 if err != nil || run == nil || *run.TriggeredByUsername != "admin" {
140 t.Fatalf("%v %v", run, err)
141 }
142 if err := d.UpsertCiSecret(ctx, repo.ID, "K", "v", nil); err != nil {
143 t.Fatal(err)
144 }
145 if err := d.UpsertCiSecret(ctx, repo.ID, "K", "v2", nil); err != nil {
146 t.Fatal(err)
147 }
148 secrets, err := d.ListCiSecrets(ctx, repo.ID)
149 if err != nil || len(secrets) != 1 || secrets[0].Name != "K" {
150 t.Fatalf("%v %v", secrets, err)
151 }
152 rel, err := d.CreateRelease(ctx, repo.ID, nil, "r1", nil, true, "now")
153 if err != nil || rel == 0 {
154 t.Fatal(err)
155 }
156 if err := d.AddReleaseAssets(ctx, rel, []NewAsset{{Filename: "a.zip", Size: 5}}, "now"); err != nil {
157 t.Fatal(err)
158 }
159 ac, _ := d.AssetCounts(ctx, []int64{rel})
160 if ac[rel] != 1 {
161 t.Fatalf("%v", ac)
162 }
163 if err := d.CreateSession(ctx, "sid", u.ID, "2999-01-01T00:00:00.000Z", "now"); err != nil {
164 t.Fatal(err)
165 }
166 su, err := d.SessionUser(ctx, "sid", "2026-01-01T00:00:00.000Z")
167 if err != nil || su == nil || !su.IsAdmin {
168 t.Fatalf("%v %v", su, err)
169 }
170 if got, _ := d.ListRepos(ctx, false, EscapeLike("dem"), "name", 20, 0); len(got) != 1 {
171 t.Fatalf("search: %v", got)
172 }
173 if c, _ := d.CountRepos(ctx, true, ""); c != 1 {
174 t.Fatalf("count %d", c)
175 }
176}
177