git_test.go
⎇
Raw
1package web
2
3import (
4 "context"
5 "net/http/httptest"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "github.com/go-chi/chi/v5"
13
14 "hearthforge/internal/ci"
15 "hearthforge/internal/config"
16 "hearthforge/internal/db"
17 "hearthforge/internal/gitcmd"
18)
19
20const gitTestPassword = "testpass123"
21
22// runGit runs a git command in dir and fails the test on error.
23func runGit(t *testing.T, dir string, args ...string) string {
24 t.Helper()
25 out, err := gitTry(dir, args...)
26 if err != nil {
27 t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
28 }
29 return out
30}
31
32func gitTry(dir string, args ...string) (string, error) {
33 cmd := exec.Command("git", args...)
34 cmd.Dir = dir
35 cmd.Env = append(os.Environ(),
36 "GIT_CONFIG_GLOBAL=/dev/null",
37 "GIT_CONFIG_SYSTEM=/dev/null",
38 "GIT_TERMINAL_PROMPT=0",
39 "GIT_ASKPASS=echo",
40 "GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com",
41 "GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com",
42 )
43 out, err := cmd.CombinedOutput()
44 return string(out), err
45}
46
47// gitTestServer creates a temp data dir, a public repo with one commit and an
48// httptest server that serves only the smart HTTP routes.
49func gitTestServer(t *testing.T) (*Server, *httptest.Server) {
50 t.Helper()
51 ctx := context.Background()
52 dir := t.TempDir()
53 database, err := db.Open(filepath.Join(dir, "hearthforge.db"))
54 if err != nil {
55 t.Fatal(err)
56 }
57 t.Cleanup(func() { database.Close() })
58 if _, err := database.InitAdmin(ctx, gitTestPassword); err != nil {
59 t.Fatal(err)
60 }
61 if _, err := database.CreateRepo(ctx, "smoke", nil, false, "main", db.NowISO()); err != nil {
62 t.Fatal(err)
63 }
64
65 cfg := &config.Config{
66 DataDir: dir, BaseURL: "http://localhost:3000",
67 CIMaxConcurrent: 1, MaxConcurrentArchives: 1, RateLimitDisabled: true,
68 }
69 if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil {
70 t.Fatal(err)
71 }
72 g := gitcmd.New(cfg)
73 if err := g.Init(ctx, "smoke", "main"); err != nil {
74 t.Fatal(err)
75 }
76 // Seed one commit by pushing from a scratch working tree.
77 seed := t.TempDir()
78 runGit(t, seed, "init", "--initial-branch=main", seed)
79 if err := os.WriteFile(filepath.Join(seed, "README.md"), []byte("hello\n"), 0o644); err != nil {
80 t.Fatal(err)
81 }
82 runGit(t, seed, "add", "README.md")
83 runGit(t, seed, "commit", "-m", "initial")
84 runGit(t, seed, "push", g.RepoPath("smoke"), "main")
85
86 // Bare repos reject a push to the checked-out branch unless told otherwise.
87 runGit(t, "", "-C", g.RepoPath("smoke"), "config", "receive.denyCurrentBranch", "ignore")
88
89 s := &Server{Cfg: cfg, DB: database, Git: g, CI: ci.New(cfg, database)}
90 r := chi.NewRouter()
91 s.gitRoutes(r)
92 ts := httptest.NewServer(r)
93 t.Cleanup(ts.Close)
94 return s, ts
95}
96
97func TestGitSmartHTTPCloneAndPush(t *testing.T) {
98 if _, err := exec.LookPath("git"); err != nil {
99 t.Skip("git not installed")
100 }
101 s, ts := gitTestServer(t)
102 ctx := context.Background()
103
104 // Anonymous clone of a public repo works.
105 work := t.TempDir()
106 clone := filepath.Join(work, "smoke")
107 runGit(t, work, "clone", ts.URL+"/smoke.git", clone)
108 if _, err := os.Stat(filepath.Join(clone, "README.md")); err != nil {
109 t.Fatalf("cloned file missing: %v", err)
110 }
111
112 // Anonymous push is rejected.
113 if err := os.WriteFile(filepath.Join(clone, "README.md"), []byte("changed\n"), 0o644); err != nil {
114 t.Fatal(err)
115 }
116 runGit(t, clone, "commit", "-am", "second commit")
117 if out, err := gitTry(clone, "push", "origin", "main"); err == nil {
118 t.Fatalf("anonymous push succeeded:\n%s", out)
119 }
120
121 // Push with admin credentials in the URL succeeds.
122 authURL := strings.Replace(ts.URL, "http://", "http://admin:"+gitTestPassword+"@", 1)
123 runGit(t, clone, "push", authURL+"/smoke.git", "main")
124
125 commits, err := s.Git.Log(ctx, "smoke", "main", 10, 0)
126 if err != nil {
127 t.Fatal(err)
128 }
129 if len(commits) != 2 || commits[0].Subject != "second commit" {
130 t.Fatalf("unexpected log: %+v", commits)
131 }
132}
133