package web import ( "context" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/go-chi/chi/v5" "hearthforge/internal/ci" "hearthforge/internal/config" "hearthforge/internal/db" "hearthforge/internal/gitcmd" ) const gitTestPassword = "testpass123" // runGit runs a git command in dir and fails the test on error. func runGit(t *testing.T, dir string, args ...string) string { t.Helper() out, err := gitTry(dir, args...) if err != nil { t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) } return out } func gitTry(dir string, args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null", "GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=echo", "GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com", "GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com", ) out, err := cmd.CombinedOutput() return string(out), err } // gitTestServer creates a temp data dir, a public repo with one commit and an // httptest server that serves only the smart HTTP routes. func gitTestServer(t *testing.T) (*Server, *httptest.Server) { t.Helper() ctx := context.Background() dir := t.TempDir() database, err := db.Open(filepath.Join(dir, "hearthforge.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) if _, err := database.InitAdmin(ctx, gitTestPassword); err != nil { t.Fatal(err) } if _, err := database.CreateRepo(ctx, "smoke", nil, false, "main", db.NowISO()); err != nil { t.Fatal(err) } cfg := &config.Config{ DataDir: dir, BaseURL: "http://localhost:3000", CIMaxConcurrent: 1, MaxConcurrentArchives: 1, RateLimitDisabled: true, } if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil { t.Fatal(err) } g := gitcmd.New(cfg) if err := g.Init(ctx, "smoke", "main", ""); err != nil { t.Fatal(err) } // Seed one commit by pushing from a scratch working tree. seed := t.TempDir() runGit(t, seed, "init", "--initial-branch=main", seed) if err := os.WriteFile(filepath.Join(seed, "README.md"), []byte("hello\n"), 0o644); err != nil { t.Fatal(err) } runGit(t, seed, "add", "README.md") runGit(t, seed, "commit", "-m", "initial") runGit(t, seed, "push", g.RepoPath("smoke"), "main") // Bare repos reject a push to the checked-out branch unless told otherwise. runGit(t, "", "-C", g.RepoPath("smoke"), "config", "receive.denyCurrentBranch", "ignore") s := &Server{Cfg: cfg, DB: database, Git: g, CI: ci.New(cfg, database)} r := chi.NewRouter() s.gitRoutes(r) ts := httptest.NewServer(r) t.Cleanup(ts.Close) return s, ts } func TestGitSmartHTTPCloneAndPush(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } s, ts := gitTestServer(t) ctx := context.Background() // Anonymous clone of a public repo works. work := t.TempDir() clone := filepath.Join(work, "smoke") runGit(t, work, "clone", ts.URL+"/smoke.git", clone) if _, err := os.Stat(filepath.Join(clone, "README.md")); err != nil { t.Fatalf("cloned file missing: %v", err) } // Anonymous push is rejected. if err := os.WriteFile(filepath.Join(clone, "README.md"), []byte("changed\n"), 0o644); err != nil { t.Fatal(err) } runGit(t, clone, "commit", "-am", "second commit") if out, err := gitTry(clone, "push", "origin", "main"); err == nil { t.Fatalf("anonymous push succeeded:\n%s", out) } // Push with admin credentials in the URL succeeds. authURL := strings.Replace(ts.URL, "http://", "http://admin:"+gitTestPassword+"@", 1) runGit(t, clone, "push", authURL+"/smoke.git", "main") commits, err := s.Git.Log(ctx, "smoke", "main", 10, 0) if err != nil { t.Fatal(err) } if len(commits) != 2 || commits[0].Subject != "second commit" { t.Fatalf("unexpected log: %+v", commits) } }