#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=6a8c6fb2" }, { 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=ce19da2a" }, { 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=948df083" }, { 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/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/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" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=96f2cf73" }, { 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( )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 bookName; const std::string bookTitle; const std::string expectedBody; }; enum ExpectedResponseDataType { expected_page_title, expected_css_url, 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 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.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[] = { R"FRAG( )FRAG", R"FRAG( )FRAG", R"FRAG( )FRAG", R"FRAG( )FRAG" }; return frag[0] + pageTitle() + frag[1] + pageCssLink() + frag[2] + expectedBody + frag[3]; } std::string TestContentIn404HtmlResponse::pageTitle() const { return expectedPageTitle.empty() ? "Content not found" : expectedPageTitle; } std::string TestContentIn404HtmlResponse::pageCssLink() const { if ( expectedCssUrl.empty() ) return ""; return R"( )"; } 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_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_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_body==R"(

Not Found

No such book: no-such-book

)" }, { /* 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_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_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_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_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_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_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_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)>

)" }, { /* 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_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_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_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_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_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_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_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_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_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\"