internal/control/import_test.go
148 lines · 5416 bytes
8 symbols in this file
1package control
2
3import (
4 "bytes"
5 "context"
6 "net"
7 "net/http/cgi"
8 "net/http/httptest"
9 "net/url"
10 "os"
11 "path/filepath"
12 "slices"
13 "strings"
14 "testing"
15
16 "gitbay.org/gitbay/internal/protocol"
17 "gitbay.org/gitbay/internal/store"
18)
19
20func importCtx(t *testing.T, allowLocal bool) (*Ctx, *bytes.Buffer, *store.Store, string) {
21 t.Helper()
22 st, _, uid := newQueueTestRepo(t)
23 root := t.TempDir()
24 c, errOut := pruneCtx(st, root, store.User{ID: uid, Username: "alice"})
25 c.Cfg.Limits.WriteRate = -1
26 c.Cfg.Limits.CloneTimeoutSec = 60
27 c.Cfg.Webhooks.AllowLocal = allowLocal
28 c.Stdin = strings.NewReader("")
29 return c, errOut, st, root
30}
31
32// importUpstream serves a bare repository with one commit on main over
33// smart HTTP and returns its URL and that commit.
34func importUpstream(t *testing.T) (string, string) {
35 t.Helper()
36 git := gitRunner(t)
37 parent := t.TempDir()
38 bare := filepath.Join(parent, "remote.git")
39 work := filepath.Join(parent, "work")
40 git(parent, "init", "-q", "--bare", "--initial-branch=main", bare)
41 git(parent, "init", "-q", "--initial-branch=main", work)
42 git(work, "commit", "-q", "--allow-empty", "-m", "one")
43 git(work, "push", "-q", bare, "main")
44 sha := strings.TrimSpace(git(work, "rev-parse", "HEAD"))
45 execPath := strings.TrimSpace(git(parent, "--exec-path"))
46 srv := httptest.NewServer(&cgi.Handler{
47 Path: filepath.Join(execPath, "git-http-backend"),
48 Env: []string{"GIT_PROJECT_ROOT=" + parent, "GIT_HTTP_EXPORT_ALL=1"},
49 })
50 t.Cleanup(srv.Close)
51 return srv.URL + "/remote.git", sha
52}
53
54// git:// cannot be held to a checked address, so import refuses it
55// before creating anything (#298).
56func TestRepoImportRefusesGitScheme(t *testing.T) {
57 c, errOut, st, _ := importCtx(t, true)
58 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", "git://example.org/x.git"})
59 if code != protocol.ExitUsage || !strings.Contains(errOut.String(), "use the repository's https:// URL") {
60 t.Fatalf("exit %d, %q", code, errOut.String())
61 }
62 if _, err := st.RepoByPath("alice/x"); err == nil {
63 t.Fatal("a refused import created a repository")
64 }
65}
66
67// A reachable source on loopback is refused on a default instance, and
68// nothing is left behind.
69func TestRepoImportRefusesALocalAddress(t *testing.T) {
70 remote, _ := importUpstream(t)
71 c, errOut, st, root := importCtx(t, false)
72 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", remote})
73 if code != protocol.ExitFailure || !strings.Contains(errOut.String(), "private or local address") {
74 t.Fatalf("exit %d, %q", code, errOut.String())
75 }
76 if _, err := st.RepoByPath("alice/x"); err == nil {
77 t.Fatal("a refused import created a repository")
78 }
79 if _, err := os.Stat(RepoDir(root, "alice", "x")); !os.IsNotExist(err) {
80 t.Fatalf("a refused import left a directory: %v", err)
81 }
82}
83
84// A query or fragment could carry a credential into the log and the
85// event row; import refuses it before either.
86func TestRepoImportRefusesAQuery(t *testing.T) {
87 for _, from := range []string{"https://git.example/x.git?token=abc", "https://git.example/x.git#abc"} {
88 c, errOut, st, _ := importCtx(t, true)
89 code := Dispatch(c, []string{"repo", "import", "alice/x", "--from", from})
90 if code != protocol.ExitUsage || !strings.Contains(errOut.String(), "plain clone URL") || strings.Contains(errOut.String(), "abc") {
91 t.Fatalf("%s: exit %d, %q", from, code, errOut.String())
92 }
93 if _, err := st.RepoByPath("alice/x"); err == nil {
94 t.Fatalf("%s: a refused import created a repository", from)
95 }
96 }
97}
98
99// import.test does not resolve; the import works only because git was
100// pinned to the address import looked up and checked.
101func TestRepoImportConnectsToTheCheckedAddress(t *testing.T) {
102 importThroughPin(t, func(string) {})
103}
104
105// git runs with its own environment, not the daemon's: a proxy or a
106// global URL rewrite there would take it around the pin.
107func TestRepoImportIgnoresTheDaemonEnvironment(t *testing.T) {
108 importThroughPin(t, func(port string) {
109 t.Setenv("http_proxy", "http://127.0.0.1:1")
110 t.Setenv("HTTP_PROXY", "http://127.0.0.1:1")
111 global := filepath.Join(t.TempDir(), "gitconfig")
112 rewrite := "[url \"http://127.0.0.1:1/\"]\n\tinsteadOf = http://import.test:" + port + "/\n"
113 if err := os.WriteFile(global, []byte(rewrite), 0o644); err != nil {
114 t.Fatal(err)
115 }
116 t.Setenv("GIT_CONFIG_GLOBAL", global)
117 })
118}
119
120func importThroughPin(t *testing.T, setup func(port string)) {
121 t.Helper()
122 remote, sha := importUpstream(t)
123 u, _ := url.Parse(remote)
124 setup(u.Port())
125 c, errOut, _, root := importCtx(t, true)
126 var asked []string
127 prev := importLookup
128 importLookup = func(ctx context.Context, host string) ([]net.IP, error) {
129 asked = append(asked, host)
130 return []net.IP{net.ParseIP("127.0.0.1")}, nil
131 }
132 t.Cleanup(func() { importLookup = prev })
133
134 code := Dispatch(c, []string{"repo", "import", "alice/copy", "--from", "http://import.test:" + u.Port() + "/remote.git"})
135 if code != protocol.ExitOK {
136 t.Fatalf("exit %d: %s", code, errOut.String())
137 }
138 if out := c.Stdout.(*bytes.Buffer).String(); !strings.Contains(out, "default main") {
139 t.Fatalf("output %q: the default branch was not read through the pin", out)
140 }
141 got := strings.TrimSpace(gitRunner(t)(RepoDir(root, "alice", "copy"), "rev-parse", "refs/heads/main"))
142 if got != sha {
143 t.Fatalf("main = %s, want %s", got, sha)
144 }
145 if !slices.Equal(asked, []string{"import.test"}) {
146 t.Fatalf("looked up %v", asked)
147 }
148}