diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index c78cd448..086068f2 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -68,6 +68,7 @@ extern "C" { #include #include #include +#include #include "kiwixlib-resources.h" #ifndef _WIN32 @@ -212,6 +213,12 @@ void checkBookNumber(const Library::BookIdSet& bookIds, size_t limit) { } } +struct CustomizedResourceData +{ + std::string mimeType; + std::string resourceFilePath; +}; + } // unnamed namespace std::pair InternalServer::selectBooks(const RequestContext& request) const @@ -327,7 +334,6 @@ zim::Query SearchInfo::getZimQuery(bool verbose) const { return query; } - static IdNameMapper defaultNameMapper; static MHD_Result staticHandlerCallback(void* cls, @@ -339,6 +345,27 @@ static MHD_Result staticHandlerCallback(void* cls, size_t* upload_data_size, void** cont_cls); +class InternalServer::CustomizedResources : public std::map +{ +public: + CustomizedResources() + { + const char* fname = ::getenv("KIWIX_SERVE_CUSTOMIZED_RESOURCES"); + if ( fname ) + { + std::cout << "Populating customized resources" << std::endl; + std::ifstream file(fname); + std::string url, mimeType, resourceFilePath; + while ( file >> url >> mimeType >> resourceFilePath ) + { + std::cout << "Got " << url << " " << mimeType << " " << resourceFilePath << std::endl; + (*this)[url] = CustomizedResourceData{mimeType, resourceFilePath}; + } + std::cout << "Done populating customized resources" << std::endl; + } + } +}; + InternalServer::InternalServer(Library* library, NameMapper* nameMapper, @@ -368,9 +395,12 @@ InternalServer::InternalServer(Library* library, mp_library(library), mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper), searchCache(getEnvVar("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)), - suggestionSearcherCache(getEnvVar("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))) + suggestionSearcherCache(getEnvVar("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))), + m_customizedResources(new CustomizedResources) {} +InternalServer::~InternalServer() = default; + bool InternalServer::start() { #ifdef _WIN32 int flags = MHD_USE_SELECT_INTERNALLY; @@ -507,6 +537,9 @@ std::unique_ptr InternalServer::handle_request(const RequestContext& r if ( etag ) return Response::build_304(*this, etag); + if ( isLocallyCustomizedResource(request.get_url()) ) + return handle_locally_customized_resource(request); + if (startsWith(request.get_url(), "/skin/")) return handle_skin(request); @@ -1067,4 +1100,34 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque } } +bool InternalServer::isLocallyCustomizedResource(const std::string& url) const +{ + return m_customizedResources->find(url) != m_customizedResources->end(); +} + +std::unique_ptr InternalServer::handle_locally_customized_resource(const RequestContext& request) +{ + if (m_verbose.load()) { + printf("** running handle_locally_customized_resource\n"); + } + + const CustomizedResourceData& crd = m_customizedResources->at(request.get_url()); + + if (m_verbose.load()) { + std::cout << "Reading " << crd.resourceFilePath << std::endl; + } + const auto resourceData = getFileContent(crd.resourceFilePath); + + auto byteRange = request.get_range().resolve(resourceData.size()); + if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) { + return Response::build_416(*this, resourceData.size()); + } + + return ContentResponse::build(*this, + resourceData, + crd.mimeType, + /*isHomePage=*/false, + /*raw=*/true); +} + } diff --git a/src/server/internalServer.h b/src/server/internalServer.h index a0cffa95..7ffc9a8b 100644 --- a/src/server/internalServer.h +++ b/src/server/internalServer.h @@ -109,7 +109,7 @@ class InternalServer { bool blockExternalLinks, std::string indexTemplateString, int ipConnectionLimit); - virtual ~InternalServer() = default; + virtual ~InternalServer(); MHD_Result handlerCallback(struct MHD_Connection* connection, const char* url, @@ -142,6 +142,7 @@ class InternalServer { std::unique_ptr handle_captured_external(const RequestContext& request); std::unique_ptr handle_content(const RequestContext& request); std::unique_ptr handle_raw(const RequestContext& request); + std::unique_ptr handle_locally_customized_resource(const RequestContext& request); std::vector search_catalog(const RequestContext& request, kiwix::OPDSDumper& opdsDumper); @@ -153,6 +154,8 @@ class InternalServer { std::pair selectBooks(const RequestContext& r) const; SearchInfo getSearchInfo(const RequestContext& r) const; + bool isLocallyCustomizedResource(const std::string& url) const; + private: // data std::string m_addr; int m_port; @@ -176,6 +179,9 @@ class InternalServer { std::string m_server_id; std::string m_library_id; + class CustomizedResources; + std::unique_ptr m_customizedResources; + friend std::unique_ptr Response::build(const InternalServer& server); friend std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage, bool raw); friend std::unique_ptr ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw); diff --git a/test/data/customized_resources.txt b/test/data/customized_resources.txt new file mode 100644 index 00000000..5c5f2b72 --- /dev/null +++ b/test/data/customized_resources.txt @@ -0,0 +1,5 @@ +/non-existent-item text/plain ./test/helloworld.txt +/ text/html ./test/welcome.html +/skin/index.css application/json ./test/helloworld.txt +/zimfile/A/Ray_Charles ray/charles ./test/welcome.html +/search text/html ./test/helloworld.txt diff --git a/test/data/helloworld.txt b/test/data/helloworld.txt new file mode 100644 index 00000000..cd087558 --- /dev/null +++ b/test/data/helloworld.txt @@ -0,0 +1 @@ +Hello world! diff --git a/test/data/welcome.html b/test/data/welcome.html new file mode 100644 index 00000000..7612b576 --- /dev/null +++ b/test/data/welcome.html @@ -0,0 +1 @@ +Welcome diff --git a/test/library_server.cpp b/test/library_server.cpp new file mode 100644 index 00000000..3f430c6a --- /dev/null +++ b/test/library_server.cpp @@ -0,0 +1,713 @@ + +#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" \ + " unittest;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" \ + " unittest;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" + ); +} diff --git a/test/meson.build b/test/meson.build index 74de624d..3137163a 100644 --- a/test/meson.build +++ b/test/meson.build @@ -17,6 +17,7 @@ tests = [ if build_machine.system() != 'windows' tests += [ 'server', + 'library_server', 'server_search' ] endif @@ -35,7 +36,10 @@ if gtest_dep.found() and not meson.is_cross_build() 'zimfile&other.zim', 'corner_cases.zim', 'poor.zim', - 'library.xml' + 'library.xml', + 'customized_resources.txt', + 'helloworld.txt', + 'welcome.html', ] foreach file : data_files # configure_file(input : 'data/' + file, diff --git a/test/server.cpp b/test/server.cpp index 5aeed662..9d7c3829 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -287,6 +287,68 @@ TEST_F(ServerTest, 404) } } +struct CustomizedServerTest : ServerTest +{ + void SetUp() + { + setenv("KIWIX_SERVE_CUSTOMIZED_RESOURCES", "./test/customized_resources.txt", 1); + ServerTest::SetUp(); + } +}; + +typedef std::vector StringCollection; + +std::string getHeaderValue(const Headers& headers, const std::string& name) +{ + const auto er = headers.equal_range(name); + const auto n = std::distance(er.first, er.second); + if (n == 0) + throw std::runtime_error("Missing header: " + name); + if (n > 1) + throw std::runtime_error("Multiple occurrences of header: " + name); + return er.first->second; +} + +TEST_F(CustomizedServerTest, NewResourcesCanBeAdded) +{ + // ServerTest.404 verifies that "/ROOT/non-existent-item" doesn't exist + const auto r = zfs1_->GET("/ROOT/non-existent-item"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), "text/plain"); + EXPECT_EQ(r->body, "Hello world!\n"); +} + +TEST_F(CustomizedServerTest, ContentOfAnyServableUrlCanBeOverriden) +{ + { + const auto r = zfs1_->GET("/ROOT/"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), "text/html"); + EXPECT_EQ(r->body, "Welcome\n"); + } + + { + const auto r = zfs1_->GET("/ROOT/skin/index.css"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), "application/json"); + EXPECT_EQ(r->body, "Hello world!\n"); + } + + { + const auto r = zfs1_->GET("/ROOT/zimfile/A/Ray_Charles"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), "ray/charles"); + EXPECT_EQ(r->body, "Welcome\n"); + } + + { + const auto r = zfs1_->GET("/ROOT/search?pattern=la+femme"); + EXPECT_EQ(r->status, 200); + EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), "text/html"); + EXPECT_EQ(r->body, "Hello world!\n"); + } +} + namespace TestingOfHtmlResponses { @@ -1476,709 +1538,3 @@ TEST_F(ServerTest, suggestions_in_range) ASSERT_EQ(currCount, 0); } } - -//////////////////////////////////////////////////////////////////////////////// -// 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" \ - " unittest;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" \ - " unittest;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" - ); -}