From 29bb01751b3c1c2bf092baf7d46d90f7a0663824 Mon Sep 17 00:00:00 2001 From: Marcin Czenko Date: Mon, 2 Jun 2025 02:18:58 +0200 Subject: [PATCH] adds integration tests for using magnet links and torrent file while retrieving BitTorrent content from Codex Net --- tests/integration/codexclient.nim | 47 +++++++++++++++ tests/integration/testbittorrent.nim | 86 +++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/tests/integration/codexclient.nim b/tests/integration/codexclient.nim index c3cdf2e1..cb16677b 100644 --- a/tests/integration/codexclient.nim +++ b/tests/integration/codexclient.nim @@ -94,6 +94,16 @@ proc info*( let response = await client.get(client.baseurl & "/debug/info") return JsonNode.parse(await response.body) +proc connect*( + client: CodexClient, peerId: string, address: string +): Future[?!void] {.async: (raises: [CancelledError, HttpError]).} = + let url = client.baseurl & "/connect/" & peerId & "?addrs=" & address + let response = await client.get(url) + if response.status != 200: + return + failure("Cannot connect to node with peerId: " & peerId & ": " & $response.status) + return success() + proc setLogLevel*( client: CodexClient, level: string ): Future[void] {.async: (raises: [CancelledError, HttpError]).} = @@ -196,6 +206,43 @@ proc downloadTorrent*( success await response.body +proc downloadTorrent*( + client: CodexClient, + contents: string, + contentType = "text/plain", + endpoint = "magnet", +): Future[?!string] {.async: (raises: [CancelledError, HttpError]).} = + if contents.len == 0: + return failure("No content provided!") + if endpoint != "magnet" and endpoint != "torrent-file": + return failure( + "Invalid endpoint: has to be either 'magnet' or 'torrent-file' but got: " & + endpoint + ) + if endpoint == "magnet" and + (contentType != "application/octet-stream" and contentType != "text/plain"): + return failure( + "Invalid content type: for 'magnet' endpoint has to be either 'application/octet-stream' or 'text/plain' but got: " & + contentType + ) + if endpoint == "torrent-file" and + (contentType != "application/octet-stream" and contentType != "application/json"): + return failure( + "Invalid content type: for 'torrent-file' endpoint has to be either 'application/octet-stream' or 'application/json' but got: " & + contentType + ) + + var headers = newSeq[HttpHeaderTuple]() + headers = @[("Content-Type", contentType)] + + let response = await client.post( + client.baseurl & "/torrent/" & endpoint, body = contents, headers = headers + ) + if not response.status == 200: + return failure($response.status) + + success await response.body + proc downloadManifestOnly*( client: CodexClient, cid: Cid ): Future[?!string] {.async: (raises: [CancelledError, HttpError]).} = diff --git a/tests/integration/testbittorrent.nim b/tests/integration/testbittorrent.nim index 23b5d085..10cd65c6 100644 --- a/tests/integration/testbittorrent.nim +++ b/tests/integration/testbittorrent.nim @@ -1,7 +1,9 @@ import std/net +import std/strformat import std/sequtils +import std/json except `%`, `%*` import pkg/nimcrypto -from pkg/libp2p import `==`, `$`, MultiHash, init +from pkg/libp2p import `==`, `$`, MultiHash, init, digest, hex import pkg/codex/units import pkg/codex/utils/iter import pkg/codex/manifest @@ -10,7 +12,6 @@ import pkg/codex/bittorrent/manifest import ./twonodes import ../examples import ../codex/examples -import json proc createInfoDictionaryForContent( content: seq[byte], pieceLength = DefaultPieceLength.int, name = string.none @@ -55,12 +56,93 @@ proc createInfoDictionaryForContent( success info twonodessuite "BitTorrent API": + setup: + # why we do not seem to need this? yet it is twice as fast with this + let infoPeer1 = (await client1.info()).tryGet + let peerId1 = infoPeer1["id"].getStr() + let announceAddress1 = infoPeer1["announceAddresses"][0].getStr() + (await client2.connect(peerId1, announceAddress1)).tryGet + test "uploading and downloading the content", twoNodesConfig: let exampleContent = exampleString(100) let infoHash = (await client1.uploadTorrent(exampleContent)).tryGet let downloadedContent = (await client2.downloadTorrent(infoHash)).tryGet check downloadedContent == exampleContent + test "downloading content using magnet link", twoNodesConfig: + let exampleContent = exampleString(100) + let multiHash = (await client1.uploadTorrent(exampleContent)).tryGet + let infoHash = byteutils.toHex(multiHash.data.buffer[multiHash.dpos .. ^1]) + let magnetLink = fmt"magnet:?xt=urn:btih:{infoHash}" + let downloadedContent = (await client2.downloadTorrent(magnetLink)).tryGet + check downloadedContent == exampleContent + + test "downloading content using torrent file", twoNodesConfig: + let exampleFileName = "example.txt" + let exampleContent = exampleString(100) + let multiHash = ( + await client1.uploadTorrent( + contents = exampleContent, + filename = some exampleFileName, + contentType = "text/plain", + ) + ).tryGet + + let expectedInfo = createInfoDictionaryForContent( + content = exampleContent.toBytes, name = some exampleFileName + ).tryGet + + let expectedInfoBencoded = expectedInfo.bencode() + let expectedMultiHash = + MultiHash.digest($Sha1HashCodec, expectedInfoBencoded).mapFailure.tryGet() + + assert expectedMultiHash == multiHash + + let torrentFileContent = "d4:info" & string.fromBytes(expectedInfoBencoded) & "e" + + let downloadedContent = ( + await client2.downloadTorrent( + contents = torrentFileContent, + contentType = "application/octet-stream", + endpoint = "torrent-file", + ) + ).tryGet + check downloadedContent == exampleContent + + test "downloading content using torrent file (JSON format)", twoNodesConfig: + let exampleFileName = "example.txt" + let exampleContent = exampleString(100) + let multiHash = ( + await client1.uploadTorrent( + contents = exampleContent, + filename = some exampleFileName, + contentType = "text/plain", + ) + ).tryGet + + let expectedInfo = createInfoDictionaryForContent( + content = exampleContent.toBytes, name = some exampleFileName + ).tryGet + + let expectedInfoBencoded = expectedInfo.bencode() + let expectedMultiHash = + MultiHash.digest($Sha1HashCodec, expectedInfoBencoded).mapFailure.tryGet() + + assert expectedMultiHash == multiHash + + let infoJson = %*{"info": %expectedInfo} + + let torrentJson = $infoJson + + let downloadedContent = ( + await client2.downloadTorrent( + contents = torrentJson, + contentType = "application/json", + endpoint = "torrent-file", + ) + ).tryGet + check downloadedContent == exampleContent + test "uploading and downloading the content (exactly one piece long)", twoNodesConfig: let numOfBlocksPerPiece = int(DefaultPieceLength div BitTorrentBlockSize) let bytes = await RandomChunker.example(