zones_test.gno
13.96 Kb · 316 lines
1package zones
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/uassert/v0"
8)
9
10// A real node id, from mainnet's published persistent_peers.
11const nodeID = "g15rcv5yqef3kvnmueqvkyw8y05sd40jz9p3n5su"
12
13func TestValidateSlug(t *testing.T) {
14 cases := []struct {
15 in, err string
16 }{
17 {"mainnet", ""},
18 {"onyx", ""},
19 {"moul-staging", ""},
20 {"t1", ""},
21 {"a", "2 to 32"},
22 {strings.Repeat("a", 33), "2 to 32"},
23 {"Mainnet", "[a-z0-9-]"},
24 {"-onyx", "[a-z0-9-]"},
25 {"onyx-", "[a-z0-9-]"},
26 {"on_yx", "[a-z0-9-]"},
27 {"on/yx", "[a-z0-9-]"},
28 {"on|yx", "[a-z0-9-]"},
29 }
30 for _, c := range cases {
31 err := ValidateSlug(c.in)
32 if c.err == "" {
33 uassert.NoError(t, err, c.in)
34 } else {
35 uassert.ErrorContains(t, err, c.err, c.in)
36 }
37 }
38}
39
40func TestValidateChainID(t *testing.T) {
41 uassert.NoError(t, ValidateChainID("gnoland-1"))
42 uassert.NoError(t, ValidateChainID("moulstaging-1"))
43 uassert.NoError(t, ValidateChainID("dev"))
44 uassert.NoError(t, ValidateChainID("test_5.x"))
45 uassert.ErrorContains(t, ValidateChainID(""), "1 to 50")
46 uassert.ErrorContains(t, ValidateChainID(strings.Repeat("x", 51)), "1 to 50")
47 uassert.ErrorContains(t, ValidateChainID("gno land"), "[A-Za-z0-9._-]")
48 uassert.ErrorContains(t, ValidateChainID("a|b"), "[A-Za-z0-9._-]")
49}
50
51func TestValidateEndpoint(t *testing.T) {
52 cases := []struct {
53 kind EndpointKind
54 addr string
55 err string
56 }{
57 {RPC, "https://rpc.gno.land", ""},
58 {RPC, "https://rpc.gno.land:443", ""},
59 {RPC, "http://127.0.0.1:26657", ""},
60 {RPC, "tcp://127.0.0.1:26657", ""},
61 {RPC, "wss://rpc.gno.land/websocket", ""},
62 {Gnoweb, "https://gno.land", ""},
63 {Indexer, "https://indexer.gno.land/graphql/query", ""},
64 {Faucet, "https://onyx.testnets.gno.land/faucet", ""},
65 {Explorer, "https://gnoscan.io/?chainId=onyx-1", ""},
66 {Seed, nodeID + "@seed-1.gno.land:26656", ""},
67 {Peer, nodeID + "@10.0.0.1:26656", ""},
68
69 {RPC, "", "is empty"},
70 {RPC, "rpc.gno.land", "needs a scheme"},
71 {RPC, "ftp://rpc.gno.land", "scheme is"},
72 {Gnoweb, "tcp://gno.land", "scheme is"},
73 {RPC, "https://", "has no host"},
74 {RPC, "https:///path", "has no host"},
75 {RPC, "https://user:[email protected]", "credentials"},
76 {Gnoweb, "https://gno.land/a b", "contains"},
77 {Gnoweb, "https://gno.land/)evil", "contains"},
78 {Gnoweb, "https://gno.land/[x]", "contains"},
79 {Gnoweb, "https://gno.land/\"x", "contains"},
80 {Gnoweb, "https://gno.land/é", "contains"},
81 {Gnoweb, "https://gno.land/" + strings.Repeat("a", MaxURLLen), "longer than"},
82
83 {RPC, "https://:26657", "has no host"},
84 {RPC, "https://host:not-a-port", "port is 1 to 65535"},
85 {RPC, "https://host:99999", "port is 1 to 65535"},
86 {RPC, "https://host:", "port is 1 to 65535"},
87 {RPC, "https://ho_st", "DNS name"},
88 {RPC, "https://[::1]:26657", "contains"},
89 {RPC, "https://rpc.gno.land:443/path?x=1", ""},
90 {RPC, "https://rpc.gno.land:443/path?x=1#f", "contains"}, // a fragment is never sent: no second listing
91 {RPC, "https://rpc.gno.land:+443", "port is 1 to 65535"},
92 {Gnoweb, "https://gno.land/%zz", "two hex digits"},
93 {Gnoweb, "https://gno.land/%2", "two hex digits"},
94 {Gnoweb, "https://gno.land/%2F", ""},
95 {Gnoweb, "https://gno.land/?a=1&b=2", ""},
96 {Gnoweb, "https://gno.land/?a=1&b=2", "entity"},
97 {Gnoweb, "https://0x7f.1", "DNS name"},
98 {Gnoweb, "https://a.1", "DNS name"},
99 {Gnoweb, "https://foo.123", "DNS name"},
100 {Gnoweb, "https://1.2.3.4", ""},
101 {Peer, "[email protected]:26656", "lowercase g1 address"},
102 {Peer, nodeID + "@seed-1.gno.land:+26656", "1 to 65535"},
103 {Peer, nodeID + "@" + strings.Repeat("a.", 120) + "example:26656", "longer than"},
104 {RPC, "https://rpc.gno.land.", ""},
105 {RPC, "http://10.0.0.1:26657", ""},
106 {RPC, "http://localhost:26657", ""},
107 {RPC, "https://-.example.com", "DNS name"},
108 {RPC, "https://bad-.example.com", "DNS name"},
109 {RPC, "https://-bad.example.com", "DNS name"},
110 {RPC, "https://foo..example.com", "DNS name"},
111 {RPC, "https://.example.com", "DNS name"},
112 {RPC, "https://example..", "DNS name"},
113 {RPC, "https://" + strings.Repeat("a", 64) + ".example.com", "DNS name"},
114 {RPC, "https://" + strings.Repeat("a", 63) + ".example.com", ""},
115 {RPC, "http://300.1.1.1", "DNS name"},
116 {RPC, "http://1.2.3", "DNS name"},
117 {RPC, "http://01.2.3.4", "DNS name"},
118 {Peer, nodeID + "@bad-.example.com:26656", "DNS name"},
119
120 {Peer, "seed-1.gno.land:26656", "<node id>@"},
121 {Peer, "[email protected]:26656", "g1 address"},
122 {Peer, nodeID + "@seed-1.gno.land", "needs a port"},
123 {Peer, nodeID + "@:26656", "needs a port"},
124 {Peer, nodeID + "@seed-1.gno.land:0", "1 to 65535"},
125 {Peer, nodeID + "@seed-1.gno.land:65536", "1 to 65535"},
126 {Peer, nodeID + "@seed-1.gno.land:080", "1 to 65535"},
127 {Peer, nodeID + "@seed-1.gno.land:port", "1 to 65535"},
128 {Seed, nodeID + "@seed_1.gno.land:26656", "DNS name"},
129 {Seed, nodeID + "@[::1]:26656", "DNS name"},
130
131 {RPC, "HTTPS://rpc.gno.land", ""},
132 {Gnoweb, "Https://gno.land", ""},
133 {" rpc ", "not a url", "unknown endpoint kind"},
134 {"RPC", "https://rpc.gno.land", "unknown endpoint kind"},
135 {"", "https://gno.land", "needs a kind"},
136 {"grpc", "https://gno.land", "unknown endpoint kind"},
137 }
138 for _, c := range cases {
139 err := ValidateEndpoint(c.kind, c.addr)
140 label := string(c.kind) + " " + c.addr
141 if c.err == "" {
142 uassert.NoError(t, err, label)
143 } else {
144 uassert.ErrorContains(t, err, c.err, label)
145 }
146 }
147}
148
149func TestValidateInfo(t *testing.T) {
150 ok := Info{ChainID: "onyx-1", Title: "Onyx", Kind: Testnet, RPCURL: "https://rpc.onyx.testnets.gno.land"}
151 uassert.NoError(t, ValidateInfo(ok))
152 // A loopback main RPC is a local zone's, and only a local zone's: it names a
153 // different machine for every reader.
154 tcp := ok
155 tcp.RPCURL = "tcp://127.0.0.1:26657"
156 uassert.ErrorContains(t, ValidateInfo(tcp), "private or special-use")
157 tcp.Kind = Local
158 uassert.NoError(t, ValidateInfo(tcp))
159 for _, h := range []string{"http://localhost:26657", "http://10.0.0.1:26657", "http://192.168.1.2", "http://172.20.0.1",
160 "http://169.254.169.254", "http://100.64.0.1", "http://0.0.0.0:26657", "http://api.localhost"} {
161 in := ok
162 in.RPCURL = h
163 uassert.ErrorContains(t, ValidateInfo(in), "private or special-use", h)
164 }
165 pub := ok
166 pub.RPCURL = "http://172.32.0.1"
167 uassert.NoError(t, ValidateInfo(pub))
168
169 bad := func(mut func(*Info), want string) {
170 in := ok
171 mut(&in)
172 uassert.ErrorContains(t, ValidateInfo(in), want)
173 }
174 bad(func(in *Info) { in.ChainID = "" }, "chain id")
175 bad(func(in *Info) { in.Title = "" }, "title is 1 to 64")
176 bad(func(in *Info) { in.Title = strings.Repeat("x", MaxTitleLen+1) }, "title is longer than 64")
177 bad(func(in *Info) { in.Title = "two\nlines" }, "control character")
178 bad(func(in *Info) { in.Title = "\u200b" }, "invisible or bidi")
179 bad(func(in *Info) { in.Title = "\u2060" }, "invisible or bidi")
180 bad(func(in *Info) { in.Title = "\u3164" }, "invisible or bidi")
181 bad(func(in *Info) { in.Title = "\u061cOnyx" }, "invisible or bidi")
182 bad(func(in *Info) { in.Title = "Onyx\U000e0041" }, "invisible or bidi")
183 bad(func(in *Info) { in.Title = "\u2800" }, "invisible or bidi")
184 bad(func(in *Info) { in.Title = "x\u17b4" }, "invisible or bidi") // Khmer inherent vowels draw nothing
185 bad(func(in *Info) { in.Description = "\u0301" }, "nothing visible") // optional, but not blank
186 bad(func(in *Info) { in.Title = "\ue000 Onyx" }, "which this chain does not draw")
187 bad(func(in *Info) { in.Title = "a\u180bb" }, "invisible or bidi") // a Mongolian selector after a Latin letter
188 bad(func(in *Info) { in.Title = "Onyx\ufe0f" }, "invisible or bidi") // after a letter it modifies nothing
189 bad(func(in *Info) { in.Title = "a\U000e0100" }, "invisible or bidi")
190 for _, r := range []rune{0x115F, 0x1160, 0xFFA0, 0x17B5, 0x13441, 0x13442, 0x16FE4} {
191 r := r
192 bad(func(in *Info) { in.Title = "on" + string(r) + "yx" }, "invisible or bidi")
193 }
194 // Enclosing marks draw a badge's frame around any character.
195 for _, s := range []string{"!\u20e4 flagged", "v\u20de verified", "x\u20e0", "x\u20dd"} {
196 s := s
197 bad(func(in *Info) { in.Title = s }, "enclosing mark")
198 }
199 // √ is maths, not a badge; × is everyday text. A variation selector after a
200 // base it modifies is text: an emoji heart as a phone writes it, a
201 // Mongolian free variant, an ideographic variation sequence.
202 for _, s := range []string{"fees scale with \u221an", "1920\u00d71080", "made with \u2764\ufe0f",
203 "\u2764\ufe0e text heart", "\u2211\ufe0f", "\u203c\ufe0f new", "\u2049\ufe0f", "\u2139\ufe0f", "\u3030\ufe0f", "\u303d\ufe0f",
204 "\u18aa\u180b",
205 "\u1828\u180b\u1821", "\u845b\U000e0100"} {
206 good := ok
207 good.Description = s
208 uassert.NoError(t, ValidateInfo(good), s)
209 }
210 for _, r := range []rune{0x2713, 0x2714, 0x2611, 0x1F5F8, 0x1F5F9, 0x1F197, 0x2716, 0x2717, 0x2718, 0x2715,
211 0x274E, 0x2612, 0x1F6AB, 0x26D4, 0x1F6D1, 0x1F5F4, 0x1F5F7, 0x231B, 0x23F8, 0x23FA, 0x2705, 0x26A0,
212 0x23F3, 0x274C, 0x23F9, 0x1FBB1, 0x237B, 0x10102, 0x1F5D9, 0x2A2F, 0x2573, 0x2BBD, 0x2BBF, 0x29D6,
213 0x29D7, 0x1F7A8, 0x1F7AE, 0x2613, 0x2A09, 0x22A0, 0x2327, 0x1F147, 0x1F187, 0x2297, 0x2A02, 0x1F167,
214 0x1F6C7, 0x2298, 0x29B8, 0x24CD, 0x24E7, 0x24B3, 0x1F127, 0x29BB, 0x2A34, 0x2A35, 0x2A37, 0x292B,
215 0x292C, 0x1F532, 0x1F533, 0x26DD, 0x1F5BE, 0x2B59, 0x2A36, 0x26D2, 0x1FBC0, 0x1D145, 0x26CC, 0x2A3B,
216 0x2BBE, 0x1F5F5, 0x1F5F6} {
217 r := r
218 bad(func(in *Info) { in.Title = string(r) + " official" }, "status glyph")
219 }
220 bad(func(in *Info) { in.Title = "\u2705 official mainnet" }, "status glyph")
221 bad(func(in *Info) { in.Title = "Z\u0301\u0301\u0301\u0301\u0301" }, "combining marks")
222 // The limit is a run: four marks pass, and a base character ends a run, so
223 // two runs of three are not a stack of six. A spacing mark is not a stack
224 // either, so real words in these scripts pass.
225 // A spacing mark (U+093E) between two runs of four ends the first run.
226 for _, word := range []string{"Z\u0301\u0301\u0301\u0301", "a\u0301\u0301\u0301b\u0301\u0301\u0301",
227 "\u0915\u0301\u0301\u0301\u0301\u093e\u0301\u0301\u0301\u0301",
228 "ज़िंदगी", "ਜ਼ਿੰਦਗੀ", "ကြိုး", "སྒྲུབ"} {
229 good := ok
230 good.Title = word
231 uassert.NoError(t, ValidateInfo(good), word)
232 }
233 bad(func(in *Info) { in.Title = "\u0301" }, "nothing visible")
234 bad(func(in *Info) { in.Title = "bad\xffbyte" }, "not valid UTF-8")
235 bad(func(in *Info) { in.Title = "alpha\u2028beta" }, "control character")
236 bad(func(in *Info) { in.Title = "alpha\u2029beta" }, "control character")
237 bad(func(in *Info) { in.Title = "alpha\u0085beta" }, "control character")
238 bad(func(in *Info) { in.Description = "x\u009by" }, "control character")
239 bad(func(in *Info) { in.Title = "Onyx\u202e" }, "invisible or bidi")
240 bad(func(in *Info) { in.Description = "fine\u200dtext" }, "invisible or bidi")
241 bad(func(in *Info) { in.Description = strings.Repeat("x", MaxDescriptionLen+1) }, "description is longer than 512")
242 bad(func(in *Info) { in.Kind = "" }, "needs a kind")
243 bad(func(in *Info) { in.Kind = "betanet" }, "unknown kind")
244 bad(func(in *Info) { in.RPCURL = "" }, "rpc url is empty")
245 // The main RPC is what gnokey -remote gets, which dials http, https, tcp.
246 bad(func(in *Info) { in.RPCURL = "wss://rpc.onyx.testnets.gno.land/websocket" }, "rpc url scheme")
247 bad(func(in *Info) { in.RPCURL = "ws://rpc.onyx.testnets.gno.land/websocket" }, "rpc url scheme")
248 bad(func(in *Info) { in.RPCURL = "https://rpc.example.com/path" }, "no path")
249 bad(func(in *Info) { in.RPCURL = "https://rpc.example.com?x=1" }, "no path")
250 bad(func(in *Info) { in.RPCURL = "https://rpc.example.com:443#f" }, "contains")
251 bad(func(in *Info) { in.RPCURL = "https://rpc.example.com/" }, "no path")
252 bad(func(in *Info) { in.Kind = " testnet " }, "unknown kind")
253 port := ok
254 port.RPCURL = "https://rpc.onyx.testnets.gno.land:443"
255 uassert.NoError(t, ValidateInfo(port))
256 bad(func(in *Info) { in.GnowebURL = "gno.land" }, "gnoweb url needs a scheme")
257 bad(func(in *Info) { in.GenesisURL = "tcp://x" }, "genesis url scheme")
258
259 // Runes, not bytes: a 64-character title of multi-byte runes is fine.
260 in := ok
261 in.Title = strings.Repeat("é", MaxTitleLen)
262 uassert.NoError(t, ValidateInfo(in))
263}
264
265func TestCanonical(t *testing.T) {
266 uassert.Equal(t, "https://rpc.gno.land/API?Q=1", Canonical(RPC, "HTTPS://RPC.Gno.Land/API?Q=1"))
267 uassert.Equal(t, "https://rpc.gno.land", Canonical(RPC, "https://RPC.gno.land"))
268 // One endpoint, one key: every spelling below is the same server.
269 for _, same := range []string{"https://rpc.gno.land", "https://rpc.gno.land/", "https://rpc.gno.land.",
270 "https://rpc.gno.land:443", "https://rpc.gno.land?", "HTTPS://RPC.gno.land.:443/"} {
271 uassert.Equal(t, "https://rpc.gno.land", Canonical(RPC, same), same)
272 }
273 uassert.Equal(t, "http://h.example.com", Canonical(Gnoweb, "http://h.example.com:80"))
274 uassert.Equal(t, "http://h.example.com:443", Canonical(Gnoweb, "http://h.example.com:443")) // not http's default
275 uassert.Equal(t, "https://h.example.com/api", Canonical(Indexer, "https://h.example.com:443/api"))
276 uassert.Equal(t, nodeID+"@seed-1.gno.land:26656", Canonical(Peer, nodeID+"@SEED-1.gno.land:26656"))
277}
278
279func TestParse(t *testing.T) {
280 for _, s := range []string{"", "approved", "pending", "rejected", "retired"} {
281 _, err := ParseStatus(s)
282 uassert.NoError(t, err, s)
283 }
284 _, err := ParseStatus("official")
285 uassert.ErrorContains(t, err, "unknown status")
286
287 _, err = ParseKind("betanet")
288 uassert.ErrorContains(t, err, "unknown kind")
289 k, err := ParseKind(" testnet ")
290 uassert.NoError(t, err)
291 uassert.Equal(t, string(Testnet), string(k))
292
293 _, err = ParseEndpointKind("grpc")
294 uassert.ErrorContains(t, err, "unknown endpoint kind")
295 _, err = ParseVerification("trusted")
296 uassert.ErrorContains(t, err, "unknown verification")
297
298 // Every listed value parses back to itself.
299 for _, v := range EndpointKinds() {
300 got, err := ParseEndpointKind(string(v))
301 uassert.NoError(t, err)
302 uassert.Equal(t, string(v), string(got))
303 }
304 for _, v := range Kinds() {
305 got, _ := ParseKind(string(v))
306 uassert.Equal(t, string(v), string(got))
307 }
308 for _, v := range Statuses() {
309 got, _ := ParseStatus(string(v))
310 uassert.Equal(t, string(v), string(got))
311 }
312 for _, v := range Verifications() {
313 got, _ := ParseVerification(string(v))
314 uassert.Equal(t, string(v), string(got))
315 }
316}