ns_test.gno
6.23 Kb · 209 lines
1package ns
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9
10 ninep "gno.land/p/moul/x/plan9/ninep/v0"
11 synfs "gno.land/p/moul/x/plan9/synfs/v0"
12)
13
14// probe is a tiny read-only server, standing in for another realm's tree.
15func probe(name, contents string) ninep.File {
16 t := synfs.New(name, "sys", func() int64 { return 1 })
17 t.Root().Add("value", func() string { return contents })
18 return t.Root()
19}
20
21func TestDemoNamespaceIsSeeded(t *testing.T) {
22 seedDemo()
23 out, err := Run(DemoKey, "ls -u /bin")
24 if err != nil {
25 t.Fatalf("ls: %v", err)
26 }
27 if out != "ls\nrc\n" {
28 t.Errorf("the demo /bin should be a union of two directories: %q", out)
29 }
30 if got := Namespace(DemoKey); !strings.Contains(got, "bind -ac /usr/glenda/bin /bin") {
31 t.Errorf("mount table: %q", got)
32 }
33 if got, _ := Run(DemoKey, "cat /tmp/greeting"); got != "hello from a namespace\n" {
34 t.Errorf("greeting: %q", got)
35 }
36}
37
38func TestRunIsReadOnly(t *testing.T) {
39 seedDemo()
40 if _, err := Run(DemoKey, "echo x > /tmp/f"); err == nil {
41 t.Fatal("Render's shell must refuse a write")
42 }
43 if _, err := Run(DemoKey, "rm /tmp/greeting"); err == nil {
44 t.Fatal("Render's shell must refuse a remove")
45 }
46 if _, err := Run("nobody", "ls /"); err == nil {
47 t.Fatal("an unknown namespace should not be created by a read")
48 }
49}
50
51func TestPostAndBind(cur realm, t *testing.T) {
52 services.Remove("probe")
53 Post(cross(cur), "probe", probe("probe", "42"))
54
55 found := false
56 for _, s := range Services() {
57 if s == "probe" {
58 found = true
59 }
60 }
61 if !found {
62 t.Fatalf("probe is not in /srv: %v", Services())
63 }
64
65 // A fresh namespace sees it through /srv without importing anything.
66 spaces.Remove("tester")
67 spaceFor("tester")
68 defer spaces.Remove("tester")
69 out, err := Run("tester", "cat /srv/probe/value")
70 if err != nil {
71 t.Fatalf("cat through /srv: %v", err)
72 }
73 if out != "42" {
74 t.Errorf("got %q, want 42", out)
75 }
76 services.Remove("probe")
77}
78
79func TestExecWritesToTheCallersOwnNamespace(cur realm, t *testing.T) {
80 alice := testutils.TestAddress("alice")
81 bob := testutils.TestAddress("bob")
82 spaces.Remove(alice.String())
83 spaces.Remove(bob.String())
84
85 testing.SetRealm(testing.NewUserRealm(alice))
86 Exec(cross(cur), "echo 'alice was here' > /tmp/note")
87
88 testing.SetRealm(testing.NewUserRealm(bob))
89 Exec(cross(cur), "echo 'bob was here' > /tmp/note")
90
91 got, err := Run(alice.String(), "cat /tmp/note")
92 if err != nil {
93 t.Fatalf("alice: %v", err)
94 }
95 if got != "alice was here\n" {
96 t.Errorf("alice's namespace: %q", got)
97 }
98 if got, _ = Run(bob.String(), "cat /tmp/note"); got != "bob was here\n" {
99 t.Errorf("bob's namespace: %q", got)
100 }
101
102 // Reset throws a namespace away; the next use rebuilds the default.
103 testing.SetRealm(testing.NewUserRealm(alice))
104 Reset(cross(cur))
105 if _, err := Run(alice.String(), "cat /tmp/note"); err == nil {
106 t.Error("the namespace should be gone after Reset")
107 }
108 spaces.Remove(alice.String())
109 spaces.Remove(bob.String())
110}
111
112func TestExecBindsIntoTheCallersNamespace(cur realm, t *testing.T) {
113 services.Remove("probe")
114 Post(cross(cur), "probe", probe("probe", "42"))
115 defer services.Remove("probe")
116
117 erin := testutils.TestAddress("erin")
118 spaces.Remove(erin.String())
119 testing.SetRealm(testing.NewUserRealm(erin))
120 Exec(cross(cur), "bind /srv/probe /dev")
121
122 if got := mustRun(t, erin.String(), "cat /dev/value"); got != "42" {
123 t.Errorf("read through the mount: %q", got)
124 }
125 if ns := Namespace(erin.String()); !strings.Contains(ns, "bind /srv/probe /dev") {
126 t.Errorf("mount table: %q", ns)
127 }
128 spaces.Remove(erin.String())
129}
130
131func TestSrvListingUsesThePostedName(cur realm, t *testing.T) {
132 services.Remove("aliased")
133 // The server calls its own root "probe"; /srv must show it as "aliased".
134 Post(cross(cur), "aliased", probe("probe", "x"))
135 defer services.Remove("aliased")
136
137 spaces.Remove("lister")
138 spaceFor("lister")
139 out := mustRun(t, "lister", "ls /srv")
140 if !strings.Contains(out, "aliased") {
141 t.Errorf("ls /srv: %q", out)
142 }
143 if strings.Contains(out, "probe") {
144 t.Errorf("/srv leaked the server's own root name: %q", out)
145 }
146 spaces.Remove("lister")
147}
148
149func mustRun(t *testing.T, key, line string) string {
150 t.Helper()
151 out, err := Run(key, line)
152 if err != nil {
153 t.Fatalf("%s: %v", line, err)
154 }
155 return out
156}
157
158// --- the boundary. Three properties the suite would be unsafe without. -------
159
160// A /srv name belongs to the realm that posted it: first come, first served,
161// and then locked.
162func TestPostRejectsASecondOwner(cur realm, t *testing.T) {
163 services.Remove("taken")
164 Post(cross(cur), "taken", probe("taken", "first"))
165 defer services.Remove("taken")
166
167 testing.SetRealm(testing.NewCodeRealm("gno.land/r/other/thing/v0"))
168 uassert.AbortsWithMessage(t, cur,
169 "post: taken is already posted by gno.land/r/moul/x/plan9/ns/v0",
170 func() { Post(cross(cur), "taken", probe("taken", "second")) })
171}
172
173// Grafting another realm's tree into your namespace gives you reads and
174// nothing else. ninep.File has no mutating method, so there is no route from a
175// mount to a write against the realm that posted it.
176func TestMountedTreesAreReadOnly(cur realm, t *testing.T) {
177 services.Remove("probe")
178 Post(cross(cur), "probe", probe("probe", "read me"))
179 defer services.Remove("probe")
180
181 frank := testutils.TestAddress("frank")
182 spaces.Remove(frank.String())
183 defer spaces.Remove(frank.String())
184
185 testing.SetRealm(testing.NewUserRealm(frank))
186 Exec(cross(cur), "bind /srv/probe /dev")
187 uassert.Equal(t, "read me", mustRun(t, frank.String(), "cat /dev/value"))
188 uassert.AbortsWithMessage(t, cur, "echo: read-only file server",
189 func() { Exec(cross(cur), "echo nope > /dev/value") })
190}
191
192// A command line stops at the first error and the error leaves the realm as an
193// abort, so a half-applied Exec reverts with its transaction instead of
194// leaving a namespace nobody asked for.
195func TestExecAbortsAtTheFirstError(cur realm, t *testing.T) {
196 grace := testutils.TestAddress("grace")
197 spaces.Remove(grace.String())
198 defer spaces.Remove(grace.String())
199
200 testing.SetRealm(testing.NewUserRealm(grace))
201 uassert.AbortsWithMessage(t, cur, "cat: file does not exist",
202 func() { Exec(cross(cur), "echo ok > /tmp/a; cat /absent; echo never > /tmp/b") })
203}
204
205func TestRenderUnknownCommand(t *testing.T) {
206 if got := Render("nope/x"); !strings.Contains(got, "404") {
207 t.Errorf("got %q", got)
208 }
209}