fierj
PublicTiny personal git forge
package main
import (
"bytes"
"mime/multipart"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// postMultipart POSTs a multipart/form-data request with fields and an optional file.
func (ts *testServer) postMultipart(t *testing.T, client *http.Client, path string, fields map[string]string, fileField, fileName string, fileContent []byte) *http.Response {
t.Helper()
var body bytes.Buffer
w := multipart.NewWriter(&body)
for k, v := range fields {
if err := w.WriteField(k, v); err != nil {
t.Fatalf("WriteField %s: %v", k, err)
}
}
if fileField != "" {
part, err := w.CreateFormFile(fileField, fileName)
if err != nil {
t.Fatalf("CreateFormFile: %v", err)
}
if _, err := part.Write(fileContent); err != nil {
t.Fatalf("write file content: %v", err)
}
}
if err := w.Close(); err != nil {
t.Fatalf("multipart close: %v", err)
}
req, err := http.NewRequest("POST", ts.URL+path, &body)
if err != nil {
t.Fatalf("NewRequest %s: %v", path, err)
}
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := client.Do(req)
if err != nil {
t.Fatalf("POST %s: %v", path, err)
}
return resp
}
// TestSmoke runs a full end-to-end workflow:
//
// blank server → create repo → git clone/push → branches → tags →
// file-based patch → branch-based patch → verify everything renders
func TestSmoke(t *testing.T) {
ts := newTestServer(t)
c := ts.client()
// ── 1. Setup: create admin, create repo ────────────────────────
ts.setupUser(t, c, "dev", "secret123")
ts.createRepo(t, c, "smoke", "End-to-end smoke test repository")
bareRepo := filepath.Join(ts.dir, "smoke.git")
// ── 2. Clone and push initial commit to main ──────────────────
localDir := t.TempDir()
gitCmd(t, localDir, "clone", "file://"+bareRepo, "smoke")
localRepo := filepath.Join(localDir, "smoke")
writeFile(t, localRepo, "README.md", "# Smoke Test\n\nThis is a smoke test repository.\n")
gitCmd(t, localRepo, "add", "README.md")
gitCmd(t, localRepo, "commit", "-m", "initial commit: README")
gitCmd(t, localRepo, "push", "origin", "main")
// Ensure the bare repo HEAD points to refs/heads/main so
// DefaultBranch() returns "main" even if git init defaulted to "master".
cmd := exec.Command("git", "--git-dir="+bareRepo, "symbolic-ref", "HEAD", "refs/heads/main")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("set HEAD to main: %v\n%s", err, out)
}
// ── 3. Create branch "add-license", push it ───────────────────
gitCmd(t, localRepo, "checkout", "-b", "add-license")
writeFile(t, localRepo, "LICENSE", "MIT License\n\nCopyright (c) 2025\n\nPermission is hereby granted...\n")
gitCmd(t, localRepo, "add", "LICENSE")
gitCmd(t, localRepo, "commit", "-m", "add MIT license")
gitCmd(t, localRepo, "push", "origin", "add-license")
// ── 4. Tag main as v0.1.0 ─────────────────────────────────────
gitCmd(t, localRepo, "checkout", "main")
gitCmd(t, localRepo, "tag", "-a", "v0.1.0", "-m", "first release")
gitCmd(t, localRepo, "push", "origin", "v0.1.0")
// ── 5. File-based patch from "add-license" ────────────────────
// Generate a mailbox-format patch.
patchContent := gitCmd(t, localRepo, "format-patch", "--stdout", "main..add-license")
resp := ts.postMultipart(t, c, "/smoke/patches/new",
map[string]string{
"title": "Add MIT license (file patch)",
"body": "This patch adds a LICENSE file via file upload.",
},
"patch_file", "add-license.patch",
[]byte(patchContent),
)
assertStatus(t, resp, http.StatusOK)
b := body(t, resp)
if !strings.Contains(b, "Add MIT license") {
t.Error("patch view should show patch title after file upload")
}
if !strings.Contains(b, "MIT License") {
t.Error("patch diff should contain the license text")
}
// Find the file-based patch ID.
rp := repoPath(ts.cfg, "smoke")
patches, _ := listPatches(rp, "open")
if len(patches) != 1 {
t.Fatalf("expected 1 open file-based patch, got %d", len(patches))
}
filePatchID := patches[0].ID
// Merge the file-based patch via clone + git am + push.
resp = ts.postForm(t, c, "/smoke/patches/"+filePatchID+"/merge", url.Values{})
assertStatus(t, resp, http.StatusOK)
// ── 6. Create branch "update-readme" (from updated main) ──────
// Pull first so the local main has the merged license.
gitCmd(t, localRepo, "pull", "origin", "main")
gitCmd(t, localRepo, "checkout", "-b", "update-readme")
writeFile(t, localRepo, "README.md", "# Smoke Test\n\nThis is a smoke test repository.\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n")
gitCmd(t, localRepo, "add", "README.md")
gitCmd(t, localRepo, "commit", "-m", "update README with license info")
gitCmd(t, localRepo, "push", "origin", "update-readme")
// ── 7. Branch-based patch from "update-readme" ────────────────
resp = ts.postForm(t, c, "/smoke/patches/new", url.Values{
"title": {"Update README (branch patch)"},
"body": {"Add a License section to the README."},
"branch": {"update-readme"},
})
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "Update README") {
t.Error("patch view should show branch-based patch title")
}
if !strings.Contains(b, "LICENSE") {
t.Error("patch diff should show README changes referencing LICENSE")
}
// Find the branch-based patch ID.
patches, _ = listPatches(rp, "open")
if len(patches) != 1 {
t.Fatalf("expected 1 open branch-based patch, got %d", len(patches))
}
branchPatchID := patches[0].ID
// Merge the branch-based patch (fast-forward).
resp = ts.postForm(t, c, "/smoke/patches/"+branchPatchID+"/merge", url.Values{})
assertStatus(t, resp, http.StatusOK)
// ── 8. Tag main as v0.2.0 ─────────────────────────────────────
gitCmd(t, localRepo, "checkout", "main")
gitCmd(t, localRepo, "pull", "origin", "main")
gitCmd(t, localRepo, "tag", "-a", "v0.2.0", "-m", "second release")
gitCmd(t, localRepo, "push", "origin", "v0.2.0")
// ── 9. Verify everything in the web UI ────────────────────────
// --- Repo tree ---
resp = ts.get(t, c, "/smoke")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "README.md") {
t.Error("tree: should show README.md")
}
if !strings.Contains(b, "LICENSE") {
t.Error("tree: should show LICENSE file (merged from file patch)")
}
// --- Blob: README ---
resp = ts.get(t, c, "/smoke/blob/main/README.md")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "LICENSE") {
t.Error("blob: README should contain merged changes referencing LICENSE")
}
// --- Blob: LICENSE ---
resp = ts.get(t, c, "/smoke/blob/main/LICENSE")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "MIT License") {
t.Error("blob: LICENSE should contain license text from file patch")
}
// --- Commit log ---
resp = ts.get(t, c, "/smoke/log/main")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
for _, want := range []string{
"initial commit",
"add MIT license",
"update README",
} {
if !strings.Contains(b, want) {
t.Errorf("log: should contain commit %q", want)
}
}
// --- Refs page: branches and tags ---
resp = ts.get(t, c, "/smoke/refs/")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
for _, want := range []string{"main", "add-license"} {
if !strings.Contains(b, want) {
t.Errorf("refs: should list branch %q", want)
}
}
for _, want := range []string{"v0.1.0", "v0.2.0"} {
if !strings.Contains(b, want) {
t.Errorf("refs: should list tag %q", want)
}
}
// --- Patches list: both should be merged ---
resp = ts.get(t, c, "/smoke/patches?state=merged")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "Add MIT license") {
t.Error("patches: should list 'Add MIT license' as merged")
}
if !strings.Contains(b, "Update README") {
t.Error("patches: should list 'Update README' as merged")
}
// --- No open patches remain ---
resp = ts.get(t, c, "/smoke/patches?state=open")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
// The page should not list any open patches.
if strings.Contains(b, "Add MIT license") || strings.Contains(b, "Update README") {
// They could appear as "No open patches" is the expected but the names
// might still be in the branch selector. That's fine.
}
// --- Commit page for v0.2.0 tag ---
// Just check the tag tree works.
resp = ts.get(t, c, "/smoke/tree/v0.2.0")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "README.md") || !strings.Contains(b, "LICENSE") {
t.Error("tree at tag v0.2.0 should show both files")
}
// --- README rendering on tree page ---
resp = ts.get(t, c, "/smoke")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "smoke test repository") {
t.Error("tree: README content should be rendered on the repo page")
}
// --- Repo appears in home page list ---
resp = ts.get(t, c, "/")
assertStatus(t, resp, http.StatusOK)
b = body(t, resp)
if !strings.Contains(b, "smoke") {
t.Error("home page: should list the smoke repo")
}
if !strings.Contains(b, "End-to-end smoke test repository") {
t.Error("home page: should show repo description")
}
}
// writeFile creates a file with content, creating parent directories as needed.
func writeFile(t *testing.T, dir, name, content string) {
t.Helper()
path := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}