e2e/apiread_test.go

v1.40.1
gitbay/e2e/apiread_test.go history · blame · raw

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