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 if err := s.Git.Init(ctx, "ci-repo", "main"); err != nil {
49 t.Fatal(err)
50 }
51 user := &db.SessionUser{ID: admin.ID, Username: admin.Username, IsAdmin: true}
52 r := chi.NewRouter()
53 r.Use(func(next http.Handler) http.Handler {
54 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userKey, user)))
56 })
57 })
58 s.ciRoutes(r)
59 return r, database, runID
60}
61
62func ciGet(t *testing.T, h http.Handler, path string) string {
63 t.Helper()
64 rec := httptest.NewRecorder()
65 h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
66 if rec.Code != http.StatusOK {
67 t.Fatalf("GET %s = %d, want 200", path, rec.Code)
68 }
69 return rec.Body.String()
70}
71
72func TestCiHistoryPage(t *testing.T) {
73 h, _, runID := ciTestServer(t)
74 body := ciGet(t, h, "/ci-repo/ci")
75 for _, want := range []string{
76 `class="ci-status-pill ci-status-success"`,
77 `href="/ci-repo/ci/` + strconv.FormatInt(runID, 10) + `"`,
78 "Run pipeline",
79 "Purge caches",
80 "How to define a pipeline file",
81 } {
82 if !strings.Contains(body, want) {
83 t.Errorf("history page misses %q", want)
84 }
85 }
86}
87
88func TestCiRunDetailPage(t *testing.T) {
89 h, _, runID := ciTestServer(t)
90 body := ciGet(t, h, "/ci-repo/ci/"+strconv.FormatInt(runID, 10))
91 for _, want := range []string{
92 `class="ci-step-name">pipeline setup<`,
93 `class="ci-step-name">hello<`,
94 "Pipeline #1",
95 "Retry",
96 } {
97 if !strings.Contains(body, want) {
98 t.Errorf("run detail page misses %q", want)
99 }
100 }
101}
102
103func TestCiBadge(t *testing.T) {
104 h, _, _ := ciTestServer(t)
105 rec := httptest.NewRecorder()
106 h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/ci-repo/ci/badge.svg", nil))
107 if rec.Code != http.StatusOK {
108 t.Fatalf("badge status = %d", rec.Code)
109 }
110 if ct := rec.Header().Get("Content-Type"); ct != "image/svg+xml" {
111 t.Errorf("badge content type = %q", ct)
112 }
113 if body := rec.Body.String(); !strings.Contains(body, "<svg") || !strings.Contains(body, "success") {
114 t.Errorf("badge body = %q", body)
115 }
116}
117
118// insertCiRun writes one finished run with the given steps. The production CI
119// runner writes these rows with raw SQL, so the test does the same.
120func insertCiRun(t *testing.T, d *db.DB, repoID, userID int64, steps ...string) int64 {
121 t.Helper()
122 ctx := context.Background()
123 res, err := d.ExecContext(ctx,
124 `INSERT INTO ci_runs (repo_id, triggered_by, trigger_source, status, finished_at, repo_run_id)
125 VALUES (?, ?, 'manual', 'success', ?, 1)`, repoID, userID, db.NowISO())
126 if err != nil {
127 t.Fatal(err)
128 }
129 runID, err := res.LastInsertId()
130 if err != nil {
131 t.Fatal(err)
132 }
133 for _, name := range steps {
134 if _, err := d.ExecContext(ctx,
135 `INSERT INTO ci_steps (run_id, name, status) VALUES (?, ?, 'success')`,
136 runID, name); err != nil {
137 t.Fatal(err)
138 }
139 }
140 return runID
141}
142