diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index ccce46185..8cca34d1f 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -1,11 +1,7 @@ name: CMake Build env: - REALM_ATLAS_ENDPOINT: ${{ secrets.ATLAS_QA_BASE_URL }} - REALM_BAAS_ENDPOINT: ${{ secrets.APPSERVICES_QA_BASE_URL }} - REALM_ATLAS_API_KEY: ${{ secrets.ATLAS_QA_API_KEY }} - REALM_ATLAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_QA_PRIVATE_API_KEY }} - REALM_ATLAS_PROJECT_ID: ${{ secrets.ATLAS_QA_PROJECT_ID}} + APIKEY: ${{ secrets.REALM_BAASAAS_API_KEY }} REALM_CI: true REALM_DISABLE_ANALYTICS: true on: @@ -15,37 +11,26 @@ on: pull_request: jobs: - deploy-cluster: + stop-all-containers: runs-on: ubuntu-latest - name: Deploy Cluster - outputs: - clusterName: ${{ steps.deploy-cluster.outputs.clusterName }} - steps: - - uses: realm/ci-actions/mdb-realm/deploy@28a12a22e135d56db2b678fbe6479c0e1b52f169 - id: deploy-cluster - with: - atlasUrl: ${{ env.REALM_ATLAS_ENDPOINT }} - realmUrl: ${{ env.REALM_BAAS_ENDPOINT }} - projectId: ${{ env.REALM_ATLAS_PROJECT_ID }} - apiKey: ${{ env.REALM_ATLAS_API_KEY }} - privateApiKey: ${{ env.REALM_ATLAS_PRIVATE_API_KEY }} - - delete-cluster: - runs-on: ubuntu-latest - name: Delete Cluster + name: Stop Stray Containers if: always() needs: - build-macos-sync - build-linux-sync - build-windows-sync steps: - - uses: realm/ci-actions/mdb-realm/cleanup@28a12a22e135d56db2b678fbe6479c0e1b52f169 + - name: Checkout Repository + uses: actions/checkout@v3 with: - atlasUrl: ${{ env.REALM_ATLAS_ENDPOINT }} - realmUrl: ${{ env.REALM_BAAS_ENDPOINT }} - projectId: ${{ env.REALM_ATLAS_PROJECT_ID }} - apiKey: ${{ env.REALM_ATLAS_API_KEY }} - privateApiKey: ${{ env.REALM_ATLAS_PRIVATE_API_KEY }} + repository: 'https://github.com/10gen/baasaas.git' + ref: 'main' + - name: Execute CLI + run: | + chmod +x ./cli.sh + ./cli.sh mine | grep '"id":' | awk -F '"' '{print $4}' | while read id; do + ./cli.sh stop "$id" + done build-macos-xcode13_1: @@ -64,10 +49,6 @@ jobs: - Release exclude: - configuration: ${{ (github.event_name == 'pull_request' && 'Release') || 'none' }} - needs: - - deploy-cluster - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Checkout uses: actions/checkout@v2 @@ -129,10 +110,6 @@ jobs: - Release exclude: - configuration: ${{ (github.event_name == 'pull_request' && 'Release') || 'none' }} - needs: - - deploy-cluster - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Checkout uses: actions/checkout@v2 @@ -194,10 +171,6 @@ jobs: - Release exclude: - configuration: ${{ (github.event_name == 'pull_request' && 'Release') || 'none' }} - needs: - - deploy-cluster - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Checkout uses: actions/checkout@v2 @@ -269,12 +242,8 @@ jobs: configuration: - Debug - Release - needs: - - deploy-cluster container: image: ${{ matrix.compiler.name == 'clang' && 'silkeh/clang' || matrix.compiler.name }}:${{ matrix.compiler.version }} - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Install Linux Dependencies run: | @@ -341,10 +310,6 @@ jobs: configuration: - Debug - Release - needs: - - deploy-cluster - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Checkout uses: actions/checkout@v2 @@ -414,10 +379,6 @@ jobs: configuration: - Debug - Release - needs: - - deploy-cluster - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Checkout uses: actions/checkout@v2 @@ -459,10 +420,6 @@ jobs: configuration: - Debug - Release - needs: - - deploy-cluster - env: - REALM_ATLAS_CLUSTER_NAME: ${{ needs.deploy-cluster.outputs.clusterName }} steps: - name: Checkout uses: actions/checkout@v2 diff --git a/src/cpprealm/analytics.cpp b/src/cpprealm/analytics.cpp index 3ce8399c3..015bf4abe 100644 --- a/src/cpprealm/analytics.cpp +++ b/src/cpprealm/analytics.cpp @@ -128,7 +128,7 @@ namespace realm { return (info.kp_proc.p_flag & P_TRACED) != 0; } -#elif __linux__ +#elif __linux__ && ndef(__ANDROID__) std::string print_hex(unsigned char *bs, unsigned int len) { std::stringstream ss; for (size_t i = 0; i < len; i++) { diff --git a/tests/admin_utils.cpp b/tests/admin_utils.cpp index 63d35bac3..9dd158468 100644 --- a/tests/admin_utils.cpp +++ b/tests/admin_utils.cpp @@ -19,162 +19,218 @@ #include #include +#include #include #include "admin_utils.hpp" #include "external/json/json.hpp" -namespace { -using namespace realm; - -static app::Response do_http_request(app::Request &&request) { - static internal::DefaultTransport transport; - - std::promise p; - transport.send_request_to_server(std::move(request), - [&p](auto&& response){ - p.set_value(std::move(response)); - }); - return p.get_future().get(); -} - -static std::pair authenticate(const std::string& baas_url, const std::string& provider_type, bson::BsonDocument&& credentials) -{ - std::stringstream body; - body << credentials; - auto request = app::Request(); - request.method = realm::app::HttpMethod::post; - request.url = util::format("%1/api/admin/v3.0/auth/providers/%2/login", baas_url, provider_type); - request.headers = { - {"Content-Type", "application/json;charset=utf-8"}, - {"Accept", "application/json"}}; - request.body = body.str(); - - auto result = do_http_request(std::move(request)); - if (result.http_status_code != 200) { - REALM_TERMINATE(util::format("Unable to authenticate at %1 with provider '%2': %3", baas_url, provider_type, result.body).c_str()); +namespace Admin { + Admin::Session *Admin::Session::instance = nullptr; + std::mutex Admin::Session::mutex; + + static app::Response do_http_request(app::Request &&request) { + internal::DefaultTransport transport; + std::promise p; + std::future f = p.get_future(); + transport.send_request_to_server(std::move(request), + [&p](auto &&response) { + p.set_value(std::move(response)); + }); + return f.get(); } - auto parsed_response = static_cast(bson::parse(result.body)); - return {static_cast(parsed_response["access_token"]), static_cast(parsed_response["refresh_token"])}; -} - -} // namespace - -app::Response Admin::Endpoint::request(app::HttpMethod method, bson::BsonDocument&& body) const -{ - std::stringstream ss; - ss << body; - auto body_str = ss.str(); - - return request(method, body_str); -} - -app::Response Admin::Endpoint::request(app::HttpMethod method, const std::string& body) const -{ - std::string url = m_url + "?bypass_service_change=DestructiveSyncProtocolVersionIncrease"; - - auto request = app::Request(); - request.method = method; - request.url = url; - request.headers = { - {"Authorization", "Bearer " + m_access_token}, - {"Content-Type", "application/json;charset=utf-8"}, - {"Accept", "application/json"}}; - request.body = std::move(body); - auto response = do_http_request(std::move(request)); - - if (response.http_status_code >= 400) { - throw std::runtime_error(util::format("An error occurred while calling %1: %2", url, response.body)); + + static std::pair authenticate(const std::string &baas_url, const std::string &provider_type, bson::BsonDocument &&credentials) { + std::stringstream body; + body << credentials; + auto request = app::Request(); + request.method = realm::app::HttpMethod::post; + request.url = util::format("%1/api/admin/v3.0/auth/providers/%2/login", baas_url, provider_type); + request.headers = { + {"Content-Type", "application/json;charset=utf-8"}, + {"Accept", "application/json"}}; + request.body = body.str(); + + auto result = do_http_request(std::move(request)); + if (result.http_status_code != 200) { + REALM_TERMINATE(util::format("Unable to authenticate at %1 with provider '%2': %3", baas_url, provider_type, result.body).c_str()); + } + auto parsed_response = static_cast(bson::parse(result.body)); + return {static_cast(parsed_response["access_token"]), static_cast(parsed_response["refresh_token"])}; + } + + + void Admin::baas_manager::start() { + auto request = realm::app::Request(); + request.method = realm::app::HttpMethod::post; + request.url = realm::util::format("%1/startContainer?branch=master", m_baas_base_url); + request.headers = { + {"Content-Type", "application/json"}, + {"apiKey", *m_baasaas_api_key}}; + + auto result = Admin::do_http_request(std::move(request)); + + if (result.http_status_code != 200) { + REALM_TERMINATE("Unable to start container"); + } + + m_container_id = nlohmann::json::parse(result.body)["id"]; + } + + void Admin::baas_manager::stop(const std::string& container_id) const { + auto request = realm::app::Request(); + request.method = realm::app::HttpMethod::post; + request.url = realm::util::format("%1/stopContainer?id=%2", m_baas_base_url, container_id); + request.headers = { + {"Content-Type", "application/json"}, + {"apiKey", *m_baasaas_api_key}}; + + auto result = Admin::do_http_request(std::move(request)); + if (result.http_status_code != 200) { + REALM_TERMINATE("Unable to stop container"); + } + } + + const std::string Admin::baas_manager::container_id() const { + if (!m_container_id) { + REALM_TERMINATE("No container has started."); + } + return *m_container_id; + } + + const std::string Admin::baas_manager::wait_for_container() const { + if (!m_container_id) { + REALM_TERMINATE("No container has started."); + } + std::optional url; + size_t attempts = 0; + while (!url) { + auto request = realm::app::Request(); + request.method = realm::app::HttpMethod::get; + request.url = realm::util::format("%1/containerStatus?id=%2", m_baas_base_url, *m_container_id); + request.headers = { + {"Content-Type", "application/json;charset=utf-8"}, + {"apiKey", *m_baasaas_api_key}}; + + auto result = Admin::do_http_request(std::move(request)); + auto json = nlohmann::json::parse(result.body); + if (json.contains("httpUrl")) { + url = json["httpUrl"]; + } + attempts++; + std::cout << "Baasaas container not ready after attempt " << attempts << " will try again in 3 seconds." + << "\n"; + std::this_thread::sleep_for(std::chrono::seconds(3)); + } + + bool has_started = false; + while (!has_started) { + auto request = realm::app::Request(); + request.method = realm::app::HttpMethod::get; + request.url = realm::util::format("%1/api/private/v1.0/version", *url); + + auto result = Admin::do_http_request(std::move(request)); + if (result.http_status_code >= 200 && result.http_status_code < 300) { + break; + } + attempts++; + std::cout << "Baasaas server not ready after attempt " << attempts << " will try again in 3 seconds." + << "\n"; + std::this_thread::sleep_for(std::chrono::seconds(3)); + } + return *url; } - return response; -} - -Admin::Session::Session(const std::string& baas_url, const std::string& access_token, const std::string& refresh_token, const std::string& group_id, std::optional cluster_name) -: group(util::format("%1/api/admin/v3.0/groups/%2", baas_url, group_id), access_token) -, apps(group["apps"]) -, m_group_id(group_id) -, m_cluster_name(std::move(cluster_name)) -, m_base_url(baas_url) -, m_access_token(access_token) -, m_refresh_token(refresh_token) -{ - -} - -std::string Admin::Session::create_app(bson::BsonArray queryable_fields, std::string app_name, bool is_asymmetric, bool disable_recovery_mode) { - refresh_access_token(); - recovery_mode_disabled = disable_recovery_mode; - auto info = static_cast(apps.post({{"name", app_name}})); - app_name = static_cast(info["client_app_id"]); - - if (m_cluster_name) { - app_name += "-" + *m_cluster_name; + realm::app::Response Admin::Endpoint::request(app::HttpMethod method, bson::BsonDocument &&body) const { + std::stringstream ss; + ss << body; + auto body_str = ss.str(); + + return request(method, body_str); } - m_app_id = static_cast(info["_id"]); - - auto app = apps[m_app_id]; - - static_cast(app["secrets"].post({ - { "name", "customTokenKey" }, - { "value", "My_very_confidential_secretttttt" } - })); - - static_cast(app["auth_providers"].post({ - {"type", "custom-token"}, - {"config", bson::BsonDocument { - {"audience", bson::BsonArray()}, - {"signingAlgorithm", "HS256"}, - {"useJWKURI", false} - }}, - {"secret_config", bson::BsonDocument { {"signingKeys", bson::BsonArray { "customTokenKey" }}} }, - {"metadata_fields", bson::BsonArray { - bson::BsonDocument {{"required", false}, {"name", "user_data.name"}, {"field_name", "name"}}, - bson::BsonDocument {{"required", false}, {"name", "user_data.occupation"}, {"field_name", "occupation"}}, - bson::BsonDocument {{"required", false}, {"name", "my_metadata.name"}, {"field_name", "anotherName"}} - }} - })); - - static_cast(app["auth_providers"].post({{"type", "anon-user"}})); - static_cast(app["auth_providers"].post({ - {"type", "local-userpass"}, - {"config", bson::BsonDocument { - {"emailConfirmationUrl", "http://foo.com"}, - {"resetPasswordUrl", "http://foo.com"}, - {"confirmEmailSubject", "Hi"}, - {"resetPasswordSubject", "Bye"}, - {"autoConfirm", true} - }} - })); - - auto auth_providers_endpoint = app["auth_providers"]; - auto providers = static_cast(auth_providers_endpoint.get()); - auto api_key_provider = std::find_if(std::begin(providers), - std::end(providers), [](auto& provider) { - return static_cast(provider)["type"] == "api-key"; - }); - auth_providers_endpoint[static_cast(static_cast(*api_key_provider)["_id"])] - ["enable"].put(); - - bson::BsonDocument mongodb_service_config; - std::string mongodb_service_type; - if (m_cluster_name) { - mongodb_service_config["clusterName"] = *m_cluster_name; - mongodb_service_type = "mongodb-atlas"; - } else { - mongodb_service_config["uri"] = "mongodb://localhost:26000"; - mongodb_service_type = "mongodb"; + + realm::app::Response Admin::Endpoint::request(app::HttpMethod method, const std::string &body) const { + std::string url = m_url + "?bypass_service_change=DestructiveSyncProtocolVersionIncrease"; + + auto request = app::Request(); + request.method = method; + request.url = url; + request.headers = { + {"Authorization", "Bearer " + m_access_token}, + {"Content-Type", "application/json;charset=utf-8"}, + {"Accept", "application/json"}}; + request.body = std::move(body); + auto response = Admin::do_http_request(std::move(request)); + + if (response.http_status_code >= 400) { + throw std::runtime_error(util::format("An error occurred while calling %1: %2", url, response.body)); + } + + return response; } - static_cast(app["functions"].post({ - {"name", "updateUserData"}, - {"private", false}, - {"can_evaluate", {}}, - {"source", - R"( + std::string Admin::Session::create_app(bson::BsonArray queryable_fields, std::string app_name, bool is_asymmetric, bool disable_recovery_mode) { + refresh_access_token(); + recovery_mode_disabled = disable_recovery_mode; + auto info = static_cast(apps.post({{"name", app_name}})); + app_name = static_cast(info["client_app_id"]); + + if (m_cluster_name) { + app_name += "-" + *m_cluster_name; + } + m_app_id = static_cast(info["_id"]); + + auto app = apps[m_app_id]; + + static_cast(app["secrets"].post({{"name", "customTokenKey"}, + {"value", "My_very_confidential_secretttttt"}})); + + static_cast(app["auth_providers"].post({{"type", "custom-token"}, + {"config", bson::BsonDocument{ + {"audience", bson::BsonArray()}, + {"signingAlgorithm", "HS256"}, + {"useJWKURI", false}}}, + {"secret_config", bson::BsonDocument{{"signingKeys", bson::BsonArray{"customTokenKey"}}}}, + {"metadata_fields", bson::BsonArray{bson::BsonDocument{{"required", false}, {"name", "user_data.name"}, {"field_name", "name"}}, bson::BsonDocument{{"required", false}, {"name", "user_data.occupation"}, {"field_name", "occupation"}}, bson::BsonDocument{{"required", false}, {"name", "my_metadata.name"}, {"field_name", "anotherName"}}}}})); + + static_cast(app["auth_providers"].post({{"type", "anon-user"}})); + static_cast(app["auth_providers"].post({{"type", "local-userpass"}, + {"config", bson::BsonDocument{ + {"emailConfirmationUrl", "http://foo.com"}, + {"resetPasswordUrl", "http://foo.com"}, + {"confirmEmailSubject", "Hi"}, + {"resetPasswordSubject", "Bye"}, + {"autoConfirm", true}}}})); + + auto auth_providers_endpoint = app["auth_providers"]; + auto providers = static_cast(auth_providers_endpoint.get()); + auto api_key_provider = std::find_if(std::begin(providers), + std::end(providers), [](auto &provider) { + return static_cast(provider)["type"] == "api-key"; + }); + auth_providers_endpoint[static_cast(static_cast(*api_key_provider)["_id"])] + ["enable"] + .put(); + + bson::BsonDocument mongodb_service_config; + std::string mongodb_service_type; + if (m_cluster_name) { + mongodb_service_config["clusterName"] = *m_cluster_name; + mongodb_service_type = "mongodb-atlas"; + } else { + mongodb_service_config["uri"] = "mongodb://localhost:26000"; + mongodb_service_type = "mongodb"; + } + + static_cast(app["functions"].post({{"name", "updateUserData"}, + {"private", false}, + {"can_evaluate", {}}, + {"source", + R"( exports = async function(data) { const user = context.user; - const mongodb = context.services.get(")" + util::format("db-%1", app_name) + R"("); + const mongodb = context.services.get(")" + util::format("db-%1", app_name) + + R"("); const userDataCollection = mongodb.db("test_data").collection("UserData"); doc = await userDataCollection.updateOne( { "user_id": user.id }, @@ -183,362 +239,300 @@ std::string Admin::Session::create_app(bson::BsonArray queryable_fields, std::st ); return doc; }; - )" - }})); - - static_cast(app["functions"].post({ - {"name", "asymmetricSyncData"}, - {"private", false}, - {"can_evaluate", {}}, - {"source", - R"( + )"}})); + + static_cast(app["functions"].post({{"name", "asymmetricSyncData"}, + {"private", false}, + {"can_evaluate", {}}, + {"source", + R"( exports = async function(data) { const user = context.user; - const mongodb = context.services.get(")" + util::format("db-%1", app_name) + R"("); - const objectCollection = mongodb.db(")" + util::format("test_data") + R"(").collection("AllTypesAsymmetricObject"); + const mongodb = context.services.get(")" + + util::format("db-%1", app_name) + R"("); + const objectCollection = mongodb.db(")" + + util::format("test_data") + R"(").collection("AllTypesAsymmetricObject"); doc = await objectCollection.find({"_id": BSON.ObjectId(data._id)}); return doc; }; - )" - }})); - - static_cast(app["functions"].post({ - {"name", "deleteClientResetObjects"}, - {"private", false}, - {"can_evaluate", {}}, - {"source", - R"( + )"}})); + + static_cast(app["functions"].post({{"name", "deleteClientResetObjects"}, + {"private", false}, + {"can_evaluate", {}}, + {"source", + R"( exports = async function(data) { const user = context.user; - const mongodb = context.services.get(")" + util::format("db-%1", app_name) + R"("); - const objectCollection = mongodb.db(")" + util::format("test_data") + R"(").collection("client_reset_obj"); + const mongodb = context.services.get(")" + + util::format("db-%1", app_name) + R"("); + const objectCollection = mongodb.db(")" + + util::format("test_data") + R"(").collection("client_reset_obj"); doc = await objectCollection.deleteMany({}); return doc; }; - )" - }})); - - static_cast(app["functions"].post({ - {"name", "insertClientResetObject"}, - {"private", false}, - {"can_evaluate", {}}, - {"source", - R"( + )"}})); + + static_cast(app["functions"].post({{"name", "insertClientResetObject"}, + {"private", false}, + {"can_evaluate", {}}, + {"source", + R"( exports = async function(data) { const user = context.user; - const mongodb = context.services.get(")" + util::format("db-%1", app_name) + R"("); - const objectCollection = mongodb.db(")" + util::format("test_data") + R"(").collection("client_reset_obj"); + const mongodb = context.services.get(")" + + util::format("db-%1", app_name) + R"("); + const objectCollection = mongodb.db(")" + + util::format("test_data") + R"(").collection("client_reset_obj"); doc = await objectCollection.insertOne({ _id: 1, str_col: 'remote obj' }); return doc; }; - )" - }})); - - bson::BsonDocument userData = { - {"schema", bson::BsonDocument { - {"properties", bson::BsonDocument { - {"_id", bson::BsonDocument {{"bsonType", "objectId"}}}, - {"name", bson::BsonDocument {{"bsonType", "string"}}}, - {"user_id", bson::BsonDocument {{"bsonType", "string"}}}, - }}, - {"required", bson::BsonArray {"_id", "name", "user_id"}}, - {"title", "UserData"} - }}, - {"metadata", bson::BsonDocument { - {"data_source", util::format("db-%1", app_name)}, - {"database", "test_data"}, - {"collection", "UserData"}} - } - }; - - auto embeddedFooSchema = bson::BsonDocument { - {"title", "EmbeddedFoo"}, - {"bsonType", "object"}, - {"required", bson::BsonArray({"bar"})}, - {"properties", bson::BsonDocument{ - {"bar", bson::BsonDocument{{"bsonType", "long"}}}} - } - }; - - bson::BsonDocument asymmetricObject = { - {"schema", bson::BsonDocument { - {"properties", bson::BsonDocument { - {"_id", bson::BsonDocument {{"bsonType", "objectId"}}}, - {"bool_col", bson::BsonDocument {{"bsonType", "bool"}}}, - {"str_col", bson::BsonDocument {{"bsonType", "string"}}}, - {"enum_col", bson::BsonDocument {{"bsonType", "long"}}}, - {"date_col", bson::BsonDocument {{"bsonType", "date"}}}, - {"uuid_col", bson::BsonDocument {{"bsonType", "uuid"}}}, - {"binary_col", bson::BsonDocument {{"bsonType", "binData"}}}, - {"mixed_col", bson::BsonDocument {{"bsonType", "mixed"}}}, - {"opt_int_col", bson::BsonDocument {{"bsonType", "long"}}}, - {"opt_str_col", bson::BsonDocument {{"bsonType", "string"}}}, - {"opt_bool_col", bson::BsonDocument {{"bsonType", "bool"}}}, - {"opt_binary_col", bson::BsonDocument {{"bsonType", "binData"}}}, - {"opt_date_col", bson::BsonDocument {{"bsonType", "date"}}}, - {"opt_enum_col", bson::BsonDocument {{"bsonType", "long"}}}, - {"opt_embedded_obj_col", embeddedFooSchema}, - {"opt_uuid_col", bson::BsonDocument {{"bsonType", "uuid"}}}, - - {"list_int_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "long"}}}}}, - {"list_bool_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "bool"}}}}}, - {"list_str_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "string"}}}}}, - {"list_uuid_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "uuid"}}}}}, - {"list_binary_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "binData"}}}}}, - {"list_date_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "date"}}}}}, - {"list_mixed_col", bson::BsonDocument {{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "mixed"}}}}}, - {"list_embedded_obj_col", bson::BsonDocument {{"bsonType", "array"}, {"items", embeddedFooSchema}}}, - - {"map_int_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "long"}}}}}, - {"map_str_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "string"}}}}}, - {"map_bool_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "bool"}}}}}, - {"map_enum_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "long"}}}}}, - {"map_date_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "date"}}}}}, - {"map_uuid_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "uuid"}}}}}, - {"map_mixed_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "mixed"}}}}}, - {"map_embedded_col", bson::BsonDocument {{"bsonType", "object"}, {"additionalProperties", embeddedFooSchema}}}, - }}, - {"required", bson::BsonArray {"_id", "bool_col", "str_col", "enum_col", "date_col", "uuid_col", "binary_col"}}, - {"title", "AllTypesAsymmetricObject"} - }}, - {"metadata", bson::BsonDocument { - {"data_source", util::format("db-%1", app_name)}, - {"database", "test_data"}, - {"collection", "AllTypesAsymmetricObject"}} - } - }; - - if (is_asymmetric) - static_cast(app["schemas"].post(std::move(asymmetricObject))); - - bson::BsonDocument mongodb_service_response(app["services"].post({ - {"name", util::format("db-%1", app_name)}, - {"type", mongodb_service_type}, - {"config", mongodb_service_config} - })); - std::string mongodb_service_id(mongodb_service_response["_id"]); - m_service_id = mongodb_service_id; - - app["custom_user_data"].patch(bson::BsonDocument { - {"mongo_service_id", mongodb_service_id}, - {"enabled", true}, - {"database_name", "test_data"}, - {"collection_name", "UserData"}, - {"user_id_field", "user_id"}, - }); - - bson::BsonDocument user_data_rule = { - {"database", "test_data"}, - {"collection", "UserData"}, - {"roles", bson::BsonArray { - bson::BsonDocument {{"name", "default"}, - {"apply_when", {}}, - {"insert", true}, - {"delete", true}, - {"additional_fields", {}}, - {"document_filters", bson::BsonDocument({{"read", true}, {"write", true}})}, - {"read", true}, - {"write", true}, - {"insert", true}, - {"delete", true} - } - } - } - }; - - bson::BsonDocument asymmetric_object_rule = { - {"database", app_name}, - {"collection", "AllTypesAsymmetricObject"}, - {"roles", bson::BsonArray { - bson::BsonDocument{{"name", "default"}, - {"apply_when", {}}, - {"insert", true}, - {"delete", true}, - {"additional_fields", {}}, - {"document_filters", bson::BsonDocument({{"read", true}, {"write", true}})}, - {"read", true}, - {"write", true}, - {"insert", true}, - {"delete", true}} - } - } - }; - - static_cast(app["services"][static_cast(mongodb_service_id)]["rules"].post(user_data_rule)); - static_cast(app["services"][static_cast(mongodb_service_id)]["rules"].post(asymmetric_object_rule)); - - bson::BsonDocument service_config = { - {"flexible_sync", bson::BsonDocument { - {"type", "flexible"}, - {"state", "enabled"}, - {"database_name", "test_data"}, - {"enabled", true}, - {"is_recovery_mode_disabled", recovery_mode_disabled}, - {"queryable_fields_names", queryable_fields}, - {"asymmetric_tables", bson::BsonArray({"AllTypesAsymmetricObject"})} - - }} - }; - - bson::BsonDocument default_rule = {{"roles", bson::BsonArray ({bson::BsonDocument({ - {"name", "all"}, - {"apply_when", {}}, - {"document_filters", bson::BsonDocument({{"read", true}, {"write", true} }) }, - {"write", true}, - {"read", true}, - {"insert", true}, - {"delete", true} - }) - })}}; - - static_cast(app["services"][mongodb_service_id]["default_rule"].post(default_rule)); - - // The cluster linking must be separated from enabling sync because Atlas - // takes a few seconds to provision a user for BaaS, meaning enabling sync - // will fail if we attempt to do it with the same request. It's nondeterministic - // how long it'll take, so we must retry for a while. - constexpr int max_attempts = 120; - for (int attempt = 0; attempt <= max_attempts; attempt++) { - try { - app["services"][mongodb_service_id]["config"].patch(std::move(service_config)); - break; - } catch(std::exception&) { - if (attempt == max_attempts) { - REALM_TERMINATE("Could not link Atlas cluster"); - } else { - util::format(std::cerr, "Could not update MongoDB service after %1 seconds. Will keep retrying.\n", (attempt + 1) * 5); - std::this_thread::sleep_for(std::chrono::seconds(5)); + )"}})); + + bson::BsonDocument userData = { + {"schema", bson::BsonDocument{ + {"properties", bson::BsonDocument{ + {"_id", bson::BsonDocument{{"bsonType", "objectId"}}}, + {"name", bson::BsonDocument{{"bsonType", "string"}}}, + {"user_id", bson::BsonDocument{{"bsonType", "string"}}}, + }}, + {"required", bson::BsonArray{"_id", "name", "user_id"}}, + {"title", "UserData"}}}, + {"metadata", bson::BsonDocument{{"data_source", util::format("db-%1", app_name)}, {"database", "test_data"}, {"collection", "UserData"}}}}; + + auto embeddedFooSchema = bson::BsonDocument{ + {"title", "EmbeddedFoo"}, + {"bsonType", "object"}, + {"required", bson::BsonArray({"bar"})}, + {"properties", bson::BsonDocument{ + {"bar", bson::BsonDocument{{"bsonType", "long"}}}}}}; + + bson::BsonDocument asymmetricObject = { + {"schema", bson::BsonDocument{ + {"properties", bson::BsonDocument{ + {"_id", bson::BsonDocument{{"bsonType", "objectId"}}}, + {"bool_col", bson::BsonDocument{{"bsonType", "bool"}}}, + {"str_col", bson::BsonDocument{{"bsonType", "string"}}}, + {"enum_col", bson::BsonDocument{{"bsonType", "long"}}}, + {"date_col", bson::BsonDocument{{"bsonType", "date"}}}, + {"uuid_col", bson::BsonDocument{{"bsonType", "uuid"}}}, + {"binary_col", bson::BsonDocument{{"bsonType", "binData"}}}, + {"mixed_col", bson::BsonDocument{{"bsonType", "mixed"}}}, + {"opt_int_col", bson::BsonDocument{{"bsonType", "long"}}}, + {"opt_str_col", bson::BsonDocument{{"bsonType", "string"}}}, + {"opt_bool_col", bson::BsonDocument{{"bsonType", "bool"}}}, + {"opt_binary_col", bson::BsonDocument{{"bsonType", "binData"}}}, + {"opt_date_col", bson::BsonDocument{{"bsonType", "date"}}}, + {"opt_enum_col", bson::BsonDocument{{"bsonType", "long"}}}, + {"opt_embedded_obj_col", embeddedFooSchema}, + {"opt_uuid_col", bson::BsonDocument{{"bsonType", "uuid"}}}, + + {"list_int_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "long"}}}}}, + {"list_bool_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "bool"}}}}}, + {"list_str_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "string"}}}}}, + {"list_uuid_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "uuid"}}}}}, + {"list_binary_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "binData"}}}}}, + {"list_date_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "date"}}}}}, + {"list_mixed_col", bson::BsonDocument{{"bsonType", "array"}, {"items", bson::BsonDocument{{"bsonType", "mixed"}}}}}, + {"list_embedded_obj_col", bson::BsonDocument{{"bsonType", "array"}, {"items", embeddedFooSchema}}}, + + {"map_int_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "long"}}}}}, + {"map_str_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "string"}}}}}, + {"map_bool_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "bool"}}}}}, + {"map_enum_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "long"}}}}}, + {"map_date_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "date"}}}}}, + {"map_uuid_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "uuid"}}}}}, + {"map_mixed_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", bson::BsonDocument{{"bsonType", "mixed"}}}}}, + {"map_embedded_col", bson::BsonDocument{{"bsonType", "object"}, {"additionalProperties", embeddedFooSchema}}}, + }}, + {"required", bson::BsonArray{"_id", "bool_col", "str_col", "enum_col", "date_col", "uuid_col", "binary_col"}}, + {"title", "AllTypesAsymmetricObject"}}}, + {"metadata", bson::BsonDocument{{"data_source", util::format("db-%1", app_name)}, {"database", "test_data"}, {"collection", "AllTypesAsymmetricObject"}}}}; + + if (is_asymmetric) + static_cast(app["schemas"].post(std::move(asymmetricObject))); + + bson::BsonDocument mongodb_service_response(app["services"].post({{"name", util::format("db-%1", app_name)}, + {"type", mongodb_service_type}, + {"config", mongodb_service_config}})); + std::string mongodb_service_id(mongodb_service_response["_id"]); + m_service_id = mongodb_service_id; + + app["custom_user_data"].patch(bson::BsonDocument{ + {"mongo_service_id", mongodb_service_id}, + {"enabled", true}, + {"database_name", "test_data"}, + {"collection_name", "UserData"}, + {"user_id_field", "user_id"}, + }); + + bson::BsonDocument user_data_rule = { + {"database", "test_data"}, + {"collection", "UserData"}, + {"roles", bson::BsonArray{ + bson::BsonDocument{{"name", "default"}, + {"apply_when", {}}, + {"insert", true}, + {"delete", true}, + {"additional_fields", {}}, + {"document_filters", bson::BsonDocument({{"read", true}, {"write", true}})}, + {"read", true}, + {"write", true}, + {"insert", true}, + {"delete", true}}}}}; + + bson::BsonDocument asymmetric_object_rule = { + {"database", app_name}, + {"collection", "AllTypesAsymmetricObject"}, + {"roles", bson::BsonArray{ + bson::BsonDocument{{"name", "default"}, + {"apply_when", {}}, + {"insert", true}, + {"delete", true}, + {"additional_fields", {}}, + {"document_filters", bson::BsonDocument({{"read", true}, {"write", true}})}, + {"read", true}, + {"write", true}, + {"insert", true}, + {"delete", true}}}}}; + + static_cast(app["services"][static_cast(mongodb_service_id)]["rules"].post(user_data_rule)); + static_cast(app["services"][static_cast(mongodb_service_id)]["rules"].post(asymmetric_object_rule)); + + bson::BsonDocument service_config = { + {"flexible_sync", bson::BsonDocument{ + {"type", "flexible"}, + {"state", "enabled"}, + {"database_name", "test_data"}, + {"enabled", true}, + {"is_recovery_mode_disabled", recovery_mode_disabled}, + {"queryable_fields_names", queryable_fields}, + {"asymmetric_tables", bson::BsonArray({"AllTypesAsymmetricObject"})} + + }}}; + + bson::BsonDocument default_rule = {{"roles", bson::BsonArray({bson::BsonDocument({{"name", "all"}, + {"apply_when", {}}, + {"document_filters", bson::BsonDocument({{"read", true}, {"write", true}})}, + {"write", true}, + {"read", true}, + {"insert", true}, + {"delete", true}})})}}; + + static_cast(app["services"][mongodb_service_id]["default_rule"].post(default_rule)); + + // The cluster linking must be separated from enabling sync because Atlas + // takes a few seconds to provision a user for BaaS, meaning enabling sync + // will fail if we attempt to do it with the same request. It's nondeterministic + // how long it'll take, so we must retry for a while. + constexpr int max_attempts = 120; + for (int attempt = 0; attempt <= max_attempts; attempt++) { + try { + app["services"][mongodb_service_id]["config"].patch(std::move(service_config)); + break; + } catch (std::exception &) { + if (attempt == max_attempts) { + REALM_TERMINATE("Could not link Atlas cluster"); + } else { + util::format(std::cerr, "Could not update MongoDB service after %1 seconds. Will keep retrying.\n", (attempt + 1) * 5); + std::this_thread::sleep_for(std::chrono::seconds(5)); + } } } - } - app["sync"]["config"].put({{"development_mode_enabled", true}}); - - return *static_cast>(info["client_app_id"]); -} - -void Admin::Session::cache_app_id(const std::string& app_id) { - m_cached_app_id = app_id; -} - - -[[nodiscard]] std::string Admin::Session::cached_app_id() const { - REALM_ASSERT(m_cached_app_id); - return *m_cached_app_id; -} - -Admin::Session Admin::Session::local(std::optional baas_url) -{ - std::string base_url = baas_url.value_or("http://localhost:9090"); - - bson::BsonDocument credentials { - {"username", "unique_user@domain.com"}, - {"password", "password"} - }; - std::pair tokens = authenticate(base_url, "local-userpass", std::move(credentials)); - - app::Request request; - request.url = base_url + "/api/admin/v3.0/auth/profile"; - request.headers = { - {"Authorization", "Bearer " + tokens.first}}; - - auto result = do_http_request(std::move(request)); - auto parsed_response = static_cast(bson::parse(result.body)); - auto roles = static_cast(parsed_response["roles"]); - auto group_id = static_cast(static_cast(roles[0])["group_id"]); - - return Session(base_url, tokens.first, tokens.second, group_id, std::nullopt); -} - -Admin::Session Admin::Session::atlas(const std::string& baas_url, std::string project_id, std::string cluster_name, std::string api_key, std::string private_api_key) -{ - bson::BsonDocument credentials { - {"username", std::move(api_key)}, - {"apiKey", std::move(private_api_key)} - }; - std::pair tokens = authenticate(baas_url, "mongodb-cloud", std::move(credentials)); - - return Session(baas_url, tokens.first, tokens.second, std::move(project_id), std::move(cluster_name)); -} - -void Admin::Session::enable_sync() { - bson::BsonDocument service_config = { - {"flexible_sync", bson::BsonDocument { - {"state", "enabled"}, - {"is_recovery_mode_disabled", recovery_mode_disabled}, - {"enabled", true}}} - }; - - apps[m_app_id]["services"][m_service_id]["config"].patch(std::move(service_config)); - for (int i = 0; i < 5; ++i) { - auto c = apps[m_app_id]["services"][m_service_id]["config"].get(); - if (c.to_string().find("enabled") != std::string::npos) { - return; - } - std::this_thread::sleep_for(std::chrono::seconds(2)); - } - std::runtime_error("Sync could not be enabled in time."); -} -void Admin::Session::disable_sync() { - bson::BsonDocument service_config = { - {"flexible_sync", bson::BsonDocument { - {"state", "disabled"}, - {"enabled", false}}} - }; - - apps[m_app_id]["services"][m_service_id]["config"].patch(std::move(service_config)); - for (int i = 0; i < 5; ++i) { - auto c = apps[m_app_id]["services"][m_service_id]["config"].get(); - if (c.to_string().find("disabled") != std::string::npos) { - return; - } - std::this_thread::sleep_for(std::chrono::seconds(2)); + app["sync"]["config"].put({{"development_mode_enabled", true}}); + + return *static_cast>(info["client_app_id"]); } - std::runtime_error("Sync could not be disabled in time."); -} -static Admin::Session make_default_session() { - if (const char* baas_endpoint = getenv("REALM_BAAS_ENDPOINT")) { - const char* project_id = getenv("REALM_ATLAS_PROJECT_ID"); - REALM_ASSERT_RELEASE(project_id); + void Admin::Session::cache_app_id(const std::string &app_id) { + m_cached_app_id = app_id; + } - const char* cluster_name = getenv("REALM_ATLAS_CLUSTER_NAME"); - REALM_ASSERT_RELEASE(cluster_name); - const char* api_key = getenv("REALM_ATLAS_API_KEY"); - REALM_ASSERT_RELEASE(api_key); + [[nodiscard]] std::string Admin::Session::cached_app_id() const { + REALM_ASSERT(m_cached_app_id); + return *m_cached_app_id; + } - const char* private_api_key = getenv("REALM_ATLAS_PRIVATE_API_KEY"); - REALM_ASSERT_RELEASE(private_api_key); + void Admin::Session::prepare(const std::optional &baas_url) { + if (baas_url) + m_base_url = *baas_url; + else + m_base_url = "localhost:9090"; + bson::BsonDocument credentials{ + {"username", "unique_user@domain.com"}, + {"password", "password"}}; + std::pair tokens = authenticate(m_base_url, "local-userpass", std::move(credentials)); + + app::Request request; + request.url = m_base_url + "/api/admin/v3.0/auth/profile"; + request.headers = { + {"Authorization", "Bearer " + tokens.first}}; + + auto result = do_http_request(std::move(request)); + auto parsed_response = static_cast(bson::parse(result.body)); + auto roles = static_cast(parsed_response["roles"]); + auto group_id = static_cast(static_cast(roles[0])["group_id"]); + + m_access_token = tokens.first; + m_refresh_token = tokens.second; + m_group_id = group_id; + + group = Endpoint(util::format("%1/api/admin/v3.0/groups/%2", baas_url, group_id), m_access_token); + apps = group["apps"]; + } - return Admin::Session::atlas(baas_endpoint, project_id, cluster_name, api_key, private_api_key); - } else if (const char* local_endpoint = getenv("REALM_LOCAL_ENDPOINT")) { - return Admin::Session::local(local_endpoint); - } else { - return Admin::Session::local(); + void Admin::Session::enable_sync() { + bson::BsonDocument service_config = { + {"flexible_sync", bson::BsonDocument{ + {"state", "enabled"}, + {"is_recovery_mode_disabled", recovery_mode_disabled}, + {"enabled", true}}}}; + + apps[m_app_id]["services"][m_service_id]["config"].patch(std::move(service_config)); + for (int i = 0; i < 5; ++i) { + auto c = apps[m_app_id]["services"][m_service_id]["config"].get(); + if (c.to_string().find("enabled") != std::string::npos) { + return; + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + std::runtime_error("Sync could not be enabled in time."); + } + void Admin::Session::disable_sync() { + bson::BsonDocument service_config = { + {"flexible_sync", bson::BsonDocument{ + {"state", "disabled"}, + {"enabled", false}}}}; + + apps[m_app_id]["services"][m_service_id]["config"].patch(std::move(service_config)); + for (int i = 0; i < 5; ++i) { + auto c = apps[m_app_id]["services"][m_service_id]["config"].get(); + if (c.to_string().find("disabled") != std::string::npos) { + return; + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + std::runtime_error("Sync could not be disabled in time."); } -} - -void Admin::Session::refresh_access_token() { - std::stringstream body; - auto request = app::Request(); - request.method = realm::app::HttpMethod::post; - request.url = util::format("%1/api/admin/v3.0/auth/session", m_base_url); - request.headers = { - {"Content-Type", "application/json;charset=utf-8"}, - {"Accept", "application/json"}, - {"Authorization", util::format("Bearer %1", m_refresh_token)}}; - request.body = body.str(); - - auto result = do_http_request(std::move(request)); - if (result.http_status_code >= 400) { - REALM_TERMINATE("Unable to refresh access token"); + + void Admin::Session::refresh_access_token() { + std::stringstream body; + auto request = app::Request(); + request.method = realm::app::HttpMethod::post; + request.url = util::format("%1/api/admin/v3.0/auth/session", m_base_url); + request.headers = { + {"Content-Type", "application/json;charset=utf-8"}, + {"Accept", "application/json"}, + {"Authorization", util::format("Bearer %1", m_refresh_token)}}; + request.body = body.str(); + + auto result = do_http_request(std::move(request)); + if (result.http_status_code >= 400) { + REALM_TERMINATE("Unable to refresh access token"); + } + auto parsed_response = static_cast(bson::parse(result.body)); + m_access_token = static_cast(parsed_response["access_token"]); } - auto parsed_response = static_cast(bson::parse(result.body)); - m_access_token = static_cast(parsed_response["access_token"]); -} - -Admin::Session& Admin::shared() { - static Admin::Session session(make_default_session()); - return session; -} +} \ No newline at end of file diff --git a/tests/admin_utils.hpp b/tests/admin_utils.hpp index 5b54dd3e9..29f109b0b 100644 --- a/tests/admin_utils.hpp +++ b/tests/admin_utils.hpp @@ -21,80 +21,119 @@ #include #include +#include + namespace Admin { -using namespace realm; - -struct Endpoint { -public: - Endpoint(std::string url, std::string access_token) - : m_url(std::move(url)) - , m_access_token(std::move(access_token)) - { } - - app::Response request(app::HttpMethod method, bson::BsonDocument&& body = {}) const; - app::Response request(app::HttpMethod method, const std::string& body) const; - - [[nodiscard]] bson::Bson get() const { - return bson::parse(request(app::HttpMethod::get).body); - } - - [[nodiscard]] bson::Bson post(bson::BsonDocument body) const { - return bson::parse(request(app::HttpMethod::post, std::move(body)).body); - } - - bson::Bson put(bson::BsonDocument&& body = {}) const { - auto response = request(app::HttpMethod::put, std::move(body)); - if (response.body.empty()) { - return {}; + using namespace realm; + + struct baas_manager { + explicit baas_manager(const std::optional& baasaas_api_key = std::nullopt) + : m_baasaas_api_key(baasaas_api_key) { + if (!m_baasaas_api_key) { + m_baasaas_api_key = getenv("BAAS_APIKEY"); + } + } + ~baas_manager() { + if (m_container_id) + stop(*m_container_id); + } + + // returns container id, subsequent calls will return + // the original container id until stop is called. + void start(); + // Stops a given container and nulls the current container_id; + void stop(const std::string& conainter_id) const; + // Polls the current container and returns the url once available. + const std::string wait_for_container() const; + const std::string container_id() const; + private: + std::optional m_container_id; + std::optional m_baasaas_api_key; + const std::string m_baas_base_url = "https://us-east-1.aws.data.mongodb-api.com/app/baas-container-service-autzb/endpoint"; + }; + + struct Endpoint { + public: + Endpoint() = default; + Endpoint(std::string url, std::string access_token) + : m_url(std::move(url)) + , m_access_token(std::move(access_token)) + { } + + app::Response request(app::HttpMethod method, bson::BsonDocument&& body = {}) const; + app::Response request(app::HttpMethod method, const std::string& body) const; + + [[nodiscard]] bson::Bson get() const { + return bson::parse(request(app::HttpMethod::get).body); + } + + [[nodiscard]] bson::Bson post(bson::BsonDocument body) const { + return bson::parse(request(app::HttpMethod::post, std::move(body)).body); + } + + bson::Bson put(bson::BsonDocument&& body = {}) const { + auto response = request(app::HttpMethod::put, std::move(body)); + if (response.body.empty()) { + return {}; + } + return bson::parse(response.body); + } + bson::Bson patch(bson::BsonDocument&& body = {}) const { + auto response = request(app::HttpMethod::patch, std::move(body)); + if (response.body.empty()) { + return {}; + } + return bson::parse(response.body); + } + Endpoint operator[](const std::string& url) const { + return {m_url + "/" + url, m_access_token}; } - return bson::parse(response.body); - } - bson::Bson patch(bson::BsonDocument&& body = {}) const { - auto response = request(app::HttpMethod::patch, std::move(body)); - if (response.body.empty()) { - return {}; + private: + std::string m_access_token; + std::string m_url; + }; + + struct Session { + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + + Endpoint group; + Endpoint apps; + + static Session* instance; + static std::mutex mutex; + + void prepare(const std::optional& baas_url = std::nullopt); + + const std::string& base_url() const { return m_base_url; } + + std::string create_app(bson::BsonArray queryable_fields = {}, std::string name = "test", bool is_asymmetric = false, bool disable_recovery_mode = true); + [[nodiscard]] std::string cached_app_id() const; + void cache_app_id(const std::string& app_id); + void enable_sync(); + void disable_sync(); + void refresh_access_token(); + + static Session& shared() { + std::lock_guard lock(mutex); + if (instance == nullptr) { + instance = new Session(); + } + return *instance; } - return bson::parse(response.body); - } - Endpoint operator[](const std::string& url) const { - return {m_url + "/" + url, m_access_token}; - } -private: - const std::string m_access_token; - const std::string m_url; -}; - -struct Session { - const Endpoint group; - const Endpoint apps; - - static Session local(std::optional baas_url = std::nullopt); - - static Session atlas(const std::string& baas_url, std::string project_id, std::string cluster_name, std::string api_key, std::string private_api_key); - - const std::string& base_url() const { return m_base_url; } - - std::string create_app(bson::BsonArray queryable_fields = {}, std::string name = "test", bool is_asymmetric = false, bool disable_recovery_mode = true); - [[nodiscard]] std::string cached_app_id() const; - void cache_app_id(const std::string& app_id); - void enable_sync(); - void disable_sync(); - void refresh_access_token(); -private: - const std::string m_base_url; - std::string m_access_token; - std::string m_refresh_token; - const std::string m_group_id; - const std::optional m_cluster_name; - std::optional m_cached_app_id; // client app id - std::string m_service_id; - std::string m_app_id; - bool recovery_mode_disabled = false; - - Session(const std::string& baas_url, const std::string& access_token, const std::string& refresh_token, const std::string& group_id, std::optional cluster_name = std::nullopt); -}; - -Session& shared(); + private: + Session() = default; + + std::string m_base_url; + std::string m_access_token; + std::string m_refresh_token; + std::string m_group_id; + std::optional m_cluster_name; + std::optional m_cached_app_id; // client app id + std::string m_service_id; + std::string m_app_id; + bool recovery_mode_disabled = false; + }; } // namespace Admin diff --git a/tests/main.cpp b/tests/main.cpp index 9fc7db1e3..e5a75f6ea 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -41,7 +41,19 @@ int main(int argc, char *argv[]) { } #ifdef CPPREALM_ENABLE_SYNC_TESTS - Admin::shared().cache_app_id(Admin::shared().create_app({"str_col", "_id"})); + std::string baas_api_key = getenv("APIKEY"); + std::optional baas_manager; + if (!baas_api_key.empty()) { + baas_manager.emplace(baas_api_key); + baas_manager->start(); + auto url = baas_manager->wait_for_container(); + Admin::Session::shared().prepare(url); + } else { + Admin::Session::shared().prepare(); + } + + auto app_id = Admin::Session::shared().create_app({"str_col", "_id"}); + Admin::Session::shared().cache_app_id(app_id); #endif Catch::Session session; diff --git a/tests/sync/app_tests.cpp b/tests/sync/app_tests.cpp index 079d5b1a7..8ae92b920 100644 --- a/tests/sync/app_tests.cpp +++ b/tests/sync/app_tests.cpp @@ -70,7 +70,7 @@ static std::string create_jwt(const std::string& appId) using namespace realm; TEST_CASE("app", "[app]") { - auto app = realm::App(realm::App::configuration({Admin::shared().cached_app_id(), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().cached_app_id(), Admin::Session::shared().base_url()})); SECTION("get_current_user") { auto user = app.login(realm::App::credentials::anonymous()).get(); @@ -94,17 +94,17 @@ TEST_CASE("app", "[app]") { } SECTION("clear_cached_apps") { - auto temp_app_id = Admin::shared().create_app({"str_col", "_id"}); - auto temp_app = realm::App(realm::App::configuration({temp_app_id, Admin::shared().base_url()})); - auto cached_app = temp_app.get_cached_app(temp_app_id, Admin::shared().base_url()); + auto temp_app_id = Admin::Session::shared().create_app({"str_col", "_id"}); + auto temp_app = realm::App(realm::App::configuration({temp_app_id, Admin::Session::shared().base_url()})); + auto cached_app = temp_app.get_cached_app(temp_app_id, Admin::Session::shared().base_url()); CHECK(cached_app.has_value()); app.clear_cached_apps(); - cached_app = temp_app.get_cached_app(temp_app_id, Admin::shared().base_url()); + cached_app = temp_app.get_cached_app(temp_app_id, Admin::Session::shared().base_url()); CHECK(cached_app.has_value() == false); } SECTION("error handling") { - auto dead_app = realm::App(realm::App::configuration({"NA", Admin::shared().base_url()})); + auto dead_app = realm::App(realm::App::configuration({"NA", Admin::Session::shared().base_url()})); REQUIRE_THROWS_AS(dead_app.login(realm::App::credentials::anonymous()).get(), realm::app_error); REQUIRE_THROWS_AS(dead_app.register_user("", "").get(), realm::app_error); @@ -130,7 +130,7 @@ TEST_CASE("app", "[app]") { app.register_user("auth_providers_promise@mongodb.com", "foobar").get(); run_login(realm::App::credentials::username_password("auth_providers_promise@mongodb.com", "foobar")); run_login(realm::App::credentials::anonymous()); - run_login(realm::App::credentials::custom(create_jwt(Admin::shared().cached_app_id()))); + run_login(realm::App::credentials::custom(create_jwt(Admin::Session::shared().cached_app_id()))); } SECTION("auth_providers_completion_hander") { diff --git a/tests/sync/asymmetric_object_tests.cpp b/tests/sync/asymmetric_object_tests.cpp index 6127c9be2..9b1754fc7 100644 --- a/tests/sync/asymmetric_object_tests.cpp +++ b/tests/sync/asymmetric_object_tests.cpp @@ -6,8 +6,8 @@ using namespace realm; TEST_CASE("asymmetric object", "[sync]") { SECTION("basic", "[sync]") { - auto asymmetric_app_id = Admin::shared().create_app({}, "test", true); - auto app = realm::App(realm::App::configuration({asymmetric_app_id, Admin::shared().base_url()})); + auto asymmetric_app_id = Admin::Session::shared().create_app({}, "test", true); + auto app = realm::App(realm::App::configuration({asymmetric_app_id, Admin::Session::shared().base_url()})); auto user = app.login(realm::App::credentials::anonymous()).get(); auto synced_realm = open(user.flexible_sync_configuration()); diff --git a/tests/sync/client_reset_tests.cpp b/tests/sync/client_reset_tests.cpp index a05b9ac94..4276c4897 100644 --- a/tests/sync/client_reset_tests.cpp +++ b/tests/sync/client_reset_tests.cpp @@ -36,7 +36,7 @@ void prepare_realm(const realm::db_config& flx_sync_config, const user& sync_use auto session = synced_realm.get_sync_session()->operator std::weak_ptr<::realm::SyncSession>().lock().get(); session->pause(); - Admin::shared().disable_sync(); + Admin::Session::shared().disable_sync(); // Add an object to the local realm that won't be synced due to the suspend synced_realm.write([&synced_realm]() { client_reset_obj o; @@ -45,13 +45,13 @@ void prepare_realm(const realm::db_config& flx_sync_config, const user& sync_use synced_realm.add(std::move(o)); }); synced_realm.refresh(); - Admin::shared().enable_sync(); + Admin::Session::shared().enable_sync(); }; TEST_CASE("client_reset", "[sync]") { SECTION("error handler") { - auto app = realm::App(realm::App::configuration({Admin::shared().create_app({"str_col", "_id"}), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().create_app({"str_col", "_id"}), Admin::Session::shared().base_url()})); app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); auto flx_sync_config = user.flexible_sync_configuration(); @@ -74,7 +74,7 @@ TEST_CASE("client_reset", "[sync]") { } SECTION("manual handler") { - auto app = realm::App(realm::App::configuration({Admin::shared().create_app({"_id", "str_col"}, "test", false, false), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().create_app({"_id", "str_col"}, "test", false, false), Admin::Session::shared().base_url()})); app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); auto flx_sync_config = user.flexible_sync_configuration(); @@ -108,21 +108,21 @@ TEST_CASE("client_reset", "[sync]") { auto session = synced_realm.get_sync_session()->operator std::weak_ptr<::realm::SyncSession>().lock().get(); session->pause(); - Admin::shared().disable_sync(); + Admin::Session::shared().disable_sync(); synced_realm.write([&synced_realm]() { client_reset_obj o; o._id = 2; o.str_col = "local only"; synced_realm.add(std::move(o)); }); - Admin::shared().enable_sync(); + Admin::Session::shared().enable_sync(); session->resume(); CHECK(flx_sync_config.get_client_reset_mode() == realm::client_reset_mode::manual); CHECK(f.wait_for(std::chrono::milliseconds(60000)) == std::future_status::ready); } SECTION("discard_unsynced_changes") { - auto app = realm::App(realm::App::configuration({Admin::shared().create_app({"str_col", "_id"}, "test", false, false), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().create_app({"str_col", "_id"}, "test", false, false), Admin::Session::shared().base_url()})); app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); @@ -165,7 +165,7 @@ TEST_CASE("client_reset", "[sync]") { } SECTION("recover_or_discard_unsynced_changes") { - auto app = realm::App(realm::App::configuration({Admin::shared().create_app({"str_col", "_id"}, "test", false, false), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().create_app({"str_col", "_id"}, "test", false, false), Admin::Session::shared().base_url()})); app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); auto flx_sync_config = user.flexible_sync_configuration(); @@ -206,7 +206,7 @@ TEST_CASE("client_reset", "[sync]") { } SECTION("recover_unsynced_changes") { - auto app = realm::App(realm::App::configuration({Admin::shared().create_app({"str_col", "_id"}, "test", false, false), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().create_app({"str_col", "_id"}, "test", false, false), Admin::Session::shared().base_url()})); app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); auto flx_sync_config = user.flexible_sync_configuration(); @@ -247,7 +247,7 @@ TEST_CASE("client_reset", "[sync]") { } SECTION("recover_unsynced_changes_with_failure") { - auto app = realm::App(realm::App::configuration({Admin::shared().create_app({"str_col", "_id"}, "test", false, true), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().create_app({"str_col", "_id"}, "test", false, true), Admin::Session::shared().base_url()})); app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); auto flx_sync_config = user.flexible_sync_configuration(); diff --git a/tests/sync/flexible_sync_tests.cpp b/tests/sync/flexible_sync_tests.cpp index e7315ac9b..0daa34eab 100644 --- a/tests/sync/flexible_sync_tests.cpp +++ b/tests/sync/flexible_sync_tests.cpp @@ -5,7 +5,7 @@ using namespace realm; TEST_CASE("flexible_sync", "[sync]") { - auto app = realm::App(realm::App::configuration({Admin::shared().cached_app_id(), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().cached_app_id(), Admin::Session::shared().base_url()})); SECTION("all") { app.get_sync_manager().set_log_level(logger::level::all); auto user = app.login(realm::App::credentials::anonymous()).get(); @@ -81,8 +81,8 @@ TEST_CASE("flexible_sync", "[sync]") { SECTION("encrypted sync realm") { std::array example_key = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0}; realm::App::configuration app_config; - app_config.app_id = Admin::shared().create_app({"str_col", "_id"}); - app_config.base_url = Admin::shared().base_url(); + app_config.app_id = Admin::Session::shared().create_app({"str_col", "_id"}); + app_config.base_url = Admin::Session::shared().base_url(); app_config.metadata_encryption_key = example_key; auto encrypted_app = realm::App(app_config); auto user = encrypted_app.login(realm::App::credentials::anonymous()).get(); @@ -109,7 +109,7 @@ void test_set(realm::managed* property, Func f, } TEST_CASE("set collection sync", "[set]") { - auto app = realm::App(realm::App::configuration({Admin::shared().cached_app_id(), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().cached_app_id(), Admin::Session::shared().base_url()})); SECTION("insert") { auto user = app.login(realm::App::credentials::anonymous()).get(); auto flx_sync_config = user.flexible_sync_configuration(); @@ -186,7 +186,7 @@ TEST_CASE("set collection sync", "[set]") { } TEST_CASE("pause_resume_sync", "[sync]") { - auto app = realm::App(realm::App::configuration({Admin::shared().cached_app_id(), Admin::shared().base_url()})); + auto app = realm::App(realm::App::configuration({Admin::Session::shared().cached_app_id(), Admin::Session::shared().base_url()})); SECTION("pause_resume") { auto user = app.login(realm::App::credentials::anonymous()).get();