#include #include #include #include #include static int testsPassed = 0; static int testsFailed = 0; #define ASSERT_TRUE(cond) \ do { \ if (!(cond)) { \ fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ testsFailed++; \ return; \ } \ } while (0) #define ASSERT_EQ(a, b) \ do { \ if ((a) != (b)) { \ fprintf(stderr, " FAIL: %s:%d: %s != %s\n", __FILE__, __LINE__, #a, #b); \ testsFailed++; \ return; \ } \ } while (0) #define ASSERT_SIZE(a, b) \ do { \ if ((a) != (b)) { \ fprintf(stderr, " FAIL: %s:%d: %s == %zu, expected %zu\n", __FILE__, __LINE__, #a, static_cast(a), \ static_cast(b)); \ testsFailed++; \ return; \ } \ } while (0) #define PASS() testsPassed++ namespace { bool parseSingleBookEntry(OpdsEntry& entryOut, const char* href, const char* type = "application/epub+zip") { const std::string xml = std::string(R"( Example Book Example Author book-1 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml.data()), xml.size()); parser.flush(); if (parser.error()) { fprintf(stderr, " FAIL: %s:%d: parser.error()\n", __FILE__, __LINE__); testsFailed++; return false; } if (entries.size() != 1) { fprintf(stderr, " FAIL: %s:%d: entries.size() == %zu, expected 1\n", __FILE__, __LINE__, entries.size()); testsFailed++; return false; } entryOut = entries.front(); return true; } bool assertSingleFormat(const OpdsEntry& entry, const char* formatKey, const char* fileExtension) { if (entry.type != OpdsEntryType::BOOK) { fprintf(stderr, " FAIL: %s:%d: entry.type != OpdsEntryType::BOOK\n", __FILE__, __LINE__); testsFailed++; return false; } if (entry.acquisitionLinks.size() != 1) { fprintf(stderr, " FAIL: %s:%d: entry.acquisitionLinks.size() == %zu, expected 1\n", __FILE__, __LINE__, entry.acquisitionLinks.size()); testsFailed++; return false; } if (entry.acquisitionLinks[0].formatKey != formatKey) { fprintf(stderr, " FAIL: %s:%d: formatKey actual='%s' expected='%s'\n", __FILE__, __LINE__, entry.acquisitionLinks[0].formatKey.c_str(), formatKey); testsFailed++; return false; } if (entry.acquisitionLinks[0].fileExtension != fileExtension) { fprintf(stderr, " FAIL: %s:%d: fileExtension actual='%s' expected='%s'\n", __FILE__, __LINE__, entry.acquisitionLinks[0].fileExtension.c_str(), fileExtension); testsFailed++; return false; } return true; } } // namespace void testEpubExtension() { printf("testEpubExtension...\n"); OpdsEntry entry; if (!parseSingleBookEntry(entry, "/books/example.epub")) return; ASSERT_TRUE(assertSingleFormat(entry, "epub", ".epub")); PASS(); } void testKepubDoubleExtension() { printf("testKepubDoubleExtension...\n"); OpdsEntry entry; if (!parseSingleBookEntry(entry, "/books/example.kepub.epub")) return; ASSERT_TRUE(assertSingleFormat(entry, "kepub", ".kepub.epub")); PASS(); } void testBareKepubExtension() { printf("testBareKepubExtension...\n"); OpdsEntry entry; if (!parseSingleBookEntry(entry, "/books/example.kepub")) return; ASSERT_TRUE(assertSingleFormat(entry, "kepub", ".kepub.epub")); PASS(); } void testSlashTerminatedKepubPath() { printf("testSlashTerminatedKepubPath...\n"); OpdsEntry entry; if (!parseSingleBookEntry(entry, "/opds/download/6516/kepub/")) return; ASSERT_TRUE(assertSingleFormat(entry, "kepub", ".kepub.epub")); PASS(); } void testSlashTerminatedEpubPath() { printf("testSlashTerminatedEpubPath...\n"); OpdsEntry entry; if (!parseSingleBookEntry(entry, "/opds/download/6516/epub/")) return; ASSERT_TRUE(assertSingleFormat(entry, "epub", ".epub")); PASS(); } void testDistinctAcquisitionFormatsRemainSeparate() { printf("testDistinctAcquisitionFormatsRemainSeparate...\n"); const char* xml = R"( Example Book Example Author book-2 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 4); ASSERT_EQ(links[0].formatKey, "epub"); ASSERT_EQ(links[1].formatKey, "kepub"); ASSERT_EQ(links[2].formatKey, "txt"); ASSERT_EQ(links[3].formatKey, "md"); PASS(); } void testUnsupportedMimeType() { printf("testUnsupportedMimeType...\n"); const char* xml = R"( Example Book Example Author book-3 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); ASSERT_SIZE(entries.size(), 0); PASS(); } void testEmptyHrefOrType() { printf("testEmptyHrefOrType...\n"); const char* xml = R"( Empty Href Example Author book-4 Empty Type Example Author book-5 Missing Href Example Author book-6 Missing Type Example Author book-7 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); ASSERT_SIZE(entries.size(), 0); PASS(); } void testDuplicateAcquisitionLinks() { printf("testDuplicateAcquisitionLinks...\n"); const char* xml = R"( Example Book Example Author book-8 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 2); ASSERT_EQ(links[0].formatKey, "epub"); ASSERT_EQ(links[0].href, "/books/example.epub"); ASSERT_EQ(links[1].formatKey, "epub"); ASSERT_EQ(links[1].href, "/books/example-copy.epub"); PASS(); } void testIdenticalHrefAcquisitionLinksAreDeduplicated() { printf("testIdenticalHrefAcquisitionLinksAreDeduplicated...\n"); const char* xml = R"( Example Book Example Author book-9 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 1); ASSERT_EQ(links[0].formatKey, "epub"); ASSERT_EQ(links[0].href, "/books/example.epub"); PASS(); } void testSlashVariantHrefAcquisitionLinksAreDeduplicated() { printf("testSlashVariantHrefAcquisitionLinksAreDeduplicated...\n"); const char* xml = R"( Example Book Example Author book-10 )"; std::vector entries; OpdsParser parser; parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); }; parser.write(reinterpret_cast(xml), strlen(xml)); parser.flush(); ASSERT_TRUE(!parser.error()); ASSERT_SIZE(entries.size(), 1); const auto& links = entries.front().acquisitionLinks; ASSERT_SIZE(links.size(), 1); ASSERT_EQ(links[0].formatKey, "epub"); ASSERT_EQ(links[0].href, "/books/example.epub"); PASS(); } int main() { printf("=== OPDS Parser Tests ===\n\n"); testEpubExtension(); testKepubDoubleExtension(); testBareKepubExtension(); testSlashTerminatedKepubPath(); testSlashTerminatedEpubPath(); testDistinctAcquisitionFormatsRemainSeparate(); testUnsupportedMimeType(); testEmptyHrefOrType(); testDuplicateAcquisitionLinks(); testIdenticalHrefAcquisitionLinksAreDeduplicated(); testSlashVariantHrefAcquisitionLinksAreDeduplicated(); printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed); return testsFailed > 0 ? 1 : 0; }