1package mygnoscan23import (4 "testing"56 "gno.land/p/nt/uassert/v0"7)89// mainnet is what a Scanner looks like on the chain that matters. Tests build10// it explicitly rather than calling Default(), because the test harness runs11// with chain-id "dev", which NetworkFor maps to no network at all: pinning12// Default()'s output would pin the absence of the parameter and prove nothing13// about the case every real link is in.14func mainnet() Scanner { return New(DefaultBase).WithNetwork("mainnet") }1516func TestNetworkFor(t *testing.T) {17 tests := []struct {18 chainID string19 want string20 }{21 {"gnoland-1", "mainnet"},22 {"pearl-1", "pearl"},23 {"sapphire-1", "sapphire"},24 {"staging", "staging"},25 {"dev", ""}, // gnodev, and what `gno test` reports26 {"", ""}, // no chain at all27 {"gnoland1", ""}, // the upstream default instance's id, not a chain-id28 {"test3", ""}, // a retired testnet29 {"mainnet", ""}, // the network id, fed back in by mistake30 }31 for _, tt := range tests {32 uassert.Equal(t, tt.want, NetworkFor(tt.chainID), "NetworkFor("+tt.chainID+")")33 }34}3536func TestTrimDomain(t *testing.T) {37 tests := []struct {38 in string39 want string40 }{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 the52 // 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}6263func 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}6869func TestTargets(t *testing.T) {70 s := mainnet()71 const b = DefaultBase72 tests := []struct {73 name string74 got string75 want string76 }{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}103104// TestEscaping covers the two routes whose argument is not URL-safe. Both are105// read back with decodeURIComponent by the explorer, so percent-escaping the106// whole segment is what round-trips; leaving the slashes alone would split the107// segment and land on a different route entirely.108func TestEscaping(t *testing.T) {109 s := mainnet()110111 // A 32-byte hash in the base64 form the indexer and gnokey print, picked112 // to carry all three of the characters that make base64 awkward in a URL.113 // Everything outside the unreserved set moves, which is what the114 // 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(""))119120 // 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}125126func TestURLQuery(cur realm, t *testing.T) {127 s := mainnet()128129 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"))132133 // An empty key or value is dropped, so an optional parameter needs no134 // 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"))137138 uassert.PanicsWithMessage(t, cur, "mygnoscan: odd number of query arguments", func() {139 s.URL("/a", "k")140 })141}142143// 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, which145// is a real answer to a different question, so this is worth being explicit146// 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}154155// TestDefaultUnderTest pins Default() as the test harness sees it: chain-id156// "dev" is indexed nowhere, so the links carry no network. A realm rendering157// under gnodev gets exactly this, which is why a local preview link is not158// 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}165166func 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}172173func TestLink(t *testing.T) {174 uassert.Equal(t, "[text](https://example.com)", Link("text", "https://example.com"))175}176177func 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}187188// 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}196197// TestMarkdownBreakout is the reason safePath and escapeSegment exist. A realm198// that links a path or a hash a caller typed would otherwise let that caller199// 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 URL202// sub-delims and it leaves both alone.203func TestMarkdownBreakout(t *testing.T) {204 s := mainnet()205206 // A path is refused outright, because escaping its separators would break207 // 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"))211212 // A hash and a token key are opaque, so they are escaped rather than213 // 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"))220221 // 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")))225226 // A page name that is not a page name lands on the front page, rather227 // than on the explorer's own unknown-path-renders-home behaviour, which228 // 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}232233func TestSafePath(t *testing.T) {234 tests := []struct {235 in string236 want bool237 }{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}259260func 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}267Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.