/ tests / test_client.nim
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)