foxygit / Torlinkc Log in
commits tags

/tests/test_magnet.cpp · 5.05 KB

raw
#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());
}