e2e/lfs_test.go

v1.32.0
gitbay/e2e/lfs_test.go history · blame · raw

175 lines · 6192 bytes

  1package e2e
  2
  3import (
  4	"bytes"
  5	"crypto/rand"
  6	"crypto/sha256"
  7	"encoding/hex"
  8	"encoding/json"
  9	"fmt"
 10	"net"
 11	"net/http"
 12	"os"
 13	"os/exec"
 14	"path/filepath"
 15	"strings"
 16	"testing"
 17	"time"
 18)
 19
 20func waitForPort(t *testing.T, port int) {
 21	t.Helper()
 22	deadline := time.Now().Add(10 * time.Second)
 23	for {
 24		conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 200*time.Millisecond)
 25		if err == nil {
 26			conn.Close()
 27			return
 28		}
 29		if time.Now().After(deadline) {
 30			t.Fatal("listener did not come back")
 31		}
 32		time.Sleep(50 * time.Millisecond)
 33	}
 34}
 35
 36func TestLFS(t *testing.T) {
 37	if _, err := exec.LookPath("git-lfs"); err != nil {
 38		t.Skip("git-lfs client not installed")
 39	}
 40	inst := startInstance(t)
 41	// LFS hands clients absolute hrefs built from site_url; point it at
 42	// the live HTTP listener so the real git-lfs client can follow them.
 43	inst.proc.Process.Kill()
 44	inst.proc.Wait()
 45	raw, err := os.ReadFile(inst.config)
 46	if err != nil {
 47		t.Fatal(err)
 48	}
 49	raw = bytes.Replace(raw, []byte(`site_url = "https://gitbay.test"`),
 50		[]byte(fmt.Sprintf(`site_url = "http://127.0.0.1:%d"`, inst.httpPort)), 1)
 51	os.WriteFile(inst.config, raw, 0o600)
 52	inst.proc = exec.Command(inst.gitbayd, "--config", inst.config, "serve")
 53	inst.proc.Stderr = os.Stderr
 54	if err := inst.proc.Start(); err != nil {
 55		t.Fatal(err)
 56	}
 57	waitForPort(t, inst.port)
 58
 59	aliceKey := inst.newKey(t, "alice")
 60	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 61
 62	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/big"); code != 0 {
 63		t.Fatalf("repo create: %s", errOut)
 64	}
 65	env := inst.gitEnv(aliceKey)
 66	work := t.TempDir()
 67	mustGit(t, work, env, "clone", inst.sshURL("alice/big"), "w")
 68	dir := filepath.Join(work, "w")
 69	mustGit(t, dir, env, "lfs", "install", "--local")
 70	mustGit(t, dir, env, "lfs", "track", "*.bin")
 71	payload := make([]byte, 1<<20)
 72	rand.Read(payload)
 73	os.WriteFile(filepath.Join(dir, "data.bin"), payload, 0o644)
 74	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 75	mustGit(t, dir, env, "add", ".")
 76	mustGit(t, dir, env, "commit", "-q", "-m", "big file")
 77	mustGit(t, dir, env, "push", "-q", "origin", "main")
 78
 79	// The object landed in content-addressed storage, not in git.
 80	oid := sha256.Sum256(payload)
 81	oidHex := hex.EncodeToString(oid[:])
 82	stored := filepath.Join(inst.root, "lfs", oidHex[:2], oidHex[2:4], oidHex)
 83	if fi, err := os.Stat(stored); err != nil || fi.Size() != int64(len(payload)) {
 84		t.Fatalf("object not in lfs store: %v", err)
 85	}
 86
 87	// A fresh SSH clone round-trips the content through the smudge filter.
 88	work2 := t.TempDir()
 89	mustGit(t, work2, env, "clone", inst.sshURL("alice/big"), "w")
 90	dir2 := filepath.Join(work2, "w")
 91	mustGit(t, dir2, env, "lfs", "install", "--local")
 92	mustGit(t, dir2, env, "lfs", "pull", "origin")
 93	got, err := os.ReadFile(filepath.Join(dir2, "data.bin"))
 94	if err != nil || !bytes.Equal(got, payload) {
 95		t.Fatalf("ssh round-trip: %v, %d bytes", err, len(got))
 96	}
 97
 98	// Anonymous HTTPS: public repos serve LFS downloads with no credentials.
 99	httpURL := fmt.Sprintf("http://127.0.0.1:%d/alice/big.git", inst.httpPort)
100	work3 := t.TempDir()
101	mustGit(t, work3, env, "clone", httpURL, "w")
102	dir3 := filepath.Join(work3, "w")
103	mustGit(t, dir3, env, "lfs", "install", "--local")
104	mustGit(t, dir3, env, "lfs", "pull", "origin")
105	if got, err := os.ReadFile(filepath.Join(dir3, "data.bin")); err != nil || !bytes.Equal(got, payload) {
106		t.Fatalf("anonymous http round-trip: %v, %d bytes", err, len(got))
107	}
108
109	// Anonymous upload is refused; so is anything on a private repo.
110	batch := func(repo, op, auth string) int {
111		body := fmt.Sprintf(`{"operation":%q,"transfers":["basic"],"objects":[{"oid":%q,"size":4}]}`, op, oidHex)
112		req, _ := http.NewRequest("POST",
113			fmt.Sprintf("http://127.0.0.1:%d/alice/%s.git/info/lfs/objects/batch", inst.httpPort, repo),
114			strings.NewReader(body))
115		req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
116		if auth != "" {
117			req.Header.Set("Authorization", auth)
118		}
119		resp, err := http.DefaultClient.Do(req)
120		if err != nil {
121			t.Fatal(err)
122		}
123		resp.Body.Close()
124		return resp.StatusCode
125	}
126	if code := batch("big", "upload", ""); code != 403 {
127		t.Fatalf("anonymous upload: %d", code)
128	}
129	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/vault", "--private"); code != 0 {
130		t.Fatal("private repo create failed")
131	}
132	if code := batch("vault", "download", ""); code != 404 {
133		t.Fatalf("anonymous private batch: %d", code)
134	}
135
136	// Access rules over SSH: a stranger's authenticate on a private repo
137	// reads as nonexistence; upload needs write.
138	bobKey := inst.newKey(t, "bob")
139	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
140	if _, errOut, code := inst.ssh(t, bobKey, "", "git-lfs-authenticate", "alice/vault", "download"); code != 3 || !strings.Contains(errOut, "not found") {
141		t.Fatalf("stranger authenticate: exit %d, %s", code, errOut)
142	}
143	if _, errOut, code := inst.ssh(t, bobKey, "", "git-lfs-authenticate", "alice/big", "upload"); code != 4 || !strings.Contains(errOut, "denied") {
144		t.Fatalf("read-only upload authenticate: exit %d, %s", code, errOut)
145	}
146
147	// A corrupt upload is refused and stores nothing: mint an upload token
148	// via authenticate, then PUT a body that does not match the oid.
149	out, _, code := inst.ssh(t, aliceKey, "", "git-lfs-authenticate", "alice/big", "upload")
150	if code != 0 {
151		t.Fatalf("authenticate: %s", out)
152	}
153	var grant struct {
154		Header map[string]string `json:"header"`
155	}
156	if err := json.Unmarshal([]byte(out), &grant); err != nil {
157		t.Fatalf("authenticate JSON: %v\n%s", err, out)
158	}
159	fakeOID := strings.Repeat("ab", 32)
160	req, _ := http.NewRequest("PUT",
161		fmt.Sprintf("http://127.0.0.1:%d/alice/big.git/info/lfs/objects/%s", inst.httpPort, fakeOID),
162		strings.NewReader("not the content"))
163	req.Header.Set("Authorization", grant.Header["Authorization"])
164	resp, err := http.DefaultClient.Do(req)
165	if err != nil {
166		t.Fatal(err)
167	}
168	resp.Body.Close()
169	if resp.StatusCode != 422 {
170		t.Fatalf("corrupt upload: %d", resp.StatusCode)
171	}
172	if _, err := os.Stat(filepath.Join(inst.root, "lfs", "ab", "ab", fakeOID)); err == nil {
173		t.Fatal("corrupt object was stored")
174	}
175}