package db import ( "context" "path/filepath" "testing" ) // npmHash was produced by the `argon2` npm package for "testpass123". It pins // the on-disk hash format, so existing accounts keep working. const npmHash = "$argon2id$v=19$m=65536,t=3,p=4$tM0uDIIEKAgByj9/dHJJvg$yrk+8WacG/bYr2106JCWtxaSv7uPFiVyO0ikDR4o6Kk" func TestInitAdminAndPasswords(t *testing.T) { ctx := context.Background() d, err := Open(filepath.Join(t.TempDir(), "hearthforge.db")) if err != nil { t.Fatal(err) } defer d.Close() created, err := d.InitAdmin(ctx, "hunter2") if err != nil { t.Fatal(err) } if !created { t.Fatal("expected admin to be created") } // A second call must be a no-op. created, err = d.InitAdmin(ctx, "other") if err != nil { t.Fatal(err) } if created { t.Fatal("admin was created twice") } user, err := d.UserByName(ctx, AdminUsername) if err != nil { t.Fatal(err) } if user == nil || user.PasswordHash == nil { t.Fatal("admin has no password hash") } ok, err := VerifyPassword(*user.PasswordHash, "hunter2") if err != nil || !ok { t.Fatalf("password did not round-trip: ok=%v err=%v", ok, err) } ok, err = VerifyPassword(*user.PasswordHash, "wrong") if err != nil { t.Fatal(err) } if ok { t.Fatal("wrong password verified") } ok, err = VerifyPassword(npmHash, "testpass123") if err != nil || !ok { t.Fatalf("npm argon2 hash did not verify: ok=%v err=%v", ok, err) } missing, err := d.UserByName(ctx, "nobody") if err != nil { t.Fatal(err) } if missing != nil { t.Fatal("expected nil for unknown user") } } // TestQueryLayer exercises one write and one read per table group. func TestQueryLayer(t *testing.T) { ctx := context.Background() d, err := Open(filepath.Join(t.TempDir(), "h.db")) if err != nil { t.Fatal(err) } defer d.Close() if _, err := d.InitAdmin(ctx, "pw"); err != nil { t.Fatal(err) } u, _ := d.UserByName(ctx, "admin") repo, err := d.CreateRepo(ctx, "demo", nil, false, "main", "2026-01-01T00:00:00.000Z") if err != nil { t.Fatal(err) } if err := d.CreateLabel(ctx, repo.ID, "bug", "#f00", "2026-01-01T00:00:00.000Z"); err != nil { t.Fatal(err) } labels, _ := d.ListLabels(ctx, repo.ID) n, err := d.CreateIssue(ctx, repo.ID, &u.ID, "t", "b", "2026-01-01T00:00:00.000Z", []int64{labels[0].ID}) if err != nil { t.Fatal(err) } if n != 1 { t.Fatalf("number %d", n) } iss, err := d.IssueByNumber(ctx, repo.ID, 1) if err != nil || iss == nil || *iss.AuthorUsername != "admin" { t.Fatalf("%v %v", iss, err) } counts, err := d.IssueCounts(ctx, repo.ID, []int64{labels[0].ID}) if err != nil || counts["open"] != 1 { t.Fatalf("%v %v", counts, err) } list, err := d.ListIssues(ctx, repo.ID, "open", []int64{labels[0].ID}, 20, 0) if err != nil || len(list) != 1 { t.Fatalf("%v %v", list, err) } if err := d.ToggleIssueReaction(ctx, iss.ID, nil, u.ID, "👍"); err != nil { t.Fatal(err) } if err := d.ToggleIssueReaction(ctx, iss.ID, nil, u.ID, "🚀"); err != nil { t.Fatal(err) } rs, _ := d.ListIssueReactions(ctx, iss.ID) if len(rs) != 1 || rs[0].Emoji != "🚀" { t.Fatalf("%v", rs) } num, pid, err := d.CreatePatch(ctx, repo.ID, &u.ID, "p", "", "diff", "a", "a@b", "v1", "2026-01-01T00:00:00.000Z", nil) if err != nil || num != 1 || pid == 0 { t.Fatalf("%v %v", num, err) } okMerge, err := d.ClaimPatchMerge(ctx, pid, "v1", "now") if err != nil || !okMerge { t.Fatalf("%v %v", okMerge, err) } // The CI runner inserts runs with raw SQL, so the test does too. res, err := d.ExecContext(ctx, `INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, status) VALUES (?, ?, 'manual', 'pending')`, repo.ID, u.ID) if err != nil { t.Fatal(err) } runID, err := res.LastInsertId() if err != nil { t.Fatal(err) } run, err := d.CiRunInRepo(ctx, runID, repo.ID) if err != nil || run == nil || *run.TriggeredByUsername != "admin" { t.Fatalf("%v %v", run, err) } if err := d.UpsertCiSecret(ctx, repo.ID, "K", "v", nil); err != nil { t.Fatal(err) } if err := d.UpsertCiSecret(ctx, repo.ID, "K", "v2", nil); err != nil { t.Fatal(err) } secrets, err := d.ListCiSecrets(ctx, repo.ID) if err != nil || len(secrets) != 1 || secrets[0].Name != "K" { t.Fatalf("%v %v", secrets, err) } rel, err := d.CreateRelease(ctx, repo.ID, nil, "r1", nil, true, "now") if err != nil || rel == 0 { t.Fatal(err) } if err := d.AddReleaseAssets(ctx, rel, []NewAsset{{Filename: "a.zip", Size: 5}}, "now"); err != nil { t.Fatal(err) } ac, _ := d.AssetCounts(ctx, []int64{rel}) if ac[rel] != 1 { t.Fatalf("%v", ac) } if err := d.CreateSession(ctx, "sid", u.ID, "2999-01-01T00:00:00.000Z", "now"); err != nil { t.Fatal(err) } su, err := d.SessionUser(ctx, "sid", "2026-01-01T00:00:00.000Z") if err != nil || su == nil || !su.IsAdmin { t.Fatalf("%v %v", su, err) } if got, _ := d.ListRepos(ctx, false, EscapeLike("dem"), "name", 20, 0); len(got) != 1 { t.Fatalf("search: %v", got) } if c, _ := d.CountRepos(ctx, true, ""); c != 1 { t.Fatalf("count %d", c) } }