#include <doctest/doctest.h>
#include "torlinkc/sources/magnet.hpp"
using namespace torlinkc;
TEST_CASE("parseMagnet keeps a full 40-char hex info hash") {
const std::string hash = "abcdef0123456789abcdef0123456789abcdef01";
auto m = parseMagnet("magnet:?xt=urn:btih:" + hash + "&dn=Cool+Movie");
REQUIRE(m.has_value());
CHECK(m->infoHash == hash);
CHECK(m->infoHash.size() == 40);
CHECK(m->name == "Cool Movie");
}
TEST_CASE("parseMagnet decodes a 32-char base32 hash to 40-char hex") {
auto m = parseMagnet("magnet:?xt=urn:btih:MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U&dn=X");
REQUIRE(m.has_value());
CHECK(m->infoHash.size() == 40);
for (char c : m->infoHash) CHECK(((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')));
}
TEST_CASE("parseMagnet falls back to the hash as the name when dn is absent") {
const std::string hash = "abcdef0123456789abcdef0123456789abcdef01";
auto m = parseMagnet("magnet:?xt=urn:btih:" + hash);
REQUIRE(m.has_value());
CHECK(m->name == hash);
}
TEST_CASE("parseMagnet returns nullopt for non-magnets and malformed hashes") {
CHECK_FALSE(parseMagnet("not a magnet").has_value());
CHECK_FALSE(parseMagnet("magnet:?xt=urn:btih:tooshort").has_value());
CHECK_FALSE(parseMagnet("prefix magnet:?xt=urn:btih:" + std::string(40, 'a')).has_value());
}
TEST_CASE("normalizeInfoHash lowercases 40-char hex") {
CHECK(normalizeInfoHash("ABCDEF0123456789ABCDEF0123456789ABCDEF01") ==
"abcdef0123456789abcdef0123456789abcdef01");
}
TEST_CASE("normalizeInfoHash decodes 32-char base32 to 40-char hex") {
auto hex = normalizeInfoHash("MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U");
CHECK(hex.size() == 40);
}
TEST_CASE("buildMagnet builds a magnet with encoded name and trackers") {
auto out = buildMagnet("abc123", "My Movie 2024");
CHECK(out.find("xt=urn:btih:abc123") != std::string::npos);
CHECK(out.find("dn=My%20Movie%202024") != std::string::npos);
CHECK(out.find("&tr=") != std::string::npos);
// At least one non-UDP tracker, so UDP-blocked networks can still announce.
CHECK(out.find("http%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce") != std::string::npos);
}
TEST_CASE("buildMagnet puts extra trackers ahead of the public defaults") {
const std::string own = "http://private.example.org/announce?pk=xyz";
auto out = buildMagnet("abc123", "Thing", {own});
auto minePos = out.find("&tr=http%3A%2F%2Fprivate.example.org%2Fannounce%3Fpk%3Dxyz");
auto defaultPos = out.find("udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce");
REQUIRE(minePos != std::string::npos);
REQUIRE(defaultPos != std::string::npos);
CHECK(minePos < defaultPos);
}
TEST_CASE("buildMagnet keeps one copy of a tracker that is also a default, and drops blanks") {
const std::string dupe = "udp://tracker.opentrackr.org:1337/announce";
auto out = buildMagnet("abc123", "Thing", {dupe, " ", dupe});
const std::string needle = "&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce";
std::size_t count = 0;
std::size_t pos = 0;
while ((pos = out.find(needle, pos)) != std::string::npos) {
count++;
pos += needle.size();
}
CHECK(count == 1);
}
TEST_CASE("isInfoHash accepts a bare 40-char hex hash") { CHECK(isInfoHash(std::string(40, 'a'))); }
TEST_CASE("isInfoHash accepts a bare 32-char base32 hash") {
CHECK(isInfoHash("MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U"));
}
TEST_CASE("isInfoHash rejects ordinary queries and malformed hashes") {
CHECK_FALSE(isInfoHash("the office 1080p"));
CHECK_FALSE(isInfoHash(std::string(40, 'g'))); // 40 chars but not hex
CHECK_FALSE(isInfoHash(std::string(39, 'a'))); // too short
CHECK_FALSE(isInfoHash(""));
}
TEST_CASE("parseInput parses a full magnet URI just like parseMagnet") {
const std::string hash = "abcdef0123456789abcdef0123456789abcdef01";
auto m = parseInput("magnet:?xt=urn:btih:" + hash + "&dn=Cool+Movie");
REQUIRE(m.has_value());
CHECK(m->infoHash == hash);
CHECK(m->name == "Cool Movie");
}
TEST_CASE("parseInput wraps a bare 40-char hex hash into a magnet with trackers") {
const std::string hash = "abcdef0123456789abcdef0123456789abcdef01";
auto m = parseInput(hash);
REQUIRE(m.has_value());
CHECK(m->infoHash == hash);
CHECK(m->name == hash);
CHECK(m->magnet.find("xt=urn:btih:" + hash) != std::string::npos);
CHECK(m->magnet.find("&tr=") != std::string::npos);
}
TEST_CASE("parseInput decodes a bare 32-char base32 hash to 40-char hex") {
auto m = parseInput("MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U");
REQUIRE(m.has_value());
CHECK(m->infoHash.size() == 40);
CHECK(m->magnet.find("xt=urn:btih:" + m->infoHash) != std::string::npos);
}
TEST_CASE("parseInput trims whitespace around a bare hash") {
const std::string hash = "abcdef0123456789abcdef0123456789abcdef01";
auto m = parseInput(" " + hash + " ");
REQUIRE(m.has_value());
CHECK(m->infoHash == hash);
}
TEST_CASE("parseInput returns nullopt for ordinary queries and junk") {
CHECK_FALSE(parseInput("the office 1080p").has_value());
CHECK_FALSE(parseInput(std::string(40, 'g')).has_value());
CHECK_FALSE(parseInput("magnet:?xt=urn:btih:tooshort").has_value());
CHECK_FALSE(parseInput("").has_value());
}