e2e/apiread_test.go
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}