#define CPPHTTPLIB_ZLIB_SUPPORT 1 #include "./httplib.h" #include "gtest/gtest.h" #define SERVER_PORT 8001 #include "server_testing_tools.h" #include "../src/tools/stringTools.h" const std::string ROOT_PREFIX("/ROOT%23%3F"); bool is_valid_etag(const std::string& etag) { return etag.size() >= 2 && etag.front() == '"' && etag.back() == '"'; } template T1 concat(T1 a, const T2& b) { a.insert(a.end(), b.begin(), b.end()); return a; } enum ResourceKind { ZIM_CONTENT, STATIC_CONTENT, DYNAMIC_CONTENT, }; struct Resource { ResourceKind kind; const char* url; bool etag_expected() const { return kind != STATIC_CONTENT; } }; std::ostream& operator<<(std::ostream& out, const Resource& r) { out << "url: " << r.url; return out; } typedef std::vector ResourceCollection; const ResourceCollection resources200Compressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/viewer" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/viewer?cacheid=whatever" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=436e0a63" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js?cacheid=bd23c4fb" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=5fc4badf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf?cacheid=84d10248" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json" }, // TODO: implement cache management of i18n resources //{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=355e4885" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/root.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/entries" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/partial_entries" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/search?content=zimfile&pattern=a" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile&term=ray" }, { ZIM_CONTENT, "/ROOT%23%3F/content/zimfile/A/index" }, { ZIM_CONTENT, "/ROOT%23%3F/content/zimfile/A/Ray_Charles" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/content/A/index" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/content/A/Ray_Charles" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/nojs"}, }; const ResourceCollection resources200Uncompressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png?cacheid=4f5c6882" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blank.html" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/blank.html?cacheid=6b1fa032" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/caret.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/caret.png?cacheid=22b942b4" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/download.png?cacheid=a39aa502" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-192x192.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-192x192.png?cacheid=bfac158b" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-512x512.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-512x512.png?cacheid=380c3653" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/apple-touch-icon.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/browserconfig.xml" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/favicon-16x16.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/favicon-32x32.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/favicon.ico" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-144x144.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-144x144.png?cacheid=c25a7641" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-150x150.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-150x150.png?cacheid=6fa6f467" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-310x150.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-310x150.png?cacheid=e0ed9032" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-310x310.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-310x310.png?cacheid=26b20530" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-70x70.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/mstile-70x70.png?cacheid=64ffd9dc" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/site.webmanifest" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/site.webmanifest?cacheid=bc396efb" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/hash.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/hash.png?cacheid=f836e872" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/magnet.png" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/magnet.png?cacheid=73b6bddf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/polyfills.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/polyfills.js?cacheid=a0e0343d" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg?cacheid=b10ae7ed" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search_results.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Description" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Language" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Name" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Tags" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Date" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Creator" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Publisher" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/root.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/searchdescription.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/categories" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/languages" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/searchdescription.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catch/external?source=www.example.com" }, { ZIM_CONTENT, "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" }, { ZIM_CONTENT, "/ROOT%23%3F/content/corner_cases%23%26/empty.html" }, { ZIM_CONTENT, "/ROOT%23%3F/content/corner_cases%23%26/empty.css" }, { ZIM_CONTENT, "/ROOT%23%3F/content/corner_cases%23%26/empty.js" }, // The following url's responses are too small to be compressed { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/root.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/searchdescription.xml" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Creator" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/nojs/download/zimfile"}, }; ResourceCollection all200Resources() { return concat(resources200Compressible, resources200Uncompressible); } TEST(indexTemplateStringTest, emptyIndexTemplate) { const int PORT = 8001; const ZimFileServer::FilePathCollection ZIMFILES { "./test/zimfile.zim", "./test/corner_cases#&.zim" }; ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, ""); EXPECT_EQ(200, zfs.GET("/ROOT%23%3F/")->status); } TEST(indexTemplateStringTest, indexTemplateCheck) { const int PORT = 8001; const ZimFileServer::FilePathCollection ZIMFILES { "./test/zimfile.zim", "./test/corner_cases#&.zim" }; ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "" "Welcome to kiwix library" "" ""); EXPECT_EQ("" "Welcome to kiwix library" "" "", zfs.GET("/ROOT%23%3F/")->body); } TEST_F(ServerTest, 200) { for ( const Resource& res : all200Resources() ) EXPECT_EQ(200, zfs1_->GET(res.url)->status) << "res.url: " << res.url; } TEST_F(ServerTest, 200_IdNameMapper) { EXPECT_EQ(404, zfs1_->GET("/ROOT%23%3F/content/6f1d19d0-633f-087b-fb55-7ac324ff9baf/A/index")->status); EXPECT_EQ(200, zfs1_->GET("/ROOT%23%3F/content/zimfile/A/index")->status); resetServer(ZimFileServer::NO_NAME_MAPPER); EXPECT_EQ(200, zfs1_->GET("/ROOT%23%3F/content/6f1d19d0-633f-087b-fb55-7ac324ff9baf/A/index")->status); EXPECT_EQ(404, zfs1_->GET("/ROOT%23%3F/content/zimfile/A/index")->status); } TEST_F(ServerTest, CompressibleContentIsCompressedIfAcceptable) { for ( const Resource& res : resources200Compressible ) { const auto x = zfs1_->GET(res.url, { {"Accept-Encoding", "gzip"} }); EXPECT_EQ(200, x->status) << res; EXPECT_EQ("gzip", x->get_header_value("Content-Encoding")) << res; EXPECT_EQ("Accept-Encoding", x->get_header_value("Vary")) << res; } } TEST_F(ServerTest, UncompressibleContentIsNotCompressed) { for ( const Resource& res : resources200Uncompressible ) { const auto x = zfs1_->GET(res.url, { {"Accept-Encoding", "gzip"} }); EXPECT_EQ(200, x->status) << res; EXPECT_EQ("", x->get_header_value("Content-Encoding")) << res; } } // Selects from text only the lines containing the specified (fixed string) // pattern std::string fgrep(const std::string& pattern, const std::string& text) { std::istringstream iss(text); std::string line; std::string result; while ( getline(iss, line) ) { if ( line.find(pattern) != std::string::npos ) { result += line + "\n"; } } return result; } TEST_F(ServerTest, CacheIdsOfStaticResources) { typedef std::pair UrlAndExpectedResult; const std::vector testData{ { /* url */ "/ROOT%23%3F/", R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" href="/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" ${$t( ${$t( ${$t( ${$t( )EXPECTEDRESULT" }, { /* url */ "/ROOT%23%3F/viewer", R"EXPECTEDRESULT( const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032"; src="./skin/langSelector.svg?cacheid=00b59961"> src="./skin/blank.html?cacheid=6b1fa032" title="ZIM content" width="100%" )EXPECTEDRESULT" }, { /* url */ "/ROOT%23%3F/content/zimfile/A/index", "" }, { // Searching in a ZIM file without a full-text index returns // a page rendered from static/templates/no_search_result_html /* url */ "/ROOT%23%3F/search?content=poor&pattern=whatever", R"EXPECTEDRESULT( window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] }; )EXPECTEDRESULT" }, }; for ( const auto& urlAndExpectedResult : testData ) { const std::string url = urlAndExpectedResult.first; const std::string expectedResult = urlAndExpectedResult.second; const TestContext ctx{ {"url", url} }; const auto r = zfs1_->GET(url.c_str()); EXPECT_EQ(r->body.find("KIWIXCACHEID"), std::string::npos) << ctx; EXPECT_EQ(fgrep("/skin/", r->body), expectedResult) << ctx; } } const char* urls400[] = { "/ROOT%23%3F/search", "/ROOT%23%3F/search?content=zimfile", "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty", "/ROOT%23%3F/search?content=non-existing-book&pattern=asdGET(url)->status) << "url: " << url; } } const char* urls404[] = { "/", "/zimfile", "/ROOT", "/ROOT%23%", "/ROOT%23%3", "/ROOT%23%3Fxyz", "/ROOT%23%3F/skin/non-existent-skin-resource", "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=wrongcacheid", "/ROOT%23%3F/catalog", "/ROOT%23%3F/catalog/", "/ROOT%23%3F/catalog/non-existent-item", "/ROOT%23%3F/catalog/v2/illustration/zimfile?size=48", "/ROOT%23%3F/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=96", "/ROOT%23%3F/random", "/ROOT%23%3F/random?content=non-existent-book", "/ROOT%23%3F/random/", "/ROOT%23%3F/random/number", "/ROOT%23%3F/suggest", "/ROOT%23%3F/suggest?content=non-existent-book&term=abcd", "/ROOT%23%3F/suggest/", "/ROOT%23%3F/suggest/fr", "/ROOT%23%3F/search/", "/ROOT%23%3F/search/anythingotherthansearchdescription.xml", "/ROOT%23%3F/catch/", "/ROOT%23%3F/catch/external", // missing ?source=URL "/ROOT%23%3F/catch/external?source=", "/ROOT%23%3F/catch/anythingotherthanexternal", "/ROOT%23%3F/content/zimfile/A/non-existent-article", "/ROOT%23%3F/raw/non-existent-book/meta/Title", "/ROOT%23%3F/raw/zimfile/wrong-kind/Foo", // zimfile has no Favicon nor Illustration_48x48@1 meta item "/ROOT%23%3F/raw/zimfile/meta/Favicon", "/ROOT%23%3F/raw/zimfile/meta/Illustration_48x48@1", "/ROOT%23%3F/nojs/download/thiszimdoesntexist" }; TEST_F(ServerTest, 404) { for ( const char* url : urls404 ) { EXPECT_EQ(404, zfs1_->GET(url)->status) << "url: " << url; } } 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; } std::string getCacheControlHeader(const httplib::Response& r) { return getHeaderValue(r.headers, "Cache-Control"); } TEST_F(CustomizedServerTest, NewResourcesCanBeAdded) { // ServerTest.404 verifies that "/ROOT%23%3F/non-existent-item" doesn't exist const auto r = zfs1_->GET("/ROOT%23%3F/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%23%3F/"); 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%23%3F/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%23%3F/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%23%3F/content/zimfile/A/Ray_Charles"); EXPECT_EQ(r->status, 200); EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), "charles/ray"); EXPECT_EQ(r->body, "Welcome\n"); } { const auto r = zfs1_->GET("/ROOT%23%3F/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"); } } TEST_F(ServerTest, MimeTypes) { struct TestData { const char* const url; const char* const mimeType; }; const TestData testData[] = { { "/", "text/html; charset=utf-8" }, { "/viewer", "text/html" }, { "/skin/blank.html", "text/html" }, { "/skin/index.css", "text/css" }, { "/skin/index.js", "application/javascript" }, { "/catalog/root.xml", "application/atom+xml;profile=opds-catalog;kind=acquisition;charset=utf-8" }, { "/catalog/v2/searchdescription.xml", "application/opensearchdescription+xml" }, { "/catalog/v2/root.xml", "application/atom+xml;profile=opds-catalog;kind=navigation;charset=utf-8" }, { "/catalog/v2/languages", "application/atom+xml;profile=opds-catalog;kind=navigation;charset=utf-8" }, { "/catalog/v2/categories", "application/atom+xml;profile=opds-catalog;kind=navigation;charset=utf-8" }, { "/catalog/v2/entries", "application/atom+xml;profile=opds-catalog;kind=acquisition;charset=utf-8" }, { "/catalog/v2/entry/6f1d19d0-633f-087b-fb55-7ac324ff9baf", "application/atom+xml;type=entry;profile=opds-catalog;charset=utf-8" }, { "/skin/search-icon.svg", "image/svg+xml" }, { "/skin/bittorrent.png", "image/png" }, { "/skin/favicon/favicon.ico", "image/x-icon" }, { "/skin/i18n/en.json", "application/json" }, { "/skin/fonts/Roboto.ttf", "application/font-ttf" }, { "/suggest?content=zimfile&term=ray", "application/json; charset=utf-8" }, }; for ( const auto& t : testData ) { const std::string url= ROOT_PREFIX + t.url; const TestContext ctx{ {"url", url} }; const auto r = zfs1_->GET(url.c_str()); EXPECT_EQ(getHeaderValue(r->headers, "Content-Type"), t.mimeType) << ctx; } } namespace TestingOfHtmlResponses { struct ExpectedResponseData { const std::string expectedPageTitle; const std::string expectedCssUrl; const std::string expectedKiwixResponseData; const std::string bookName; const std::string bookTitle; const std::string expectedBody; }; enum ExpectedResponseDataType { expected_page_title, expected_css_url, expected_kiwix_response_data, book_name, book_title, expected_body }; // Operator overloading is used as a means of defining a mini-DSL for // defining test data in a concise way (see usage in // TEST_F(ServerTest, Http404HtmlError)) ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s) { switch (t) { case expected_page_title: return ExpectedResponseData{s, "", "", "", "", ""}; case expected_css_url: return ExpectedResponseData{"", s, "", "", "", ""}; case expected_kiwix_response_data: return ExpectedResponseData{"", "", s, "", "", ""}; case book_name: return ExpectedResponseData{"", "", "", s, "", ""}; case book_title: return ExpectedResponseData{"", "", "", "", s, ""}; case expected_body: return ExpectedResponseData{"", "", "", "", "", s}; default: assert(false); return ExpectedResponseData{}; } } std::string selectNonEmpty(const std::string& a, const std::string& b) { if ( a.empty() ) return b; assert(b.empty()); return a; } ExpectedResponseData operator&&(const ExpectedResponseData& a, const ExpectedResponseData& b) { return ExpectedResponseData{ selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle), selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl), selectNonEmpty(a.expectedKiwixResponseData, b.expectedKiwixResponseData), selectNonEmpty(a.bookName, b.bookName), selectNonEmpty(a.bookTitle, b.bookTitle), selectNonEmpty(a.expectedBody, b.expectedBody) }; } class TestContentIn404HtmlResponse : public ExpectedResponseData { public: TestContentIn404HtmlResponse(const std::string& url, const ExpectedResponseData& erd) : ExpectedResponseData(erd) , url(url) {} virtual ~TestContentIn404HtmlResponse() = default; const std::string url; std::string expectedResponse() const; private: virtual std::string pageTitle() const; std::string pageCssLink() const; }; std::string TestContentIn404HtmlResponse::expectedResponse() const { const std::string frag[] = { // frag[0] R"FRAG( )FRAG", // frag[1] R"FRAG( )FRAG", // frag[2] R"( )FRAG", // frag[4] R"FRAG( )FRAG" }; return frag[0] + pageTitle() + frag[1] + pageCssLink() + frag[2] + expectedKiwixResponseData + frag[3] + expectedBody + frag[4]; } std::string TestContentIn404HtmlResponse::pageTitle() const { return expectedPageTitle.empty() ? "Content not found" : expectedPageTitle; } std::string TestContentIn404HtmlResponse::pageCssLink() const { if ( expectedCssUrl.empty() ) return ""; return R"( )" + "\n"; } class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse { public: TestContentIn400HtmlResponse(const std::string& url, const ExpectedResponseData& erd) : TestContentIn404HtmlResponse(url, erd) {} private: std::string pageTitle() const; }; std::string TestContentIn400HtmlResponse::pageTitle() const { return expectedPageTitle.empty() ? "Invalid request" : expectedPageTitle; } } // namespace TestingOfHtmlResponses TEST_F(ServerTest, Http404HtmlError) { using namespace TestingOfHtmlResponses; const std::vector testData{ { /* url */ "/ROOT%23%3F/random?content=non-existent-book", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" && expected_body==R"(

Not Found

No such book: non-existent-book

)" }, { /* url */ "/ROOT%23%3F/random?content=non-existent-book&userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

[I18N TESTING] No such book: non-existent-book. Sorry.

)" }, { /* url */ "/ROOT%23%3F/suggest?content=no-such-book&term=whatever", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" && expected_body==R"(

Not Found

No such book: no-such-book

)" }, { /* url */ "/ROOT%23%3F/catalog/", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/catalog/" was not found on this server.

)" }, { /* url */ "/ROOT%23%3F/catalog/?userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

[I18N TESTING] URL not found: /ROOT%23%3F/catalog/

)" }, { /* url */ "/ROOT%23%3F/catalog/invalid_endpoint", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/invalid_endpoint" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/catalog/invalid_endpoint" was not found on this server.

)" }, { /* url */ "/ROOT%23%3F/catalog/invalid_endpoint?userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/invalid_endpoint" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

[I18N TESTING] URL not found: /ROOT%23%3F/catalog/invalid_endpoint

)" }, { /* url */ "/ROOT%23%3F/content/invalid-book/whatever", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/invalid-book/whatever" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "whatever", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=whatever" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/content/invalid-book/whatever" was not found on this server.

Make a full text search for whatever

)" }, { /* url */ "/ROOT%23%3F/content/zimfile/invalid-article", book_name=="zimfile" && book_title=="Ray Charles" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/content/zimfile/invalid-article" was not found on this server.

Make a full text search for invalid-article

)" }, { /* url */ R"(/ROOT%23%3F/content/">)", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/\">" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\">", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/content/"><svg onload%3Dalert(1)>" was not found on this server.

Make a full text search for "><svg onload=alert(1)>

)" }, { /* url */ R"(/ROOT%23%3F/content/zimfile/">)", book_name=="zimfile" && book_title=="Ray Charles" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/\">" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\">", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/content/zimfile/"><svg onload%3Dalert(1)>" was not found on this server.

Make a full text search for "><svg onload=alert(1)>

)" }, // XXX: This test case is against a "" string appearing inside // XXX: javascript code that will confuse the HTML parser { /* url */ R"(/ROOT%23%3F/content/zimfile/)", book_name=="zimfile" && book_title=="Ray Charles" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "script>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=script%3E" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/content/zimfile/</script>" was not found on this server.

Make a full text search for script>

)" }, { /* url */ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test", expected_page_title=="[I18N TESTING] Not Found - Try Again" && book_name=="zimfile" && book_title=="Ray Charles" && expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" && expected_body==R"(

[I18N TESTING] Content not found, but at least the server is alive

[I18N TESTING] URL not found: /ROOT%23%3F/content/zimfile/invalid-article

[I18N TESTING] Make a full text search for invalid-article

)" }, { /* url */ "/ROOT%23%3F/raw/no-such-book/meta/Title", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/no-such-book/meta/Title" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/raw/no-such-book/meta/Title" was not found on this server.

No such book: no-such-book

)" }, { /* url */ "/ROOT%23%3F/raw/zimfile/XYZ", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/XYZ" } } }, { "p" : { "msgid" : "invalid-raw-data-type", "params" : { "DATATYPE" : "XYZ" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/raw/zimfile/XYZ" was not found on this server.

XYZ is not a valid request for raw content.

)" }, { /* url */ "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "meta", "ENTRY" : "invalid-metadata" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata" was not found on this server.

Cannot find meta entry invalid-metadata

)" }, { /* url */ "/ROOT%23%3F/raw/zimfile/content/invalid-article", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/content/invalid-article" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "content", "ENTRY" : "invalid-article" } } } ] })" && expected_body==R"(

Not Found

The requested URL "/ROOT%23%3F/raw/zimfile/content/invalid-article" was not found on this server.

Cannot find content entry invalid-article

)" }, { /* url */ "/ROOT%23%3F/search?content=poor&pattern=whatever", expected_page_title=="Fulltext search unavailable" && expected_css_url=="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" && book_name=="poor" && book_title=="poor" && expected_kiwix_response_data==R"({ "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] })" && expected_body==R"(

Not Found

The fulltext search engine is not available for this content.

)" }, }; for ( const auto& t : testData ) { const TestContext ctx{ {"url", t.url} }; const auto r = zfs1_->GET(t.url.c_str()); EXPECT_EQ(r->status, 404) << ctx; EXPECT_EQ(r->body, t.expectedResponse()) << ctx; } } TEST_F(ServerTest, Http400HtmlError) { using namespace TestingOfHtmlResponses; const std::vector testData{ { /* url */ "/ROOT%23%3F/search", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" && expected_body== R"(

Invalid request

The requested URL "/ROOT%23%3F/search" is not a valid request.

Too many books requested (4) where limit is 3

)" }, { /* url */ "/ROOT%23%3F/search?content=zimfile", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=zimfile" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" && expected_body==R"(

Invalid request

The requested URL "/ROOT%23%3F/search?content=zimfile" is not a valid request.

No query provided.

)" }, { /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty", expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" && expected_body==R"(

Invalid request

The requested URL "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty" is not a valid request.

No such book: non-existing-book

)" }, { /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=a\"

Internal Server Error

An internal server error occured. We are sorry about that :/

Entry redirect_loop.html is a redirect entry.

)"; { const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html"); EXPECT_EQ(r->status, 500); EXPECT_EQ(r->body, expectedBody); EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8"); } } TEST_F(ServerTest, UserLanguageList) { const auto r = zfs1_->GET("/ROOT%23%3F/skin/languages.js"); EXPECT_EQ(r->body, R"EXPECTEDRESPONSE(const uiLanguages = [ { "iso_code": "ar", "self_name": "الإنجليزية", "translation_count": 25 }, { "iso_code": "bn", "self_name": "বাংলা", "translation_count": 14 }, { "iso_code": "br", "self_name": "brezhoneg", "translation_count": 35 }, { "iso_code": "cs", "self_name": "Čeština", "translation_count": 25 }, { "iso_code": "dag", "self_name": "Silimiinsili", "translation_count": 48 }, { "iso_code": "de", "self_name": "Deutsch", "translation_count": 57 }, { "iso_code": "en", "self_name": "English", "translation_count": 58 }, { "iso_code": "es", "self_name": "español", "translation_count": 48 }, { "iso_code": "fi", "self_name": "suomi", "translation_count": 23 }, { "iso_code": "fr", "self_name": "Français", "translation_count": 57 }, { "iso_code": "ha", "self_name": "Turanci", "translation_count": 57 }, { "iso_code": "he", "self_name": "עברית", "translation_count": 57 }, { "iso_code": "hi", "self_name": "हिन्दी", "translation_count": 49 }, { "iso_code": "hy", "self_name": "Հայերեն", "translation_count": 15 }, { "iso_code": "ia", "self_name": "interlingua", "translation_count": 49 }, { "iso_code": "ig", "self_name": "Bekee", "translation_count": 57 }, { "iso_code": "it", "self_name": "italiano", "translation_count": 34 }, { "iso_code": "ja", "self_name": "日本語", "translation_count": 26 }, { "iso_code": "ko", "self_name": "한국어", "translation_count": 13 }, { "iso_code": "ku-latn", "self_name": "kurdî", "translation_count": 26 }, { "iso_code": "lb", "self_name": "Lëtzebuergesch", "translation_count": 22 }, { "iso_code": "mk", "self_name": "македонски", "translation_count": 57 }, { "iso_code": "ms", "self_name": "Bahasa Melayu", "translation_count": 14 }, { "iso_code": "nl", "self_name": "Nederlands", "translation_count": 49 }, { "iso_code": "nqo", "self_name": "ߒߞߏ", "translation_count": 43 }, { "iso_code": "or", "self_name": "ଓଡ଼ିଆ", "translation_count": 49 }, { "iso_code": "pl", "self_name": "Polski", "translation_count": 31 }, { "iso_code": "ru", "self_name": "русский", "translation_count": 57 }, { "iso_code": "sc", "self_name": "Sardu", "translation_count": 49 }, { "iso_code": "sk", "self_name": "slovenčina", "translation_count": 25 }, { "iso_code": "skr-arab", "self_name": "سرائیکی", "translation_count": 20 }, { "iso_code": "sl", "self_name": "slovenščina", "translation_count": 57 }, { "iso_code": "sq", "self_name": "Shqip", "translation_count": 49 }, { "iso_code": "sv", "self_name": "Svenska", "translation_count": 57 }, { "iso_code": "sw", "self_name": "Kiswahili", "translation_count": 57 }, { "iso_code": "te", "self_name": "ఇంగ్లీషు", "translation_count": 49 }, { "iso_code": "tr", "self_name": "Türkçe", "translation_count": 57 }, { "iso_code": "zh-hans", "self_name": "英语", "translation_count": 54 }, { "iso_code": "zh-hant", "self_name": "繁體中文", "translation_count": 57 } ])EXPECTEDRESPONSE"); } TEST_F(ServerTest, UserLanguageControl) { struct TestData { const std::string description; const std::string url; const std::string acceptLanguageHeader; const std::string expectedH1; operator TestContext() const { TestContext ctx{ {"description", description}, {"url", url}, {"acceptLanguageHeader", acceptLanguageHeader}, }; return ctx; } }; const TestData testData[] = { { "Default user language is English", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*Accept-Language:*/ "", /* expected

*/ "Not Found" }, { "userlang URL query parameter is respected", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en", /*Accept-Language:*/ "", /* expected

*/ "Not Found" }, { "userlang URL query parameter is respected", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test", /*Accept-Language:*/ "", /* expected

*/ "[I18N TESTING] Content not found, but at least the server is alive" }, { "'Accept-Language: *' is handled", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*Accept-Language:*/ "*", /* expected

*/ "Not Found" }, { "Accept-Language: header is respected", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*Accept-Language:*/ "test", /* expected

*/ "[I18N TESTING] Content not found, but at least the server is alive" }, { "userlang query parameter takes precedence over Accept-Language", /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en", /*Accept-Language:*/ "test", /* expected

*/ "Not Found" }, { "Most suitable language is selected from the Accept-Language header", // In case of a comma separated list of languages (optionally weighted // with quality values) the most suitable language is selected. /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*Accept-Language:*/ "test;q=0.9, en;q=0.2", /* expected

*/ "[I18N TESTING] Content not found, but at least the server is alive" }, { "Most suitable language is selected from the Accept-Language header", // In case of a comma separated list of languages (optionally weighted // with quality values) the most suitable language is selected. /*url*/ "/ROOT%23%3F/content/zimfile/invalid-article", /*Accept-Language:*/ "test;q=0.2, en;q=0.9", /* expected

*/ "Not Found" }, }; const std::regex h1Regex("

(.+)

"); for ( const auto& t : testData ) { std::smatch h1Match; Headers headers; if ( !t.acceptLanguageHeader.empty() ) { headers.insert({"Accept-Language", t.acceptLanguageHeader}); } const auto r = zfs1_->GET(t.url.c_str(), headers); EXPECT_FALSE(r->has_header("Set-Cookie")); std::regex_search(r->body, h1Match, h1Regex); const std::string h1(h1Match[1]); EXPECT_EQ(h1, t.expectedH1) << t; } } TEST_F(ServerTest, SlashlessRootURLIsRedirectedToSlashfulURL) { const std::pair test_data[] = { // URL redirect { "/ROOT%23%3F", "/ROOT%23%3F/" }, { "/ROOT%23%3F?abcd=123&xyz=890", "/ROOT%23%3F/?abcd=123&xyz=890" } }; for ( const auto& t : test_data ) { const TestContext ctx{ {"url", t.first} }; const auto g = zfs1_->GET(t.first); ASSERT_EQ(302, g->status) << ctx; ASSERT_TRUE(g->has_header("Location")) << ctx; ASSERT_EQ(g->get_header_value("Location"), t.second) << ctx; ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate") << ctx; ASSERT_FALSE(g->has_header("ETag")) << ctx; } } TEST_F(ServerTest, EmptyRootIsNotRedirected) { ZimFileServer::Cfg serverCfg; serverCfg.root = ""; resetServer(serverCfg); ASSERT_EQ(200, zfs1_->GET("/")->status); } TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle) { auto g = zfs1_->GET("/ROOT%23%3F/random?content=zimfile"); ASSERT_EQ(302, g->status); ASSERT_TRUE(g->has_header("Location")); ASSERT_TRUE(kiwix::startsWith(g->get_header_value("Location"), "/ROOT%23%3F/content/zimfile/A/")); ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate"); ASSERT_FALSE(g->has_header("ETag")); } TEST_F(ServerTest, RandomPageRedirectionsAreUriEncoded) { // The purpose of this unit test is to validate that the URL to which the // /random endpoint redirects is in URI-encoded form. Due to the difficulty // of checking non-deterministic functionality, it is implemented by running // /random sufficiently many times so that all articles are returned with // close to 100% probability (this is done on an artificial small ZIM file // with at least one article with a special symbol in its name). Such an // approach effectively tests more than the original goal was, so this test // may over time evolve into something more general whereupon it must be // renamed. auto g = zfs1_->GET("/ROOT%23%3F/random?content=corner_cases%23%26"); typedef std::set StringSet; const StringSet frontArticles{ {"/ROOT%23%3F/content/corner_cases%23%26/c%23.html"}, // empty.html is missing because of a subtle bug // in libzim::randomNumber(max) which returns a value equal to // it argument with much lower probability than other numbers // {"/ROOT%23%3F/content/corner_cases%23%26/empty.html"} }; StringSet randomPageUrls; auto n = 10 * frontArticles.size(); while ( --n && randomPageUrls.size() != frontArticles.size() ) { ASSERT_EQ(302, g->status); ASSERT_TRUE(g->has_header("Location")); randomPageUrls.insert(g->get_header_value("Location")); } ASSERT_EQ(frontArticles, randomPageUrls); } TEST_F(ServerTest, NonEndpointUrlsAreRedirectedToContentUrls) { const std::string paths[] = { "/zimfile", "/zimfile/A/index", "/some/path", "/non-existent-item", // Validate that paths starting with a string matching an endpoint name // are not misinterpreted as endpoint URLs "/catalogarithm", "/catalogistics/root.xml", "/catch22", "/catchy/song", "/contention", "/contents/is/not/the/same/as/content", "/randomize", "/randomestic/animals", "/rawman", "/rawkward/url", "/searcheology", "/searchitecture/history", "/skinhead", "/skineffect/formula", "/suggesture", "/suggestonia/tallinn", // The /meta endpoint has been replaced with /raw//meta "/meta", // Make sure that the query is preserved in the redirect URL "/does?P=NP" // Make sure that reserved URI encoded symbols stay URI encoded "/C%23", // # --> /C# "/100%25_guarantee", // % --> /100%_guarantee "/AT%26T", // & --> /AT&T "/Quo_vadis%3F", // ? --> /Quo_vadis? "/1%2B1%3D2", // +,= --> 1+1=2 // Make sure that URI-encoded query stays URI-encoded "/encode?string=%23%25%26%2B%3D%3F", // There seems to be a bug in httplib client - the '+' symbols // in query parameters are URI encoded. Therefore using %20 (a URI-encoded // space) instead. //"/route?from=current+location&to=girlfriend%238", "/route?from=current%20location&to=girlfriend%238", }; for ( const std::string& p : paths ) { auto g = zfs1_->GET(("/ROOT%23%3F" + p).c_str()); const TestContext ctx{ { "path", p } }; ASSERT_EQ(302, g->status) << ctx; ASSERT_TRUE(g->has_header("Location")) << ctx; ASSERT_EQ("/ROOT%23%3F/content" + p, g->get_header_value("Location")) << ctx; ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate"); ASSERT_FALSE(g->has_header("ETag")); } } TEST_F(ServerTest, RedirectionsToURLsWithSpecialSymbols) { auto g = zfs1_->GET("/ROOT%23%3F/content/corner_cases%23%26/c_sharp.html"); ASSERT_EQ(302, g->status); ASSERT_TRUE(g->has_header("Location")); ASSERT_EQ(g->get_header_value("Location"), "/ROOT%23%3F/content/corner_cases%23%26/c%23.html"); ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate"); ASSERT_FALSE(g->has_header("ETag")); } TEST_F(ServerTest, BookMainPageIsRedirectedToArticleIndex) { { auto g = zfs1_->GET("/ROOT%23%3F/content/zimfile"); ASSERT_EQ(302, g->status); ASSERT_TRUE(g->has_header("Location")); ASSERT_EQ("/ROOT%23%3F/content/zimfile/A/index", g->get_header_value("Location")); } } TEST_F(ServerTest, RawEntry) { auto p = zfs1_->GET("/ROOT%23%3F/raw/zimfile/meta/Title"); EXPECT_EQ(200, p->status); EXPECT_EQ(p->body, std::string("Ray Charles")); p = zfs1_->GET("/ROOT%23%3F/raw/zimfile/meta/Creator"); EXPECT_EQ(200, p->status); EXPECT_EQ(p->body, std::string("Wikipedia")); // The raw content of Ray_Charles returned by the server is // the same as the one in the zim file. auto archive = zim::Archive("./test/zimfile.zim"); auto entry = archive.getEntryByPath("A/Ray_Charles"); p = zfs1_->GET("/ROOT%23%3F/raw/zimfile/content/A/Ray_Charles"); EXPECT_EQ(200, p->status); EXPECT_EQ(std::string(p->body), std::string(entry.getItem(true).getData())); /* Now normal content is not decorated in any way, either // ... but the "normal" content is not p = zfs1_->GET("/ROOT%23%3F/content/zimfile/A/Ray_Charles"); EXPECT_EQ(200, p->status); EXPECT_NE(std::string(p->body), std::string(entry.getItem(true).getData())); EXPECT_TRUE(p->body.find("") != std::string::npos); */ } TEST_F(ServerTest, HeadMethodIsSupported) { for ( const Resource& res : all200Resources() ) { EXPECT_EQ(200, zfs1_->HEAD(res.url)->status) << res; } } TEST_F(ServerTest, TheResponseToHeadRequestHasNoBody) { for ( const Resource& res : all200Resources() ) { EXPECT_TRUE(zfs1_->HEAD(res.url)->body.empty()) << res; } } TEST_F(ServerTest, HeadersAreTheSameInResponsesToHeadAndGetRequests) { for ( const Resource& res : all200Resources() ) { httplib::Headers g = zfs1_->GET(res.url)->headers; httplib::Headers h = zfs1_->HEAD(res.url)->headers; EXPECT_EQ(invariantHeaders(g), invariantHeaders(h)) << res; } } TEST_F(ServerTest, CacheControlOfZimContent) { for ( const Resource& res : all200Resources() ) { if ( res.kind == ZIM_CONTENT ) { const auto g = zfs1_->GET(res.url); EXPECT_EQ(getCacheControlHeader(*g), "max-age=3600, must-revalidate") << res; EXPECT_TRUE(g->has_header("ETag")) << res; } } } TEST_F(ServerTest, CacheControlOfStaticContent) { for ( const Resource& res : all200Resources() ) { if ( res.kind == STATIC_CONTENT ) { const auto g = zfs1_->GET(res.url); EXPECT_EQ(getCacheControlHeader(*g), "max-age=31536000, immutable") << res; EXPECT_FALSE(g->has_header("ETag")) << res; } } } TEST_F(ServerTest, CacheControlOfDynamicContent) { for ( const Resource& res : all200Resources() ) { if ( res.kind == DYNAMIC_CONTENT ) { const auto g = zfs1_->GET(res.url); EXPECT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate") << res; EXPECT_TRUE(g->has_header("ETag")) << res; } } } TEST_F(ServerTest, ETagHeaderIsSetAsNeeded) { for ( const Resource& res : all200Resources() ) { const auto responseToGet = zfs1_->GET(res.url); EXPECT_EQ(res.etag_expected(), responseToGet->has_header("ETag")) << res; if ( res.etag_expected() ) { EXPECT_TRUE(is_valid_etag(responseToGet->get_header_value("ETag"))); } } } TEST_F(ServerTest, ETagIsTheSameInResponsesToDifferentRequestsOfTheSameURL) { for ( const Resource& res : all200Resources() ) { const auto h1 = zfs1_->HEAD(res.url); const auto h2 = zfs1_->HEAD(res.url); EXPECT_EQ(h1->get_header_value("ETag"), h2->get_header_value("ETag")); } } TEST_F(ServerTest, ETagIsTheSameAcrossHeadAndGet) { for ( const Resource& res : all200Resources() ) { const auto g = zfs1_->GET(res.url); const auto h = zfs1_->HEAD(res.url); EXPECT_EQ(h->get_header_value("ETag"), g->get_header_value("ETag")); } } TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETagsForDynamicContent) { ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES); for ( const Resource& res : all200Resources() ) { if ( res.kind != DYNAMIC_CONTENT ) continue; const auto h1 = zfs1_->HEAD(res.url); const auto h2 = zfs2.HEAD(res.url); EXPECT_NE(h1->get_header_value("ETag"), h2->get_header_value("ETag")); } } TEST_F(ServerTest, DifferentServerInstancesProduceIdenticalETagsForZimContent) { ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES); for ( const Resource& res : all200Resources() ) { if ( res.kind != ZIM_CONTENT ) continue; const auto h1 = zfs1_->HEAD(res.url); const auto h2 = zfs2.HEAD(res.url); EXPECT_EQ(h1->get_header_value("ETag"), h2->get_header_value("ETag")); } } TEST_F(ServerTest, CompressionInfluencesETag) { for ( const Resource& res : resources200Compressible ) { if ( ! res.etag_expected() ) continue; const auto g1 = zfs1_->GET(res.url); const auto g2 = zfs1_->GET(res.url, { {"Accept-Encoding", ""} } ); const auto g3 = zfs1_->GET(res.url, { {"Accept-Encoding", "gzip"} } ); const auto etag = g1->get_header_value("ETag"); EXPECT_EQ(etag, g2->get_header_value("ETag")); EXPECT_NE(etag, g3->get_header_value("ETag")); } } TEST_F(ServerTest, ETagOfUncompressibleContentIsNotAffectedByAcceptEncoding) { for ( const Resource& res : resources200Uncompressible ) { if ( ! res.etag_expected() ) continue; const auto g1 = zfs1_->GET(res.url); const auto g2 = zfs1_->GET(res.url, { {"Accept-Encoding", ""} } ); const auto g3 = zfs1_->GET(res.url, { {"Accept-Encoding", "gzip"} } ); const auto etag = g1->get_header_value("ETag"); EXPECT_EQ(etag, g2->get_header_value("ETag")) << res; EXPECT_EQ(etag, g3->get_header_value("ETag")) << res; } } // Pick from the response those headers that are required to be present in the // 304 (Not Modified) response if they would be set in the 200 (OK) response. // NOTE: The "Date" header (which should belong to that list as required // NOTE: by RFC 7232) is not included (since the result of this function // NOTE: will be used to check the equality of headers from the 200 and 304 // NOTE: responses). Headers special304Headers(const httplib::Response& r) { Headers result; std::copy_if( r.headers.begin(), r.headers.end(), std::inserter(result, result.end()), [](const Headers::value_type& x) { return x.first == "Cache-Control" || x.first == "Content-Location" || x.first == "ETag" || x.first == "Expires" || x.first == "Vary"; }); return result; } // make a list of three etags with the given one in the middle std::string make_etag_list(const std::string& etag) { return "\"x" + etag.substr(1) + ", " + etag + ", " + etag.substr(0, etag.size()-2) + "\""; } TEST_F(ServerTest, IfNoneMatchRequestsWithMatchingETagResultIn304Responses) { const char* const encodings[] = { "", "gzip" }; for ( const Resource& res : all200Resources() ) { for ( const char* enc: encodings ) { if ( ! res.etag_expected() ) continue; const TestContext ctx{ {"url", res.url}, {"encoding", enc} }; const auto g = zfs1_->GET(res.url, { {"Accept-Encoding", enc} }); const auto etag = g->get_header_value("ETag"); const std::string etags = make_etag_list(etag); const Headers headers{{"If-None-Match", etags}, {"Accept-Encoding", enc}}; const auto g2 = zfs1_->GET(res.url, headers ); const auto h = zfs1_->HEAD(res.url, headers ); EXPECT_EQ(304, h->status) << ctx; EXPECT_EQ(304, g2->status) << ctx; EXPECT_EQ(special304Headers(*g), special304Headers(*g2)) << ctx; EXPECT_EQ(special304Headers(*g2), special304Headers(*h)) << ctx; EXPECT_TRUE(g2->body.empty()) << ctx; } } } TEST_F(ServerTest, IfNoneMatchRequestsWithMismatchingETagResultIn200Responses) { for ( const Resource& res : all200Resources() ) { const auto g = zfs1_->GET(res.url); const auto etag = g->get_header_value("ETag"); const auto etag2 = etag.substr(0, etag.size() - 1) + "x\""; const auto h = zfs1_->HEAD(res.url, { {"If-None-Match", etag2} } ); const auto g2 = zfs1_->GET(res.url, { {"If-None-Match", etag2} } ); EXPECT_EQ(200, h->status) << res; EXPECT_EQ(200, g2->status) << res; } } TEST_F(ServerTest, ValidSingleRangeByteRangeRequestsAreHandledProperly) { const char url[] = "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg"; const auto full = zfs1_->GET(url); EXPECT_FALSE(full->has_header("Content-Range")); EXPECT_EQ("bytes", full->get_header_value("Accept-Ranges")); { const auto p = zfs1_->GET(url, { {"Range", "bytes=0-100000"} } ); EXPECT_EQ(206, p->status); EXPECT_EQ(full->body, p->body); EXPECT_EQ("bytes 0-20076/20077", p->get_header_value("Content-Range")); EXPECT_EQ("bytes", p->get_header_value("Accept-Ranges")); } { const auto p = zfs1_->GET(url, { {"Range", "bytes=0-10"} } ); EXPECT_EQ(206, p->status); EXPECT_EQ("bytes 0-10/20077", p->get_header_value("Content-Range")); EXPECT_EQ(11U, p->body.size()); EXPECT_EQ(full->body.substr(0, 11), p->body); EXPECT_EQ("bytes", p->get_header_value("Accept-Ranges")); } { const auto p = zfs1_->GET(url, { {"Range", "bytes=123-456"} } ); EXPECT_EQ(206, p->status); EXPECT_EQ("bytes 123-456/20077", p->get_header_value("Content-Range")); EXPECT_EQ(334U, p->body.size()); EXPECT_EQ(full->body.substr(123, 334), p->body); EXPECT_EQ("bytes", p->get_header_value("Accept-Ranges")); } { const auto p = zfs1_->GET(url, { {"Range", "bytes=20000-"} } ); EXPECT_EQ(206, p->status); EXPECT_EQ(full->body.substr(20000), p->body); EXPECT_EQ("bytes 20000-20076/20077", p->get_header_value("Content-Range")); EXPECT_EQ("bytes", p->get_header_value("Accept-Ranges")); } { const auto p = zfs1_->GET(url, { {"Range", "bytes=-100"} } ); EXPECT_EQ(206, p->status); EXPECT_EQ(full->body.substr(19977), p->body); EXPECT_EQ("bytes 19977-20076/20077", p->get_header_value("Content-Range")); EXPECT_EQ("bytes", p->get_header_value("Accept-Ranges")); } } TEST_F(ServerTest, InvalidAndMultiRangeByteRangeRequestsResultIn416Responses) { const char url[] = "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg"; const char* invalidRanges[] = { "0-10", "bytes=", "bytes=123", "bytes=-10-20", "bytes=10-20xxx", "bytes=10-0", // reversed range "bytes=10-20, 30-40", // multi-range "bytes=1000000-", "bytes=30000-30100" // unsatisfiable ranges }; for( const char* range : invalidRanges ) { const TestContext ctx{ {"Range", range} }; const auto p = zfs1_->GET(url, { {"Range", range } } ); EXPECT_EQ(416, p->status) << ctx; EXPECT_TRUE(p->body.empty()) << ctx; EXPECT_EQ("bytes */20077", p->get_header_value("Content-Range")) << ctx; } } TEST_F(ServerTest, ValidByteRangeRequestsOfZeroSizedEntriesResultIn416Responses) { const char url[] = "/ROOT%23%3F/content/corner_cases%23%26/empty.js"; const char* ranges[] = { "bytes=0-", "bytes=-100" }; for( const char* range : ranges ) { const TestContext ctx{ {"Range", range} }; const auto p = zfs1_->GET(url, { {"Range", range } } ); EXPECT_EQ(416, p->status) << ctx; EXPECT_TRUE(p->body.empty()) << ctx; EXPECT_EQ("bytes */0", p->get_header_value("Content-Range")) << ctx; } } TEST_F(ServerTest, RangeHasPrecedenceOverCompression) { const char url[] = "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg"; const Headers onlyRange{ {"Range", "bytes=123-456"} }; Headers rangeAndCompression(onlyRange); rangeAndCompression.insert({"Accept-Encoding", "gzip"}); const auto p1 = zfs1_->GET(url, onlyRange); const auto p2 = zfs1_->GET(url, rangeAndCompression); EXPECT_EQ(p1->status, p2->status); EXPECT_EQ(invariantHeaders(p1->headers), invariantHeaders(p2->headers)); EXPECT_EQ(p1->body, p2->body); } TEST_F(ServerTest, RangeHeaderIsCaseInsensitive) { const char url[] = "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg"; const auto r0 = zfs1_->GET(url, { {"Range", "bytes=100-200"} } ); const char* header_variations[] = { "RANGE", "range", "rAnGe", "RaNgE" }; for ( const char* header : header_variations ) { const auto r = zfs1_->GET(url, { {header, "bytes=100-200"} } ); EXPECT_EQ(206, r->status); EXPECT_EQ("bytes 100-200/20077", r->get_header_value("Content-Range")); EXPECT_EQ(r0->body, r->body); } } TEST_F(ServerTest, suggestions) { typedef std::pair UrlAndExpectedResponse; const std::vector testData{ { /* url: */ "/ROOT%23%3F/suggest?content=zimfile&term=thing", R"EXPECTEDRESPONSE([ { "value" : "Doing His Thing", "label" : "Doing His <b>Thing</b>", "kind" : "path" , "path" : "A/Doing_His_Thing" }, { "value" : "We Didn't See a Thing", "label" : "We Didn't See a <b>Thing</b>", "kind" : "path" , "path" : "A/We_Didn't_See_a_Thing" }, { "value" : "thing ", "label" : "containing 'thing'...", "kind" : "pattern" //EOLWHITESPACEMARKER } ] )EXPECTEDRESPONSE" }, { /* url: */ "/ROOT%23%3F/suggest?content=zimfile&term=old%20sun", R"EXPECTEDRESPONSE([ { "value" : "That Lucky Old Sun", "label" : "That Lucky <b>Old</b> <b>Sun</b>", "kind" : "path" , "path" : "A/That_Lucky_Old_Sun" }, { "value" : "old sun ", "label" : "containing 'old sun'...", "kind" : "pattern" //EOLWHITESPACEMARKER } ] )EXPECTEDRESPONSE" }, { /* url: */ "/ROOT%23%3F/suggest?content=zimfile&term=öld%20suñ", R"EXPECTEDRESPONSE([ { "value" : "That Lucky Old Sun", "label" : "That Lucky <b>Old</b> <b>Sun</b>", "kind" : "path" , "path" : "A/That_Lucky_Old_Sun" }, { "value" : "öld suñ ", "label" : "containing 'öld suñ'...", "kind" : "pattern" //EOLWHITESPACEMARKER } ] )EXPECTEDRESPONSE" }, { /* url: */ "/ROOT%23%3F/suggest?content=zimfile&term=abracadabra", R"EXPECTEDRESPONSE([ { "value" : "abracadabra ", "label" : "containing 'abracadabra'...", "kind" : "pattern" //EOLWHITESPACEMARKER } ] )EXPECTEDRESPONSE" }, { // Test handling of & (%26 when url-encoded) in the search string /* url: */ "/ROOT%23%3F/suggest?content=zimfile&term=A%26B", R"EXPECTEDRESPONSE([ { "value" : "A&B ", "label" : "containing 'A&B'...", "kind" : "pattern" //EOLWHITESPACEMARKER } ] )EXPECTEDRESPONSE" }, { /* url: */ "/ROOT%23%3F/suggest?content=zimfile&term=abracadabra&userlang=test", R"EXPECTEDRESPONSE([ { "value" : "abracadabra ", "label" : "[I18N TESTING] cOnTaInInG 'abracadabra'...", "kind" : "pattern" //EOLWHITESPACEMARKER } ] )EXPECTEDRESPONSE" }, }; for ( const auto& urlAndExpectedResponse : testData ) { const std::string url = urlAndExpectedResponse.first; const std::string expectedResponse = urlAndExpectedResponse.second; const TestContext ctx{ {"url", url} }; const auto r = zfs1_->GET(url.c_str()); EXPECT_EQ(r->status, 200) << ctx; EXPECT_EQ(r->body, removeEOLWhitespaceMarkers(expectedResponse)) << ctx; } } TEST_F(ServerTest, suggestions_in_range) { /** * Attempt to get 50 suggestions in steps of 5 * The suggestions are returned in the json format * [{sugg1}, {sugg2}, ... , {suggN}, {suggest ft search}] * Assuming the number of suggestions = (occurance of "{" - 1) */ { int suggCount = 0; for (int i = 0; i < 10; i++) { std::string url = "/ROOT%23%3F/suggest?content=zimfile&term=ray&start=" + std::to_string(i*5) + "&count=5"; const auto r = zfs1_->GET(url.c_str()); std::string body = r->body; int currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 5); suggCount += currCount; } ASSERT_EQ(suggCount, 50); } // Attempt to get 10 suggestions in steps of 5 even though there are only 8 { std::string url = "/ROOT%23%3F/suggest?content=zimfile&term=song+for+you&start=0&count=5"; const auto r1 = zfs1_->GET(url.c_str()); std::string body = r1->body; int currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 5); url = "/ROOT%23%3F/suggest?content=zimfile&term=song+for+you&start=5&count=5"; const auto r2 = zfs1_->GET(url.c_str()); body = r2->body; currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 3); } // Attempt to get 10 suggestions even though there is only 1 { std::string url = "/ROOT%23%3F/suggest?content=zimfile&term=strong&start=0&count=5"; const auto r = zfs1_->GET(url.c_str()); std::string body = r->body; int currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 1); } // No Suggestion { std::string url = "/ROOT%23%3F/suggest?content=zimfile&term=oops&start=0&count=5"; const auto r = zfs1_->GET(url.c_str()); std::string body = r->body; int currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 0); } // Out of bound value { std::string url = "/ROOT%23%3F/suggest?content=zimfile&term=ray&start=-2&count=-1"; const auto r = zfs1_->GET(url.c_str()); std::string body = r->body; int currCount = std::count(body.begin(), body.end(), '{') - 1; ASSERT_EQ(currCount, 0); } } TEST_F(ServerTest, viewerSettings) { const auto JS_CONTENT_TYPE = "application/javascript; charset=utf-8"; { resetServer(ZimFileServer::NO_TASKBAR_NO_LINK_BLOCKING); const auto r = zfs1_->GET("/ROOT%23%3F/viewer_settings.js"); ASSERT_EQ(r->status, 200); ASSERT_EQ(getHeaderValue(r->headers, "Content-Type"), JS_CONTENT_TYPE); ASSERT_EQ(r->body, R"(const viewerSettings = { toolbarEnabled: false, linkBlockingEnabled: false, libraryButtonEnabled: false } )"); } { resetServer(ZimFileServer::BLOCK_EXTERNAL_LINKS); ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js")->body, R"(const viewerSettings = { toolbarEnabled: false, linkBlockingEnabled: true, libraryButtonEnabled: false } )"); } { resetServer(ZimFileServer::WITH_TASKBAR); ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js")->body, R"(const viewerSettings = { toolbarEnabled: true, linkBlockingEnabled: false, libraryButtonEnabled: false } )"); } { resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON); ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js")->body, R"(const viewerSettings = { toolbarEnabled: true, linkBlockingEnabled: false, libraryButtonEnabled: true } )"); } }