New Sign in

fierj

Public

Tiny personal git forge

← fierj / repo_test.go
package main

import (
	"net/http"
	"strings"
	"testing"
)

// ---------- Repository tree, blob, log, diff ----------

// TestTreeSubdirectories verifies navigating into nested directories.
func TestTreeSubdirectories(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "nested", "Nested dirs")

	localDir := t.TempDir()
	bareRepo := repoPath(ts.cfg, "nested")
	gitCmd(t, localDir, "clone", "file://"+bareRepo, "repo")
	localRepo := localDir + "/repo"

	// Create nested structure: src/lib/util.go
	writeFile(t, localRepo, "src/lib/util.go", "package lib\n")
	gitCmd(t, localRepo, "add", "src/lib/util.go")
	gitCmd(t, localRepo, "commit", "-m", "add util")
	gitCmd(t, localRepo, "push", "origin", "main")

	// Root tree should show src/
	resp := ts.get(t, c, "/nested")
	assertStatus(t, resp, http.StatusOK)
	b := body(t, resp)
	if !strings.Contains(b, "src") {
		t.Error("root tree should show src directory")
	}

	// Navigate into src/
	resp = ts.get(t, c, "/nested/tree/main/src")
	assertStatus(t, resp, http.StatusOK)
	b = body(t, resp)
	if !strings.Contains(b, "lib") {
		t.Error("src/ tree should show lib directory")
	}

	// Navigate into src/lib/
	resp = ts.get(t, c, "/nested/tree/main/src/lib")
	assertStatus(t, resp, http.StatusOK)
	b = body(t, resp)
	if !strings.Contains(b, "util.go") {
		t.Error("src/lib/ tree should show util.go")
	}

	// View blob
	resp = ts.get(t, c, "/nested/blob/main/src/lib/util.go")
	assertStatus(t, resp, http.StatusOK)
	b = body(t, resp)
	if !strings.Contains(b, "package lib") {
		t.Error("blob should show file content")
	}
}

// TestCommitDiff verifies the commit diff view (was broken by --,hash bug).
func TestCommitDiff(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "diffrepo", "Diff test")

	localDir := t.TempDir()
	bareRepo := repoPath(ts.cfg, "diffrepo")
	gitCmd(t, localDir, "clone", "file://"+bareRepo, "repo")
	localRepo := localDir + "/repo"

	writeFile(t, localRepo, "hello.txt", "hello world\n")
	gitCmd(t, localRepo, "add", "hello.txt")
	gitCmd(t, localRepo, "commit", "-m", "add hello")
	gitCmd(t, localRepo, "push", "origin", "main")

	// Get the commit hash
	hash := strings.TrimSpace(gitCmd(t, localRepo, "rev-parse", "HEAD"))

	// View the commit diff
	resp := ts.get(t, c, "/diffrepo/commit/"+hash)
	assertStatus(t, resp, http.StatusOK)
	b := body(t, resp)
	if !strings.Contains(b, "hello world") {
		t.Error("commit diff should show file content")
	}
	if !strings.Contains(b, hash[:8]) {
		t.Error("commit diff page should show the hash")
	}

	// Short hash (8 chars) should also work
	shortHash := hash[:8]
	resp = ts.get(t, c, "/diffrepo/commit/"+shortHash)
	assertStatus(t, resp, http.StatusOK)
}

// TestTreeShowsLastCommit verifies files show their last commit message and date.
func TestTreeShowsLastCommit(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "lastcommit", "Last commit test")

	localDir := t.TempDir()
	bareRepo := repoPath(ts.cfg, "lastcommit")
	gitCmd(t, localDir, "clone", "file://"+bareRepo, "repo")
	localRepo := localDir + "/repo"

	writeFile(t, localRepo, "a.txt", "file a\n")
	gitCmd(t, localRepo, "add", "a.txt")
	gitCmd(t, localRepo, "commit", "-m", "add file a")
	gitCmd(t, localRepo, "push", "origin", "main")

	writeFile(t, localRepo, "b.txt", "file b\n")
	gitCmd(t, localRepo, "add", "b.txt")
	gitCmd(t, localRepo, "commit", "-m", "add file b")
	gitCmd(t, localRepo, "push", "origin", "main")

	// Tree should show last commit info for each file
	resp := ts.get(t, c, "/lastcommit")
	assertStatus(t, resp, http.StatusOK)
	b := body(t, resp)
	// Both files should appear
	if !strings.Contains(b, "a.txt") {
		t.Error("tree should show a.txt")
	}
	if !strings.Contains(b, "b.txt") {
		t.Error("tree should show b.txt")
	}
	// Each file should have commit author relative dates
	// "add file a" or "add file b" messages
	if !strings.Contains(b, "add file a") && !strings.Contains(b, "add file b") {
		t.Error("tree should show last commit messages for files")
	}
}

// TestRepoNameCollision verifies duplicate repo creation returns error.
func TestRepoNameCollision(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "unique", "First")

	// Try to create another with same name
	resp := ts.postForm(t, c, "/new", map[string][]string{
		"name":        {"unique"},
		"description": {"Second attempt"},
	})
	assertStatus(t, resp, http.StatusOK)
	b := body(t, resp)
	// Duplicate repos currently succeed (InitRepo reinitializes).
	// Check that the repo page renders.
	if !strings.Contains(b, "unique") {
		t.Error("redirect should go to the repo page")
	}
}

// TestInvalidRepoName verifies bad repo names are rejected.
func TestInvalidRepoName(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")

	for _, name := range []string{"", "a/b", "..", "../escape"} {
		resp := ts.postForm(t, c, "/new", map[string][]string{
			"name":        {name},
			"description": {"bad"},
		})
		// Should return error (400 or redirect to error page)
		b := body(t, resp)
		if !strings.Contains(b, "invalid") && !strings.Contains(b, "failed") && !strings.Contains(b, "required") {
			t.Logf("repo name %q returned status %d, body snippet: %.100s", name, resp.StatusCode, b)
		}
	}
}

// TestEmptyRepoRefsPage verifies refs page renders for empty repos.
func TestEmptyRepoRefsPage(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "emptyrefs", "Empty refs")

	resp := ts.get(t, c, "/emptyrefs/refs/")
	assertStatus(t, resp, http.StatusOK)
	// Should render without crashing
	b := body(t, resp)
	if !strings.Contains(b, "emptyrefs") {
		t.Error("empty refs page should render without error")
	}
}

// TestBranchDeletionAfterMerge verifies merged branches are cleaned up.
func TestBranchDeletionAfterMerge(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "cleanup", "Branch cleanup")

	localDir := t.TempDir()
	bareRepo := repoPath(ts.cfg, "cleanup")
	gitCmd(t, localDir, "clone", "file://"+bareRepo, "repo")
	localRepo := localDir + "/repo"

	writeFile(t, localRepo, "f.txt", "v1")
	gitCmd(t, localRepo, "add", "f.txt")
	gitCmd(t, localRepo, "commit", "-m", "init")
	gitCmd(t, localRepo, "push", "origin", "main")

	// Create and push a feature branch
	gitCmd(t, localRepo, "checkout", "-b", "to-merge")
	writeFile(t, localRepo, "f.txt", "v2")
	gitCmd(t, localRepo, "add", "f.txt")
	gitCmd(t, localRepo, "commit", "-m", "improve")
	gitCmd(t, localRepo, "push", "origin", "to-merge")

	// Verify the branch exists on server
	refs := gitCmd(t, ts.dir, "--git-dir="+bareRepo, "for-each-ref", "refs/heads/")
	if !strings.Contains(refs, "to-merge") {
		t.Fatal("branch should exist before merge")
	}

	// Create and merge patch
	rp := repoPath(ts.cfg, "cleanup")
	patches, _ := listPatches(rp, "open")
	// Create patch if none exist (branches may appear as free branches)
	ts.postForm(t, c, "/cleanup/patches/new", map[string][]string{
		"title":  {"Merge me"},
		"body":   {"Body"},
		"branch": {"to-merge"},
	})
	patches, _ = listPatches(rp, "open")
	if len(patches) != 1 {
		t.Fatalf("expected 1 patch, got %d", len(patches))
	}

	ts.postForm(t, c, "/cleanup/patches/"+patches[0].ID+"/merge", map[string][]string{})

	// Branch should be deleted after merge
	refs = gitCmd(t, ts.dir, "--git-dir="+bareRepo, "for-each-ref", "refs/heads/")
	if strings.Contains(refs, "to-merge") {
		t.Error("merged branch should be deleted")
	}
}

// TestTagsListedOnRefsPage verifies tags appear on the refs page.
func TestTagsListedOnRefsPage(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "tagged", "Tagged repo")

	localDir := t.TempDir()
	bareRepo := repoPath(ts.cfg, "tagged")
	gitCmd(t, localDir, "clone", "file://"+bareRepo, "repo")
	localRepo := localDir + "/repo"

	writeFile(t, localRepo, "f.txt", "tag test")
	gitCmd(t, localRepo, "add", "f.txt")
	gitCmd(t, localRepo, "commit", "-m", "init")
	gitCmd(t, localRepo, "push", "origin", "main")

	gitCmd(t, localRepo, "tag", "-a", "v1.0.0", "-m", "first tag")
	gitCmd(t, localRepo, "push", "origin", "v1.0.0")

	resp := ts.get(t, c, "/tagged/refs/")
	assertStatus(t, resp, http.StatusOK)
	b := body(t, resp)
	if !strings.Contains(b, "v1.0.0") {
		t.Error("refs page should list tags")
	}
}

// TestTagTreeView verifies browsing a tree at a tag.
func TestTagTreeView(t *testing.T) {
	ts := newTestServer(t)
	c := ts.client()
	ts.setupUser(t, c, "dev", "secret123")
	ts.createRepo(t, c, "tagview", "Tag view")

	localDir := t.TempDir()
	bareRepo := repoPath(ts.cfg, "tagview")
	gitCmd(t, localDir, "clone", "file://"+bareRepo, "repo")
	localRepo := localDir + "/repo"

	writeFile(t, localRepo, "README.md", "# v1\n")
	gitCmd(t, localRepo, "add", "README.md")
	gitCmd(t, localRepo, "commit", "-m", "first")
	gitCmd(t, localRepo, "push", "origin", "main")
	gitCmd(t, localRepo, "tag", "-a", "v0.1.0", "-m", "release")
	gitCmd(t, localRepo, "push", "origin", "v0.1.0")

	// Change file after tag
	writeFile(t, localRepo, "README.md", "# v2\n")
	gitCmd(t, localRepo, "add", "README.md")
	gitCmd(t, localRepo, "commit", "-m", "second")
	gitCmd(t, localRepo, "push", "origin", "main")

	// Tree at tag should show old content
	resp := ts.get(t, c, "/tagview/tree/v0.1.0")
	assertStatus(t, resp, http.StatusOK)
	b := body(t, resp)
	// Blob at tag
	resp = ts.get(t, c, "/tagview/blob/v0.1.0/README.md")
	assertStatus(t, resp, http.StatusOK)
	b = body(t, resp)
	if !strings.Contains(b, "# v1") {
		t.Error("tag blob should show v1 content")
	}
	if strings.Contains(b, "# v2") {
		t.Error("tag blob should not show v2 content")
	}
}