e2e/snippet_test.go
143 lines · 6012 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "regexp"
6 "strings"
7 "testing"
8)
9
10// Snippets over SSH: create from stdin, read back, list by visibility,
11// edit files and metadata, and the not-found rule for private ones.
12func TestSnippets(t *testing.T) {
13 t.Parallel()
14 inst := startInstanceWith(t, "[limits]\nmax_snippets_per_user = 3\n")
15 aliceKey := inst.newKey(t, "alice")
16 bobKey := inst.newKey(t, "bob")
17 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
18 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub", "--email", "bob@example.test", "--verified")
19 must := func(key, stdin string, args ...string) string {
20 t.Helper()
21 out, errOut, code := inst.ssh(t, key, stdin, args...)
22 if code != 0 {
23 t.Fatalf("%v: exit %d %s", args, code, errOut)
24 }
25 return out
26 }
27 fails := func(key, stdin string, want int, args ...string) string {
28 t.Helper()
29 _, errOut, code := inst.ssh(t, key, stdin, args...)
30 if code != want {
31 t.Fatalf("%v: exit %d, want %d: %s", args, code, want, errOut)
32 }
33 return errOut
34 }
35 idOf := func(out string) string {
36 t.Helper()
37 var env struct {
38 Data struct {
39 ID string `json:"id"`
40 URL string `json:"url"`
41 } `json:"data"`
42 }
43 if err := json.Unmarshal([]byte(out), &env); err != nil || !regexp.MustCompile(`^[0-9a-f]{12}$`).MatchString(env.Data.ID) {
44 t.Fatalf("create output: %s", out)
45 }
46 if !strings.HasSuffix(env.Data.URL, "/alice/-/snippets/"+env.Data.ID) {
47 t.Fatalf("url: %s", env.Data.URL)
48 }
49 return env.Data.ID
50 }
51
52 // Create with the default visibility, read back byte for byte.
53 body := "line one\nline two\n"
54 unlisted := idOf(must(aliceKey, body, "snippet", "create", "build.log", "--description", "'a log'", "--json"))
55 if got := must(aliceKey, "", "snippet", "file", "get", unlisted, "build.log"); got != body {
56 t.Fatalf("file get: %q", got)
57 }
58 out := must(aliceKey, "", "snippet", "show", unlisted, "--json")
59 if !strings.Contains(out, `"visibility":"unlisted"`) || !strings.Contains(out, `"content":"line one\nline two\n"`) {
60 t.Fatalf("show: %s", out)
61 }
62 public := idOf(must(aliceKey, "pub\n", "snippet", "create", "a.txt", "--visibility", "public", "--json"))
63 private := idOf(must(aliceKey, "sec\n", "snippet", "create", "b.txt", "--visibility", "private", "--json"))
64
65 // The per-account cap: three snippets exist, a fourth is refused.
66 if msg := fails(aliceKey, "x\n", 2, "snippet", "create", "c.txt"); !strings.Contains(msg, "snippet limit reached") {
67 t.Fatalf("cap refusal: %s", msg)
68 }
69
70 // Refusals on create: empty, not text, over the limit, bad name.
71 fails(aliceKey, "", 2, "snippet", "create", "x.txt")
72 fails(aliceKey, "\xff\xfe\n", 2, "snippet", "create", "x.bin")
73 fails(aliceKey, strings.Repeat("x", 1<<20+1), 2, "snippet", "create", "big.txt")
74 fails(aliceKey, "x\n", 2, "snippet", "create", "../x")
75 fails(aliceKey, "x\n", 2, "snippet", "create", "x.txt", "--visibility", "secret")
76
77 // Visibility from the other side. Private is not-found, never denied.
78 fails(bobKey, "", 3, "snippet", "show", private)
79 fails(bobKey, "", 3, "snippet", "file", "get", private, "b.txt")
80 must(bobKey, "", "snippet", "show", unlisted)
81 out = must(bobKey, "", "snippet", "list", "alice", "--json")
82 if !strings.Contains(out, public) || strings.Contains(out, unlisted) || strings.Contains(out, private) {
83 t.Fatalf("bob's view of alice's list: %s", out)
84 }
85 out = must(aliceKey, "", "snippet", "list", "--json")
86 for _, id := range []string{public, unlisted, private} {
87 if !strings.Contains(out, id) {
88 t.Fatalf("alice's own list lacks %s: %s", id, out)
89 }
90 }
91 fails(bobKey, "", 3, "snippet", "list", "nobody")
92
93 // Paging: two pages of one, the second reached by cursor.
94 out = must(aliceKey, "", "snippet", "list", "--limit", "1", "--json")
95 var page struct {
96 Data struct {
97 Items []struct {
98 ID string `json:"id"`
99 } `json:"items"`
100 Next string `json:"next"`
101 } `json:"data"`
102 }
103 json.Unmarshal([]byte(out), &page)
104 if len(page.Data.Items) != 1 || page.Data.Items[0].ID != private || page.Data.Next == "" {
105 t.Fatalf("first page: %s", out)
106 }
107 out = must(aliceKey, "", "snippet", "list", "--limit", "1", "--cursor", page.Data.Next, "--json")
108 if !strings.Contains(out, public) {
109 t.Fatalf("second page: %s", out)
110 }
111
112 // Files: set adds, set replaces, remove drops, the last one stays.
113 must(aliceKey, "notes\n", "snippet", "file", "set", unlisted, "notes.txt")
114 must(aliceKey, "changed\n", "snippet", "file", "set", unlisted, "build.log")
115 if got := must(aliceKey, "", "snippet", "file", "get", unlisted, "build.log"); got != "changed\n" {
116 t.Fatalf("after replace: %q", got)
117 }
118 must(aliceKey, "", "snippet", "file", "remove", unlisted, "notes.txt")
119 fails(aliceKey, "", 3, "snippet", "file", "remove", unlisted, "notes.txt")
120 if msg := fails(aliceKey, "", 2, "snippet", "file", "remove", unlisted, "build.log"); !strings.Contains(msg, "at least one file") {
121 t.Fatalf("last file removal: %s", msg)
122 }
123
124 // Only the owner writes: denied on a readable one, not-found on a private one.
125 fails(bobKey, "x\n", 4, "snippet", "file", "set", unlisted, "x.txt")
126 fails(bobKey, "", 4, "snippet", "edit", unlisted, "--description", "mine")
127 fails(bobKey, "", 4, "snippet", "delete", unlisted)
128 fails(bobKey, "", 3, "snippet", "delete", private)
129
130 // Edit moves visibility and the listing follows.
131 fails(aliceKey, "", 2, "snippet", "edit", unlisted)
132 must(aliceKey, "", "snippet", "edit", unlisted, "--visibility", "public", "--description", "shared")
133 out = must(bobKey, "", "snippet", "list", "alice", "--json")
134 if !strings.Contains(out, unlisted) || !strings.Contains(out, `"description":"shared"`) {
135 t.Fatalf("list after edit: %s", out)
136 }
137
138 // Delete, then gone; deleting the user takes the rest.
139 must(aliceKey, "", "snippet", "delete", unlisted)
140 fails(aliceKey, "", 3, "snippet", "show", unlisted)
141 inst.admin(t, "admin", "user", "delete", "alice", "--yes")
142 fails(bobKey, "", 3, "snippet", "show", public)
143}