settings_test.gno
10.91 Kb · 321 lines
1package config
2
3import (
4 "strconv"
5 "testing"
6
7 "chain/runtime"
8
9 "gno.land/p/moul/authz/v0"
10 "gno.land/p/moul/mygnoscan/v0"
11 "gno.land/p/nt/avl/v0"
12 "gno.land/p/nt/uassert/v0"
13)
14
15// resetSettings puts the realm back to a just-deployed state. Realm globals
16// live for the whole test binary and gno runs every Example AFTER every Test,
17// so without this the pinned ExampleRender output would depend on which tests
18// ran before it and in what order.
19func resetSettings() {
20 settings = avl.NewTree()
21 proxies = avl.NewTree()
22 settingsRev = 0
23 Authorizer = authz.NewWithAuthority(authz.NewMemberAuthority(originAddr))
24}
25
26func TestValidKey(t *testing.T) {
27 tests := []struct {
28 key string
29 want bool
30 }{
31 {"mygnoscan.url", true},
32 {"a", true},
33 {"a-b_c.d0", true},
34 {"", false},
35 {"Upper", false},
36 {"has space", false},
37 {"tab\there", false},
38 {"colon:here", false},
39 // A bare slash is not a name; it is only legal inside a scope.
40 {"slash/here", false},
41 // Scoped keys.
42 {"pause" + ScopeSep + "r/moul/home", true},
43 {"block.top" + ScopeSep + "p/moul/kit/ui", true},
44 {"pause" + ScopeSep, false}, // scope separator with no scope
45 {ScopeSep + "r/moul/home", false}, // scope with no name
46 {"pause" + ScopeSep + "r//moul", false},
47 {"pause" + ScopeSep + "r/moul/", false},
48 {"pause" + ScopeSep + "R/Moul/Home", false},
49 {"pause" + ScopeSep + "a" + ScopeSep + "b", false},
50 }
51 for _, tt := range tests {
52 uassert.Equal(t, tt.want, validKey(tt.key), "validKey("+tt.key+")")
53 }
54
55 // 64 bytes is the last legal length, 65 the first illegal one.
56 long := ""
57 for i := 0; i < maxNameLen; i++ {
58 long += "a"
59 }
60 uassert.True(t, validKey(long), "a name of exactly maxNameLen is legal")
61 uassert.False(t, validKey(long+"a"), "one byte over maxNameLen is not")
62}
63
64func TestSetGetUnset(cur realm, t *testing.T) {
65 resetSettings()
66
67 uassert.Equal(t, "", Get("mygnoscan.url"))
68 uassert.False(t, Has("mygnoscan.url"))
69 uassert.Equal(t, "fallback", GetOr("mygnoscan.url", "fallback"))
70 uassert.Equal(t, 0, SettingsRevision())
71
72 testing.SetRealm(testing.NewUserRealm(originAddr))
73 Set(cross(cur), KeyMygnoscanURL, "https://scan.example.com")
74
75 uassert.Equal(t, "https://scan.example.com", Get(KeyMygnoscanURL))
76 uassert.True(t, Has(KeyMygnoscanURL))
77 uassert.Equal(t, "https://scan.example.com", GetOr(KeyMygnoscanURL, "fallback"))
78 uassert.Equal(t, 1, SettingsRevision())
79 uassert.Equal(t, 1, Size())
80
81 // Overwriting is an ordinary write and bumps the revision again.
82 testing.SetRealm(testing.NewUserRealm(originAddr))
83 Set(cross(cur), KeyMygnoscanURL, "https://scan2.example.com")
84 uassert.Equal(t, "https://scan2.example.com", Get(KeyMygnoscanURL))
85 uassert.Equal(t, 2, SettingsRevision())
86 uassert.Equal(t, 1, Size())
87
88 testing.SetRealm(testing.NewUserRealm(originAddr))
89 Unset(cross(cur), KeyMygnoscanURL)
90 uassert.Equal(t, "", Get(KeyMygnoscanURL))
91 uassert.False(t, Has(KeyMygnoscanURL))
92 uassert.Equal(t, 0, Size())
93 uassert.Equal(t, 3, SettingsRevision())
94}
95
96// TestEmptyValueIsSet pins the one case GetOr cannot see: a key set to "" is
97// set, and every reader falls back anyway. That is deliberate (a reader's
98// default beats an empty string) but it means Unset and Set("") are not the
99// same operation, and only Has tells them apart.
100func TestEmptyValueIsSet(cur realm, t *testing.T) {
101 resetSettings()
102
103 testing.SetRealm(testing.NewUserRealm(originAddr))
104 Set(cross(cur), "some.key", "")
105
106 uassert.True(t, Has("some.key"))
107 uassert.Equal(t, "", Get("some.key"))
108 uassert.Equal(t, "fallback", GetOr("some.key", "fallback"))
109}
110
111func TestSetRequiresAuthority(cur realm, t *testing.T) {
112 resetSettings()
113
114 testing.SetRealm(testing.NewUserRealm(nonManagerAddr))
115 uassert.AbortsContains(t, cur, "unauthorized", func() {
116 Set(cross(cur), "some.key", "value")
117 })
118 uassert.Equal(t, 0, Size(), "a rejected write leaves nothing behind")
119 uassert.Equal(t, 0, SettingsRevision(), "and does not move the revision")
120
121 // A manager added by the origin can write, which is the whole point of
122 // routing through the Authorizer instead of a hardcoded address.
123 testing.SetRealm(testing.NewUserRealm(originAddr))
124 uassert.NoError(t, AddManager(cross(cur), manager1Addr))
125
126 testing.SetRealm(testing.NewUserRealm(manager1Addr))
127 Set(cross(cur), "some.key", "value")
128 uassert.Equal(t, "value", Get("some.key"))
129
130 // And loses it again when removed.
131 testing.SetRealm(testing.NewUserRealm(originAddr))
132 uassert.NoError(t, RemoveManager(cross(cur), manager1Addr))
133
134 testing.SetRealm(testing.NewUserRealm(manager1Addr))
135 uassert.AbortsContains(t, cur, "unauthorized", func() {
136 Set(cross(cur), "other.key", "value")
137 })
138}
139
140func TestUnsetRequiresAuthority(cur realm, t *testing.T) {
141 resetSettings()
142
143 testing.SetRealm(testing.NewUserRealm(originAddr))
144 Set(cross(cur), "some.key", "value")
145
146 testing.SetRealm(testing.NewUserRealm(nonManagerAddr))
147 uassert.AbortsContains(t, cur, "unauthorized", func() {
148 Unset(cross(cur), "some.key")
149 })
150 uassert.Equal(t, "value", Get("some.key"), "a rejected unset leaves the value")
151}
152
153func TestUnsetMissingKeyAborts(cur realm, t *testing.T) {
154 resetSettings()
155
156 testing.SetRealm(testing.NewUserRealm(originAddr))
157 uassert.AbortsWithMessage(t, cur, "no such setting: never.set", func() {
158 Unset(cross(cur), "never.set")
159 })
160}
161
162func TestInvalidInputAborts(cur realm, t *testing.T) {
163 resetSettings()
164
165 testing.SetRealm(testing.NewUserRealm(originAddr))
166 uassert.AbortsContains(t, cur, "invalid key", func() {
167 Set(cross(cur), "Bad Key", "value")
168 })
169
170 long := ""
171 for i := 0; i < maxValueLen+1; i++ {
172 long += "x"
173 }
174 testing.SetRealm(testing.NewUserRealm(originAddr))
175 uassert.AbortsContains(t, cur, "value too long", func() {
176 Set(cross(cur), "some.key", long)
177 })
178
179 uassert.Equal(t, 0, Size(), "neither rejection wrote anything")
180}
181
182// TestKeyValidatedBeforeAuthority pins the check order: an unauthorised caller
183// passing a malformed key is told the key is wrong, not that they are. Nothing
184// secret leaks either way, and validating first means a manager's typo gets
185// the useful message rather than a misleading "unauthorized".
186func TestKeyValidatedBeforeAuthority(cur realm, t *testing.T) {
187 resetSettings()
188
189 testing.SetRealm(testing.NewUserRealm(nonManagerAddr))
190 uassert.AbortsContains(t, cur, "invalid key", func() {
191 Set(cross(cur), "Bad Key", "value")
192 })
193}
194
195func TestKeysAndManifest(cur realm, t *testing.T) {
196 resetSettings()
197
198 testing.SetRealm(testing.NewUserRealm(originAddr))
199 Set(cross(cur), "zzz.last", "z")
200 testing.SetRealm(testing.NewUserRealm(originAddr))
201 Set(cross(cur), "aaa.first", "a")
202
203 keys := Keys()
204 uassert.Equal(t, 2, len(keys))
205 uassert.Equal(t, "aaa.first", keys[0], "Keys is sorted, not insertion-ordered")
206 uassert.Equal(t, "zzz.last", keys[1])
207
208 // rev is the realm-wide counter at the time of that key's last write, so
209 // zzz.last carries 1 and aaa.first carries 2 despite sorting first. The
210 // height column is read back from the chain rather than hardcoded: gno's
211 // test harness has no absolute height setter, so tests inherit whatever
212 // height ran before them and a literal here would break on the next test
213 // added above this one.
214 h := strconv.FormatInt(runtime.ChainHeight(), 10)
215 uassert.Equal(t,
216 "aaa.first\t2\t"+h+"\ta\nzzz.last\t1\t"+h+"\tz\n",
217 Manifest())
218}
219
220func TestScannerFollowsSettings(cur realm, t *testing.T) {
221 resetSettings()
222
223 // Unset, every reader is on the package default.
224 uassert.Equal(t, mygnoscan.DefaultBase, MygnoscanURL())
225 uassert.Equal(t,
226 mygnoscan.DefaultBase+"/realm/r/moul/home",
227 MygnoscanFor("gno.land/r/moul/home"))
228
229 // One transaction moves every realm that reads this one.
230 testing.SetRealm(testing.NewUserRealm(originAddr))
231 Set(cross(cur), KeyMygnoscanURL, "https://scan.example.com")
232
233 uassert.Equal(t, "https://scan.example.com", MygnoscanURL())
234 uassert.Equal(t,
235 "https://scan.example.com/realm/r/moul/home",
236 MygnoscanFor("gno.land/r/moul/home"))
237
238 // The network override is independent of the base, for an instance that
239 // names the chain differently. The test harness runs as chain-id "dev",
240 // which maps to no network at all, so this is the only way to see one.
241 uassert.Equal(t, "", Scanner().Network())
242 testing.SetRealm(testing.NewUserRealm(originAddr))
243 Set(cross(cur), KeyMygnoscanNetwork, "gnoland1")
244 uassert.Equal(t, "gnoland1", Scanner().Network())
245 uassert.Equal(t,
246 "https://scan.example.com/realm/r/moul/home?network=gnoland1",
247 MygnoscanFor("gno.land/r/moul/home"))
248}
249
250func TestMygnoscanFooter(cur realm, t *testing.T) {
251 resetSettings()
252 uassert.Equal(t,
253 mygnoscan.DefaultBase+"/realm/r/moul/config",
254 mygnoscan.Default().Realm("gno.land/r/moul/config"),
255 "the footer and a direct build agree while nothing is configured")
256 uassert.True(t, len(MygnoscanFooterFor("gno.land/r/moul/config")) > 0)
257 uassert.Equal(t, "", MygnoscanFooterFor(""))
258}
259
260// ExampleRender pins the page as it looks right after deployment. It resets
261// first: examples run after every Test in the binary and would otherwise
262// render whatever state they left.
263func ExampleRender() {
264 resetSettings()
265 print(Render(""))
266 // Output:
267 // # gno.land/r/moul/config
268 //
269 // moul's settings, read by his other realms. rev 0 · 0 setting(s)
270 //
271 // ## Settings
272 //
273 // _none set; every reader is on its built-in default_
274 //
275 // ## Pause
276 //
277 // _running; no global pause_
278 //
279 // ## Authority
280 //
281 // `member_authority[g1daexjemfde047h6lta047h6lta047h6ld2pug6]`
282 //
283 // ---
284 //
285 // [explorer](https://mygnoscan.moul.p2p.team/realm/r/moul/config/v1) · [source](https://mygnoscan.moul.p2p.team/realm/r/moul/config/v1?tab=source) · [calls](https://mygnoscan.moul.p2p.team/realm/r/moul/config/v1?tab=calls) · [deps](https://mygnoscan.moul.p2p.team/realm/r/moul/config/v1?tab=deps)
286}
287
288// ExampleRender_populated pins the settings table and the global pause banner,
289// which together are the view a manager actually looks at during an incident.
290func ExampleRender_populated() {
291 resetSettings()
292 settings.Set(KeyMygnoscanURL, &setting{value: "https://scan.example.com", rev: 1, updated: 42})
293 settings.Set(KeyPause, &setting{value: "readonly: migrating storage", rev: 2, updated: 43})
294 settings.Set(KeyFor(KeyBlockTop, "gno.land/r/moul/gns"), &setting{value: "> v2 shipped", rev: 3, updated: 44})
295 settingsRev = 3
296 print(Render(""))
297 // Output:
298 // # gno.land/r/moul/config
299 //
300 // moul's settings, read by his other realms. rev 3 · 3 setting(s)
301 //
302 // ## Settings
303 //
304 // | key | value | rev | height |
305 // | --- | --- | ---: | ---: |
306 // | `block.top@r/moul/gns` | `> v2 shipped` | 3 | 44 |
307 // | `mygnoscan.url` | `https://scan.example.com` | 1 | 42 |
308 // | `pause` | `readonly: migrating storage` | 2 | 43 |
309 //
310 // ## Pause
311 //
312 // > **Read-only.** This realm is still readable, but not accepting changes. migrating storage
313 //
314 // ## Authority
315 //
316 // `member_authority[g1daexjemfde047h6lta047h6lta047h6ld2pug6]`
317 //
318 // ---
319 //
320 // [explorer](https://scan.example.com/realm/r/moul/config/v1) · [source](https://scan.example.com/realm/r/moul/config/v1?tab=source) · [calls](https://scan.example.com/realm/r/moul/config/v1?tab=calls) · [deps](https://scan.example.com/realm/r/moul/config/v1?tab=deps)
321}