#define CPPHTTPLIB_ZLIB_SUPPORT 1 #include "./httplib.h" #include "gtest/gtest.h" #define SERVER_PORT 8001 #include "server_testing_tools.h" //////////////////////////////////////////////////////////////////////////////// // Testing of the library-related functionality of the server //////////////////////////////////////////////////////////////////////////////// class LibraryServerTest : public ::testing::Test { protected: std::unique_ptr zfs1_; const int PORT = 8002; protected: void SetUp() override { zfs1_.reset(new ZimFileServer(PORT, "./test/library.xml")); } void TearDown() override { zfs1_.reset(); } }; // Returns a copy of 'text' where every line that fully matches 'pattern' // preceded by optional whitespace is replaced with the fixed string // 'replacement' preserving the leading whitespace std::string replaceLines(const std::string& text, const std::string& pattern, const std::string& replacement) { std::regex regex("^ *" + pattern + "$"); std::ostringstream oss; std::istringstream iss(text); std::string line; while ( std::getline(iss, line) ) { if ( std::regex_match(line, regex) ) { for ( size_t i = 0; i < line.size() && line[i] == ' '; ++i ) oss << ' '; oss << replacement << "\n"; } else { oss << line << "\n"; } } return oss.str(); } std::string maskVariableOPDSFeedData(std::string s) { s = replaceLines(s, R"(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ)", "YYYY-MM-DDThh:mm:ssZ"); s = replaceLines(s, "[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}", "12345678-90ab-cdef-1234-567890abcdef"); return s; } #define OPDS_FEED_TAG \ "\n" #define CATALOG_LINK_TAGS \ " \n" \ " \n" #define CHARLES_RAY_CATALOG_ENTRY \ " \n" \ " urn:uuid:charlesray\n" \ " Charles, Ray\n" \ " YYYY-MM-DDThh:mm:ssZ\n" \ " Wikipedia articles about Ray Charles\n" \ " fra\n" \ " wikipedia_fr_ray_charles\n" \ " \n" \ " jazz\n" \ " unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes\n" \ " 284\n" \ " 2\n" \ " \n" \ " \n" \ " Wikipedia\n" \ " \n" \ " \n" \ " Kiwix\n" \ " \n" \ " 2020-03-31T00:00:00Z\n" \ " \n" \ " \n" #define RAY_CHARLES_CATALOG_ENTRY \ " \n" \ " urn:uuid:raycharles\n" \ " Ray Charles\n" \ " YYYY-MM-DDThh:mm:ssZ\n" \ " Wikipedia articles about Ray Charles\n" \ " eng\n" \ " wikipedia_en_ray_charles\n" \ " \n" \ " wikipedia\n" \ " public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes\n" \ " 284\n" \ " 2\n" \ " \n" \ " \n" \ " \n" \ " Wikipedia\n" \ " \n" \ " \n" \ " Kiwix\n" \ " \n" \ " 2020-03-31T00:00:00Z\n" \ " \n" \ " \n" #define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY \ " \n" \ " urn:uuid:raycharles_uncategorized\n" \ " Ray (uncategorized) Charles\n" \ " YYYY-MM-DDThh:mm:ssZ\n" \ " No category is assigned to this library entry.\n" \ " rus\n" \ " wikipedia_ru_ray_charles\n" \ " \n" \ " \n" \ " public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no\n" \ " 284\n" \ " 2\n" \ " \n" \ " \n" \ " Wikipedia\n" \ " \n" \ " \n" \ " Kiwix\n" \ " \n" \ " 2020-03-31T00:00:00Z\n" \ " \n" \ " \n" TEST_F(LibraryServerTest, catalog_root_xml) { const auto r = zfs1_->GET("/ROOT/catalog/root.xml"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " All zims\n" " YYYY-MM-DDThh:mm:ssZ\n" "\n" CATALOG_LINK_TAGS CHARLES_RAY_CATALOG_ENTRY RAY_CHARLES_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_searchdescription_xml) { const auto r = zfs1_->GET("/ROOT/catalog/searchdescription.xml"); EXPECT_EQ(r->status, 200); EXPECT_EQ(r->body, "\n" "\n" " Zim catalog search\n" " Search zim files in the catalog.\n" " \n" "\n" ); } TEST_F(LibraryServerTest, catalog_search_by_phrase) { const auto r = zfs1_->GET("/ROOT/catalog/search?q=\"ray%20charles\""); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (q="ray charles")\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" " 2\n" CATALOG_LINK_TAGS RAY_CHARLES_CATALOG_ENTRY CHARLES_RAY_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_search_by_words) { const auto r = zfs1_->GET("/ROOT/catalog/search?q=ray%20charles"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (q=ray charles)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" " 3\n" CATALOG_LINK_TAGS RAY_CHARLES_CATALOG_ENTRY CHARLES_RAY_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_prefix_search) { { const auto r = zfs1_->GET("/ROOT/catalog/search?q=description:ray%20description:charles"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (q=description:ray description:charles)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" " 2\n" CATALOG_LINK_TAGS RAY_CHARLES_CATALOG_ENTRY CHARLES_RAY_CATALOG_ENTRY "\n" ); } { const auto r = zfs1_->GET("/ROOT/catalog/search?q=title:\"ray%20charles\""); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (q=title:"ray charles")\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" " 1\n" CATALOG_LINK_TAGS RAY_CHARLES_CATALOG_ENTRY "\n" ); } } TEST_F(LibraryServerTest, catalog_search_with_word_exclusion) { const auto r = zfs1_->GET("/ROOT/catalog/search?q=ray%20-uncategorized"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (q=ray -uncategorized)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" " 2\n" CATALOG_LINK_TAGS RAY_CHARLES_CATALOG_ENTRY CHARLES_RAY_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_search_by_tag) { const auto r = zfs1_->GET("/ROOT/catalog/search?tag=_category:jazz"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (tag=_category:jazz)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" " 1\n" CATALOG_LINK_TAGS CHARLES_RAY_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_search_by_category) { const auto r = zfs1_->GET("/ROOT/catalog/search?category=jazz"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (category=jazz)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" " 1\n" CATALOG_LINK_TAGS CHARLES_RAY_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_search_results_pagination) { { const auto r = zfs1_->GET("/ROOT/catalog/search?count=0"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (count=0)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" " 3\n" CATALOG_LINK_TAGS CHARLES_RAY_CATALOG_ENTRY RAY_CHARLES_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY "\n" ); } { const auto r = zfs1_->GET("/ROOT/catalog/search?count=1"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (count=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" " 1\n" CATALOG_LINK_TAGS CHARLES_RAY_CATALOG_ENTRY "\n" ); } { const auto r = zfs1_->GET("/ROOT/catalog/search?start=1&count=1"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (count=1&start=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 1\n" " 1\n" CATALOG_LINK_TAGS RAY_CHARLES_CATALOG_ENTRY "\n" ); } { const auto r = zfs1_->GET("/ROOT/catalog/search?start=100&count=10"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" " Filtered zims (count=10&start=100)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 100\n" " 0\n" CATALOG_LINK_TAGS "\n" ); } } TEST_F(LibraryServerTest, catalog_v2_root) { const auto r = zfs1_->GET("/ROOT/catalog/v2/root.xml"); EXPECT_EQ(r->status, 200); const char expected_output[] = R"( 12345678-90ab-cdef-1234-567890abcdef OPDS Catalog Root YYYY-MM-DDThh:mm:ssZ All entries YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef All entries from this catalog. All entries (partial) YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef All entries from this catalog in partial format. List of categories YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef List of all categories in this catalog. List of languages YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef List of all languages in this catalog. )"; EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output); } TEST_F(LibraryServerTest, catalog_v2_searchdescription_xml) { const auto r = zfs1_->GET("/ROOT/catalog/v2/searchdescription.xml"); EXPECT_EQ(r->status, 200); EXPECT_EQ(r->body, "\n" "\n" " Zim catalog search\n" " Search zim files in the catalog.\n" " \n" "\n" ); } TEST_F(LibraryServerTest, catalog_v2_categories) { const auto r = zfs1_->GET("/ROOT/catalog/v2/categories"); EXPECT_EQ(r->status, 200); const char expected_output[] = R"( 12345678-90ab-cdef-1234-567890abcdef List of categories YYYY-MM-DDThh:mm:ssZ jazz YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef All entries with category of 'jazz'. wikipedia YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef All entries with category of 'wikipedia'. )"; EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output); } TEST_F(LibraryServerTest, catalog_v2_languages) { const auto r = zfs1_->GET("/ROOT/catalog/v2/languages"); EXPECT_EQ(r->status, 200); const char expected_output[] = R"( 12345678-90ab-cdef-1234-567890abcdef List of languages YYYY-MM-DDThh:mm:ssZ English eng 1 YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef français fra 1 YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef русский rus 1 YYYY-MM-DDThh:mm:ssZ 12345678-90ab-cdef-1234-567890abcdef )"; EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output); } #define CATALOG_V2_ENTRIES_PREAMBLE0(x) \ "\n" \ "\n" \ " 12345678-90ab-cdef-1234-567890abcdef\n" \ "\n" \ " \n" \ " \n" \ " \n" \ "\n" \ #define CATALOG_V2_ENTRIES_PREAMBLE(q) \ CATALOG_V2_ENTRIES_PREAMBLE0("entries" q) #define CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE(q) \ CATALOG_V2_ENTRIES_PREAMBLE0("partial_entries" q) TEST_F(LibraryServerTest, catalog_v2_entries) { const auto r = zfs1_->GET("/ROOT/catalog/v2/entries"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_ENTRIES_PREAMBLE("") " All Entries\n" " YYYY-MM-DDThh:mm:ssZ\n" "\n" CHARLES_RAY_CATALOG_ENTRY RAY_CHARLES_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range) { { const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_ENTRIES_PREAMBLE("?start=1") " Filtered Entries (start=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 1\n" " 2\n" RAY_CHARLES_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY "\n" ); } { const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?count=2"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_ENTRIES_PREAMBLE("?count=2") " Filtered Entries (count=2)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" " 2\n" CHARLES_RAY_CATALOG_ENTRY RAY_CHARLES_CATALOG_ENTRY "\n" ); } { const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1&count=1"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_ENTRIES_PREAMBLE("?count=1&start=1") " Filtered Entries (count=1&start=1)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 1\n" " 1\n" RAY_CHARLES_CATALOG_ENTRY "\n" ); } } TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms) { const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?q=\"ray%20charles\""); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22") " Filtered Entries (q="ray charles")\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" " 2\n" RAY_CHARLES_CATALOG_ENTRY CHARLES_RAY_CATALOG_ENTRY "\n" ); } TEST_F(LibraryServerTest, catalog_v2_individual_entry_access) { const auto r = zfs1_->GET("/ROOT/catalog/v2/entry/raycharles"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), "\n" RAY_CHARLES_CATALOG_ENTRY ); const auto r1 = zfs1_->GET("/ROOT/catalog/v2/entry/non-existent-entry"); EXPECT_EQ(r1->status, 404); } TEST_F(LibraryServerTest, catalog_v2_partial_entries) { const auto r = zfs1_->GET("/ROOT/catalog/v2/partial_entries"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE("") " All Entries\n" " YYYY-MM-DDThh:mm:ssZ\n" "\n" " \n" " urn:uuid:charlesray\n" " Charles, Ray\n" " YYYY-MM-DDThh:mm:ssZ\n" " \n" " \n" " \n" " urn:uuid:raycharles\n" " Ray Charles\n" " YYYY-MM-DDThh:mm:ssZ\n" " \n" " \n" " \n" " urn:uuid:raycharles_uncategorized\n" " Ray (uncategorized) Charles\n" " YYYY-MM-DDThh:mm:ssZ\n" " \n" " \n" "\n" ); } #define EXPECT_SEARCH_RESULTS(SEARCH_TERM, RESULT_COUNT, OPDS_ENTRIES) \ { \ const auto r = zfs1_->GET("/ROOT/catalog/search?q=" SEARCH_TERM); \ EXPECT_EQ(r->status, 200); \ EXPECT_EQ(maskVariableOPDSFeedData(r->body), \ OPDS_FEED_TAG \ " 12345678-90ab-cdef-1234-567890abcdef\n" \ " Filtered zims (q=" SEARCH_TERM ")\n" \ " YYYY-MM-DDThh:mm:ssZ\n" \ " " #RESULT_COUNT "\n" \ " 0\n" \ " " #RESULT_COUNT "\n" \ CATALOG_LINK_TAGS \ \ OPDS_ENTRIES \ \ "\n" \ ); \ } TEST_F(LibraryServerTest, catalog_search_includes_public_tags) { EXPECT_SEARCH_RESULTS("public_tag_without_a_value", 1, RAY_CHARLES_CATALOG_ENTRY ); EXPECT_SEARCH_RESULTS("public_tag_with_a_value", 1, UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY ); // prefix search works on tag names EXPECT_SEARCH_RESULTS("public_tag", 2, RAY_CHARLES_CATALOG_ENTRY UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY ); EXPECT_SEARCH_RESULTS("value_of_a_public_tag", 1, UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY ); // prefix search works on tag values EXPECT_SEARCH_RESULTS("value_of", 1, UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY ); } #define EXPECT_ZERO_RESULTS(SEARCH_TERM) EXPECT_SEARCH_RESULTS(SEARCH_TERM, 0, ) TEST_F(LibraryServerTest, catalog_search_on_tags_is_not_an_any_substring_match) { EXPECT_ZERO_RESULTS("tag_with") EXPECT_ZERO_RESULTS("alue_of_a_public_tag") } TEST_F(LibraryServerTest, catalog_search_excludes_hidden_tags) { EXPECT_ZERO_RESULTS("_private_tag_without_a_value"); EXPECT_ZERO_RESULTS("private_tag_without_a_value"); EXPECT_ZERO_RESULTS("value_of_a_private_tag"); #undef EXPECT_ZERO_RESULTS } #undef EXPECT_SEARCH_RESULTS