mygnoscan_test.gno
10.60 Kb · 266 lines
1package mygnoscan
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9// mainnet is what a Scanner looks like on the chain that matters. Tests build
10// it explicitly rather than calling Default(), because the test harness runs
11// with chain-id "dev", which NetworkFor maps to no network at all: pinning
12// Default()'s output would pin the absence of the parameter and prove nothing
13// about the case every real link is in.
14func mainnet() Scanner { return New(DefaultBase).WithNetwork("mainnet") }
15
16func TestNetworkFor(t *testing.T) {
17 tests := []struct {
18 chainID string
19 want string
20 }{
21 {"gnoland-1", "mainnet"},
22 {"pearl-1", "pearl"},
23 {"sapphire-1", "sapphire"},
24 {"staging", "staging"},
25 {"dev", ""}, // gnodev, and what `gno test` reports
26 {"", ""}, // no chain at all
27 {"gnoland1", ""}, // the upstream default instance's id, not a chain-id
28 {"test3", ""}, // a retired testnet
29 {"mainnet", ""}, // the network id, fed back in by mistake
30 }
31 for _, tt := range tests {
32 uassert.Equal(t, tt.want, NetworkFor(tt.chainID), "NetworkFor("+tt.chainID+")")
33 }
34}
35
36func TestTrimDomain(t *testing.T) {
37 tests := []struct {
38 in string
39 want string
40 }{
41 {"gno.land/r/moul/home", "r/moul/home"},
42 {"r/moul/home", "r/moul/home"},
43 {"/r/moul/home", "r/moul/home"},
44 {"gno.land/p/moul/mygnoscan/v0", "p/moul/mygnoscan/v0"},
45 {"gno.land/r/gnoland/blog", "r/gnoland/blog"},
46 {"", ""},
47 {"gno.land/", ""},
48 // A path whose own name starts with the domain is only trimmed once,
49 // so a realm cannot be linked out of its own namespace by its name.
50 {"gno.land/r/moul/gno.land", "r/moul/gno.land"},
51 // Anything not shaped like a package path comes back empty, so the
52 // caller falls back to a list page instead of emitting it.
53 {"r/moul/home)", ""},
54 {"r/moul/ho me", ""},
55 {"r/moul//home", ""},
56 {"r/moul/home\n", ""},
57 }
58 for _, tt := range tests {
59 uassert.Equal(t, tt.want, TrimDomain(tt.in), "TrimDomain("+tt.in+")")
60 }
61}
62
63func TestBaseNormalisation(t *testing.T) {
64 uassert.Equal(t, DefaultBase, New(DefaultBase+"/").Base())
65 uassert.Equal(t, DefaultBase, New(DefaultBase).Base())
66 uassert.Equal(t, "https://scan.example.com", New("https://scan.example.com/").Base())
67}
68
69func TestTargets(t *testing.T) {
70 s := mainnet()
71 const b = DefaultBase
72 tests := []struct {
73 name string
74 got string
75 want string
76 }{
77 {"home", s.Home(), b + "/?network=mainnet"},
78 {"page", s.Page(PageTxs), b + "/txs?network=mainnet"},
79 {"page nested", s.Page(PageGasRealms), b + "/gas/realms?network=mainnet"},
80 {"page slashed", s.Page("/blocks/"), b + "/blocks?network=mainnet"},
81 {"realm qualified", s.Realm("gno.land/r/moul/home"), b + "/realm/r/moul/home?network=mainnet"},
82 {"realm bare", s.Realm("r/moul/home"), b + "/realm/r/moul/home?network=mainnet"},
83 {"realm tab", s.Realm("r/moul/home", TabSource), b + "/realm/r/moul/home?network=mainnet&tab=source"},
84 // TabInfo is the tab the explorer lands on, so asking for it adds nothing.
85 {"realm tab info", s.Realm("r/moul/home", TabInfo), b + "/realm/r/moul/home?network=mainnet"},
86 {"realm empty", s.Realm(""), b + "/?network=mainnet"},
87 {"realm func", s.RealmFunc("r/moul/config", "Set"), b + "/realm/r/moul/config?fn=Set&network=mainnet&tab=source"},
88 {"realm file", s.RealmFile("r/moul/home", "render.gno", 42), b + "/realm/r/moul/home?file=render.gno&line=42&network=mainnet&tab=source"},
89 {"realm file no line", s.RealmFile("r/moul/home", "render.gno", 0), b + "/realm/r/moul/home?file=render.gno&network=mainnet&tab=source"},
90 {"address", s.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5"), b + "/address/g1manfred47kzduec920z88wfr64ylksmdcedlf5?network=mainnet"},
91 {"address empty", s.Address(""), b + "/accounts?network=mainnet"},
92 {"block", s.Block(80618), b + "/block/80618?network=mainnet"},
93 {"block zero", s.Block(0), b + "/blocks?network=mainnet"},
94 {"validator", s.Validator("g1manfred47kzduec920z88wfr64ylksmdcedlf5"), b + "/validator/g1manfred47kzduec920z88wfr64ylksmdcedlf5?network=mainnet"},
95 {"proposal", s.Proposal(7), b + "/govdao/7?network=mainnet"},
96 {"proposal negative", s.Proposal(-1), b + "/govdao/proposals?network=mainnet"},
97 {"token empty", s.Token(""), b + "/grc20?network=mainnet"},
98 }
99 for _, tt := range tests {
100 uassert.Equal(t, tt.want, tt.got, tt.name)
101 }
102}
103
104// TestEscaping covers the two routes whose argument is not URL-safe. Both are
105// read back with decodeURIComponent by the explorer, so percent-escaping the
106// whole segment is what round-trips; leaving the slashes alone would split the
107// segment and land on a different route entirely.
108func TestEscaping(t *testing.T) {
109 s := mainnet()
110
111 // A 32-byte hash in the base64 form the indexer and gnokey print, picked
112 // to carry all three of the characters that make base64 awkward in a URL.
113 // Everything outside the unreserved set moves, which is what the
114 // explorer's decodeURIComponent undoes byte for byte.
115 uassert.Equal(t,
116 DefaultBase+"/tx/BppSxd7qRiAPsMHzDx5mys%2BYpeDCMUzxI1qeb%2FEhaYc%3D?network=mainnet",
117 s.Tx("BppSxd7qRiAPsMHzDx5mys+YpeDCMUzxI1qeb/EhaYc="))
118 uassert.Equal(t, DefaultBase+"/txs?network=mainnet", s.Tx(""))
119
120 // A GRC20 ledger key carries both slashes and dots.
121 uassert.Equal(t,
122 DefaultBase+"/grc20/gno.land%2Fr%2Fgnoswap%2Fgns.GNS.0000000?network=mainnet",
123 s.Token("gno.land/r/gnoswap/gns.GNS.0000000"))
124}
125
126func TestURLQuery(cur realm, t *testing.T) {
127 s := mainnet()
128
129 uassert.Equal(t, DefaultBase+"/anything?network=mainnet", s.URL("anything"))
130 uassert.Equal(t, DefaultBase+"/anything?network=mainnet", s.URL("/anything"))
131 uassert.Equal(t, DefaultBase+"/a?k=v&network=mainnet", s.URL("/a", "k", "v"))
132
133 // An empty key or value is dropped, so an optional parameter needs no
134 // branch at the call site.
135 uassert.Equal(t, DefaultBase+"/a?network=mainnet", s.URL("/a", "k", ""))
136 uassert.Equal(t, DefaultBase+"/a?network=mainnet", s.URL("/a", "", "v"))
137
138 uassert.PanicsWithMessage(t, cur, "mygnoscan: odd number of query arguments", func() {
139 s.URL("/a", "k")
140 })
141}
142
143// TestNoNetwork pins what a Scanner emits when it does not know the chain:
144// nothing. The explorer then answers for every chain it indexes at once, which
145// is a real answer to a different question, so this is worth being explicit
146// about rather than discovering from a plausible-looking page.
147func TestNoNetwork(t *testing.T) {
148 s := New(DefaultBase).WithNetwork("")
149 uassert.Equal(t, "", s.Network())
150 uassert.Equal(t, DefaultBase+"/realm/r/moul/home", s.Realm("r/moul/home"))
151 uassert.Equal(t, DefaultBase+"/realm/r/moul/home?tab=calls", s.Realm("r/moul/home", TabCalls))
152 uassert.Equal(t, DefaultBase+"/", s.Home())
153}
154
155// TestDefaultUnderTest pins Default() as the test harness sees it: chain-id
156// "dev" is indexed nowhere, so the links carry no network. A realm rendering
157// under gnodev gets exactly this, which is why a local preview link is not
158// proof that the mainnet one is right.
159func TestDefaultUnderTest(t *testing.T) {
160 s := Default()
161 uassert.Equal(t, DefaultBase, s.Base())
162 uassert.Equal(t, "", s.Network())
163 uassert.Equal(t, DefaultBase+"/realm/r/moul/home", s.Realm("gno.land/r/moul/home"))
164}
165
166func TestTabArity(cur realm, t *testing.T) {
167 s := mainnet()
168 uassert.PanicsWithMessage(t, cur, "mygnoscan: at most one tab", func() {
169 s.Realm("r/moul/home", TabSource, TabCalls)
170 })
171}
172
173func TestLink(t *testing.T) {
174 uassert.Equal(t, "[text](https://example.com)", Link("text", "https://example.com"))
175}
176
177func TestRealmFooter(t *testing.T) {
178 s := mainnet()
179 const want = "[explorer](https://mygnoscan.moul.p2p.team/realm/r/moul/home?network=mainnet)" +
180 " · [source](https://mygnoscan.moul.p2p.team/realm/r/moul/home?network=mainnet&tab=source)" +
181 " · [calls](https://mygnoscan.moul.p2p.team/realm/r/moul/home?network=mainnet&tab=calls)" +
182 " · [deps](https://mygnoscan.moul.p2p.team/realm/r/moul/home?network=mainnet&tab=deps)"
183 uassert.Equal(t, want, s.RealmFooter("gno.land/r/moul/home"))
184 uassert.Equal(t, want, s.RealmFooter("r/moul/home"))
185 uassert.Equal(t, "", s.RealmFooter(""))
186}
187
188// TestOtherInstance is the whole reason base and network are separate knobs:
189// a second instance serving the same chain under a different id.
190func TestOtherInstance(t *testing.T) {
191 s := New("https://scan.example.com").WithNetwork("gnoland1")
192 uassert.Equal(t,
193 "https://scan.example.com/realm/r/moul/home?network=gnoland1",
194 s.Realm("gno.land/r/moul/home"))
195}
196
197// TestMarkdownBreakout is the reason safePath and escapeSegment exist. A realm
198// that links a path or a hash a caller typed would otherwise let that caller
199// close the markdown link early and write page text of their choosing.
200//
201// url.PathEscape is not enough on its own: "(" and ")" are legal URL
202// sub-delims and it leaves both alone.
203func TestMarkdownBreakout(t *testing.T) {
204 s := mainnet()
205
206 // A path is refused outright, because escaping its separators would break
207 // the route. The caller gets the front page, not a broken link.
208 uassert.Equal(t, DefaultBase+"/?network=mainnet",
209 s.Realm("r/evil/x)](https://evil.example.com"))
210 uassert.Equal(t, "", s.RealmFooter("r/evil/x)](https://evil.example.com"))
211
212 // A hash and a token key are opaque, so they are escaped rather than
213 // refused, and the parentheses move.
214 uassert.Equal(t,
215 DefaultBase+"/tx/x%29%5D%28evil?network=mainnet",
216 s.Tx("x)](evil"))
217 uassert.Equal(t,
218 DefaultBase+"/grc20/x%29%5D%28evil?network=mainnet",
219 s.Token("x)](evil"))
220
221 // An address is a string type; nothing stops a realm casting into it.
222 uassert.Equal(t,
223 DefaultBase+"/address/g1%29%5D%28evil?network=mainnet",
224 s.Address(address("g1)](evil")))
225
226 // A page name that is not a page name lands on the front page, rather
227 // than on the explorer's own unknown-path-renders-home behaviour, which
228 // would look identical and mean something else.
229 uassert.Equal(t, DefaultBase+"/?network=mainnet", s.Page("txs)](evil"))
230 uassert.Equal(t, DefaultBase+"/?network=mainnet", s.Page(""))
231}
232
233func TestSafePath(t *testing.T) {
234 tests := []struct {
235 in string
236 want bool
237 }{
238 {"r/moul/home", true},
239 {"p/moul/mygnoscan/v0", true},
240 {"gas/realms", true},
241 {"a", true},
242 {"a.b-c_d", true},
243 {"", false},
244 {"a//b", false},
245 {"a/", false},
246 {"a b", false},
247 {"a)b", false},
248 {"a(b", false},
249 {"a]b", false},
250 {"a\nb", false},
251 {"a?b", false},
252 {"a#b", false},
253 {"a%b", false},
254 }
255 for _, tt := range tests {
256 uassert.Equal(t, tt.want, safePath(tt.in), "safePath("+tt.in+")")
257 }
258}
259
260func TestSubViews(t *testing.T) {
261 s := mainnet()
262 uassert.Equal(t, DefaultBase+"/packages?network=mainnet&pv=inert", s.Packages(PackagesInert))
263 uassert.Equal(t, DefaultBase+"/packages?network=mainnet", s.Packages(""))
264 uassert.Equal(t, DefaultBase+"/accounts?av=balances&network=mainnet", s.Accounts(AccountsBalances))
265 uassert.Equal(t, DefaultBase+"/accounts?network=mainnet", s.Accounts(""))
266}