e2e/http_test.go

8dcfa45a8ac03a5ff9c36828274d05acadcf846c
gitbay/e2e/http_test.go history · blame · raw

165 lines · 5828 bytes

  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	t.Parallel()
 68	inst := startInstance(t)
 69	aliceKey := setupPublicRepo(t, inst, "alice/pub")
 70
 71	// Anonymous clone of a public repo over HTTP.
 72	work := t.TempDir()
 73	mustGit(t, work, anonEnv(), "clone", inst.httpURL("alice/pub"), "c")
 74	dir := filepath.Join(work, "c")
 75	if data, err := os.ReadFile(filepath.Join(dir, "README")); err != nil || string(data) != "public\n" {
 76		t.Fatalf("cloned content wrong: %q, %v", data, err)
 77	}
 78
 79	// Push over HTTP: fatal remote error with the SSH URL, no credential
 80	// prompting of any kind — checked against every git version on this
 81	// machine (the pkt-line ERR mechanism must be version-independent).
 82	mustGit(t, dir, anonEnv(), "commit", "-q", "--allow-empty", "-m", "x")
 83	for _, gitBin := range gitBinaries() {
 84		cmd := exec.Command(gitBin, "push", "origin", "main")
 85		cmd.Dir = dir
 86		cmd.Env = anonEnv()
 87		rawOut, err := cmd.CombinedOutput()
 88		out := string(rawOut)
 89		if err == nil {
 90			t.Fatalf("[%s] push over http succeeded", gitBin)
 91		}
 92		if !strings.Contains(out, "remote error:") ||
 93			!strings.Contains(out, "pushes to this forge go over SSH") ||
 94			!strings.Contains(out, "git@gitbay.test:alice/pub.git") {
 95			t.Fatalf("[%s] push refusal output:\n%s", gitBin, out)
 96		}
 97		for _, banned := range []string{"Username", "Password", "Authentication failed", "terminal prompts disabled", "401", "403"} {
 98			if strings.Contains(out, banned) {
 99				t.Fatalf("[%s] push refusal fell into credential path (%q):\n%s", gitBin, banned, out)
100			}
101		}
102	}
103
104	// Private repo: 404 on the wire for anonymous HTTP, for both services
105	// and for a nonexistent repo — all indistinguishable.
106	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private")
107	if code != 0 {
108		t.Fatalf("create private: %s", errOut)
109	}
110	for _, u := range []string{
111		inst.httpURL("alice/secret") + "/info/refs?service=git-upload-pack",
112		inst.httpURL("alice/secret") + "/info/refs?service=git-receive-pack",
113		inst.httpURL("alice/nonexistent") + "/info/refs?service=git-upload-pack",
114	} {
115		resp, err := http.Get(u)
116		if err != nil {
117			t.Fatal(err)
118		}
119		body, _ := io.ReadAll(resp.Body)
120		resp.Body.Close()
121		if resp.StatusCode != http.StatusNotFound {
122			t.Fatalf("GET %s = %d, want 404\n%s", u, resp.StatusCode, body)
123		}
124	}
125	if out, code := gitRun(t, t.TempDir(), anonEnv(), "clone", inst.httpURL("alice/secret")); code == 0 {
126		t.Fatalf("anonymous clone of private repo succeeded:\n%s", out)
127	}
128}
129
130func TestGitDaemon(t *testing.T) {
131	t.Parallel()
132	inst := startInstance(t)
133	aliceKey := setupPublicRepo(t, inst, "alice/pub")
134	gitURL := func(repo string) string {
135		return fmt.Sprintf("git://127.0.0.1:%d/%s.git", inst.gitPort, repo)
136	}
137
138	// Not opted in yet: refused even though public.
139	if out, code := gitRun(t, t.TempDir(), anonEnv(), "clone", gitURL("alice/pub")); code == 0 {
140		t.Fatalf("git:// clone before opt-in succeeded:\n%s", out)
141	} else if !strings.Contains(out, "repository not exported") {
142		t.Fatalf("opt-out message:\n%s", out)
143	}
144
145	// Opt in, clone works.
146	_, errOut, code := inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/pub", "on")
147	if code != 0 {
148		t.Fatalf("git-daemon on: %s", errOut)
149	}
150	work := t.TempDir()
151	mustGit(t, work, anonEnv(), "clone", gitURL("alice/pub"), "c")
152	if data, _ := os.ReadFile(filepath.Join(work, "c", "README")); string(data) != "public\n" {
153		t.Fatalf("git:// clone content wrong: %q", data)
154	}
155
156	// Private repos cannot be opted in.
157	_, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private")
158	if code != 0 {
159		t.Fatal("create private failed")
160	}
161	_, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/secret", "on")
162	if code != 2 || !strings.Contains(errOut, "only public repositories") {
163		t.Fatalf("private opt-in: exit %d, %s", code, errOut)
164	}
165}