matrix-coro

A WIP highlevel matrix c++ SDK with a MAS first approach
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-coro.git
Log | Files | Refs | README | LICENSE

commit e8c19f02f7fc45a0eee4a3fbb79c123964fbde0d
parent ef07b66cec85de5a34a1d5c19d7d6b3030464b07
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date:   Sat, 21 Sep 2024 10:45:06 +0200

Increase test coverage

Diffstat:
M.idea/misc.xml | 2+-
Minclude/json.hpp | 1+
Minclude/matrix_coro.hpp | 14++++++++------
Msrc/matrix_coro.cpp | 18+++++++++++-------
Mtests/test.cpp | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 &registration_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 &registration_endpoint, +cppcoro::task<ClientRegistrationResponse> Client::register_client(std::string registration_endpoint, const ClientRegistrationData &registration_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 &registration_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); + } + } + } +}