internal/control/import_test.go

v1.43.1
gitbay/internal/control/import_test.go history · blame · raw

148 lines · 5416 bytes

  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}