e2e/apiread_test.go

2d65be8962980c4c1c966517f54c1e8ff5d62b72
gitbay/e2e/apiread_test.go history · blame · raw

171 lines · 6288 bytes

  1package e2e
  2
  3import (
  4	"encoding/base64"
  5	"encoding/json"
  6	"os"
  7	"path/filepath"
  8	"strings"
  9	"testing"
 10)
 11
 12// TestRepoTreeAndCat covers reading repository contents over the control
 13// plane — the capability a native client needs and could not reach at all
 14// before: no command returned file contents, and the web's raw route
 15// authenticates by session cookie, not bearer token.
 16func TestRepoTreeAndCat(t *testing.T) {
 17	inst := startInstance(t)
 18	aliceKey := inst.newKey(t, "alice")
 19	bobKey := inst.newKey(t, "bob")
 20	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 21	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
 22	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app", "--private"); code != 0 {
 23		t.Fatalf("repo create: %s", errOut)
 24	}
 25
 26	work := t.TempDir()
 27	env := inst.gitEnv(aliceKey)
 28	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
 29	dir := filepath.Join(work, "w")
 30	os.WriteFile(filepath.Join(dir, "README.md"), []byte("# app\n\nhello\n"), 0o644)
 31	os.MkdirAll(filepath.Join(dir, "src"), 0o755)
 32	os.WriteFile(filepath.Join(dir, "src", "main.go"), []byte("package main\n"), 0o644)
 33	os.WriteFile(filepath.Join(dir, "logo.bin"), []byte{0x00, 0x01, 0x02, 0xff, 0x00}, 0o644)
 34	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 35	mustGit(t, dir, env, "add", ".")
 36	mustGit(t, dir, env, "commit", "-q", "-m", "base")
 37	mustGit(t, dir, env, "push", "-q", "origin", "main")
 38	mustGit(t, dir, env, "checkout", "-q", "-b", "feature")
 39	mustGit(t, dir, env, "push", "-q", "origin", "feature")
 40	mustGit(t, dir, env, "tag", "v1.0.0")
 41	mustGit(t, dir, env, "push", "-q", "origin", "v1.0.0")
 42
 43	// Refs are a control-plane read so native clients can present the same
 44	// branch and tag choices as the web without synthesizing them.
 45	out, errOut, code := inst.ssh(t, aliceKey, "", "repo", "refs", "alice/app", "--json")
 46	if code != 0 {
 47		t.Fatalf("repo refs: %s", errOut)
 48	}
 49	if !strings.Contains(out, `"name":"main"`) ||
 50		!strings.Contains(out, `"name":"feature"`) ||
 51		!strings.Contains(out, `"name":"v1.0.0"`) {
 52		t.Fatalf("repo refs output: %s", out)
 53	}
 54
 55	// Tree at the root: directories and files, with sizes and object ids.
 56	out, errOut, code = inst.ssh(t, aliceKey, "", "repo", "tree", "alice/app", "--json")
 57	if code != 0 {
 58		t.Fatalf("repo tree: %s", errOut)
 59	}
 60	var tree struct {
 61		Data struct {
 62			Ref     string `json:"ref"`
 63			Dir     string `json:"dir"`
 64			Entries []struct {
 65				Name string `json:"name"`
 66				Type string `json:"type"`
 67				SHA  string `json:"sha"`
 68				Size int64  `json:"size"`
 69			} `json:"entries"`
 70		} `json:"data"`
 71	}
 72	if err := json.Unmarshal([]byte(out), &tree); err != nil {
 73		t.Fatalf("tree JSON: %v\n%s", err, out)
 74	}
 75	if tree.Data.Ref != "main" {
 76		t.Errorf("ref = %q, want main", tree.Data.Ref)
 77	}
 78	byName := map[string]string{}
 79	for _, e := range tree.Data.Entries {
 80		byName[e.Name] = e.Type
 81		if e.SHA == "" {
 82			t.Errorf("%s has no object id, so a client cannot cache it", e.Name)
 83		}
 84		if e.Type == "blob" && e.Size == 0 {
 85			t.Errorf("%s reports no size", e.Name)
 86		}
 87	}
 88	if byName["src"] != "tree" || byName["README.md"] != "blob" {
 89		t.Fatalf("root listing wrong: %v", byName)
 90	}
 91
 92	// A subdirectory, and a file's contents.
 93	out, _, _ = inst.ssh(t, aliceKey, "", "repo", "tree", "alice/app", "src", "--json")
 94	if !strings.Contains(out, `"main.go"`) {
 95		t.Errorf("subdirectory listing: %s", out)
 96	}
 97	out, errOut, code = inst.ssh(t, aliceKey, "", "repo", "cat", "alice/app", "README.md", "--json")
 98	if code != 0 {
 99		t.Fatalf("repo cat: %s", errOut)
100	}
101	var file struct {
102		Data struct {
103			File      string `json:"file"`
104			Content   string `json:"content"`
105			Base64    string `json:"base64"`
106			Binary    bool   `json:"binary"`
107			Truncated bool   `json:"truncated"`
108		} `json:"data"`
109	}
110	if err := json.Unmarshal([]byte(out), &file); err != nil {
111		t.Fatalf("cat JSON: %v\n%q", err, out)
112	}
113	if file.Data.Content != "# app\n\nhello\n" || file.Data.Binary {
114		t.Fatalf("cat returned %+v", file.Data)
115	}
116
117	// Binary content comes back base64, never as broken UTF-8 in "content".
118	file.Data = struct {
119		File      string `json:"file"`
120		Content   string `json:"content"`
121		Base64    string `json:"base64"`
122		Binary    bool   `json:"binary"`
123		Truncated bool   `json:"truncated"`
124	}{}
125	out, errOut, code = inst.ssh(t, aliceKey, "", "repo", "cat", "alice/app", "logo.bin", "--json")
126	if code != 0 {
127		t.Fatalf("cat binary: exit %d %s", code, errOut)
128	}
129	if err := json.Unmarshal([]byte(out), &file); err != nil {
130		t.Fatalf("binary cat JSON: %v\n%q", err, out)
131	}
132	if !file.Data.Binary || file.Data.Content != "" || file.Data.Base64 == "" {
133		t.Fatalf("binary file not base64: %+v", file.Data)
134	}
135	if raw, err := base64.StdEncoding.DecodeString(file.Data.Base64); err != nil ||
136		len(raw) != 5 || raw[3] != 0xff {
137		t.Fatalf("base64 does not round-trip: %v %v", raw, err)
138	}
139
140	// Plain output is the file itself, so `repo cat` pipes like cat does.
141	out, _, code = inst.ssh(t, aliceKey, "", "repo", "cat", "alice/app", "README.md")
142	if code != 0 || out != "# app\n\nhello\n" {
143		t.Fatalf("plain cat = %q", out)
144	}
145
146	// Reads honour repository visibility: this repo is private.
147	if _, _, code := inst.ssh(t, bobKey, "", "repo", "tree", "alice/app"); code == 0 {
148		t.Error("a stranger listed a private repository's tree")
149	}
150	if _, _, code := inst.ssh(t, bobKey, "", "repo", "cat", "alice/app", "README.md"); code == 0 {
151		t.Error("a stranger read a private repository's file")
152	}
153	if _, _, code := inst.ssh(t, bobKey, "", "repo", "refs", "alice/app"); code == 0 {
154		t.Error("a stranger listed a private repository's refs")
155	}
156
157	// Paths cannot climb out of the repository.
158	for _, bad := range []string{"../../etc/passwd", "/etc/passwd", "src/../../.."} {
159		if _, _, code := inst.ssh(t, aliceKey, "", "repo", "cat", "alice/app", bad); code == 0 {
160			t.Errorf("path %q was accepted", bad)
161		}
162	}
163
164	// Missing things are not found rather than server errors.
165	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "cat", "alice/app", "nope.txt"); code != 3 {
166		t.Errorf("missing file exit = %d, want 3", code)
167	}
168	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "tree", "alice/app", "--ref", "nosuch"); code != 3 {
169		t.Errorf("missing ref exit = %d, want 3", code)
170	}
171}