commit e8c19f02f7fc45a0eee4a3fbb79c123964fbde0d
parent ef07b66cec85de5a34a1d5c19d7d6b3030464b07
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date: Sat, 21 Sep 2024 10:45:06 +0200
Increase test coverage
Diffstat:
5 files changed, 242 insertions(+), 14 deletions(-)
diff --git a/.idea/misc.xml b/.idea/misc.xml
@@ -4,7 +4,7 @@
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
- <option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="7" type="long" />
+ <option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="12" type="long" />
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexRemoved" />
</component>
<component name="CMakePythonSetting">
diff --git a/include/json.hpp b/include/json.hpp
@@ -67,6 +67,7 @@ struct ClientRegistrationData {
std::vector<std::string> redirect_uris;
std::vector<std::string> response_types;
std::vector<std::string> grant_types;
+ std::vector<std::string> contacts;
};
struct ClientRegistrationResponse {
diff --git a/include/matrix_coro.hpp b/include/matrix_coro.hpp
@@ -47,7 +47,7 @@ private:
* \param cs_endpoint The URL of the client-server endpoint from which to fetch the authentication issuer information.
* \return A cppcoro::task that resolves to an AuthIssuerResponse containing the authentication issuer information.
*/
- [[nodiscard]] cppcoro::task<AuthIssuerResponse> fetch_auth_issuer(const std::string &cs_endpoint) const;
+ [[nodiscard]] cppcoro::task<AuthIssuerResponse> fetch_auth_issuer(std::string cs_endpoint) const;
/**
* \brief Registers a client with the specified authentication endpoint (MSC2966).
@@ -59,7 +59,7 @@ private:
* \param registration_data The data required for client registration.
* \return A cppcoro::task that resolves to a ClientRegistrationResponse containing the registration result.
*/
- [[nodiscard]] cppcoro::task<ClientRegistrationResponse> register_client(const std::string ®istration_endpoint,
+ [[nodiscard]] cppcoro::task<ClientRegistrationResponse> register_client(std::string registration_endpoint,
const ClientRegistrationData &
registration_data) const;
@@ -85,10 +85,12 @@ private:
}
[[nodiscard]] cppcoro::task<TokenResponse> exchange_code_for_token(
- const std::string &token_endpoint, const std::string &code,
+ std::string token_endpoint,
+ const std::string &code,
const std::string &code_verifier,
- const std::string &client_id, const std::string &redirect_url) const;
+ const std::string &client_id,
+ const std::string &redirect_url) const;
- [[nodiscard]] cppcoro::task<OpenIDConfiguration> fetch_openid_configration(
- const std::string &auth_endpoint) const;
+ [[nodiscard]] cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(
+ std::string auth_endpoint) const;
};
diff --git a/src/matrix_coro.cpp b/src/matrix_coro.cpp
@@ -46,7 +46,7 @@ cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &home
co_return response;
}
-cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(const std::string &cs_endpoint) const {
+cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpoint) const {
if (!curl) {
throw std::runtime_error("http client is not initialized");
}
@@ -84,7 +84,7 @@ cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(const std::string &c
co_return response;
}
-cppcoro::task<ClientRegistrationResponse> Client::register_client(const std::string ®istration_endpoint,
+cppcoro::task<ClientRegistrationResponse> Client::register_client(std::string registration_endpoint,
const ClientRegistrationData ®istration_data)
const {
if (!curl) {
@@ -121,6 +121,10 @@ const {
}
root["token_endpoint_auth_method"] = registration_data.token_endpoint_auth_method;
root["client_uri"] = registration_data.client_uri;
+ root["contacts"] = Json::arrayValue;
+ for (const auto &contact: registration_data.contacts) {
+ root["contacts"].append(contact);
+ }
// Convert the JSON to a string
Json::StreamWriterBuilder writer;
@@ -130,7 +134,7 @@ const {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_str.c_str());
// Set the Content-Type header
- struct curl_slist *headers = nullptr;
+ curl_slist *headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
@@ -138,13 +142,13 @@ const {
//curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) {
- throw std::runtime_error("failed to find auth_issuer information: " + std::string(curl_easy_strerror(res)));
+ throw std::runtime_error("failed to find registration information: " + std::string(curl_easy_strerror(res)));
}
Json::Value resp_root;
Json::Reader reader;
if (const bool parse_status = reader.parse(str_buffer, resp_root); !parse_status) {
- throw std::runtime_error("failed to parse auth_issuer information");
+ throw std::runtime_error("failed to parse registration information");
}
ClientRegistrationResponse response;
response.client_id = resp_root["client_id"].asString();
@@ -152,7 +156,7 @@ const {
co_return response;
}
-cppcoro::task<TokenResponse> Client::exchange_code_for_token(const std::string &token_endpoint,
+cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_endpoint,
const std::string &code,
const std::string &code_verifier,
const std::string &client_id,
@@ -215,7 +219,7 @@ cppcoro::task<TokenResponse> Client::exchange_code_for_token(const std::string &
co_return response;
}
-cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configration(const std::string &auth_endpoint) const {
+cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::string auth_endpoint) const {
if (!curl) {
throw std::runtime_error("http client is not initialized");
}
diff --git a/tests/test.cpp b/tests/test.cpp
@@ -11,6 +11,41 @@ public:
static cppcoro::task<WellKnownResponse> test_fetch_wellknown(const Client &client, const std::string &homeserver) {
return client.fetch_wellknown(homeserver);
}
+
+ static cppcoro::task<AuthIssuerResponse> test_fetch_auth_issuer(const Client &client,
+ const std::string &cs_endpoint) {
+ return client.fetch_auth_issuer(cs_endpoint);
+ }
+
+ static cppcoro::task<ClientRegistrationResponse> test_register_client(const Client &client,
+ const std::string ®istration_endpoint,
+ const ClientRegistrationData &
+ registration_data) {
+ return client.register_client(registration_endpoint, registration_data);
+ }
+
+ static std::string test_generate_authorize_url(const Client &client,
+ const std::string &auth_endpoint,
+ const ClientRegistrationResponse &auth_data,
+ const std::string &redirect_url,
+ const std::string &state,
+ const std::string &code_verifier) {
+ return client.generate_authorize_url(auth_endpoint, auth_data, redirect_url, state, code_verifier);
+ }
+
+ static cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(const Client &client,
+ const std::string &auth_endpoint) {
+ return client.fetch_openid_configuration(auth_endpoint);
+ }
+
+ static cppcoro::task<TokenResponse> exchange_code_for_token(const Client &client,
+ const std::string &token_endpoint,
+ const std::string &client_id,
+ const std::string &code,
+ const std::string &redirect_uri,
+ const std::string &code_verifier) {
+ return client.exchange_code_for_token(token_endpoint, code, code_verifier, client_id, redirect_uri);
+ }
};
void initLogging() {
@@ -60,3 +95,189 @@ SCENARIO("fetch_wellknown throws runtime_error if JSON parsing fails") {
}
}
}
+
+SCENARIO("fetch_auth_issuer can find and parse auth issuer at https://synapse-oidc.element.dev") {
+ initLogging();
+ GIVEN("A Client instance") {
+ WHEN("fetch_auth_issuer is called with https://synapse-oidc.element.dev") {
+ const Client client;
+ auto task = ClientTest::test_fetch_auth_issuer(client, "https://synapse-oidc.element.dev");
+ auto [issuer] = sync_wait(task);
+ THEN("A valid AuthIssuerResponse should be returned") {
+ REQUIRE(issuer == "https://auth-oidc.element.dev/");
+ }
+ }
+ }
+}
+
+SCENARIO("fetch_auth_issuer throws runtime_error if curl_easy_perform fails") {
+ initLogging();
+ GIVEN("A Client instance with an invalid URL") {
+ WHEN("fetch_auth_issuer is called with an invalid URL") {
+ THEN("A runtime_error should be thrown") {
+ const Client client;
+ REQUIRE_THROWS_AS(sync_wait(ClientTest::test_fetch_auth_issuer(client,"invalid_url")),
+ std::runtime_error);
+ }
+ }
+ }
+}
+
+SCENARIO("fetch_auth_issuer throws runtime_error if JSON parsing fails") {
+ initLogging();
+ GIVEN("A Client instance with a URL returning invalid JSON") {
+ WHEN("fetch_auth_issuer is called with a URL returning invalid JSON") {
+ THEN("A runtime_error should be thrown") {
+ const Client client;
+ REQUIRE_THROWS_AS(
+ sync_wait(ClientTest::test_fetch_auth_issuer(client,"https://example.com/invalid-json")),
+ std::runtime_error);
+ }
+ }
+ }
+}
+
+SCENARIO("register_client can register a client at https://synapse-oidc.element.dev") {
+ initLogging();
+ GIVEN("A Client instance") {
+ WHEN("register_client is called with https://synapse-oidc.element.dev") {
+ const Client client;
+ ClientRegistrationData registration_data;
+ registration_data.application_type = "web";
+ registration_data.client_name = "Test Client";
+ registration_data.client_uri = "https://example.com";
+ registration_data.token_endpoint_auth_method = "none";
+ registration_data.redirect_uris = {"https://example.com"};
+ registration_data.response_types = {"code"};
+ registration_data.grant_types = {"authorization_code", "refresh_token"};
+ registration_data.contacts = {"mailto:hello@example.com"};
+ auto task = ClientTest::test_register_client(client, "https://auth-oidc.element.dev/oauth2/registration",
+ registration_data);
+ auto [client_id, client_id_issued_at] = sync_wait(task);
+ THEN("A valid ClientRegistrationResponse should be returned") {
+ REQUIRE(!client_id.empty());
+ REQUIRE(client_id_issued_at > 0);
+ }
+ }
+ }
+}
+
+SCENARIO("register_client throws runtime_error if curl_easy_perform fails") {
+ initLogging();
+ GIVEN("A Client instance with an invalid URL") {
+ WHEN("register_client is called with an invalid URL") {
+ THEN("A runtime_error should be thrown") {
+ const Client client;
+ ClientRegistrationData registration_data;
+ registration_data.application_type = "web";
+ registration_data.client_name = "Test Client";
+ registration_data.client_uri = "https://example.com";
+ registration_data.token_endpoint_auth_method = "none";
+ registration_data.redirect_uris = {"https://example.com"};
+ registration_data.response_types = {"code"};
+ registration_data.grant_types = {"authorization_code", "refresh_token"};
+ registration_data.contacts = {"mailto:hello@example.com"};
+ REQUIRE_THROWS_AS(sync_wait(ClientTest::test_register_client(client,"invalid_url", registration_data)),
+ std::runtime_error);
+ }
+ }
+ }
+}
+
+SCENARIO("register_client throws runtime_error if JSON parsing fails") {
+ initLogging();
+ GIVEN("A Client instance with a URL returning invalid JSON") {
+ WHEN("register_client is called with a URL returning invalid JSON") {
+ THEN("A runtime_error should be thrown") {
+ const Client client;
+ ClientRegistrationData registration_data;
+ registration_data.application_type = "web";
+ registration_data.client_name = "Test Client";
+ registration_data.client_uri = "https://example.com";
+ registration_data.token_endpoint_auth_method = "none";
+ registration_data.redirect_uris = {"https://example.com"};
+ registration_data.response_types = {"code"};
+ registration_data.grant_types = {"authorization_code", "refresh_token"};
+ registration_data.contacts = {"mailto:hello@example.com"};
+ REQUIRE_THROWS_AS(
+ sync_wait(ClientTest::test_register_client(client,"https://example.com/invalid-json",
+ registration_data)),
+ std::runtime_error);
+ }
+ }
+ }
+}
+
+SCENARIO("generate_authorize_url can generate a valid authorize URL") {
+ initLogging();
+ GIVEN("A Client") {
+ WHEN("generate_authorize_url is called") {
+ const Client client;
+ ClientRegistrationResponse auth_data;
+ auth_data.client_id = "test_client_id";
+ auth_data.client_id_issued_at = 1630000000;
+ std::string redirect_url = "https://example.com";
+ std::string state = "test_state";
+ std::string code_verifier = "test_code_verifier";
+ auto authorize_url = ClientTest::test_generate_authorize_url(client, "https://auth-oidc.element.dev",
+ auth_data, redirect_url, state, code_verifier);
+ THEN("A valid authorize URL should be returned") {
+ REQUIRE(
+ authorize_url ==
+ "https://auth-oidc.element.dev/authorize?response_type=code&response_mode=fragment&client_id=test_client_id&redirect_uri=https%3A%2F%2Fexample.com&scope=urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*%20urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AABCDEFGHIJKL&state=test_state&code_challenge_method=S256&code_challenge="
+ + cthash::base64url_encode(cthash::simple<cthash::sha256>(code_verifier)).to_string());
+ }
+ }
+ }
+}
+
+// Fetch openid configuration
+SCENARIO("fetch_openid_configuration can find and parse openid configuration at https://auth-oidc.element.dev") {
+ initLogging();
+ GIVEN("A Client instance") {
+ WHEN("fetch_openid_configuration is called with https://auth-oidc.element.dev") {
+ const Client client;
+ auto task = ClientTest::fetch_openid_configuration(client, "https://auth-oidc.element.dev");
+ auto resp = sync_wait(task);
+ THEN("A valid OpenIDConfiguration should be returned") {
+ REQUIRE(resp.issuer == "https://auth-oidc.element.dev/");
+ REQUIRE(resp.authorization_endpoint == "https://auth-oidc.element.dev/authorize");
+ REQUIRE(resp.token_endpoint == "https://auth-oidc.element.dev/oauth2/token");
+ REQUIRE(resp.jwks_uri == "https://auth-oidc.element.dev/oauth2/keys.json");
+ REQUIRE(resp.registration_endpoint == "https://auth-oidc.element.dev/oauth2/registration");
+ REQUIRE(resp.revocation_endpoint == "https://auth-oidc.element.dev/oauth2/revoke");
+ REQUIRE(resp.introspection_endpoint == "https://auth-oidc.element.dev/oauth2/introspect");
+ REQUIRE(resp.userinfo_endpoint == "https://auth-oidc.element.dev/oauth2/userinfo");
+ REQUIRE(resp.device_authorization_endpoint == "https://auth-oidc.element.dev/oauth2/device");
+ REQUIRE(resp.account_management_uri == "https://auth-oidc.element.dev/account/");
+ }
+ }
+ }
+}
+
+SCENARIO("fetch_openid_configuration throws runtime_error if curl_easy_perform fails") {
+ initLogging();
+ GIVEN("A Client instance with an invalid URL") {
+ WHEN("fetch_openid_configuration is called with an invalid URL") {
+ THEN("A runtime_error should be thrown") {
+ const Client client;
+ REQUIRE_THROWS_AS(sync_wait(ClientTest::fetch_openid_configuration(client,"invalid_url")),
+ std::runtime_error);
+ }
+ }
+ }
+}
+
+SCENARIO("fetch_openid_configuration throws runtime_error if JSON parsing fails") {
+ initLogging();
+ GIVEN("A Client instance with a URL returning invalid JSON") {
+ WHEN("fetch_openid_configuration is called with a URL returning invalid JSON") {
+ THEN("A runtime_error should be thrown") {
+ const Client client;
+ REQUIRE_THROWS_AS(
+ sync_wait(ClientTest::fetch_openid_configuration(client,"https://example.com/invalid-json")),
+ std::runtime_error);
+ }
+ }
+ }
+}