test_client.nim
1 {.used.} 2 3 import 4 std/[sequtils, strutils, tables], 5 chronos, 6 stew/base64, 7 testutils/unittests, 8 ../dnsdisc/[tree, client] 9 10 procSuite "Test DNS Discovery: Client": 11 12 # Suite setup 13 # Create sample tree from EIP-1459 14 var treeRecords {.threadvar.}: Table[string, string] 15 16 treeRecords["nodes.example.org"] = "enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA" 17 treeRecords["C7HRFPF3BLGF3YR4DY5KX3SMBE.nodes.example.org"] = "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org" 18 treeRecords["JWXYDBPXYWG6FX3GMDIBFA6CJ4.nodes.example.org"] = "enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,H4FHT4B454P6UXFD7JCYQ5PWDY,MHTDO6TMUBRIA2XWG5LUDACK24" 19 treeRecords["2XS2367YHAXJFGLZHVAWLQD4ZY.nodes.example.org"] = "enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA" 20 treeRecords["H4FHT4B454P6UXFD7JCYQ5PWDY.nodes.example.org"] = "enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI" 21 treeRecords["MHTDO6TMUBRIA2XWG5LUDACK24.nodes.example.org"] = "enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o" 22 23 proc resolver(domain: string): Future[string] {.async.} = 24 return treeRecords[domain] 25 26 asyncTest "Resolve root": 27 ## This tests resolving a root TXT entry at a given domain location, 28 ## parsing the entry and verifying the signature. 29 30 # Expected case 31 32 let 33 loc = parseLinkEntry("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org").tryGet() 34 root = waitFor resolveRoot(resolver, loc) 35 36 check: 37 root.isOk() 38 root[].eroot == "JWXYDBPXYWG6FX3GMDIBFA6CJ4" 39 root[].lroot == "C7HRFPF3BLGF3YR4DY5KX3SMBE" 40 root[].seqNo == 1 41 root[].signature == Base64Url.decode("o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA") 42 43 # Invalid cases 44 45 check: 46 # Invalid signature 47 (waitFor resolveRoot(resolver, parseLinkEntry("enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@nodes.example.org").tryGet())) 48 .error() 49 .contains("Could not verify signature") 50 51 asyncTest "Resolve subtree entry": 52 ## This tests resolving a subtree TXT entry at a given subdomain, 53 ## parsing the entry and verifying the subdomain hash. 54 55 # Expected case 56 57 let 58 loc = parseLinkEntry("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org").tryGet() 59 entry = waitFor resolveSubtreeEntry(resolver, loc, "2XS2367YHAXJFGLZHVAWLQD4ZY") 60 61 check: 62 entry.isOk() 63 entry[].kind == Enr 64 entry[].enrEntry == parseEnrEntry("enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA").tryGet() 65 66 # Invalid cases 67 # Add invalid entry to example tree 68 treeRecords["2XS2367YHAXJFGLZHVAWLQE4ZY.nodes.example.org"] = "enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA" 69 70 check: 71 # Invalid hash 72 (waitFor resolveSubtreeEntry(resolver, loc, "2XS2367YHAXJFGLZHVAWLQE4ZY")) 73 .error() 74 .contains("Could not verify subdomain hash") 75 76 # Remove invalid entry for future tests 77 treeRecords.del("2XS2367YHAXJFGLZHVAWLQE4ZY.nodes.example.org") 78 79 asyncTest "Resolve all subtree entries": 80 ## This tests resolving all subtree entries at a given root, 81 ## parsing and verifying the entries 82 83 # Expected case 84 85 let 86 loc = parseLinkEntry("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org").tryGet() 87 rootEntry = (waitFor resolveRoot(resolver, loc)).tryGet() 88 entries = waitFor resolveAllEntries(resolver, loc, rootEntry) 89 90 # We expect 3 ENR entries and one link entry 91 let 92 enrs = entries.filterIt(it.kind == Enr).mapIt(it.enrEntry) 93 links = entries.filterIt(it.kind == Link).mapIt(it.linkEntry) 94 95 check: 96 entries.len == 4 97 enrs.len == 3 98 enrs.contains(parseEnrEntry("enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA").tryGet()) 99 enrs.contains(parseEnrEntry("enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI").tryGet()) 100 enrs.contains(parseEnrEntry("enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o").tryGet()) 101 links.len == 1 102 links.contains(parseLinkEntry("enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org").tryGet()) 103 104 # Invalid case 105 proc invalidResolver(domain: string): Future[string] {.async.} = 106 return "" 107 108 check: 109 # If no entries can be resolved without error, empty set will be returned 110 (waitFor resolveAllEntries(invalidResolver, loc, rootEntry)).len == 0 111 112 asyncTest "Sync tree": 113 ## This tests creating a client at a specific domain location 114 ## and syncing the entire tree at that location 115 116 # Expected case 117 118 let loc = parseLinkEntry("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org").tryGet() 119 120 var 121 client = Client(loc: loc, tree: Tree()) 122 tree = client.getTree(resolver) 123 124 # Verify root 125 check: 126 tree.rootEntry.eroot == "JWXYDBPXYWG6FX3GMDIBFA6CJ4" 127 tree.rootEntry.lroot == "C7HRFPF3BLGF3YR4DY5KX3SMBE" 128 tree.rootEntry.seqNo == 1 129 tree.rootEntry.signature == Base64Url.decode("o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA") 130 131 # Verify subtree entries 132 let 133 enrs = tree.getNodes() 134 links = tree.getLinks() 135 136 check: 137 tree.entries.len == 4 138 enrs.len == 3 139 enrs.contains(parseEnrEntry("enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA").tryGet()) 140 enrs.contains(parseEnrEntry("enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI").tryGet()) 141 enrs.contains(parseEnrEntry("enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o").tryGet()) 142 links.len == 1 143 links.contains(parseLinkEntry("enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org").tryGet()) 144 145 # Invalid cases 146 proc invalidResolver(domain: string): Future[string] {.async.} = 147 return "" 148 149 proc validRootResolver(domain: string): Future[string] {.async.} = 150 return "enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA" 151 152 # Invalid case 1: Root entry fails to parse 153 expect CatchableError: 154 # Expect ResultError if not even root entry can be resolved 155 discard client.getTree(invalidResolver) 156 157 # Invalid case 2: Root parses, but no subtree entries 158 let errTree = client.getTree(validRootResolver) 159 160 check: 161 # Root parses as expected, but no entries resolved 162 errTree.rootEntry == parseRootEntry("enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA").tryGet() 163 errTree.entries.len == 0 164 165 asyncTest "Get node records": 166 ## This tests getting node records from a client tree 167 168 let loc = parseLinkEntry("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org").tryGet() 169 170 var client = Client(loc: loc, tree: Tree()) 171 172 discard client.getTree(resolver) # This syncs the tree 173 174 # Verify enrs 175 var 176 expEnr1, expEnr2, expEnr3: Record 177 178 check: 179 expEnr1.fromURI("enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA") 180 expEnr2.fromURI("enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI") 181 expEnr3.fromURI("enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o") 182 183 let enrs = client.getNodeRecords() 184 185 check: 186 enrs.len == 3 187 enrs.contains(expEnr1) 188 enrs.contains(expEnr2) 189 enrs.contains(expEnr3)