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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

ns_test.gno

10.00 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/nym-moultest123/x/plan9/ninep/v0"
 12	synfs "gno.land/p/nym-moultest123/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/nym-moultest123/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/nym-moultest123/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}