krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: 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}