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

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}