ci_test.go
| 1 | package web |
| 2 | |
| 3 | import ( |
| 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. |
| 21 | func 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 | |
| 62 | func 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 | |
| 72 | func 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 | |
| 88 | func 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 | |
| 103 | func 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. |
| 120 | func 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 |