internal/mirror/mirror_test.go

v1.37.0
gitbay/internal/mirror/mirror_test.go history · blame · raw

227 lines · 7418 bytes

  1package mirror
  2
  3import (
  4	"context"
  5	"net"
  6	"net/http/cgi"
  7	"net/http/httptest"
  8	"net/url"
  9	"os"
 10	"os/exec"
 11	"path/filepath"
 12	"slices"
 13	"strings"
 14	"testing"
 15
 16	"gitbay.org/gitbay/internal/config"
 17	"gitbay.org/gitbay/internal/control"
 18	"gitbay.org/gitbay/internal/gitpin"
 19	"gitbay.org/gitbay/internal/store"
 20)
 21
 22func git(t *testing.T, dir string, args ...string) string {
 23	t.Helper()
 24	cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
 25	cmd.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "HOME="+t.TempDir(),
 26		"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
 27		"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test")
 28	out, err := cmd.CombinedOutput()
 29	if err != nil {
 30		t.Fatalf("git %v: %v\n%s", args, err, out)
 31	}
 32	return strings.TrimSpace(string(out))
 33}
 34
 35// upstream serves a bare repository with one commit on main over smart
 36// HTTP and returns its URL and that commit.
 37func upstream(t *testing.T) (string, string) {
 38	t.Helper()
 39	parent := t.TempDir()
 40	bare := filepath.Join(parent, "remote.git")
 41	work := filepath.Join(parent, "work")
 42	git(t, parent, "init", "-q", "--bare", "--initial-branch=main", bare)
 43	git(t, parent, "init", "-q", "--initial-branch=main", work)
 44	git(t, work, "commit", "-q", "--allow-empty", "-m", "one")
 45	git(t, work, "push", "-q", bare, "main")
 46	sha := git(t, work, "rev-parse", "HEAD")
 47	execPath := git(t, parent, "--exec-path")
 48	srv := httptest.NewServer(&cgi.Handler{
 49		Path: filepath.Join(execPath, "git-http-backend"),
 50		Env:  []string{"GIT_PROJECT_ROOT=" + parent, "GIT_HTTP_EXPORT_ALL=1"},
 51	})
 52	t.Cleanup(srv.Close)
 53	return srv.URL + "/remote.git", sha
 54}
 55
 56// local returns a store with alice/app, its bare repository under root,
 57// and the pull mirror row for url.
 58func local(t *testing.T, root, mirrorURL string) (*store.Store, store.Mirror, string) {
 59	t.Helper()
 60	st, err := store.Open(filepath.Join(t.TempDir(), "gitbay.db"))
 61	if err != nil {
 62		t.Fatal(err)
 63	}
 64	t.Cleanup(func() { st.Close() })
 65	if err := st.MigrateUp(); err != nil {
 66		t.Fatal(err)
 67	}
 68	uid, err := st.CreateUser("alice", false)
 69	if err != nil {
 70		t.Fatal(err)
 71	}
 72	repoID, err := st.CreateRepo("user", uid, "app", "public")
 73	if err != nil {
 74		t.Fatal(err)
 75	}
 76	dir := control.RepoDir(root, "alice", "app")
 77	os.MkdirAll(filepath.Dir(dir), 0o755)
 78	git(t, root, "init", "-q", "--bare", dir)
 79	if _, err := st.AddMirror(repoID, "pull", mirrorURL, "", ""); err != nil {
 80		t.Fatal(err)
 81	}
 82	due, err := st.DueMirrors(900)
 83	if err != nil || len(due) != 1 {
 84		t.Fatalf("due mirrors: %v %v", due, err)
 85	}
 86	return st, due[0], dir
 87}
 88
 89// mirror.test does not resolve; the fetch works only because git was
 90// pinned to the address the worker looked up and checked.
 91func TestSyncConnectsToTheCheckedAddress(t *testing.T) {
 92	remote, sha := upstream(t)
 93	u, _ := url.Parse(remote)
 94	root := t.TempDir()
 95	st, m, dir := local(t, root, "http://mirror.test:"+u.Port()+"/remote.git")
 96	var cfg config.Config
 97	cfg.Server.Root = root
 98	cfg.Webhooks.AllowLocal = true
 99	var asked []string
100	w := &Worker{St: st, Cfg: cfg, Lookup: func(ctx context.Context, host string) ([]net.IP, error) {
101		asked = append(asked, host)
102		return []net.IP{net.ParseIP("127.0.0.1")}, nil
103	}}
104	if err := w.sync(m); err != nil {
105		t.Fatal(err)
106	}
107	if got := git(t, dir, "rev-parse", "refs/heads/main"); got != sha {
108		t.Fatalf("main = %s, want %s", got, sha)
109	}
110	if !slices.Equal(asked, []string{"mirror.test"}) {
111		t.Fatalf("looked up %v", asked)
112	}
113}
114
115// The server account's own gitconfig cannot route git around the pin:
116// a proxy and a URL rewrite in HOME's config are both ignored.
117func TestSyncIgnoresGlobalGitConfig(t *testing.T) {
118	remote, sha := upstream(t)
119	u, _ := url.Parse(remote)
120	root := t.TempDir()
121	st, m, dir := local(t, root, "http://mirror.test:"+u.Port()+"/remote.git")
122	conf := "[http]\n\tproxy = http://127.0.0.1:9\n[url \"http://elsewhere.test/\"]\n\tinsteadOf = http://mirror.test:" + u.Port() + "/\n"
123	if err := os.WriteFile(filepath.Join(root, ".gitconfig"), []byte(conf), 0o644); err != nil {
124		t.Fatal(err)
125	}
126	var cfg config.Config
127	cfg.Server.Root = root
128	cfg.Webhooks.AllowLocal = true
129	w := &Worker{St: st, Cfg: cfg, Lookup: func(context.Context, string) ([]net.IP, error) {
130		return []net.IP{net.ParseIP("127.0.0.1")}, nil
131	}}
132	if err := w.sync(m); err != nil {
133		t.Fatal(err)
134	}
135	if got := git(t, dir, "rev-parse", "refs/heads/main"); got != sha {
136		t.Fatalf("main = %s, want %s", got, sha)
137	}
138}
139
140// A git too old for http.curloptResolve would ignore the pin; the
141// sweep refuses to sync and says why on every due mirror.
142func TestSweepRefusesWithAnOldGit(t *testing.T) {
143	root := t.TempDir()
144	st, m, _ := local(t, root, "https://mirror.test/x.git")
145	var cfg config.Config
146	cfg.Server.Root = root
147	cfg.Mirrors.PullIntervalMinutes = 15
148	w := &Worker{St: st, Cfg: cfg, Lookup: func(context.Context, string) ([]net.IP, error) {
149		t.Fatal("looked up a host with an old git")
150		return nil, nil
151	}}
152	w.gitErr = gitpin.VersionOK("git version 2.36.1")
153	w.sweep()
154	ms, err := st.ListMirrors(m.RepoID)
155	if err != nil || len(ms) != 1 {
156		t.Fatalf("mirrors: %v %v", ms, err)
157	}
158	if !strings.Contains(ms[0].LastError, "2.37") {
159		t.Fatalf("last error = %q", ms[0].LastError)
160	}
161}
162
163// The URL passed the check when it was saved; the answer at sync time
164// is what counts.
165func TestSyncRefusesAPrivateAddressAtSyncTime(t *testing.T) {
166	root := t.TempDir()
167	st, m, _ := local(t, root, "https://mirror.test/x.git")
168	var cfg config.Config
169	cfg.Server.Root = root
170	w := &Worker{St: st, Cfg: cfg, Lookup: func(context.Context, string) ([]net.IP, error) {
171		return []net.IP{net.ParseIP("10.0.0.7")}, nil
172	}}
173	err := w.sync(m)
174	if err == nil || !strings.Contains(err.Error(), "10.0.0.7") {
175		t.Fatalf("sync = %v, want a refusal naming 10.0.0.7", err)
176	}
177}
178
179// A refusal is a sync failure like any other: the sweep records it on
180// the mirror, where repo mirror list shows it.
181func TestSweepRecordsTheRefusal(t *testing.T) {
182	root := t.TempDir()
183	st, m, _ := local(t, root, "https://mirror.test/x.git")
184	var cfg config.Config
185	cfg.Server.Root = root
186	cfg.Mirrors.PullIntervalMinutes = 15
187	w := &Worker{St: st, Cfg: cfg, Lookup: func(context.Context, string) ([]net.IP, error) {
188		return []net.IP{net.ParseIP("100.64.0.9")}, nil
189	}}
190	w.sweep()
191	ms, err := st.ListMirrors(m.RepoID)
192	if err != nil || len(ms) != 1 {
193		t.Fatalf("mirrors: %v %v", ms, err)
194	}
195	if !strings.Contains(ms[0].LastError, "100.64.0.9") {
196		t.Fatalf("last error = %q", ms[0].LastError)
197	}
198}
199
200func TestSyncRefusesAnEmptyAnswer(t *testing.T) {
201	root := t.TempDir()
202	st, m, _ := local(t, root, "https://mirror.test/x.git")
203	var cfg config.Config
204	cfg.Server.Root = root
205	cfg.Webhooks.AllowLocal = true
206	w := &Worker{St: st, Cfg: cfg, Lookup: func(context.Context, string) ([]net.IP, error) {
207		return nil, nil
208	}}
209	if err := w.sync(m); err == nil || !strings.Contains(err.Error(), "no address") {
210		t.Fatalf("sync = %v, want a refusal", err)
211	}
212}
213
214func TestSyncRefusesANonHTTPScheme(t *testing.T) {
215	root := t.TempDir()
216	st, m, _ := local(t, root, "ssh://mirror.test/x.git")
217	var cfg config.Config
218	cfg.Server.Root = root
219	cfg.Webhooks.AllowLocal = true
220	w := &Worker{St: st, Cfg: cfg, Lookup: func(context.Context, string) ([]net.IP, error) {
221		t.Fatal("looked up a host for an ssh URL")
222		return nil, nil
223	}}
224	if err := w.sync(m); err == nil || !strings.Contains(err.Error(), "not http or https") {
225		t.Fatalf("sync = %v, want a refusal", err)
226	}
227}