From e3e4bfa533d6662c6370a83370057e451f72aacb Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sat, 4 Jun 2022 13:18:16 +0400 Subject: [PATCH 1/3] Support for serving customized resources During work on the kiwix-serve front-end, the edit-save-test cycle is a multistep procedure: 1. build and install libkiwix 2. build kiwix-tools 3. run kiwix-serve 4. reload the web-page in the browser When making changes in static resources that are served by kiwix-serve unmodified, the steps 1-3 can be eliminated if kiwix-serve is capable of serving resources from the file-system. This commit adds such a functionality to kiwix-serve. Now, if during startup of kiwix-serve the environment variable `KIWIX_SERVE_CUSTOMIZED_RESOURCES` is defined it is assumed to point to a file where every line has the following format: URL MIMETYPE RESOURCE_FILE_PATH When a request is received by kiwix-serve and its URL matches any of the URLs read from the customized resource file, then the resource data is read from the respective file RESOURCE_FILE_PATH and served with mime-type MIMETYPE. Though this feature was introduced in order to facilitate the development of the iframe-based content viewer, it can also be useful to users who would like to customize the kiwix-serve front-end on their own (without re-building all of kiwix-serve). There is some overlap with a feature of the kiwix-compile-resources script that also allows to override resources. The differences are: 1. The new way of customizing front-end resources has all such resources listed in a text file and there is a single environment variable from which the path of that file is read. kiwix-compile-resources associates a separate environment variable with each resource. 2. The new way uses regular paths to identify a resource. The kiwix-compile-resources method encodes the resource path by replacing any non-alphanumeric characters (including the path separator) with underscores (so that the resulting resource identifier can be used to construct the name of the environment variable controlling that resource). 3. The new method allows adding new front-end resources. The old method only allows to modify existing resources. 4. The new method allows (actually requires) to specify the URL at which the overriden resource should be served (similarly, the MIME-type can/must be specified, too). The old method only allows to override the contents of a resource. 5. The new method only allows to override front-end resources that are served without any preprocessing by kiwix-serve at runtime. The old method allows to override template resources as well (note that internationalization/translation resources cannot be overriden using the old method, either). --- src/server/internalServer.cpp | 67 +++++++++++++++++++++++++++++++++-- src/server/internalServer.h | 8 ++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index c78cd448e..086068f23 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 a0cffa95b..7ffc9a8b6 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); From 0086049d4fee16d8556aa06908da0e8bf61db775 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Wed, 22 Jun 2022 15:22:12 +0400 Subject: [PATCH 2/3] Extracted LibraryServerTest into a file of its own --- test/library_server.cpp | 713 ++++++++++++++++++++++++++++++++++++++++ test/meson.build | 1 + test/server.cpp | 706 --------------------------------------- 3 files changed, 714 insertions(+), 706 deletions(-) create mode 100644 test/library_server.cpp diff --git a/test/library_server.cpp b/test/library_server.cpp new file mode 100644 index 000000000..3f430c6a3 --- /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 74de624d5..0783691f8 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 diff --git a/test/server.cpp b/test/server.cpp index 5aeed662e..88ace8067 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -1476,709 +1476,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" - ); -} From 1139f2cb4ccfc9bbbbaaf18aa23939bcbb16acc7 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Wed, 22 Jun 2022 17:03:15 +0400 Subject: [PATCH 3/3] Testing of static front-end resource customization One important missing test is that the content of the customized resource is read from storage every time rather than once. Testing that requirement would involve creating temporary files which is a little more work. --- test/data/customized_resources.txt | 5 +++ test/data/helloworld.txt | 1 + test/data/welcome.html | 1 + test/meson.build | 5 ++- test/server.cpp | 62 ++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 test/data/customized_resources.txt create mode 100644 test/data/helloworld.txt create mode 100644 test/data/welcome.html diff --git a/test/data/customized_resources.txt b/test/data/customized_resources.txt new file mode 100644 index 000000000..5c5f2b72e --- /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 000000000..cd0875583 --- /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 000000000..7612b5763 --- /dev/null +++ b/test/data/welcome.html @@ -0,0 +1 @@ +Welcome diff --git a/test/meson.build b/test/meson.build index 0783691f8..3137163a7 100644 --- a/test/meson.build +++ b/test/meson.build @@ -36,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 88ace8067..9d7c38293 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 {