package web import ( "context" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/go-chi/chi/v5" "hearthforge/internal/config" "hearthforge/internal/db" "hearthforge/internal/gitcmd" "hearthforge/internal/highlight" "hearthforge/internal/markdown" ) // repoTestServer returns a router with the repo routes and an admin user. func repoTestServer(t *testing.T) (http.Handler, *Server, *db.SessionUser) { t.Helper() ctx := context.Background() dataDir := t.TempDir() d, err := db.Open(filepath.Join(dataDir, "hearthforge.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) if _, err := d.InitAdmin(ctx, "hunter2"); err != nil { t.Fatal(err) } admin, err := d.UserByName(ctx, db.AdminUsername) if err != nil || admin == nil { t.Fatalf("admin lookup failed: %v", err) } cfg := &config.Config{ DataDir: dataDir, OwnerDisplayName: "Admin", BaseURL: "http://localhost:3000", SSHPort: 2222, InlineMaxBytes: 512 << 10, MaxRenderBytes: 10 << 20, MaxTitleBytes: 500, MaxTextBodyBytes: 100000, CommitterName: "Admin", CommitterEmail: "admin@localhost", RateLimitDisabled: true, } cfg.SSHHostKeyPath = filepath.Join(dataDir, "ssh_host_key") if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil { t.Fatal(err) } // Commits made through the UI are ssh-signed, so the host key must exist. if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } keygen := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", "-f", cfg.SSHHostKeyPath, "-C", "test") if out, err := keygen.CombinedOutput(); err != nil { t.Skipf("ssh-keygen unavailable: %v: %s", err, out) } s := &Server{ Cfg: cfg, DB: d, MD: markdown.New(), HL: highlight.New(cfg.InlineMaxBytes), Git: gitcmd.New(cfg), } r := chi.NewRouter() s.repoRoutes(r) return r, s, &db.SessionUser{ID: admin.ID, Username: db.AdminUsername, IsAdmin: true} } func TestRepoCreateBrowseAndEdit(t *testing.T) { h, s, admin := repoTestServer(t) res := do(t, h, admin, "POST", "/new", url.Values{ "name": {"my-repo"}, "description": {"A demo"}, "default_branch": {"main"}, }) if res.Code != http.StatusFound { t.Fatalf("create repo status = %d, body %s", res.Code, res.Body.String()) } if got := res.Header().Get("Location"); got != "/my-repo" { t.Fatalf("create repo redirected to %q", got) } bare := filepath.Join(s.Cfg.ReposDir(), "my-repo.git") if _, err := os.Stat(filepath.Join(bare, "HEAD")); err != nil { t.Fatalf("bare repo missing on disk: %v", err) } // The UI create-file route must produce a real commit. res = do(t, h, admin, "POST", "/my-repo/new-file/main", url.Values{ "path": {"README.md"}, "content": {"# Hello\n\nworld\n"}, "message": {"Add README"}, }) if res.Code != http.StatusFound { t.Fatalf("create file status = %d, body %s", res.Code, res.Body.String()) } commitURL := res.Header().Get("Location") if !strings.HasPrefix(commitURL, "/my-repo/commit/") { t.Fatalf("create file redirected to %q", commitURL) } commits, err := s.Git.Log(context.Background(), "my-repo", "main", 10, 0) if err != nil || len(commits) != 1 { t.Fatalf("log = %v, err %v", commits, err) } if commits[0].Subject != "Add README" { t.Errorf("commit subject = %q", commits[0].Subject) } res = do(t, h, admin, "GET", "/my-repo", nil) body := res.Body.String() if res.Code != http.StatusOK { t.Fatalf("home status = %d", res.Code) } if !strings.Contains(body, "README.md") { t.Error("repo home did not list the file") } if !strings.Contains(body, "