Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

ns_test.gno

9.96 Kb · 299 lines
  1package ns
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nt/testutils/v0"
  9	"gno.land/p/nt/uassert/v0"
 10
 11	ninep "gno.land/p/moul/x/plan9/ninep/v0"
 12	synfs "gno.land/p/moul/x/plan9/synfs/v0"
 13)
 14
 15// probe is a tiny read-only server, standing in for another realm's tree.
 16func probe(name, contents string) ninep.File {
 17	t := synfs.New(name, "sys", func() int64 { return 1 })
 18	t.Root().Add("value", func() string { return contents })
 19	return t.Root()
 20}
 21
 22func TestDemoNamespaceIsSeeded(t *testing.T) {
 23	seedDemo()
 24	out, err := Run(DemoKey, "ls -u /bin")
 25	if err != nil {
 26		t.Fatalf("ls: %v", err)
 27	}
 28	if out != "ls\nrc\n" {
 29		t.Errorf("the demo /bin should be a union of two directories: %q", out)
 30	}
 31	if got := Namespace(DemoKey); !strings.Contains(got, "bind -ac /usr/glenda/bin /bin") {
 32		t.Errorf("mount table: %q", got)
 33	}
 34	if got, _ := Run(DemoKey, "cat /tmp/greeting"); got != "hello from a namespace\n" {
 35		t.Errorf("greeting: %q", got)
 36	}
 37}
 38
 39func TestRunIsReadOnly(t *testing.T) {
 40	seedDemo()
 41	if _, err := Run(DemoKey, "echo x > /tmp/f"); err == nil {
 42		t.Fatal("Render's shell must refuse a write")
 43	}
 44	if _, err := Run(DemoKey, "rm /tmp/greeting"); err == nil {
 45		t.Fatal("Render's shell must refuse a remove")
 46	}
 47	if _, err := Run("nobody", "ls /"); err == nil {
 48		t.Fatal("an unknown namespace should not be created by a read")
 49	}
 50}
 51
 52func TestPostAndBind(cur realm, t *testing.T) {
 53	services.Remove("probe")
 54	Post(cross(cur), "probe", probe("probe", "42"))
 55
 56	found := false
 57	for _, s := range Services() {
 58		if s == "probe" {
 59			found = true
 60		}
 61	}
 62	if !found {
 63		t.Fatalf("probe is not in /srv: %v", Services())
 64	}
 65
 66	// A fresh namespace sees it through /srv without importing anything.
 67	spaces.Remove("tester")
 68	spaceFor("tester")
 69	defer spaces.Remove("tester")
 70	out, err := Run("tester", "cat /srv/probe/value")
 71	if err != nil {
 72		t.Fatalf("cat through /srv: %v", err)
 73	}
 74	if out != "42" {
 75		t.Errorf("got %q, want 42", out)
 76	}
 77	services.Remove("probe")
 78}
 79
 80func TestExecWritesToTheCallersOwnNamespace(cur realm, t *testing.T) {
 81	alice := testutils.TestAddress("alice")
 82	bob := testutils.TestAddress("bob")
 83	spaces.Remove(alice.String())
 84	spaces.Remove(bob.String())
 85
 86	testing.SetRealm(testing.NewUserRealm(alice))
 87	Exec(cross(cur), "echo 'alice was here' > /tmp/note")
 88
 89	testing.SetRealm(testing.NewUserRealm(bob))
 90	Exec(cross(cur), "echo 'bob was here' > /tmp/note")
 91
 92	got, err := Run(alice.String(), "cat /tmp/note")
 93	if err != nil {
 94		t.Fatalf("alice: %v", err)
 95	}
 96	if got != "alice was here\n" {
 97		t.Errorf("alice's namespace: %q", got)
 98	}
 99	if got, _ = Run(bob.String(), "cat /tmp/note"); got != "bob was here\n" {
100		t.Errorf("bob's namespace: %q", got)
101	}
102
103	// Reset throws a namespace away; the next use rebuilds the default.
104	testing.SetRealm(testing.NewUserRealm(alice))
105	Reset(cross(cur))
106	if _, err := Run(alice.String(), "cat /tmp/note"); err == nil {
107		t.Error("the namespace should be gone after Reset")
108	}
109	spaces.Remove(alice.String())
110	spaces.Remove(bob.String())
111}
112
113func TestExecBindsIntoTheCallersNamespace(cur realm, t *testing.T) {
114	services.Remove("probe")
115	Post(cross(cur), "probe", probe("probe", "42"))
116	defer services.Remove("probe")
117
118	erin := testutils.TestAddress("erin")
119	spaces.Remove(erin.String())
120	testing.SetRealm(testing.NewUserRealm(erin))
121	Exec(cross(cur), "bind /srv/probe /dev")
122
123	if got := mustRun(t, erin.String(), "cat /dev/value"); got != "42" {
124		t.Errorf("read through the mount: %q", got)
125	}
126	if ns := Namespace(erin.String()); !strings.Contains(ns, "bind /srv/probe /dev") {
127		t.Errorf("mount table: %q", ns)
128	}
129	spaces.Remove(erin.String())
130}
131
132func TestSrvListingUsesThePostedName(cur realm, t *testing.T) {
133	services.Remove("aliased")
134	// The server calls its own root "probe"; /srv must show it as "aliased".
135	Post(cross(cur), "aliased", probe("probe", "x"))
136	defer services.Remove("aliased")
137
138	spaces.Remove("lister")
139	spaceFor("lister")
140	out := mustRun(t, "lister", "ls /srv")
141	if !strings.Contains(out, "aliased") {
142		t.Errorf("ls /srv: %q", out)
143	}
144	if strings.Contains(out, "probe") {
145		t.Errorf("/srv leaked the server's own root name: %q", out)
146	}
147	spaces.Remove("lister")
148}
149
150func mustRun(t *testing.T, key, line string) string {
151	t.Helper()
152	out, err := Run(key, line)
153	if err != nil {
154		t.Fatalf("%s: %v", line, err)
155	}
156	return out
157}
158
159// --- the boundary. Three properties the suite would be unsafe without. -------
160
161// A /srv name belongs to the realm that posted it: first come, first served,
162// and then locked.
163func TestPostRejectsASecondOwner(cur realm, t *testing.T) {
164	services.Remove("taken")
165	Post(cross(cur), "taken", probe("taken", "first"))
166	defer services.Remove("taken")
167
168	testing.SetRealm(testing.NewCodeRealm("gno.land/r/other/thing/v0"))
169	uassert.AbortsWithMessage(t, cur,
170		"post: taken is already posted by gno.land/r/moul/x/plan9/ns/v1",
171		func() { Post(cross(cur), "taken", probe("taken", "second")) })
172}
173
174// Grafting another realm's tree into your namespace gives you reads and
175// nothing else. ninep.File has no mutating method, so there is no route from a
176// mount to a write against the realm that posted it.
177func TestMountedTreesAreReadOnly(cur realm, t *testing.T) {
178	services.Remove("probe")
179	Post(cross(cur), "probe", probe("probe", "read me"))
180	defer services.Remove("probe")
181
182	frank := testutils.TestAddress("frank")
183	spaces.Remove(frank.String())
184	defer spaces.Remove(frank.String())
185
186	testing.SetRealm(testing.NewUserRealm(frank))
187	Exec(cross(cur), "bind /srv/probe /dev")
188	uassert.Equal(t, "read me", mustRun(t, frank.String(), "cat /dev/value"))
189	uassert.AbortsWithMessage(t, cur, "echo: read-only file server",
190		func() { Exec(cross(cur), "echo nope > /dev/value") })
191}
192
193// A command line stops at the first error and the error leaves the realm as an
194// abort, so a half-applied Exec reverts with its transaction instead of
195// leaving a namespace nobody asked for.
196func TestExecAbortsAtTheFirstError(cur realm, t *testing.T) {
197	grace := testutils.TestAddress("grace")
198	spaces.Remove(grace.String())
199	defer spaces.Remove(grace.String())
200
201	testing.SetRealm(testing.NewUserRealm(grace))
202	uassert.AbortsWithMessage(t, cur, "cat: file does not exist",
203		func() { Exec(cross(cur), "echo ok > /tmp/a; cat /absent; echo never > /tmp/b") })
204}
205
206func TestRenderUnknownCommand(t *testing.T) {
207	if got := Render("nope/x"); !strings.Contains(got, "404") {
208		t.Errorf("got %q", got)
209	}
210}
211
212// ?u= is the only caller-controlled part of every link the pages build, so a
213// value that is neither an address nor the demo key is refused outright.
214func TestRenderRefusesANonAddressNamespace(t *testing.T) {
215	out := Render("ns?u=x%29%20[evil](https://example)")
216	uassert.True(t, strings.Contains(out, "# 404"), out)
217	uassert.False(t, strings.Contains(out, "[evil](https://example)"), out)
218}
219
220// A file's contents are its owner's text. A line of backticks in one used to
221// close the hand-written fence around `cat`'s output, and everything after it
222// rendered as live markdown on the realm's own page.
223func TestCatOutputCannotCloseItsFence(cur realm, t *testing.T) {
224	who := testutils.TestAddress("fencer")
225	testing.SetRealm(testing.NewUserRealm(who))
226	Exec(cross(cur), "echo '```' > /tmp/f; echo '[claim](https://evil.example)' >> /tmp/f")
227	out := Render("cat/tmp/f?u=" + who.String())
228	// md.CodeBlock picks a fence longer than any run of backticks inside, so
229	// the user's three backticks and the link after them stay inside the block.
230	uassert.True(t, strings.Contains(out, "````\n```\n[claim](https://evil.example)\n````\n"), out)
231}
232
233// The command name and the rest of the path come from the URL: escaped in the
234// heading and in the unknown-command page.
235func TestPathDerivedTextIsEscaped(t *testing.T) {
236	for _, path := range []string{"x` [claim](https://evil.example) `", "cat/x` [claim](https://evil.example) `"} {
237		out := Render(path)
238		uassert.False(t, strings.Contains(out, "[claim](https://evil.example)"), out)
239	}
240}
241
242// A service name is chosen by whichever realm posts it, and ValidName refuses
243// only "", ".", ".." and "/": the /srv table escapes it as a cell.
244func TestServiceNameIsTableSafe(cur realm, t *testing.T) {
245	name := "x` | [claim](https://evil.example) |"
246	services.Set(name, &service{owner: "gno.land/r/someone/poster"})
247	defer services.Remove(name)
248	out := Render("")
249	uassert.False(t, strings.Contains(out, "[claim](https://evil.example)"), out)
250}
251
252// A path route reads what it names: the rest of the path is one quoted
253// argument, so a ";" in it is part of the file name and runs nothing.
254func TestPathRouteIsOneArgument(t *testing.T) {
255	// Unquoted, rc would cat /tmp/greeting and then run the second command;
256	// quoted, it looks for one file with a ";" in its name and finds none.
257	out := Render("cat/tmp/greeting;echo")
258	uassert.True(t, strings.Contains(out, "cat: file does not exist"), out)
259	uassert.Equal(t, "'/a;b'", quote("/a;b"))
260}
261
262// Namespaces and services are bounded, and the home page lists a fixed number.
263func TestNamespacesAndServicesAreBounded(cur realm, t *testing.T) {
264	var added []string
265	for i := 0; spaces.Size() < MaxNamespaces; i++ {
266		k := testutils.TestAddress("ns" + strconv.Itoa(i)).String()
267		if spaces.Get(k) == nil {
268			spaces.Set(k, newSpace(k))
269			added = append(added, k)
270		}
271	}
272	defer func() {
273		for _, k := range added {
274			spaces.Remove(k)
275		}
276	}()
277	uassert.PanicsContains(t, cur, "full at", func() { spaceFor(testutils.TestAddress("one-more").String()) })
278	uassert.Equal(t, HomeRecent, strings.Count(Render(""), "](/r/moul/x/plan9/ns/v1:ns?u="))
279}
280
281// /srv is bounded: once MaxServices names are posted, a new one is refused,
282// and re-posting is not how a realm gets around it.
283func TestServicesAreBounded(cur realm, t *testing.T) {
284	var added []string
285	for i := 0; services.Size() < MaxServices; i++ {
286		name := "svc" + strconv.Itoa(i)
287		if services.Get(name) == nil {
288			services.Set(name, &service{owner: "gno.land/r/someone/poster"})
289			added = append(added, name)
290		}
291	}
292	defer func() {
293		for _, n := range added {
294			services.Remove(n)
295		}
296	}()
297	testing.SetRealm(testing.NewCodeRealm("gno.land/r/someone/late"))
298	uassert.AbortsContains(t, cur, "/srv is full", func() { Post(cross(cur), "late", probe("late", "x")) })
299}