krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: e2e/http_test.go · raw

  1package e2e
  2
  3import (
  4	"fmt"
  5	"io"
  6	"net/http"
  7	"os"
  8	"os/exec"
  9	"path/filepath"
 10	"strings"
 11	"testing"
 12)
 13
 14// gitBinaries returns every distinct git on this machine, so transport
 15// behavior is verified against more than one client version.
 16func gitBinaries() []string {
 17	bins := []string{"git"}
 18	if _, err := os.Stat("/usr/bin/git"); err == nil {
 19		bins = append(bins, "/usr/bin/git")
 20	}
 21	return bins
 22}
 23
 24// setupPublicRepo creates alice with a public repo containing one commit and
 25// returns her key path.
 26func setupPublicRepo(t *testing.T, inst *instance, repo string) string {
 27	t.Helper()
 28	aliceKey := inst.newKey(t, "alice")
 29	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 30	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", repo)
 31	if code != 0 {
 32		t.Fatalf("repo create: %s", errOut)
 33	}
 34	work := t.TempDir()
 35	env := inst.gitEnv(aliceKey)
 36	mustGit(t, work, env, "clone", inst.sshURL(repo), "w")
 37	dir := filepath.Join(work, "w")
 38	if err := os.WriteFile(filepath.Join(dir, "README"), []byte("public\n"), 0o644); err != nil {
 39		t.Fatal(err)
 40	}
 41	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 42	mustGit(t, dir, env, "add", "README")
 43	mustGit(t, dir, env, "commit", "-q", "-m", "init")
 44	mustGit(t, dir, env, "push", "-q", "origin", "main")
 45	return aliceKey
 46}
 47
 48// anonEnv is a git environment with no credentials and prompting hard-failed:
 49// if git ever tries to ask for a username or password, the command errors
 50// with a distinctive message instead of hanging.
 51func anonEnv() []string {
 52	return append(os.Environ(),
 53		"GIT_TERMINAL_PROMPT=0",
 54		"GIT_ASKPASS=false",
 55		"GIT_CONFIG_NOSYSTEM=1",
 56		"GIT_CONFIG_GLOBAL=/dev/null", // no ~/.gitconfig credential helpers or signing
 57		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
 58		"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test",
 59	)
 60}
 61
 62func (i *instance) httpURL(repo string) string {
 63	return fmt.Sprintf("http://127.0.0.1:%d/%s.git", i.httpPort, repo)
 64}
 65
 66func TestHTTPTransport(t *testing.T) {
 67	inst := startInstance(t)
 68	aliceKey := setupPublicRepo(t, inst, "alice/pub")
 69
 70	// Anonymous clone of a public repo over HTTP.
 71	work := t.TempDir()
 72	mustGit(t, work, anonEnv(), "clone", inst.httpURL("alice/pub"), "c")
 73	dir := filepath.Join(work, "c")
 74	if data, err := os.ReadFile(filepath.Join(dir, "README")); err != nil || string(data) != "public\n" {
 75		t.Fatalf("cloned content wrong: %q, %v", data, err)
 76	}
 77
 78	// Push over HTTP: fatal remote error with the SSH URL, no credential
 79	// prompting of any kind — checked against every git version on this
 80	// machine (the pkt-line ERR mechanism must be version-independent).
 81	mustGit(t, dir, anonEnv(), "commit", "-q", "--allow-empty", "-m", "x")
 82	for _, gitBin := range gitBinaries() {
 83		cmd := exec.Command(gitBin, "push", "origin", "main")
 84		cmd.Dir = dir
 85		cmd.Env = anonEnv()
 86		rawOut, err := cmd.CombinedOutput()
 87		out := string(rawOut)
 88		if err == nil {
 89			t.Fatalf("[%s] push over http succeeded", gitBin)
 90		}
 91		if !strings.Contains(out, "remote error:") ||
 92			!strings.Contains(out, "pushes to this forge go over SSH") ||
 93			!strings.Contains(out, "git@gitbay.test:alice/pub.git") {
 94			t.Fatalf("[%s] push refusal output:\n%s", gitBin, out)
 95		}
 96		for _, banned := range []string{"Username", "Password", "Authentication failed", "terminal prompts disabled", "401", "403"} {
 97			if strings.Contains(out, banned) {
 98				t.Fatalf("[%s] push refusal fell into credential path (%q):\n%s", gitBin, banned, out)
 99			}
100		}
101	}
102
103	// Private repo: 404 on the wire for anonymous HTTP, for both services
104	// and for a nonexistent repo — all indistinguishable.
105	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private")
106	if code != 0 {
107		t.Fatalf("create private: %s", errOut)
108	}
109	for _, u := range []string{
110		inst.httpURL("alice/secret") + "/info/refs?service=git-upload-pack",
111		inst.httpURL("alice/secret") + "/info/refs?service=git-receive-pack",
112		inst.httpURL("alice/nonexistent") + "/info/refs?service=git-upload-pack",
113	} {
114		resp, err := http.Get(u)
115		if err != nil {
116			t.Fatal(err)
117		}
118		body, _ := io.ReadAll(resp.Body)
119		resp.Body.Close()
120		if resp.StatusCode != http.StatusNotFound {
121			t.Fatalf("GET %s = %d, want 404\n%s", u, resp.StatusCode, body)
122		}
123	}
124	if out, code := gitRun(t, t.TempDir(), anonEnv(), "clone", inst.httpURL("alice/secret")); code == 0 {
125		t.Fatalf("anonymous clone of private repo succeeded:\n%s", out)
126	}
127}
128
129func TestGitDaemon(t *testing.T) {
130	inst := startInstance(t)
131	aliceKey := setupPublicRepo(t, inst, "alice/pub")
132	gitURL := func(repo string) string {
133		return fmt.Sprintf("git://127.0.0.1:%d/%s.git", inst.gitPort, repo)
134	}
135
136	// Not opted in yet: refused even though public.
137	if out, code := gitRun(t, t.TempDir(), anonEnv(), "clone", gitURL("alice/pub")); code == 0 {
138		t.Fatalf("git:// clone before opt-in succeeded:\n%s", out)
139	} else if !strings.Contains(out, "repository not exported") {
140		t.Fatalf("opt-out message:\n%s", out)
141	}
142
143	// Opt in, clone works.
144	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/pub", "on")
145	if code != 0 {
146		t.Fatalf("git-daemon on: %s", errOut)
147	}
148	work := t.TempDir()
149	mustGit(t, work, anonEnv(), "clone", gitURL("alice/pub"), "c")
150	if data, _ := os.ReadFile(filepath.Join(work, "c", "README")); string(data) != "public\n" {
151		t.Fatalf("git:// clone content wrong: %q", data)
152	}
153
154	// Private repos cannot be opted in.
155	_, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private")
156	if code != 0 {
157		t.Fatal("create private failed")
158	}
159	_, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/secret", "on")
160	if code != 2 || !strings.Contains(errOut, "only public repositories") {
161		t.Fatalf("private opt-in: exit %d, %s", code, errOut)
162	}
163}