ci_test.go
⎇
Raw
1package web
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "path/filepath"
8 "strconv"
9 "strings"
10 "testing"
11
12 "github.com/go-chi/chi/v5"
13
14 "hearthforge/internal/config"
15 "hearthforge/internal/db"
16 "hearthforge/internal/gitcmd"
17)
18
19// ciTestServer builds a server on a temp database with one repository and one
20// finished run. Every request runs as the admin.
21func ciTestServer(t *testing.T) (http.Handler, *db.DB, int64) {
22 t.Helper()
23 ctx := context.Background()
24 dir := t.TempDir()
25 database, err := db.Open(filepath.Join(dir, "hearthforge.db"))
26 if err != nil {
27 t.Fatal(err)
28 }
29 t.Cleanup(func() { database.Close() })
30 if _, err := database.InitAdmin(ctx, "hunter2"); err != nil {
31 t.Fatal(err)
32 }
33 admin, err := database.UserByName(ctx, db.AdminUsername)
34 if err != nil || admin == nil {
35 t.Fatalf("admin missing: %v", err)
36 }
37 repo, err := database.CreateRepo(ctx, "ci-repo", nil, false, "main", db.NowISO())
38 if err != nil {
39 t.Fatal(err)
40 }
41 runID := insertCiRun(t, database, repo.ID, admin.ID, "pipeline setup", "hello")
42
43 cfg := &config.Config{
44 DataDir: dir, OwnerDisplayName: "Admin",
45 BaseURL: "http://localhost:3000", CIMaxConcurrent: 1, MaxConcurrentArchives: 1,
46 }
47 s := &Server{Cfg: cfg, DB: database, Git: gitcmd.New(cfg)}
48 user := &db.SessionUser{ID: admin.ID, Username: admin.Username, IsAdmin: true}
49 r := chi.NewRouter()
50 r.Use(func(next http.Handler) http.Handler {
51 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userKey, user)))
53 })
54 })
55 s.ciRoutes(r)
56 return r, database, runID
57}
58
59func ciGet(t *testing.T, h http.Handler, path string) string {
60 t.Helper()
61 rec := httptest.NewRecorder()
62 h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
63 if rec.Code != http.StatusOK {
64 t.Fatalf("GET %s = %d, want 200", path, rec.Code)
65 }
66 return rec.Body.String()
67}
68
69func TestCiHistoryPage(t *testing.T) {
70 h, _, runID := ciTestServer(t)
71 body := ciGet(t, h, "/ci-repo/ci")
72 for _, want := range []string{
73 `class="ci-status-pill ci-status-success"`,
74 `href="/ci-repo/ci/` + strconv.FormatInt(runID, 10) + `"`,
75 "Run pipeline",
76 "Purge caches",
77 "How to define a pipeline file",
78 } {
79 if !strings.Contains(body, want) {
80 t.Errorf("history page misses %q", want)
81 }
82 }
83}
84
85func TestCiRunDetailPage(t *testing.T) {
86 h, _, runID := ciTestServer(t)
87 body := ciGet(t, h, "/ci-repo/ci/"+strconv.FormatInt(runID, 10))
88 for _, want := range []string{
89 `class="ci-step-name">pipeline setup<`,
90 `class="ci-step-name">hello<`,
91 "Pipeline #1",
92 "Retry",
93 } {
94 if !strings.Contains(body, want) {
95 t.Errorf("run detail page misses %q", want)
96 }
97 }
98}
99
100func TestCiBadge(t *testing.T) {
101 h, _, _ := ciTestServer(t)
102 rec := httptest.NewRecorder()
103 h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/ci-repo/ci/badge.svg", nil))
104 if rec.Code != http.StatusOK {
105 t.Fatalf("badge status = %d", rec.Code)
106 }
107 if ct := rec.Header().Get("Content-Type"); ct != "image/svg+xml" {
108 t.Errorf("badge content type = %q", ct)
109 }
110 if body := rec.Body.String(); !strings.Contains(body, "<svg") || !strings.Contains(body, "success") {
111 t.Errorf("badge body = %q", body)
112 }
113}
114
115// insertCiRun writes one finished run with the given steps. The production CI
116// runner writes these rows with raw SQL, so the test does the same.
117func insertCiRun(t *testing.T, d *db.DB, repoID, userID int64, steps ...string) int64 {
118 t.Helper()
119 ctx := context.Background()
120 res, err := d.ExecContext(ctx,
121 `INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, status, finished_at, repo_run_id)
122 VALUES (?, ?, 'manual', 'success', ?, 1)`, repoID, userID, db.NowISO())
123 if err != nil {
124 t.Fatal(err)
125 }
126 runID, err := res.LastInsertId()
127 if err != nil {
128 t.Fatal(err)
129 }
130 for _, name := range steps {
131 if _, err := d.ExecContext(ctx,
132 `INSERT INTO ci_steps (run_id, name, status) VALUES (?, ?, 'success')`,
133 runID, name); err != nil {
134 t.Fatal(err)
135 }
136 }
137 return runID
138}
139