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