diff --git a/.github/workflows/sparql-conformance.yml b/.github/workflows/sparql-conformance.yml index 38152b5c79..3e4bdfd63d 100644 --- a/.github/workflows/sparql-conformance.yml +++ b/.github/workflows/sparql-conformance.yml @@ -80,7 +80,7 @@ jobs: echo ${{github.event.pull_request.head.sha}} > ./conformance-report/sha mv ${{ github.workspace}}/qlever-test-suite/results/${{ github.sha }}.json.bz2 conformance-report/${{ github.event.pull_request.head.sha }}.json.bz2 - name: Upload coverage artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: conformance-report path: conformance-report/ \ No newline at end of file diff --git a/src/engine/Server.cpp b/src/engine/Server.cpp index 8c1248203c..4f7fc97a4e 100644 --- a/src/engine/Server.cpp +++ b/src/engine/Server.cpp @@ -169,6 +169,37 @@ void Server::run(const string& indexBaseName, bool useText, bool usePatterns, httpServer.run(); } +std::optional Server::extractAccessToken( + const ad_utility::httpUtils::HttpRequest auto& request, + const ad_utility::url_parser::ParamValueMap& params) { + std::optional tokenFromAuthorizationHeader; + std::optional tokenFromParameter; + if (request.find(http::field::authorization) != request.end()) { + string_view authorization = request[http::field::authorization]; + const std::string prefix = "Bearer "; + if (!authorization.starts_with(prefix)) { + throw std::runtime_error(absl::StrCat( + "Authorization header doesn't start with \"", prefix, "\".")); + } + authorization.remove_prefix(prefix.length()); + tokenFromAuthorizationHeader = std::string(authorization); + } + if (params.contains("access-token")) { + tokenFromParameter = ad_utility::url_parser::getParameterCheckAtMostOnce( + params, "access-token"); + } + // If both are specified, they must be equal. This way there is no hidden + // precedence. + if (tokenFromAuthorizationHeader && tokenFromParameter && + tokenFromAuthorizationHeader != tokenFromParameter) { + throw std::runtime_error( + "Access token is specified both in the `Authorization` header and by " + "the `access-token` parameter, but they are not the same"); + } + return tokenFromAuthorizationHeader ? std::move(tokenFromAuthorizationHeader) + : std::move(tokenFromParameter); +} + // _____________________________________________________________________________ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest( const ad_utility::httpUtils::HttpRequest auto& request) { @@ -177,7 +208,8 @@ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest( // This is a concatenation of the URL path and the query strings. auto parsedUrl = ad_utility::url_parser::parseRequestTarget(request.target()); ad_utility::url_parser::ParsedRequest parsedRequest{ - std::move(parsedUrl.path_), std::move(parsedUrl.parameters_), None{}}; + std::move(parsedUrl.path_), std::nullopt, + std::move(parsedUrl.parameters_), None{}}; // Some valid requests (e.g. QLever's custom commands like retrieving index // statistics) don't have a query. So an empty operation is not necessarily an @@ -208,10 +240,15 @@ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest( addToDatasetClausesIfOperationIs(ti, "using-graph-uri", false); addToDatasetClausesIfOperationIs(ti, "using-named-graph-uri", true); }; + auto extractAccessTokenFromRequest = [&parsedRequest, &request]() { + parsedRequest.accessToken_ = + extractAccessToken(request, parsedRequest.parameters_); + }; if (request.method() == http::verb::get) { setOperationIfSpecifiedInParams(ti, "query"); addDatasetClauses(); + extractAccessTokenFromRequest(); if (parsedRequest.parameters_.contains("update")) { throw std::runtime_error("SPARQL Update is not allowed as GET request."); @@ -283,16 +320,22 @@ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest( setOperationIfSpecifiedInParams(ti, "query"); setOperationIfSpecifiedInParams(ti, "update"); addDatasetClauses(); + // We parse the access token from the url-encoded parameters in the body. + // The URL parameters must be empty for URL-encoded POST (see above). + extractAccessTokenFromRequest(); + return parsedRequest; } if (contentType.starts_with(contentTypeSparqlQuery)) { parsedRequest.operation_ = Query{request.body(), {}}; addDatasetClauses(); + extractAccessTokenFromRequest(); return parsedRequest; } if (contentType.starts_with(contentTypeSparqlUpdate)) { parsedRequest.operation_ = Update{request.body(), {}}; addDatasetClauses(); + extractAccessTokenFromRequest(); return parsedRequest; } throw std::runtime_error(absl::StrCat( @@ -369,15 +412,13 @@ Awaitable Server::process( // Check the access token. If an access token is provided and the check fails, // throw an exception and do not process any part of the query (even if the // processing had been allowed without access token). - bool accessTokenOk = - checkAccessToken(checkParameter("access-token", std::nullopt)); + bool accessTokenOk = checkAccessToken(parsedHttpRequest.accessToken_); auto requireValidAccessToken = [&accessTokenOk]( const std::string& actionName) { if (!accessTokenOk) { throw std::runtime_error(absl::StrCat( actionName, - " requires a valid access token. No valid access token is present.", - "Processing of request aborted.")); + " requires a valid access token but no access token was provided")); } }; @@ -1181,17 +1222,15 @@ bool Server::checkAccessToken( if (!accessToken) { return false; } - auto accessTokenProvidedMsg = absl::StrCat( - "Access token \"access-token=", accessToken.value(), "\" provided"); - auto requestIgnoredMsg = ", request is ignored"; + const auto accessTokenProvidedMsg = "Access token was provided"; if (accessToken_.empty()) { - throw std::runtime_error(absl::StrCat( - accessTokenProvidedMsg, - " but server was started without --access-token", requestIgnoredMsg)); + throw std::runtime_error( + absl::StrCat(accessTokenProvidedMsg, + " but server was started without --access-token")); } else if (!ad_utility::constantTimeEquals(accessToken.value(), accessToken_)) { - throw std::runtime_error(absl::StrCat( - accessTokenProvidedMsg, " but not correct", requestIgnoredMsg)); + throw std::runtime_error( + absl::StrCat(accessTokenProvidedMsg, " but it was invalid")); } else { LOG(DEBUG) << accessTokenProvidedMsg << " and correct" << std::endl; return true; diff --git a/src/engine/Server.h b/src/engine/Server.h index 4e0889b48a..9558f8743e 100644 --- a/src/engine/Server.h +++ b/src/engine/Server.h @@ -35,6 +35,7 @@ class Server { FRIEND_TEST(ServerTest, parseHttpRequest); FRIEND_TEST(ServerTest, getQueryId); FRIEND_TEST(ServerTest, createMessageSender); + FRIEND_TEST(ServerTest, extractAccessToken); public: explicit Server(unsigned short port, size_t numThreads, @@ -115,6 +116,12 @@ class Server { static ad_utility::url_parser::ParsedRequest parseHttpRequest( const ad_utility::httpUtils::HttpRequest auto& request); + /// Extract the Access token for that request from the `Authorization` header + /// or the URL query parameters. + static std::optional extractAccessToken( + const ad_utility::httpUtils::HttpRequest auto& request, + const ad_utility::url_parser::ParamValueMap& params); + /// Handle a single HTTP request. Check whether a file request or a query was /// sent, and dispatch to functions handling these cases. This function /// requires the constraints for the `HttpHandler` in `HttpServer.h`. diff --git a/src/util/http/UrlParser.h b/src/util/http/UrlParser.h index 8c1d272aac..45b05e4334 100644 --- a/src/util/http/UrlParser.h +++ b/src/util/http/UrlParser.h @@ -71,10 +71,12 @@ struct None { // Representation of parsed HTTP request. // - `path_` is the URL path +// - `accessToken_` is the access token for that request // - `parameters_` is a hashmap of the parameters // - `operation_` the operation that should be performed struct ParsedRequest { std::string path_; + std::optional accessToken_; ParamValueMap parameters_; std::variant diff --git a/test/ServerTest.cpp b/test/ServerTest.cpp index dd7d776da7..0319b138d5 100644 --- a/test/ServerTest.cpp +++ b/test/ServerTest.cpp @@ -21,11 +21,14 @@ using namespace ad_utility::testing; namespace { auto ParsedRequestIs = [](const std::string& path, + const std::optional& accessToken, const ParamValueMap& parameters, const std::variant& operation) -> testing::Matcher { return testing::AllOf( AD_FIELD(ad_utility::url_parser::ParsedRequest, path_, testing::Eq(path)), + AD_FIELD(ad_utility::url_parser::ParsedRequest, accessToken_, + testing::Eq(accessToken)), AD_FIELD(ad_utility::url_parser::ParsedRequest, parameters_, testing::ContainerEq(parameters)), AD_FIELD(ad_utility::url_parser::ParsedRequest, operation_, @@ -41,19 +44,20 @@ TEST(ServerTest, parseHttpRequest) { "application/x-www-form-urlencoded;charset=UTF-8"; const std::string QUERY = "application/sparql-query"; const std::string UPDATE = "application/sparql-update"; - EXPECT_THAT(parse(makeGetRequest("/")), ParsedRequestIs("/", {}, None{})); + EXPECT_THAT(parse(makeGetRequest("/")), + ParsedRequestIs("/", std::nullopt, {}, None{})); EXPECT_THAT(parse(makeGetRequest("/ping")), - ParsedRequestIs("/ping", {}, None{})); + ParsedRequestIs("/ping", std::nullopt, {}, None{})); EXPECT_THAT(parse(makeGetRequest("/?cmd=stats")), - ParsedRequestIs("/", {{"cmd", {"stats"}}}, None{})); + ParsedRequestIs("/", std::nullopt, {{"cmd", {"stats"}}}, None{})); EXPECT_THAT(parse(makeGetRequest( "/?query=SELECT+%2A%20WHERE%20%7B%7D&action=csv_export")), - ParsedRequestIs("/", {{"action", {"csv_export"}}}, + ParsedRequestIs("/", std::nullopt, {{"action", {"csv_export"}}}, Query{"SELECT * WHERE {}", {}})); EXPECT_THAT( parse(makePostRequest("/", URLENCODED, "query=SELECT+%2A%20WHERE%20%7B%7D&send=100")), - ParsedRequestIs("/", {{"send", {"100"}}}, + ParsedRequestIs("/", std::nullopt, {{"send", {"100"}}}, Query{"SELECT * WHERE {}", {}})); AD_EXPECT_THROW_WITH_MESSAGE( parse(makePostRequest("/", URLENCODED, @@ -79,11 +83,12 @@ TEST(ServerTest, parseHttpRequest) { EXPECT_THAT( parse(makePostRequest("/", "application/x-www-form-urlencoded", "query=SELECT%20%2A%20WHERE%20%7B%7D&send=100")), - ParsedRequestIs("/", {{"send", {"100"}}}, + ParsedRequestIs("/", std::nullopt, {{"send", {"100"}}}, Query{"SELECT * WHERE {}", {}})); - EXPECT_THAT(parse(makePostRequest("/", URLENCODED, - "query=SELECT%20%2A%20WHERE%20%7B%7D")), - ParsedRequestIs("/", {}, Query{"SELECT * WHERE {}", {}})); + EXPECT_THAT( + parse(makePostRequest("/", URLENCODED, + "query=SELECT%20%2A%20WHERE%20%7B%7D")), + ParsedRequestIs("/", std::nullopt, {}, Query{"SELECT * WHERE {}", {}})); auto Iri = ad_utility::triple_component::Iri::fromIriref; EXPECT_THAT( parse(makePostRequest( @@ -92,7 +97,7 @@ TEST(ServerTest, parseHttpRequest) { "2Fw3.org%2Fdefault&named-graph-uri=https%3A%2F%2Fw3.org%2F1&named-" "graph-uri=https%3A%2F%2Fw3.org%2F2")), ParsedRequestIs( - "/", + "/", std::nullopt, {{"default-graph-uri", {"https://w3.org/default"}}, {"named-graph-uri", {"https://w3.org/1", "https://w3.org/2"}}}, Query{"SELECT * WHERE {}", @@ -105,12 +110,14 @@ TEST(ServerTest, parseHttpRequest) { "query=SELECT%20%2A%20WHERE%20%7B%7D")), testing::StrEq("URL-encoded POST requests must not contain query " "parameters in the URL.")); - EXPECT_THAT(parse(makePostRequest("/", URLENCODED, "cmd=clear-cache")), - ParsedRequestIs("/", {{"cmd", {"clear-cache"}}}, None{})); - EXPECT_THAT(parse(makePostRequest("/", QUERY, "SELECT * WHERE {}")), - ParsedRequestIs("/", {}, Query{"SELECT * WHERE {}", {}})); + EXPECT_THAT( + parse(makePostRequest("/", URLENCODED, "cmd=clear-cache")), + ParsedRequestIs("/", std::nullopt, {{"cmd", {"clear-cache"}}}, None{})); + EXPECT_THAT( + parse(makePostRequest("/", QUERY, "SELECT * WHERE {}")), + ParsedRequestIs("/", std::nullopt, {}, Query{"SELECT * WHERE {}", {}})); EXPECT_THAT(parse(makePostRequest("/?send=100", QUERY, "SELECT * WHERE {}")), - ParsedRequestIs("/", {{"send", {"100"}}}, + ParsedRequestIs("/", std::nullopt, {{"send", {"100"}}}, Query{"SELECT * WHERE {}", {}})); AD_EXPECT_THROW_WITH_MESSAGE( parse(makeRequest(http::verb::patch, "/")), @@ -125,20 +132,23 @@ TEST(ServerTest, parseHttpRequest) { AD_EXPECT_THROW_WITH_MESSAGE( parse(makeGetRequest("/?update=DELETE%20%2A%20WHERE%20%7B%7D")), testing::StrEq("SPARQL Update is not allowed as GET request.")); - EXPECT_THAT(parse(makePostRequest("/", UPDATE, "DELETE * WHERE {}")), - ParsedRequestIs("/", {}, Update{"DELETE * WHERE {}", {}})); - EXPECT_THAT(parse(makePostRequest("/", URLENCODED, - "update=DELETE%20%2A%20WHERE%20%7B%7D")), - ParsedRequestIs("/", {}, Update{"DELETE * WHERE {}", {}})); - EXPECT_THAT(parse(makePostRequest("/", URLENCODED, - "update=DELETE+%2A+WHERE%20%7B%7D")), - ParsedRequestIs("/", {}, Update{"DELETE * WHERE {}", {}})); + EXPECT_THAT( + parse(makePostRequest("/", UPDATE, "DELETE * WHERE {}")), + ParsedRequestIs("/", std::nullopt, {}, Update{"DELETE * WHERE {}", {}})); + EXPECT_THAT( + parse(makePostRequest("/", URLENCODED, + "update=DELETE%20%2A%20WHERE%20%7B%7D")), + ParsedRequestIs("/", std::nullopt, {}, Update{"DELETE * WHERE {}", {}})); + EXPECT_THAT( + parse( + makePostRequest("/", URLENCODED, "update=DELETE+%2A+WHERE%20%7B%7D")), + ParsedRequestIs("/", std::nullopt, {}, Update{"DELETE * WHERE {}", {}})); // Check that the correct datasets for the method (GET or POST) are added EXPECT_THAT( parse(makeGetRequest("/?query=SELECT%20%2A%20WHERE%20%7B%7D&default-" "graph-uri=foo&named-graph-uri=bar&using-graph-uri=" "baz&using-named-graph-uri=cat")), - ParsedRequestIs("/", + ParsedRequestIs("/", std::nullopt, {{"default-graph-uri", {"foo"}}, {"named-graph-uri", {"bar"}}, {"using-graph-uri", {"baz"}}, @@ -151,7 +161,7 @@ TEST(ServerTest, parseHttpRequest) { "graph-uri=foo&named-graph-uri=bar&using-graph-uri=" "baz&using-named-graph-uri=cat", QUERY, "SELECT * WHERE {}")), - ParsedRequestIs("/", + ParsedRequestIs("/", std::nullopt, {{"default-graph-uri", {"foo"}}, {"named-graph-uri", {"bar"}}, {"using-graph-uri", {"baz"}}, @@ -164,7 +174,7 @@ TEST(ServerTest, parseHttpRequest) { "query=SELECT%20%2A%20WHERE%20%7B%7D&default-graph-" "uri=foo&named-graph-uri=bar&using-graph-uri=baz&" "using-named-graph-uri=cat")), - ParsedRequestIs("/", + ParsedRequestIs("/", std::nullopt, {{"default-graph-uri", {"foo"}}, {"named-graph-uri", {"bar"}}, {"using-graph-uri", {"baz"}}, @@ -177,7 +187,7 @@ TEST(ServerTest, parseHttpRequest) { "update=INSERT%20DATA%20%7B%7D&default-graph-uri=" "foo&named-graph-uri=bar&using-graph-uri=baz&" "using-named-graph-uri=cat")), - ParsedRequestIs("/", + ParsedRequestIs("/", std::nullopt, { {"default-graph-uri", {"foo"}}, {"named-graph-uri", {"bar"}}, @@ -192,7 +202,7 @@ TEST(ServerTest, parseHttpRequest) { "/?default-graph-uri=foo&named-graph-uri=bar&using-graph-uri=baz&" "using-named-graph-uri=cat", UPDATE, "INSERT DATA {}")), - ParsedRequestIs("/", + ParsedRequestIs("/", std::nullopt, { {"default-graph-uri", {"foo"}}, {"named-graph-uri", {"bar"}}, @@ -202,6 +212,109 @@ TEST(ServerTest, parseHttpRequest) { Update{"INSERT DATA {}", {DatasetClause{Iri(""), false}, DatasetClause{Iri(""), true}}})); + auto testAccessTokenCombinations = + [&](const http::verb& method, std::string_view pathBase, + const std::variant& expectedOperation, + const ad_utility::HashMap& headers = {}, + const std::optional& body = std::nullopt, + ad_utility::source_location l = + ad_utility::source_location::current()) { + auto t = generateLocationTrace(l); + // Test the cases: + // 1. No access token + // 2. Access token in query + // 3. Access token in `Authorization` header + // 4. Different access tokens + // 5. Same access token + boost::urls::url pathWithAccessToken{pathBase}; + pathWithAccessToken.params().append({"access-token", "foo"}); + ad_utility::HashMap + headersWithDifferentAccessToken{headers}; + headersWithDifferentAccessToken.insert( + {http::field::authorization, "Bearer bar"}); + ad_utility::HashMap + headersWithSameAccessToken{headers}; + headersWithSameAccessToken.insert( + {http::field::authorization, "Bearer foo"}); + EXPECT_THAT(parse(makeRequest(method, pathBase, headers, body)), + ParsedRequestIs("/", std::nullopt, {}, expectedOperation)); + EXPECT_THAT(parse(makeRequest(method, pathWithAccessToken.buffer(), + headers, body)), + ParsedRequestIs("/", "foo", {{"access-token", {"foo"}}}, + expectedOperation)); + EXPECT_THAT(parse(makeRequest(method, pathBase, + headersWithDifferentAccessToken, body)), + ParsedRequestIs("/", "bar", {}, expectedOperation)); + EXPECT_THAT(parse(makeRequest(method, pathWithAccessToken.buffer(), + headersWithSameAccessToken, body)), + ParsedRequestIs("/", "foo", {{"access-token", {"foo"}}}, + expectedOperation)); + AD_EXPECT_THROW_WITH_MESSAGE( + parse(makeRequest(method, pathWithAccessToken.buffer(), + headersWithDifferentAccessToken, body)), + testing::HasSubstr( + "Access token is specified both in the " + "`Authorization` header and by the `access-token` " + "parameter, but they are not the same")); + }; + testAccessTokenCombinations(http::verb::get, "/?query=a", Query{"a", {}}); + testAccessTokenCombinations(http::verb::post, "/", Query{"a", {}}, + {{http::field::content_type, QUERY}}, "a"); + testAccessTokenCombinations(http::verb::post, "/", Update{"a", {}}, + {{http::field::content_type, UPDATE}}, "a"); + auto testAccessTokenCombinationsUrlEncoded = + [&](const std::string& bodyBase, + const std::variant& expectedOperation, + ad_utility::source_location l = + ad_utility::source_location::current()) { + auto t = generateLocationTrace(l); + // Test the cases: + // 1. No access token + // 2. Access token in query + // 3. Access token in `Authorization` header + // 4. Different access tokens + // 5. Same access token + boost::urls::url paramsWithAccessToken{absl::StrCat("/?", bodyBase)}; + paramsWithAccessToken.params().append({"access-token", "foo"}); + std::string bodyWithAccessToken{ + paramsWithAccessToken.encoded_params().buffer()}; + ad_utility::HashMap headers{ + {http::field::content_type, {URLENCODED}}}; + ad_utility::HashMap + headersWithDifferentAccessToken{ + {http::field::content_type, {URLENCODED}}, + {http::field::authorization, "Bearer bar"}}; + ad_utility::HashMap + headersWithSameAccessToken{ + {http::field::content_type, {URLENCODED}}, + {http::field::authorization, "Bearer foo"}}; + EXPECT_THAT( + parse(makeRequest(http::verb::post, "/", headers, bodyBase)), + ParsedRequestIs("/", std::nullopt, {}, expectedOperation)); + EXPECT_THAT(parse(makeRequest(http::verb::post, "/", headers, + bodyWithAccessToken)), + ParsedRequestIs("/", "foo", {{"access-token", {"foo"}}}, + expectedOperation)); + EXPECT_THAT( + parse(makeRequest(http::verb::post, "/", + headersWithDifferentAccessToken, bodyBase)), + ParsedRequestIs("/", "bar", {}, expectedOperation)); + EXPECT_THAT(parse(makeRequest(http::verb::post, "/", + headersWithSameAccessToken, bodyBase)), + ParsedRequestIs("/", "foo", {}, expectedOperation)); + AD_EXPECT_THROW_WITH_MESSAGE( + parse(makeRequest(http::verb::post, "/", + headersWithDifferentAccessToken, + bodyWithAccessToken)), + testing::HasSubstr( + "Access token is specified both in the " + "`Authorization` header and by the `access-token` " + "parameter, but they are not the same")); + }; + testAccessTokenCombinationsUrlEncoded("query=SELECT%20%2A%20WHERE%20%7B%7D", + Query{"SELECT * WHERE {}", {}}); + testAccessTokenCombinationsUrlEncoded("update=DELETE%20WHERE%20%7B%7D", + Update{"DELETE WHERE {}", {}}); } TEST(ServerTest, determineResultPinning) { @@ -370,3 +483,47 @@ TEST(ServerTest, createResponseMetadata) { EXPECT_THAT(metadata["delta-triples"], testing::Eq(deltaTriplesJson)); EXPECT_THAT(metadata["located-triples"], testing::Eq(locatedTriplesJson)); } + +TEST(ServerTest, extractAccessToken) { + auto extract = [](const ad_utility::httpUtils::HttpRequest auto& request) { + auto parsedUrl = parseRequestTarget(request.target()); + return Server::extractAccessToken(request, parsedUrl.parameters_); + }; + EXPECT_THAT(extract(makeGetRequest("/")), testing::Eq(std::nullopt)); + EXPECT_THAT(extract(makeGetRequest("/?access-token=foo")), + testing::Optional(testing::Eq("foo"))); + EXPECT_THAT( + extract(makeRequest(http::verb::get, "/", + {{http::field::authorization, "Bearer foo"}})), + testing::Optional(testing::Eq("foo"))); + EXPECT_THAT( + extract(makeRequest(http::verb::get, "/?access-token=foo", + {{http::field::authorization, "Bearer foo"}})), + testing::Optional(testing::Eq("foo"))); + AD_EXPECT_THROW_WITH_MESSAGE( + extract(makeRequest(http::verb::get, "/?access-token=bar", + {{http::field::authorization, "Bearer foo"}})), + testing::HasSubstr( + "Access token is specified both in the `Authorization` header and by " + "the `access-token` parameter, but they are not the same")); + AD_EXPECT_THROW_WITH_MESSAGE( + extract(makeRequest(http::verb::get, "/", + {{http::field::authorization, "foo"}})), + testing::HasSubstr( + "Authorization header doesn't start with \"Bearer \".")); + EXPECT_THAT(extract(makePostRequest("/", "text/turtle", "")), + testing::Eq(std::nullopt)); + EXPECT_THAT(extract(makePostRequest("/?access-token=foo", "text/turtle", "")), + testing::Optional(testing::Eq("foo"))); + AD_EXPECT_THROW_WITH_MESSAGE( + extract(makeRequest(http::verb::post, "/?access-token=bar", + {{http::field::authorization, "Bearer foo"}})), + testing::HasSubstr( + "Access token is specified both in the `Authorization` header and by " + "the `access-token` parameter, but they are not the same")); + AD_EXPECT_THROW_WITH_MESSAGE( + extract(makeRequest(http::verb::post, "/?access-token=bar", + {{http::field::authorization, "foo"}})), + testing::HasSubstr( + "Authorization header doesn't start with \"Bearer \".")); +} diff --git a/test/util/HttpRequestHelpers.h b/test/util/HttpRequestHelpers.h index 4505e828db..cb57ead971 100644 --- a/test/util/HttpRequestHelpers.h +++ b/test/util/HttpRequestHelpers.h @@ -14,7 +14,8 @@ namespace http = boost::beast::http; // Construct a boost::beast request with the HTTP method, target path, headers // and body. inline auto makeRequest( - const http::verb method = http::verb::get, const std::string& target = "/", + const http::verb method = http::verb::get, + const std::string_view target = "/", const ad_utility::HashMap& headers = {}, const std::optional& body = std::nullopt) { // version 11 stands for HTTP/1.1