• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2503

19 Jul 2024 12:39AM UTC coverage: 90.996%. Remained the same
2503

push

Evergreen

web-flow
Merge pull request #7897 from realm/feature/role-change

* RCORE-1872 Sync client should allow server bootstrapping at any time (#7440)
* First round of changes for server-initiated bootstraps
* Added test for role change bootstraps
* Updated test for handle role bootstraps
* Updated baas/baasaas to use branch with fixes
* Updated test to verify bootstrap actually occurred
* Fixed tsan warning
* Updates from review; added comments to clarify bootstrap detection logic
* Reverted baas branch to master and protocol version to 12
* Added comments to changes needed when merging to master; update baas version to not use master
* Pulled over changes from other branch and tweaking download params
* Refactored tests to validate different bootstrap types
* Updated tests to get passing using the server params
* Updated to support new batch_state protocol changes; updated tests
* Updated role change tests and merged test from separate PR
* Fixed issue with flx query verion 0 not being treated as a bootstrap
* Cleaned up the tests a bit and reworked query version 0 handling
* Updates from review; updated batch_state for schema bootstraps
* Removed extra mutex in favor of state machine's mutex
* Increased timeout when waiting for app initial sync to complete
* Updated role change test to use test commands
* Update resume and ident message handling
* Updated future waits for the pause/resume test command
* Added session connected event for when session multiplexing is disabled
* Added wait_until() to state machine to wait for callback; updated role change test

* RCORE-1973 Add role/permissions tests for new bootstrap feature (#7675)
* Moved role change tests to separate test file
* Fixed building of new flx_role_change.cpp file
* Added local changes w/role bootstrap test - fixed exception in subscription store during server initiated boostrap
* Updated local change test to include valid offline writes during role change
* Added role c... (continued)

102634 of 181458 branches covered (56.56%)

920 of 998 new or added lines in 12 files covered. (92.18%)

54 existing lines in 11 files now uncovered.

216311 of 237715 relevant lines covered (91.0%)

5880250.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.37
/test/object-store/util/sync/baas_admin_api.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2021 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <util/sync/baas_admin_api.hpp>
20

21
#include <realm/object-store/sync/app_credentials.hpp>
22

23
#include <external/mpark/variant.hpp>
24

25
#if REALM_ENABLE_AUTH_TESTS
26

27
#include <realm/exceptions.hpp>
28
#include <realm/object_id.hpp>
29

30
#include <realm/util/scope_exit.hpp>
31

32
#include <catch2/catch_all.hpp>
33
#include <curl/curl.h>
34

35
#include <iostream>
36
#include <mutex>
37

38
namespace realm {
39
namespace {
40
std::string property_type_to_bson_type_str(PropertyType type)
41
{
5,809✔
42
    switch (type & ~PropertyType::Flags) {
5,809✔
43
        case PropertyType::UUID:
28✔
44
            return "uuid";
28✔
45
        case PropertyType::Mixed:
190✔
46
            return "mixed";
190✔
47
        case PropertyType::Bool:
✔
48
            return "bool";
×
49
        case PropertyType::Data:
2✔
50
            return "binData";
2✔
51
        case PropertyType::Date:
15✔
52
            return "date";
15✔
53
        case PropertyType::Decimal:
2✔
54
            return "decimal";
2✔
55
        case PropertyType::Double:
8✔
56
            return "double";
8✔
57
        case PropertyType::Float:
✔
58
            return "float";
×
59
        case PropertyType::Int:
1,133✔
60
            return "long";
1,133✔
61
        case PropertyType::Object:
✔
62
            return "object";
×
63
        case PropertyType::ObjectId:
2,028✔
64
            return "objectId";
2,028✔
65
        case PropertyType::String:
2,403✔
66
            return "string";
2,403✔
67
        case PropertyType::LinkingObjects:
✔
68
            return "linkingObjects";
×
69
        default:
✔
70
            REALM_COMPILER_HINT_UNREACHABLE();
×
71
    }
5,809✔
72
}
5,809✔
73

74
class BaasRuleBuilder {
75
public:
76
    using IncludePropCond = util::UniqueFunction<bool(const Property&)>;
77
    BaasRuleBuilder(const Schema& schema, const Property& partition_key, const std::string& service_name,
78
                    const std::string& db_name, bool is_flx_sync)
79
        : m_schema(schema)
204✔
80
        , m_partition_key(partition_key)
204✔
81
        , m_mongo_service_name(service_name)
204✔
82
        , m_mongo_db_name(db_name)
204✔
83
        , m_is_flx_sync(is_flx_sync)
204✔
84
    {
425✔
85
    }
425✔
86

87
    nlohmann::json property_to_jsonschema(const Property& prop);
88
    nlohmann::json object_schema_to_jsonschema(const ObjectSchema& obj_schema, const IncludePropCond& include_prop,
89
                                               bool clear_path = false);
90
    nlohmann::json object_schema_to_baas_schema(const ObjectSchema& obj_schema, IncludePropCond include_prop);
91

92
private:
93
    const Schema& m_schema;
94
    const Property& m_partition_key;
95
    const std::string& m_mongo_service_name;
96
    const std::string& m_mongo_db_name;
97
    const bool m_is_flx_sync;
98
    nlohmann::json m_relationships;
99
    std::vector<std::string> m_current_path;
100
};
101

102
nlohmann::json BaasRuleBuilder::object_schema_to_jsonschema(const ObjectSchema& obj_schema,
103
                                                            const IncludePropCond& include_prop, bool clear_path)
104
{
2,028✔
105
    nlohmann::json required = nlohmann::json::array();
2,028✔
106
    nlohmann::json properties = nlohmann::json::object();
2,028✔
107
    for (const auto& prop : obj_schema.persisted_properties) {
7,144✔
108
        if (include_prop && !include_prop(prop)) {
7,144✔
109
            continue;
1,553✔
110
        }
1,553✔
111
        if (clear_path) {
5,591✔
112
            m_current_path.clear();
5,473✔
113
        }
5,473✔
114
        properties.emplace(prop.name, property_to_jsonschema(prop));
5,591✔
115
        if (!is_nullable(prop.type) && !is_collection(prop.type)) {
5,591✔
116
            required.push_back(prop.name);
2,491✔
117
        }
2,491✔
118
    }
5,591✔
119

120
    return {
2,028✔
121
        {"properties", properties},
2,028✔
122
        {"required", required},
2,028✔
123
        {"title", obj_schema.name},
2,028✔
124
    };
2,028✔
125
}
2,028✔
126

127
nlohmann::json BaasRuleBuilder::property_to_jsonschema(const Property& prop)
128
{
5,735✔
129
    nlohmann::json type_output;
5,735✔
130

131
    if ((prop.type & ~PropertyType::Flags) == PropertyType::Object) {
5,735✔
132
        auto target_obj = m_schema.find(prop.object_type);
562✔
133
        REALM_ASSERT(target_obj != m_schema.end());
562✔
134

135
        if (target_obj->table_type == ObjectSchema::ObjectType::Embedded) {
562✔
136
            m_current_path.push_back(prop.name);
112✔
137
            if (is_collection(prop.type)) {
112✔
138
                m_current_path.push_back("[]");
10✔
139
            }
10✔
140

141
            // embedded objects are normally not allowed to be queryable,
142
            // except if it is a GeoJSON type, and in that case the server
143
            // needs to know if it conforms to the expected schema shape.
144
            IncludePropCond always = [](const Property&) -> bool {
118✔
145
                return true;
118✔
146
            };
118✔
147
            type_output = object_schema_to_jsonschema(*target_obj, always);
112✔
148
            type_output.emplace("bsonType", "object");
112✔
149
        }
112✔
150
        else {
450✔
151
            REALM_ASSERT(target_obj->primary_key_property());
450✔
152
            std::string rel_name;
450✔
153
            for (const auto& path_elem : m_current_path) {
450✔
154
                rel_name.append(path_elem);
×
155
                rel_name.append(".");
×
156
            }
×
157
            rel_name.append(prop.name);
450✔
158
            m_relationships[rel_name] = {
450✔
159
                {"ref",
450✔
160
                 util::format("#/relationship/%1/%2/%3", m_mongo_service_name, m_mongo_db_name, target_obj->name)},
450✔
161
                {"foreign_key", target_obj->primary_key_property()->name},
450✔
162
                {"is_list", is_collection(prop.type)},
450✔
163
            };
450✔
164
            type_output.emplace("bsonType", property_type_to_bson_type_str(target_obj->primary_key_property()->type));
450✔
165
        }
450✔
166
    }
562✔
167
    else {
5,173✔
168
        type_output = {{"bsonType", property_type_to_bson_type_str(prop.type)}};
5,173✔
169
    }
5,173✔
170

171
    if (is_array(prop.type)) {
5,735✔
172
        return nlohmann::json{{"bsonType", "array"}, {"items", type_output}};
274✔
173
    }
274✔
174
    if (is_set(prop.type)) {
5,461✔
175
        return nlohmann::json{{"bsonType", "array"}, {"uniqueItems", true}, {"items", type_output}};
4✔
176
    }
4✔
177
    if (is_dictionary(prop.type)) {
5,457✔
178
        return nlohmann::json{
56✔
179
            {"bsonType", "object"}, {"properties", nlohmann::json::object()}, {"additionalProperties", type_output}};
56✔
180
    }
56✔
181

182
    // At this point we should have handled all the collection types and it's safe to return the prop_obj,
183
    REALM_ASSERT(!is_collection(prop.type));
5,401✔
184
    return type_output;
5,401✔
185
}
5,457✔
186

187
nlohmann::json BaasRuleBuilder::object_schema_to_baas_schema(const ObjectSchema& obj_schema,
188
                                                             IncludePropCond include_prop)
189
{
1,916✔
190
    m_relationships.clear();
1,916✔
191

192
    auto schema_json = object_schema_to_jsonschema(obj_schema, include_prop, true);
1,916✔
193
    auto& prop_sub_obj = schema_json["properties"];
1,916✔
194
    if (!prop_sub_obj.contains(m_partition_key.name) && !m_is_flx_sync) {
1,916✔
195
        prop_sub_obj.emplace(m_partition_key.name, property_to_jsonschema(m_partition_key));
144✔
196
        if (!is_nullable(m_partition_key.type)) {
144✔
197
            schema_json["required"].push_back(m_partition_key.name);
×
198
        }
×
199
    }
144✔
200
    return {
1,916✔
201
        {"schema", schema_json},
1,916✔
202
        {"metadata", nlohmann::json::object({{"database", m_mongo_db_name},
1,916✔
203
                                             {"collection", obj_schema.name},
1,916✔
204
                                             {"data_source", m_mongo_service_name}})},
1,916✔
205
        {"relationships", m_relationships},
1,916✔
206
    };
1,916✔
207
}
1,916✔
208

209
class CurlGlobalGuard {
210
public:
211
    CurlGlobalGuard()
212
    {
17,121✔
213
        std::lock_guard<std::mutex> lk(m_mutex);
17,121✔
214
        if (++m_users == 1) {
17,121✔
215
            curl_global_init(CURL_GLOBAL_ALL);
17,121✔
216
        }
17,121✔
217
    }
17,121✔
218

219
    ~CurlGlobalGuard()
220
    {
17,121✔
221
        std::lock_guard<std::mutex> lk(m_mutex);
17,121✔
222
        if (--m_users == 0) {
17,121✔
223
            curl_global_cleanup();
17,121✔
224
        }
17,121✔
225
    }
17,121✔
226

227
    CurlGlobalGuard(const CurlGlobalGuard&) = delete;
228
    CurlGlobalGuard(CurlGlobalGuard&&) = delete;
229
    CurlGlobalGuard& operator=(const CurlGlobalGuard&) = delete;
230
    CurlGlobalGuard& operator=(CurlGlobalGuard&&) = delete;
231

232
private:
233
    static std::mutex m_mutex;
234
    static int m_users;
235
};
236

237
std::mutex CurlGlobalGuard::m_mutex = {};
238
int CurlGlobalGuard::m_users = 0;
239

240
size_t curl_write_cb(char* ptr, size_t size, size_t nmemb, std::string* response)
241
{
13,915✔
242
    REALM_ASSERT(response);
13,915✔
243
    size_t realsize = size * nmemb;
13,915✔
244
    response->append(ptr, realsize);
13,915✔
245
    return realsize;
13,915✔
246
}
13,915✔
247

248
size_t curl_header_cb(char* buffer, size_t size, size_t nitems, std::map<std::string, std::string>* response_headers)
249
{
155,859✔
250
    REALM_ASSERT(response_headers);
155,859✔
251
    std::string_view combined(buffer, size * nitems);
155,859✔
252
    if (auto pos = combined.find(':'); pos != std::string::npos) {
155,859✔
253
        std::string_view key = combined.substr(0, pos);
121,621✔
254
        std::string_view value = combined.substr(pos + 1);
121,621✔
255
        if (auto first_not_space = value.find_first_not_of(' '); first_not_space != std::string::npos) {
121,621✔
256
            value = value.substr(first_not_space);
121,621✔
257
        }
121,621✔
258
        if (auto last_not_nl = value.find_last_not_of("\r\n"); last_not_nl != std::string::npos) {
121,621✔
259
            value = value.substr(0, last_not_nl + 1);
121,621✔
260
        }
121,621✔
261
        response_headers->insert({std::string{key}, std::string{value}});
121,621✔
262
    }
121,621✔
263
    else {
34,238✔
264
        if (combined.size() > 5 && combined.substr(0, 5) != "HTTP/") { // ignore for now HTTP/1.1 ...
34,238✔
265
            std::cerr << "test transport skipping header: " << combined << std::endl;
×
266
        }
×
267
    }
34,238✔
268
    return nitems * size;
155,859✔
269
}
155,859✔
270

271
std::string_view getenv_sv(const char* name) noexcept
272
{
1,580✔
273
    if (auto ptr = ::getenv(name); ptr != nullptr) {
1,580✔
274
        return std::string_view(ptr);
6✔
275
    }
6✔
276

277
    return {};
1,574✔
278
}
1,580✔
279

280
const static std::string g_baas_coid_header_name("x-appservices-request-id");
281

282
} // namespace
283

284
app::Response do_http_request(const app::Request& request)
285
{
17,121✔
286
    CurlGlobalGuard curl_global_guard;
17,121✔
287
    auto curl = curl_easy_init();
17,121✔
288
    if (!curl) {
17,121✔
289
        return app::Response{500, -1};
×
290
    }
×
291

292
    struct curl_slist* list = nullptr;
17,121✔
293
    auto curl_cleanup = util::ScopeExit([&]() noexcept {
17,121✔
294
        curl_easy_cleanup(curl);
17,121✔
295
        curl_slist_free_all(list);
17,121✔
296
    });
17,121✔
297

298
    std::string response;
17,121✔
299
    app::HttpHeaders response_headers;
17,121✔
300

301
    /* First set the URL that is about to receive our POST. This URL can
302
     just as well be a https:// URL if that is what should receive the
303
     data. */
304
    curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
17,121✔
305

306
    /* Now specify the POST data */
307
    if (request.method == app::HttpMethod::post) {
17,121✔
308
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
8,158✔
309
    }
8,158✔
310
    else if (request.method == app::HttpMethod::put) {
8,963✔
311
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
2,152✔
312
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
2,152✔
313
    }
2,152✔
314
    else if (request.method == app::HttpMethod::patch) {
6,811✔
315
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
792✔
316
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
792✔
317
    }
792✔
318
    else if (request.method == app::HttpMethod::del) {
6,019✔
319
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
461✔
320
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
461✔
321
    }
461✔
322
    else if (request.method == app::HttpMethod::patch) {
5,558✔
323
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
×
324
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
×
325
    }
×
326

327
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, request.timeout_ms);
17,121✔
328

329
    for (auto header : request.headers) {
47,167✔
330
        auto header_str = util::format("%1: %2", header.first, header.second);
47,167✔
331
        list = curl_slist_append(list, header_str.c_str());
47,167✔
332
    }
47,167✔
333

334
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
17,121✔
335
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
17,121✔
336
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
17,121✔
337
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_cb);
17,121✔
338
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers);
17,121✔
339

340
#ifdef REALM_CURL_CACERTS
341
    auto ca_info = unquote_string(REALM_QUOTE(REALM_CURL_CACERTS));
342
    curl_easy_setopt(curl, CURLOPT_CAINFO, ca_info.c_str());
343
#endif
344

345
    auto start_time = std::chrono::steady_clock::now();
17,121✔
346
    auto response_code = curl_easy_perform(curl);
17,121✔
347
    auto total_time = std::chrono::steady_clock::now() - start_time;
17,121✔
348

349
    auto logger = util::Logger::get_default_logger();
17,121✔
350
    if (response_code != CURLE_OK) {
17,121✔
351
        std::string message = curl_easy_strerror(response_code);
2✔
352
        logger->error("curl_easy_perform() failed when sending request to '%1' with body '%2': %3", request.url,
2✔
353
                      request.body, message);
2✔
354
        // Return a failing response with the CURL error as the custom code
355
        return {0, response_code, {}, message};
2✔
356
    }
2✔
357
    if (logger->would_log(util::Logger::Level::trace)) {
17,119✔
358
        std::string coid = [&] {
×
359
            auto coid_header = response_headers.find(g_baas_coid_header_name);
×
360
            if (coid_header == response_headers.end()) {
×
361
                return std::string{};
×
362
            }
×
363
            return util::format("BaaS Coid: \"%1\"", coid_header->second);
×
364
        }();
×
365

366
        logger->trace("Baas API %1 request to %2 took %3 %4\n", request.method, request.url,
×
367
                      std::chrono::duration_cast<std::chrono::milliseconds>(total_time), coid);
×
368
    }
×
369

370
    int http_code = 0;
17,119✔
371
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
17,119✔
372
    return {
17,119✔
373
        http_code,
17,119✔
374
        0, // binding_response_code
17,119✔
375
        std::move(response_headers),
17,119✔
376
        std::move(response),
17,119✔
377
    };
17,119✔
378
}
17,121✔
379

380
class Baasaas {
381
public:
382
    enum class StartMode { Default, GitHash, Branch, PatchId };
383
    explicit Baasaas(std::string api_key, StartMode mode, std::string ref_spec)
384
        : m_api_key(std::move(api_key))
1✔
385
        , m_base_url(get_baasaas_base_url())
1✔
386
        , m_externally_managed_instance(false)
1✔
387
    {
2✔
388
        auto logger = util::Logger::get_default_logger();
2✔
389
        std::string url_path = "startContainer";
2✔
390
        if (mode == StartMode::GitHash) {
2✔
391
            url_path = util::format("startContainer?githash=%1", ref_spec);
2✔
392
            logger->info("Starting baasaas container with githash of %1", ref_spec);
2✔
393
        }
2✔
394
        else if (mode == StartMode::Branch) {
×
395
            url_path = util::format("startContainer?branch=%1", ref_spec);
×
396
            logger->info("Starting baasaas container on branch %1", ref_spec);
×
397
        }
×
398
        else if (mode == StartMode::PatchId) {
×
399
            url_path = util::format("startContainer?patchId=%1", ref_spec);
×
400
            logger->info("Starting baasaas container for patch id %1", ref_spec);
×
401
        }
×
402
        else {
×
403
            logger->info("Starting baasaas container");
×
404
        }
×
405

406
        auto [resp, baas_coid] = do_request(std::move(url_path), app::HttpMethod::post);
2✔
407
        if (!resp["id"].is_string()) {
2✔
408
            throw RuntimeError(
×
409
                ErrorCodes::RuntimeError,
×
410
                util::format(
×
411
                    "Failed to start baas container, got response without container ID: \"%1\" (baas coid: %2)",
×
412
                    resp.dump(), baas_coid));
×
413
        }
×
414
        m_container_id = resp["id"].get<std::string>();
2✔
415
        if (m_container_id.empty()) {
2✔
416
            throw RuntimeError(
×
417
                ErrorCodes::InvalidArgument,
×
418
                util::format("Failed to start baas container, got response with empty container ID (baas coid: %1)",
×
419
                             baas_coid));
×
420
        }
×
421
        logger->info("Baasaas container started with id \"%1\"", m_container_id);
2✔
422
        util::File lock_file(s_baasaas_lock_file_name, util::File::mode_Write);
2✔
423
        lock_file.write(0, m_container_id);
2✔
424
    }
2✔
425

426
    explicit Baasaas(std::string api_key, std::string baasaas_instance_id)
427
        : m_api_key(std::move(api_key))
428
        , m_base_url(get_baasaas_base_url())
429
        , m_container_id(std::move(baasaas_instance_id))
430
        , m_externally_managed_instance(true)
431
    {
×
432
        auto logger = util::Logger::get_default_logger();
×
433
        logger->info("Using externally managed baasaas instance \"%1\"", m_container_id);
×
434
    }
×
435

436
    Baasaas(const Baasaas&) = delete;
437
    Baasaas(Baasaas&&) = delete;
438
    Baasaas& operator=(const Baasaas&) = delete;
439
    Baasaas& operator=(Baasaas&&) = delete;
440

441
    ~Baasaas()
442
    {
2✔
443
        stop();
2✔
444
    }
2✔
445

446
    void poll()
447
    {
1,181✔
448
        if (!m_http_endpoint.empty() || m_container_id.empty()) {
1,181✔
449
            return;
1,179✔
450
        }
1,179✔
451

452
        auto logger = util::Logger::get_default_logger();
2✔
453
        auto poll_start_at = std::chrono::system_clock::now();
2✔
454
        std::string http_endpoint;
2✔
455
        std::string mongo_endpoint;
2✔
456
        bool logged = false;
2✔
457
        while (std::chrono::system_clock::now() - poll_start_at < std::chrono::minutes(2) &&
8✔
458
               m_http_endpoint.empty()) {
8✔
459
            if (http_endpoint.empty()) {
8✔
460
                auto [status_obj, baas_coid] =
4✔
461
                    do_request(util::format("containerStatus?id=%1", m_container_id), app::HttpMethod::get);
4✔
462
                if (!status_obj["httpUrl"].is_null()) {
4✔
463
                    if (!status_obj["httpUrl"].is_string() || !status_obj["mongoUrl"].is_string()) {
2✔
464
                        throw RuntimeError(ErrorCodes::RuntimeError,
×
465
                                           util::format("Error polling for baasaas instance. httpUrl or mongoUrl is "
×
466
                                                        "the wrong format: \"%1\" (baas coid: %2)",
×
467
                                                        status_obj.dump(), baas_coid));
×
468
                    }
×
469
                    http_endpoint = status_obj["httpUrl"].get<std::string>();
2✔
470
                    mongo_endpoint = status_obj["mongoUrl"].get<std::string>();
2✔
471
                }
2✔
472
            }
4✔
473
            else {
4✔
474
                app::Request baas_req;
4✔
475
                baas_req.url = util::format("%1/api/private/v1.0/version", http_endpoint);
4✔
476
                baas_req.method = app::HttpMethod::get;
4✔
477
                baas_req.headers.insert_or_assign("Content-Type", "application/json");
4✔
478
                auto baas_resp = do_http_request(baas_req);
4✔
479
                if (baas_resp.http_status_code >= 200 && baas_resp.http_status_code < 300) {
4✔
480
                    m_http_endpoint = http_endpoint;
2✔
481
                    m_mongo_endpoint = mongo_endpoint;
2✔
482
                    break;
2✔
483
                }
2✔
484
            }
4✔
485

486
            if (!logged) {
6✔
487
                logger->info("Waiting for baasaas container \"%1\" to be ready", m_container_id);
2✔
488
                logged = true;
2✔
489
            }
2✔
490
            std::this_thread::sleep_for(std::chrono::seconds(3));
6✔
491
        }
6✔
492

493
        if (m_http_endpoint.empty()) {
2✔
494
            throw std::runtime_error(
×
495
                util::format("Failed to launch baasaas container %1 within 2 minutes", m_container_id));
×
496
        }
×
497
    }
2✔
498

499
    void stop()
500
    {
4✔
501
        if (m_externally_managed_instance) {
4✔
502
            return;
×
503
        }
×
504
        auto container_id = std::move(m_container_id);
4✔
505
        if (container_id.empty()) {
4✔
506
            return;
2✔
507
        }
2✔
508

509
        auto logger = util::Logger::get_default_logger();
2✔
510
        logger->info("Stopping baasaas container with id \"%1\"", container_id);
2✔
511
        do_request(util::format("stopContainer?id=%1", container_id), app::HttpMethod::post);
2✔
512
        auto lock_file = util::File(std::string{s_baasaas_lock_file_name}, util::File::mode_Write);
2✔
513
        lock_file.resize(0);
2✔
514
        lock_file.close();
2✔
515
        util::File::remove(lock_file.get_path());
2✔
516
    }
2✔
517

518
    const std::string& http_endpoint()
519
    {
796✔
520
        poll();
796✔
521
        return m_http_endpoint;
796✔
522
    }
796✔
523

524
    const std::string& mongo_endpoint()
525
    {
385✔
526
        poll();
385✔
527
        return m_mongo_endpoint;
385✔
528
    }
385✔
529

530
private:
531
    std::pair<nlohmann::json, std::string> do_request(std::string api_path, app::HttpMethod method)
532
    {
8✔
533
        app::Request request;
8✔
534

535
        request.url = util::format("%1/%2", m_base_url, api_path);
8✔
536
        request.method = method;
8✔
537
        request.headers.insert_or_assign("apiKey", m_api_key);
8✔
538
        request.headers.insert_or_assign("Content-Type", "application/json");
8✔
539
        auto response = do_http_request(request);
8✔
540
        if (response.http_status_code < 200 || response.http_status_code >= 300) {
8✔
541
            throw RuntimeError(ErrorCodes::HTTPError,
×
542
                               util::format("Baasaas api response code: %1 Response body: %2, Baas coid: %3",
×
543
                                            response.http_status_code, response.body,
×
544
                                            baas_coid_from_response(response)));
×
545
        }
×
546
        try {
8✔
547
            return {nlohmann::json::parse(response.body), baas_coid_from_response(response)};
8✔
548
        }
8✔
549
        catch (const nlohmann::json::exception& e) {
8✔
550
            throw RuntimeError(
×
551
                ErrorCodes::MalformedJson,
×
552
                util::format("Error making baasaas request to %1 (baas coid %2): Invalid json returned \"%3\" (%4)",
×
553
                             request.url, baas_coid_from_response(response), response.body, e.what()));
×
554
        }
×
555
    }
8✔
556

557
    std::string baas_coid_from_response(const app::Response& resp)
558
    {
8✔
559
        if (auto it = resp.headers.find(g_baas_coid_header_name); it != resp.headers.end()) {
8✔
560
            return it->second;
8✔
561
        }
8✔
562
        return "<not found>";
×
563
    }
8✔
564

565
    static std::string get_baasaas_base_url()
566
    {
2✔
567
        auto env_value = getenv_sv("BAASAAS_BASE_URL");
2✔
568
        if (env_value.empty()) {
2✔
569
            // This is the current default endpoint for baasaas maintained by the sync team.
570
            // You can reach out for help in #appx-device-sync-internal if there are problems.
571
            return "https://us-east-1.aws.data.mongodb-api.com/app/baas-container-service-autzb/endpoint";
2✔
572
        }
2✔
573

574
        return unquote_string(env_value);
×
575
    }
2✔
576

577
    constexpr static std::string_view s_baasaas_lock_file_name = "baasaas_instance.lock";
578

579
    std::string m_api_key;
580
    std::string m_base_url;
581
    std::string m_container_id;
582
    bool m_externally_managed_instance;
583
    std::string m_http_endpoint;
584
    std::string m_mongo_endpoint;
585
};
586

587
class BaasaasLauncher : public Catch::EventListenerBase {
588
public:
589
    static std::optional<Baasaas>& get_baasaas_holder()
590
    {
1,187✔
591
        static std::optional<Baasaas> global_baasaas = std::nullopt;
1,187✔
592
        return global_baasaas;
1,187✔
593
    }
1,187✔
594

595
    using Catch::EventListenerBase::EventListenerBase;
596

597
    void testRunStarting(Catch::TestRunInfo const&) override
598
    {
4✔
599
        std::string_view api_key(getenv_sv("BAASAAS_API_KEY"));
4✔
600
        if (api_key.empty()) {
4✔
601
            return;
2✔
602
        }
2✔
603

604
        // Allow overriding the baas base url at runtime via an environment variable, even if BAASAAS_API_KEY
605
        // is also specified.
606
        if (!getenv_sv("BAAS_BASE_URL").empty()) {
2✔
607
            return;
×
608
        }
×
609

610
        // If we've started a baasaas container outside of running the tests, then use that instead of
611
        // figuring out how to start our own.
612
        if (auto baasaas_instance = getenv_sv("BAASAAS_INSTANCE_ID"); !baasaas_instance.empty()) {
2✔
613
            auto& baasaas_holder = get_baasaas_holder();
×
614
            REALM_ASSERT(!baasaas_holder);
×
615
            baasaas_holder.emplace(std::string{api_key}, std::string{baasaas_instance});
×
616
            return;
×
617
        }
×
618

619
        std::string_view ref_spec(getenv_sv("BAASAAS_REF_SPEC"));
2✔
620
        std::string_view mode_spec(getenv_sv("BAASAAS_START_MODE"));
2✔
621
        Baasaas::StartMode mode = Baasaas::StartMode::Default;
2✔
622
        if (mode_spec == "branch") {
2✔
623
            if (ref_spec.empty()) {
×
624
                throw std::runtime_error("Expected branch name in BAASAAS_REF_SPEC env variable, but it was empty");
×
625
            }
×
626
            mode = Baasaas::StartMode::Branch;
×
627
        }
×
628
        else if (mode_spec == "githash") {
2✔
629
            if (ref_spec.empty()) {
2✔
630
                throw std::runtime_error("Expected git hash in BAASAAS_REF_SPEC env variable, but it was empty");
×
631
            }
×
632
            mode = Baasaas::StartMode::GitHash;
2✔
633
        }
2✔
634
        else if (mode_spec == "patchid") {
×
635
            if (ref_spec.empty()) {
×
636
                throw std::runtime_error("Expected patch id in BAASAAS_REF_SPEC env variable, but it was empty");
×
637
            }
×
638
            mode = Baasaas::StartMode::PatchId;
×
639
        }
×
640
        else {
×
641
            if (!mode_spec.empty()) {
×
NEW
642
                throw std::runtime_error("Expected BAASAAS_START_MODE to be \"githash\", \"patchid\", or \"branch\"");
×
643
            }
×
644
            ref_spec = {};
×
645
        }
×
646

647
        auto& baasaas_holder = get_baasaas_holder();
2✔
648
        REALM_ASSERT(!baasaas_holder);
2✔
649
        baasaas_holder.emplace(std::string{api_key}, mode, std::string{ref_spec});
2✔
650

651
        get_runtime_app_session();
2✔
652
    }
2✔
653

654
    void testRunEnded(Catch::TestRunStats const&) override
655
    {
4✔
656
        if (auto& baasaas_holder = get_baasaas_holder()) {
4✔
657
            baasaas_holder->stop();
2✔
658
        }
2✔
659
    }
4✔
660
};
661

662
CATCH_REGISTER_LISTENER(BaasaasLauncher)
663

664
AdminAPIEndpoint AdminAPIEndpoint::operator[](StringData name) const
665
{
19,487✔
666
    return AdminAPIEndpoint(util::format("%1/%2", m_url, name), m_access_token);
19,487✔
667
}
19,487✔
668

669
app::Response AdminAPIEndpoint::do_request(app::Request request) const
670
{
12,212✔
671
    if (request.url.find('?') == std::string::npos) {
12,212✔
672
        request.url = util::format("%1?bypass_service_change=SyncSchemaVersionIncrease", request.url);
12,212✔
673
    }
12,212✔
674
    else {
×
675
        request.url = util::format("%1&bypass_service_change=SyncSchemaVersionIncrease", request.url);
×
676
    }
×
677
    request.headers["Content-Type"] = "application/json;charset=utf-8";
12,212✔
678
    request.headers["Accept"] = "application/json";
12,212✔
679
    request.headers["Authorization"] = util::format("Bearer %1", m_access_token);
12,212✔
680
    return do_http_request(std::move(request));
12,212✔
681
}
12,212✔
682

683
app::Response AdminAPIEndpoint::get(const std::vector<std::pair<std::string, std::string>>& params) const
684
{
3,895✔
685
    app::Request req;
3,895✔
686
    req.method = app::HttpMethod::get;
3,895✔
687
    std::stringstream ss;
3,895✔
688
    bool needs_and = false;
3,895✔
689
    ss << m_url;
3,895✔
690
    if (!params.empty() && m_url.find('?') != std::string::npos) {
3,895!
691
        needs_and = true;
×
692
    }
×
693
    for (const auto& param : params) {
3,895✔
694
        if (needs_and) {
×
695
            ss << "&";
×
696
        }
×
697
        else {
×
698
            ss << "?";
×
699
        }
×
700
        needs_and = true;
×
701
        ss << param.first << "=" << param.second;
×
702
    }
×
703
    req.url = ss.str();
3,895✔
704
    return do_request(std::move(req));
3,895✔
705
}
3,895✔
706

707
app::Response AdminAPIEndpoint::del() const
708
{
417✔
709
    app::Request req;
417✔
710
    req.method = app::HttpMethod::del;
417✔
711
    req.url = m_url;
417✔
712
    return do_request(std::move(req));
417✔
713
}
417✔
714

715
nlohmann::json AdminAPIEndpoint::get_json(const std::vector<std::pair<std::string, std::string>>& params) const
716
{
3,895✔
717
    auto resp = get(params);
3,895✔
718
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, resp.http_status_code,
3,895✔
719
                    resp.body);
3,895✔
720
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
3,895✔
721
}
3,895✔
722

723
app::Response AdminAPIEndpoint::post(std::string body) const
724
{
4,980✔
725
    app::Request req;
4,980✔
726
    req.method = app::HttpMethod::post;
4,980✔
727
    req.url = m_url;
4,980✔
728
    req.body = std::move(body);
4,980✔
729
    return do_request(std::move(req));
4,980✔
730
}
4,980✔
731

732
nlohmann::json AdminAPIEndpoint::post_json(nlohmann::json body) const
733
{
4,958✔
734
    auto resp = post(body.dump());
4,958✔
735
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(),
4,958✔
736
                    resp.http_status_code, resp.body);
4,958✔
737
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
4,958✔
738
}
4,958✔
739

740
app::Response AdminAPIEndpoint::put(std::string body) const
741
{
2,128✔
742
    app::Request req;
2,128✔
743
    req.method = app::HttpMethod::put;
2,128✔
744
    req.url = m_url;
2,128✔
745
    req.body = std::move(body);
2,128✔
746
    return do_request(std::move(req));
2,128✔
747
}
2,128✔
748

749
nlohmann::json AdminAPIEndpoint::put_json(nlohmann::json body) const
750
{
1,733✔
751
    auto resp = put(body.dump());
1,733✔
752
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(),
1,733✔
753
                    resp.http_status_code, resp.body);
1,733✔
754
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
1,733✔
755
}
1,733✔
756

757
app::Response AdminAPIEndpoint::patch(std::string body) const
758
{
792✔
759
    app::Request req;
792✔
760
    req.method = app::HttpMethod::patch;
792✔
761
    req.url = m_url;
792✔
762
    req.body = std::move(body);
792✔
763
    return do_request(std::move(req));
792✔
764
}
792✔
765

766
nlohmann::json AdminAPIEndpoint::patch_json(nlohmann::json body) const
767
{
792✔
768
    auto resp = patch(body.dump());
792✔
769
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(),
792✔
770
                    resp.http_status_code, resp.body);
792✔
771
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
792✔
772
}
792✔
773

774
AdminAPISession AdminAPISession::login(const AppCreateConfig& config)
775
{
387✔
776
    std::string admin_url = config.admin_url;
387✔
777
    nlohmann::json login_req_body{
387✔
778
        {"provider", "userpass"},
387✔
779
        {"username", config.admin_username},
387✔
780
        {"password", config.admin_password},
387✔
781
    };
387✔
782
    if (config.logger) {
387✔
783
        config.logger->trace("Logging into baas admin api: %1", admin_url);
387✔
784
    }
387✔
785
    app::Request auth_req{
387✔
786
        app::HttpMethod::post,
387✔
787
        util::format("%1/api/admin/v3.0/auth/providers/local-userpass/login", admin_url),
387✔
788
        60000, // 1 minute timeout
387✔
789
        {
387✔
790
            {"Content-Type", "application/json;charset=utf-8"},
387✔
791
            {"Accept", "application/json"},
387✔
792
        },
387✔
793
        login_req_body.dump(),
387✔
794
    };
387✔
795
    auto login_resp = do_http_request(std::move(auth_req));
387✔
796
    REALM_ASSERT_EX(login_resp.http_status_code == 200, login_resp.http_status_code, login_resp.body);
387✔
797
    auto login_resp_body = nlohmann::json::parse(login_resp.body);
387✔
798

799
    std::string access_token = login_resp_body["access_token"];
387✔
800

801
    AdminAPIEndpoint user_profile(util::format("%1/api/admin/v3.0/auth/profile", admin_url), access_token);
387✔
802
    auto profile_resp = user_profile.get_json();
387✔
803

804
    std::string group_id = profile_resp["roles"][0]["group_id"];
387✔
805

806
    return AdminAPISession(std::move(admin_url), std::move(access_token), std::move(group_id));
387✔
807
}
387✔
808

809
void AdminAPISession::revoke_user_sessions(const std::string& user_id, const std::string& app_id) const
810
{
4✔
811
    auto endpoint = apps()[app_id]["users"][user_id]["logout"];
4✔
812
    auto response = endpoint.put("");
4✔
813
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
4✔
814
}
4✔
815

816
void AdminAPISession::disable_user_sessions(const std::string& user_id, const std::string& app_id) const
817
{
2✔
818
    auto endpoint = apps()[app_id]["users"][user_id]["disable"];
2✔
819
    auto response = endpoint.put("");
2✔
820
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
2✔
821
}
2✔
822

823
void AdminAPISession::enable_user_sessions(const std::string& user_id, const std::string& app_id) const
824
{
2✔
825
    auto endpoint = apps()[app_id]["users"][user_id]["enable"];
2✔
826
    auto response = endpoint.put("");
2✔
827
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
2✔
828
}
2✔
829

830
// returns false for an invalid/expired access token
831
bool AdminAPISession::verify_access_token(const std::string& access_token, const std::string& app_id) const
832
{
22✔
833
    auto endpoint = apps()[app_id]["users"]["verify_token"];
22✔
834
    nlohmann::json request_body{
22✔
835
        {"token", access_token},
22✔
836
    };
22✔
837
    auto response = endpoint.post(request_body.dump());
22✔
838
    if (response.http_status_code == 200) {
22✔
839
        auto resp_json = nlohmann::json::parse(response.body.empty() ? "{}" : response.body);
16✔
840
        try {
16✔
841
            // if these fields are found, then the token is valid according to the server.
842
            // if it is invalid or expired then an error response is sent.
843
            int64_t issued_at = resp_json["iat"];
16✔
844
            int64_t expires_at = resp_json["exp"];
16✔
845
            return issued_at != 0 && expires_at != 0;
16✔
846
        }
16✔
847
        catch (...) {
16✔
848
            return false;
×
849
        }
×
850
    }
16✔
851
    return false;
6✔
852
}
22✔
853

854
void AdminAPISession::set_development_mode_to(const std::string& app_id, bool enable) const
855
{
24✔
856
    auto endpoint = apps()[app_id]["sync"]["config"];
24✔
857
    endpoint.put_json({{"development_mode_enabled", enable}});
24✔
858
}
24✔
859

860
void AdminAPISession::delete_app(const std::string& app_id) const
861
{
385✔
862
    auto app_endpoint = apps()[app_id];
385✔
863
    auto resp = app_endpoint.del();
385✔
864
    REALM_ASSERT_EX(resp.http_status_code == 204, resp.http_status_code, resp.body);
385✔
865
}
385✔
866

867
std::vector<AdminAPISession::Service> AdminAPISession::get_services(const std::string& app_id) const
868
{
216✔
869
    auto endpoint = apps()[app_id]["services"];
216✔
870
    auto response = endpoint.get_json();
216✔
871
    std::vector<AdminAPISession::Service> services;
216✔
872
    for (auto service : response) {
432✔
873
        services.push_back(
432✔
874
            {service["_id"], service["name"], service["type"], service["version"], service["last_modified"]});
432✔
875
    }
432✔
876
    return services;
216✔
877
}
216✔
878

879

880
std::vector<std::string> AdminAPISession::get_errors(const std::string& app_id) const
881
{
×
882
    auto endpoint = apps()[app_id]["logs"];
×
883
    auto response = endpoint.get_json({{"errors_only", "true"}});
×
884
    std::vector<std::string> errors;
×
885
    const auto& logs = response["logs"];
×
886
    std::transform(logs.begin(), logs.end(), std::back_inserter(errors), [](const auto& err) {
×
887
        return err["error"];
×
888
    });
×
889
    return errors;
×
890
}
×
891

892

893
AdminAPISession::Service AdminAPISession::get_sync_service(const std::string& app_id) const
894
{
216✔
895
    auto services = get_services(app_id);
216✔
896
    auto sync_service = std::find_if(services.begin(), services.end(), [&](auto s) {
216✔
897
        return s.type == "mongodb";
216✔
898
    });
216✔
899
    REALM_ASSERT(sync_service != services.end());
216✔
900
    return *sync_service;
216✔
901
}
216✔
902

903
void AdminAPISession::trigger_client_reset(const std::string& app_id, int64_t file_ident) const
904
{
178✔
905
    auto endpoint = apps(APIFamily::Admin)[app_id]["sync"]["force_reset"];
178✔
906
    endpoint.put_json(nlohmann::json{{"file_ident", file_ident}});
178✔
907
}
178✔
908

909
void AdminAPISession::migrate_to_flx(const std::string& app_id, const std::string& service_id,
910
                                     bool migrate_to_flx) const
911
{
30✔
912
    auto endpoint = apps()[app_id]["sync"]["migration"];
30✔
913
    endpoint.put_json(nlohmann::json{{"serviceId", service_id}, {"action", migrate_to_flx ? "start" : "rollback"}});
30✔
914
}
30✔
915

916
// Each breaking change bumps the schema version, so you can create a new version for each breaking change if
917
// 'use_draft' is false. Set 'use_draft' to true if you want all changes to the schema to be deployed at once
918
// resulting in only one schema version.
919
void AdminAPISession::create_schema(const std::string& app_id, const AppCreateConfig& config, bool use_draft) const
920
{
425✔
921
    static const std::string mongo_service_name = "BackingDB";
425✔
922

923
    auto drafts = apps()[app_id]["drafts"];
425✔
924
    std::string draft_id;
425✔
925
    if (use_draft) {
425✔
926
        auto draft_create_resp = drafts.post_json({});
38✔
927
        draft_id = draft_create_resp["_id"];
38✔
928
    }
38✔
929

930
    auto schemas = apps()[app_id]["schemas"];
425✔
931
    auto current_schema = schemas.get_json();
425✔
932
    auto target_schema = config.schema;
425✔
933

934
    std::unordered_map<std::string, std::string> current_schema_tables;
425✔
935
    for (const auto& schema : current_schema) {
425✔
936
        current_schema_tables[schema["metadata"]["collection"]] = schema["_id"];
148✔
937
    }
148✔
938

939
    // Add new tables
940

941
    auto pk_and_queryable_only = [&](const Property& prop) {
3,353✔
942
        if (config.flx_sync_config) {
3,353✔
943
            const auto& queryable_fields = config.flx_sync_config->queryable_fields;
1,523✔
944

945
            if (std::find(queryable_fields.begin(), queryable_fields.end(), prop.name) != queryable_fields.end()) {
1,523✔
946
                return true;
536✔
947
            }
536✔
948
        }
1,523✔
949
        return prop.name == "_id" || prop.name == config.partition_key.name;
2,817✔
950
    };
3,353✔
951

952
    // Create the schemas in two passes: first populate just the primary key and
953
    // partition key, then add the rest of the properties. This ensures that the
954
    // targets of links exist before adding the links.
955
    std::vector<std::pair<std::string, const ObjectSchema*>> object_schema_to_create;
425✔
956
    BaasRuleBuilder rule_builder(target_schema, config.partition_key, mongo_service_name, config.mongo_dbname,
425✔
957
                                 static_cast<bool>(config.flx_sync_config));
425✔
958
    for (const auto& obj_schema : target_schema) {
1,016✔
959
        auto it = current_schema_tables.find(obj_schema.name);
1,016✔
960
        if (it != current_schema_tables.end()) {
1,016✔
961
            object_schema_to_create.push_back({it->second, &obj_schema});
116✔
962
            continue;
116✔
963
        }
116✔
964

965
        auto schema_to_create = rule_builder.object_schema_to_baas_schema(obj_schema, pk_and_queryable_only);
900✔
966
        auto schema_create_resp = schemas.post_json(schema_to_create);
900✔
967
        object_schema_to_create.push_back({schema_create_resp["_id"], &obj_schema});
900✔
968
    }
900✔
969

970
    // Update existing tables (including the ones just created)
971
    for (const auto& [id, obj_schema] : object_schema_to_create) {
1,016✔
972
        auto schema_to_create = rule_builder.object_schema_to_baas_schema(*obj_schema, nullptr);
1,016✔
973
        schema_to_create["_id"] = id;
1,016✔
974
        schemas[id].put_json(schema_to_create);
1,016✔
975
    }
1,016✔
976

977
    // Delete removed tables
978
    for (const auto& table : current_schema_tables) {
425✔
979
        if (target_schema.find(table.first) == target_schema.end()) {
148✔
980
            schemas[table.second].del();
32✔
981
        }
32✔
982
    }
148✔
983

984
    if (use_draft) {
425✔
985
        drafts[draft_id]["deployment"].post_json({});
38✔
986
    }
38✔
987
}
425✔
988

989
bool AdminAPISession::set_feature_flag(const std::string& app_id, const std::string& flag_name, bool enable) const
NEW
990
{
×
NEW
991
    auto features = apps(APIFamily::Private)[app_id]["features"];
×
NEW
992
    auto flag_response =
×
NEW
993
        features.post_json(nlohmann::json{{"action", enable ? "enable" : "disable"}, {"feature_flags", {flag_name}}});
×
NEW
994
    return flag_response.empty();
×
NEW
995
}
×
996

997
bool AdminAPISession::get_feature_flag(const std::string& app_id, const std::string& flag_name) const
NEW
998
{
×
NEW
999
    auto features = apps(APIFamily::Private)[app_id]["features"];
×
NEW
1000
    auto response = features.get_json();
×
NEW
1001
    if (auto feature_list = response["enabled"]; !feature_list.empty()) {
×
NEW
1002
        return std::find_if(feature_list.begin(), feature_list.end(), [&flag_name](const auto& feature) {
×
NEW
1003
                   return feature == flag_name;
×
NEW
1004
               }) != feature_list.end();
×
NEW
1005
    }
×
NEW
1006
    return false;
×
NEW
1007
}
×
1008

1009
nlohmann::json AdminAPISession::get_default_rule(const std::string& app_id) const
1010
{
50✔
1011
    auto baas_sync_service = get_sync_service(app_id);
50✔
1012
    auto rule_endpoint = apps()[app_id]["services"][baas_sync_service.id]["default_rule"];
50✔
1013
    auto rule = rule_endpoint.get_json();
50✔
1014
    return rule;
50✔
1015
}
50✔
1016

1017
bool AdminAPISession::update_default_rule(const std::string& app_id, nlohmann::json rule_json) const
1018
{
98✔
1019
    if (auto id = rule_json.find("_id");
98✔
1020
        id == rule_json.end() || !id->is_string() || id->get<std::string>().empty()) {
98✔
NEW
1021
        return false;
×
NEW
1022
    }
×
1023

1024
    auto baas_sync_service = get_sync_service(app_id);
98✔
1025
    auto rule_endpoint = apps()[app_id]["services"][baas_sync_service.id]["default_rule"];
98✔
1026
    auto response = rule_endpoint.put_json(rule_json);
98✔
1027
    return response.empty();
98✔
1028
}
98✔
1029

1030
nlohmann::json AdminAPISession::get_app_settings(const std::string& app_id) const
NEW
1031
{
×
NEW
1032
    auto settings_endpoint = apps(APIFamily::Private)[app_id]["settings"];
×
NEW
1033
    return settings_endpoint.get_json();
×
NEW
1034
}
×
1035

1036
bool AdminAPISession::patch_app_settings(const std::string& app_id, nlohmann::json&& json) const
1037
{
12✔
1038
    auto settings_endpoint = apps(APIFamily::Private)[app_id]["settings"];
12✔
1039
    auto response = settings_endpoint.patch_json(std::move(json));
12✔
1040
    return response.empty();
12✔
1041
}
12✔
1042

1043
static nlohmann::json convert_config(AdminAPISession::ServiceConfig config)
1044
{
393✔
1045
    if (config.mode == AdminAPISession::ServiceConfig::SyncMode::Flexible) {
393✔
1046
        auto payload = nlohmann::json{{"database_name", config.database_name},
203✔
1047
                                      {"state", config.state},
203✔
1048
                                      {"is_recovery_mode_disabled", config.recovery_is_disabled}};
203✔
1049
        if (config.queryable_field_names) {
203✔
1050
            payload["queryable_fields_names"] = *config.queryable_field_names;
203✔
1051
        }
203✔
1052
        if (config.permissions) {
203✔
1053
            payload["permissions"] = *config.permissions;
2✔
1054
        }
2✔
1055
        if (config.asymmetric_tables) {
203✔
1056
            payload["asymmetric_tables"] = *config.asymmetric_tables;
201✔
1057
        }
201✔
1058
        return payload;
203✔
1059
    }
203✔
1060
    return nlohmann::json{{"database_name", config.database_name},
190✔
1061
                          {"partition", *config.partition},
190✔
1062
                          {"state", config.state},
190✔
1063
                          {"is_recovery_mode_disabled", config.recovery_is_disabled}};
190✔
1064
}
393✔
1065

1066
AdminAPIEndpoint AdminAPISession::service_config_endpoint(const std::string& app_id,
1067
                                                          const std::string& service_id) const
1068
{
435✔
1069
    return apps()[app_id]["services"][service_id]["config"];
435✔
1070
}
435✔
1071

1072
AdminAPISession::ServiceConfig AdminAPISession::disable_sync(const std::string& app_id, const std::string& service_id,
1073
                                                             AdminAPISession::ServiceConfig sync_config) const
1074
{
×
1075
    auto endpoint = service_config_endpoint(app_id, service_id);
×
1076
    if (sync_config.state != "") {
×
1077
        sync_config.state = "";
×
1078
        endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
×
1079
    }
×
1080
    return sync_config;
×
1081
}
×
1082

1083
AdminAPISession::ServiceConfig AdminAPISession::pause_sync(const std::string& app_id, const std::string& service_id,
1084
                                                           AdminAPISession::ServiceConfig sync_config) const
1085
{
×
1086
    auto endpoint = service_config_endpoint(app_id, service_id);
×
1087
    if (sync_config.state != "disabled") {
×
1088
        sync_config.state = "disabled";
×
1089
        endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
×
1090
    }
×
1091
    return sync_config;
×
1092
}
×
1093

1094
AdminAPISession::ServiceConfig AdminAPISession::enable_sync(const std::string& app_id, const std::string& service_id,
1095
                                                            AdminAPISession::ServiceConfig sync_config) const
1096
{
389✔
1097
    auto endpoint = service_config_endpoint(app_id, service_id);
389✔
1098
    sync_config.state = "enabled";
389✔
1099
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
389✔
1100
    return sync_config;
389✔
1101
}
389✔
1102

1103
AdminAPISession::ServiceConfig AdminAPISession::set_disable_recovery_to(const std::string& app_id,
1104
                                                                        const std::string& service_id,
1105
                                                                        ServiceConfig sync_config, bool disable) const
1106
{
4✔
1107
    auto endpoint = service_config_endpoint(app_id, service_id);
4✔
1108
    sync_config.recovery_is_disabled = disable;
4✔
1109
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
4✔
1110
    return sync_config;
4✔
1111
}
4✔
1112

1113
std::vector<AdminAPISession::SchemaVersionInfo> AdminAPISession::get_schema_versions(const std::string& app_id) const
1114
{
114✔
1115
    std::vector<AdminAPISession::SchemaVersionInfo> ret;
114✔
1116
    auto endpoint = apps()[app_id]["sync"]["schemas"]["versions"];
114✔
1117
    auto res = endpoint.get_json();
114✔
1118
    for (auto&& version : res["versions"].get<std::vector<nlohmann::json>>()) {
165✔
1119
        SchemaVersionInfo info;
165✔
1120
        info.version_major = version["version_major"];
165✔
1121
        ret.push_back(std::move(info));
165✔
1122
    }
165✔
1123

1124
    return ret;
114✔
1125
}
114✔
1126

1127
AdminAPISession::ServiceConfig AdminAPISession::get_config(const std::string& app_id,
1128
                                                           const AdminAPISession::Service& service) const
1129
{
42✔
1130
    auto endpoint = service_config_endpoint(app_id, service.id);
42✔
1131
    auto response = endpoint.get_json();
42✔
1132
    AdminAPISession::ServiceConfig config;
42✔
1133
    if (response.contains("flexible_sync")) {
42✔
1134
        auto sync = response["flexible_sync"];
16✔
1135
        config.mode = AdminAPISession::ServiceConfig::SyncMode::Flexible;
16✔
1136
        config.state = sync["state"];
16✔
1137
        config.database_name = sync["database_name"];
16✔
1138
        config.permissions = sync["permissions"];
16✔
1139
        config.queryable_field_names = sync["queryable_fields_names"];
16✔
1140
        auto recovery_disabled = sync["is_recovery_mode_disabled"];
16✔
1141
        config.recovery_is_disabled = recovery_disabled.is_boolean() ? recovery_disabled.get<bool>() : false;
16✔
1142
    }
16✔
1143
    else if (response.contains("sync")) {
26✔
1144
        auto sync = response["sync"];
26✔
1145
        config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
26✔
1146
        config.state = sync["state"];
26✔
1147
        config.database_name = sync["database_name"];
26✔
1148
        config.partition = sync["partition"];
26✔
1149
        auto recovery_disabled = sync["is_recovery_mode_disabled"];
26✔
1150
        config.recovery_is_disabled = recovery_disabled.is_boolean() ? recovery_disabled.get<bool>() : false;
26✔
1151
    }
26✔
1152
    else {
×
1153
        throw std::runtime_error(util::format("Unsupported config format from server: %1", response));
×
1154
    }
×
1155
    return config;
42✔
1156
}
42✔
1157

1158
bool AdminAPISession::is_sync_enabled(const std::string& app_id) const
1159
{
30✔
1160
    auto sync_service = get_sync_service(app_id);
30✔
1161
    auto config = get_config(app_id, sync_service);
30✔
1162
    return config.state == "enabled";
30✔
1163
}
30✔
1164

1165
bool AdminAPISession::is_sync_terminated(const std::string& app_id) const
1166
{
×
1167
    auto sync_service = get_sync_service(app_id);
×
1168
    auto config = get_config(app_id, sync_service);
×
1169
    if (config.state == "enabled") {
×
1170
        return false;
×
1171
    }
×
1172
    auto state_endpoint = apps()[app_id]["sync"]["state"];
×
1173
    auto state_result = state_endpoint.get_json(
×
1174
        {{"sync_type", config.mode == ServiceConfig::SyncMode::Flexible ? "flexible" : "partition"}});
×
1175
    return state_result["state"].get<std::string>().empty();
×
1176
}
×
1177

1178
bool AdminAPISession::is_initial_sync_complete(const std::string& app_id) const
1179
{
1,529✔
1180
    auto progress_endpoint = apps()[app_id]["sync"]["progress"];
1,529✔
1181
    auto progress_result = progress_endpoint.get_json();
1,529✔
1182
    if (auto it = progress_result.find("progress"); it != progress_result.end() && it->is_object() && !it->empty()) {
1,529✔
1183
        for (auto& elem : *it) {
1,697✔
1184
            auto is_complete = elem["complete"];
1,697✔
1185
            if (!is_complete.is_boolean() || !is_complete.get<bool>()) {
1,697✔
1186
                return false;
513✔
1187
            }
513✔
1188
        }
1,697✔
1189
        return true;
477✔
1190
    }
990✔
1191
    return false;
539✔
1192
}
1,529✔
1193

1194
AdminAPISession::MigrationStatus AdminAPISession::get_migration_status(const std::string& app_id) const
1195
{
745✔
1196
    MigrationStatus status;
745✔
1197
    auto progress_endpoint = apps()[app_id]["sync"]["migration"];
745✔
1198
    auto progress_result = progress_endpoint.get_json();
745✔
1199
    auto errorMessage = progress_result["errorMessage"];
745✔
1200
    if (errorMessage.is_string() && !errorMessage.get<std::string>().empty()) {
745✔
1201
        throw Exception(Status{ErrorCodes::RuntimeError, errorMessage.get<std::string>()});
×
1202
    }
×
1203
    if (!progress_result["statusMessage"].is_string() || !progress_result["isMigrated"].is_boolean()) {
745✔
1204
        throw Exception(
×
1205
            Status{ErrorCodes::RuntimeError, util::format("Invalid result returned from migration status request: %1",
×
1206
                                                          progress_result.dump(4, 32, true))});
×
1207
    }
×
1208

1209
    status.statusMessage = progress_result["statusMessage"].get<std::string>();
745✔
1210
    status.isMigrated = progress_result["isMigrated"].get<bool>();
745✔
1211
    status.isCancelable = progress_result["isCancelable"].get<bool>();
745✔
1212
    status.isRevertible = progress_result["isRevertible"].get<bool>();
745✔
1213
    status.complete = status.statusMessage.empty();
745✔
1214
    return status;
745✔
1215
}
745✔
1216

1217
AdminAPIEndpoint AdminAPISession::apps(APIFamily family) const
1218
{
5,470✔
1219
    switch (family) {
5,470✔
1220
        case APIFamily::Admin:
5,458✔
1221
            return AdminAPIEndpoint(util::format("%1/api/admin/v3.0/groups/%2/apps", m_base_url, m_group_id),
5,458✔
1222
                                    m_access_token);
5,458✔
1223
        case APIFamily::Private:
12✔
1224
            return AdminAPIEndpoint(util::format("%1/api/private/v1.0/groups/%2/apps", m_base_url, m_group_id),
12✔
1225
                                    m_access_token);
12✔
1226
    }
5,470✔
1227
    REALM_UNREACHABLE();
1228
}
×
1229

1230
realm::Schema get_default_schema()
1231
{
82✔
1232
    const auto dog_schema =
82✔
1233
        ObjectSchema("Dog", {realm::Property("_id", PropertyType::ObjectId | PropertyType::Nullable, true),
82✔
1234
                             realm::Property("breed", PropertyType::String | PropertyType::Nullable),
82✔
1235
                             realm::Property("name", PropertyType::String),
82✔
1236
                             realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
82✔
1237
    const auto cat_schema =
82✔
1238
        ObjectSchema("Cat", {realm::Property("_id", PropertyType::String | PropertyType::Nullable, true),
82✔
1239
                             realm::Property("breed", PropertyType::String | PropertyType::Nullable),
82✔
1240
                             realm::Property("name", PropertyType::String),
82✔
1241
                             realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
82✔
1242
    const auto person_schema =
82✔
1243
        ObjectSchema("Person", {realm::Property("_id", PropertyType::ObjectId | PropertyType::Nullable, true),
82✔
1244
                                realm::Property("age", PropertyType::Int),
82✔
1245
                                realm::Property("dogs", PropertyType::Object | PropertyType::Array, "Dog"),
82✔
1246
                                realm::Property("firstName", PropertyType::String),
82✔
1247
                                realm::Property("lastName", PropertyType::String),
82✔
1248
                                realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
82✔
1249
    return realm::Schema({dog_schema, cat_schema, person_schema});
82✔
1250
}
82✔
1251

1252
std::string get_base_url()
1253
{
796✔
1254
    if (auto baas_url = getenv_sv("BAAS_BASE_URL"); !baas_url.empty()) {
796✔
1255
        return std::string{baas_url};
×
1256
    }
×
1257
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
796✔
1258
        return baasaas_holder->http_endpoint();
796✔
1259
    }
796✔
1260

1261
    return get_compile_time_base_url();
×
1262
}
796✔
1263

1264
std::string get_admin_url()
1265
{
385✔
1266
    if (auto baas_admin_url = getenv_sv("BAAS_ADMIN_URL"); !baas_admin_url.empty()) {
385✔
1267
        return std::string{baas_admin_url};
×
1268
    }
×
1269
    if (auto compile_url = get_compile_time_admin_url(); !compile_url.empty()) {
385✔
1270
        return compile_url;
×
1271
    }
×
1272

1273
    return get_base_url();
385✔
1274
}
385✔
1275

1276
std::string get_mongodb_server()
1277
{
385✔
1278
    if (auto baas_url = getenv_sv("BAAS_MONGO_URL"); !baas_url.empty()) {
385✔
1279
        return std::string{baas_url};
×
1280
    }
×
1281

1282
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
385✔
1283
        return baasaas_holder->mongo_endpoint();
385✔
1284
    }
385✔
1285
    return "mongodb://localhost:26000";
×
1286
}
385✔
1287

1288

1289
AppCreateConfig default_app_config()
1290
{
14✔
1291
    ObjectId id = ObjectId::gen();
14✔
1292
    std::string db_name = util::format("test_data_%1", id.to_string());
14✔
1293
    std::string app_url = get_base_url();
14✔
1294
    std::string admin_url = get_admin_url();
14✔
1295
    REALM_ASSERT(!app_url.empty());
14✔
1296
    REALM_ASSERT(!admin_url.empty());
14✔
1297

1298
    std::string update_user_data_func = util::format(R"(
14✔
1299
        exports = async function(data) {
14✔
1300
            const user = context.user;
14✔
1301
            const mongodb = context.services.get("BackingDB");
14✔
1302
            const userDataCollection = mongodb.db("%1").collection("UserData");
14✔
1303
            await userDataCollection.updateOne(
14✔
1304
                                               { "user_id": user.id },
14✔
1305
                                               { "$set": data },
14✔
1306
                                               { "upsert": true }
14✔
1307
                                               );
14✔
1308
            return true;
14✔
1309
        };
14✔
1310
    )",
14✔
1311
                                                     db_name);
14✔
1312

1313
    constexpr const char* sum_func = R"(
14✔
1314
        exports = function(...args) {
14✔
1315
            return args.reduce((a,b) => a + b, 0);
14✔
1316
        };
14✔
1317
    )";
14✔
1318

1319
    constexpr const char* confirm_func = R"(
14✔
1320
        exports = ({ token, tokenId, username }) => {
14✔
1321
            // process the confirm token, tokenId and username
14✔
1322
            if (username.includes("realm_tests_do_autoverify")) {
14✔
1323
              return { status: 'success' }
14✔
1324
            }
14✔
1325
            // do not confirm the user
14✔
1326
            return { status: 'fail' };
14✔
1327
        };
14✔
1328
    )";
14✔
1329

1330
    constexpr const char* auth_func = R"(
14✔
1331
        exports = (loginPayload) => {
14✔
1332
            return loginPayload["realmCustomAuthFuncUserId"];
14✔
1333
        };
14✔
1334
    )";
14✔
1335

1336
    constexpr const char* reset_func = R"(
14✔
1337
        exports = ({ token, tokenId, username, password }) => {
14✔
1338
            // process the reset token, tokenId, username and password
14✔
1339
            if (password.includes("realm_tests_do_reset")) {
14✔
1340
              return { status: 'success' };
14✔
1341
            }
14✔
1342
            // will not reset the password
14✔
1343
            return { status: 'fail' };
14✔
1344
        };
14✔
1345
    )";
14✔
1346

1347
    std::vector<AppCreateConfig::FunctionDef> funcs = {
14✔
1348
        {"updateUserData", update_user_data_func, false},
14✔
1349
        {"sumFunc", sum_func, false},
14✔
1350
        {"confirmFunc", confirm_func, false},
14✔
1351
        {"authFunc", auth_func, false},
14✔
1352
        {"resetFunc", reset_func, false},
14✔
1353
    };
14✔
1354

1355
    Property partition_key("realm_id", PropertyType::String | PropertyType::Nullable);
14✔
1356

1357
    AppCreateConfig::UserPassAuthConfig user_pass_config{
14✔
1358
        false,
14✔
1359
        "",
14✔
1360
        "confirmFunc",
14✔
1361
        "http://localhost/confirmEmail",
14✔
1362
        "resetFunc",
14✔
1363
        "",
14✔
1364
        "http://localhost/resetPassword",
14✔
1365
        true,
14✔
1366
        true,
14✔
1367
    };
14✔
1368

1369
    return AppCreateConfig{
14✔
1370
        "test",
14✔
1371
        std::move(app_url),
14✔
1372
        std::move(admin_url), // BAAS Admin API URL may be different
14✔
1373
        "unique_user@domain.com",
14✔
1374
        "password",
14✔
1375
        get_mongodb_server(),
14✔
1376
        db_name,
14✔
1377
        get_default_schema(),
14✔
1378
        std::move(partition_key),
14✔
1379
        false,                              // Dev mode disabled
14✔
1380
        util::none,                         // Default to no FLX sync config
14✔
1381
        std::move(funcs),                   // Add default functions
14✔
1382
        std::move(user_pass_config),        // enable basic user/pass auth
14✔
1383
        std::string{"authFunc"},            // custom auth function
14✔
1384
        true,                               // enable_api_key_auth
14✔
1385
        true,                               // enable_anonymous_auth
14✔
1386
        true,                               // enable_custom_token_auth
14✔
1387
        {},                                 // no service roles on the default rule
14✔
1388
        util::Logger::get_default_logger(), // provide the logger to the admin api
14✔
1389
    };
14✔
1390
}
14✔
1391

1392
AppCreateConfig minimal_app_config(const std::string& name, const Schema& schema)
1393
{
371✔
1394
    Property partition_key("realm_id", PropertyType::String | PropertyType::Nullable);
371✔
1395
    std::string app_url = get_base_url();
371✔
1396
    std::string admin_url = get_admin_url();
371✔
1397
    REALM_ASSERT(!app_url.empty());
371✔
1398
    REALM_ASSERT(!admin_url.empty());
371✔
1399

1400
    AppCreateConfig::UserPassAuthConfig user_pass_config{
371✔
1401
        true,  "Confirm", "", "http://example.com/confirmEmail", "", "Reset", "http://exmaple.com/resetPassword",
371✔
1402
        false, false,
371✔
1403
    };
371✔
1404

1405
    ObjectId id = ObjectId::gen();
371✔
1406
    return AppCreateConfig{
371✔
1407
        name,
371✔
1408
        std::move(app_url),
371✔
1409
        std::move(admin_url), // BAAS Admin API URL may be different
371✔
1410
        "unique_user@domain.com",
371✔
1411
        "password",
371✔
1412
        get_mongodb_server(),
371✔
1413
        util::format("test_data_%1_%2", name, id.to_string()),
371✔
1414
        schema,
371✔
1415
        std::move(partition_key),
371✔
1416
        false,                              // Dev mode disabled
371✔
1417
        util::none,                         // no FLX sync config
371✔
1418
        {},                                 // no functions
371✔
1419
        std::move(user_pass_config),        // enable basic user/pass auth
371✔
1420
        util::none,                         // disable custom auth
371✔
1421
        true,                               // enable api key auth
371✔
1422
        true,                               // enable anonymous auth
371✔
1423
        false,                              // enable_custom_token_auth
371✔
1424
        {},                                 // no service roles on the default rule
371✔
1425
        util::Logger::get_default_logger(), // provide the logger to the admin api
371✔
1426
    };
371✔
1427
}
371✔
1428

1429
nlohmann::json transform_service_role(const AppCreateConfig::ServiceRole& role_def)
1430
{
401✔
1431
    return {
401✔
1432
        {"name", role_def.name},
401✔
1433
        {"apply_when", role_def.apply_when},
401✔
1434
        {"document_filters",
401✔
1435
         {
401✔
1436
             {"read", role_def.document_filters.read},
401✔
1437
             {"write", role_def.document_filters.write},
401✔
1438
         }},
401✔
1439
        {"insert", role_def.insert_filter},
401✔
1440
        {"delete", role_def.delete_filter},
401✔
1441
        {"read", role_def.read},
401✔
1442
        {"write", role_def.write},
401✔
1443
    };
401✔
1444
}
401✔
1445

1446
AppSession create_app(const AppCreateConfig& config)
1447
{
387✔
1448
    auto session = AdminAPISession::login(config);
387✔
1449
    auto create_app_resp = session.apps().post_json(nlohmann::json{{"name", config.app_name}});
387✔
1450
    std::string app_id = create_app_resp["_id"];
387✔
1451
    std::string client_app_id = create_app_resp["client_app_id"];
387✔
1452

1453
    auto app = session.apps()[app_id];
387✔
1454

1455
    auto functions = app["functions"];
387✔
1456
    std::unordered_map<std::string, std::string> function_name_to_id;
387✔
1457
    for (const auto& func : config.functions) {
387✔
1458
        auto create_func_resp = functions.post_json({
80✔
1459
            {"name", func.name},
80✔
1460
            {"private", func.is_private},
80✔
1461
            {"can_evaluate", nlohmann::json::object()},
80✔
1462
            {"source", func.source},
80✔
1463
        });
80✔
1464
        function_name_to_id.insert({func.name, create_func_resp["_id"]});
80✔
1465
    }
80✔
1466

1467
    auto auth_providers = app["auth_providers"];
387✔
1468
    if (config.enable_anonymous_auth) {
387✔
1469
        auth_providers.post_json({{"type", "anon-user"}});
387✔
1470
    }
387✔
1471
    if (config.user_pass_auth) {
387✔
1472
        auto user_pass_config_obj = nlohmann::json{
387✔
1473
            {"autoConfirm", config.user_pass_auth->auto_confirm},
387✔
1474
            {"confirmEmailSubject", config.user_pass_auth->confirm_email_subject},
387✔
1475
            {"emailConfirmationUrl", config.user_pass_auth->email_confirmation_url},
387✔
1476
            {"resetPasswordSubject", config.user_pass_auth->reset_password_subject},
387✔
1477
            {"resetPasswordUrl", config.user_pass_auth->reset_password_url},
387✔
1478
        };
387✔
1479
        if (!config.user_pass_auth->confirmation_function_name.empty()) {
387✔
1480
            const auto& confirm_func_name = config.user_pass_auth->confirmation_function_name;
16✔
1481
            user_pass_config_obj.emplace("confirmationFunctionName", confirm_func_name);
16✔
1482
            user_pass_config_obj.emplace("confirmationFunctionId", function_name_to_id[confirm_func_name]);
16✔
1483
            user_pass_config_obj.emplace("runConfirmationFunction", config.user_pass_auth->run_confirmation_function);
16✔
1484
        }
16✔
1485
        if (!config.user_pass_auth->reset_function_name.empty()) {
387✔
1486
            const auto& reset_func_name = config.user_pass_auth->reset_function_name;
16✔
1487
            user_pass_config_obj.emplace("resetFunctionName", reset_func_name);
16✔
1488
            user_pass_config_obj.emplace("resetFunctionId", function_name_to_id[reset_func_name]);
16✔
1489
            user_pass_config_obj.emplace("runResetFunction", config.user_pass_auth->run_reset_function);
16✔
1490
        }
16✔
1491
        auth_providers.post_json({{"type", "local-userpass"}, {"config", std::move(user_pass_config_obj)}});
387✔
1492
    }
387✔
1493
    if (config.custom_function_auth) {
387✔
1494
        auth_providers.post_json({{"type", "custom-function"},
16✔
1495
                                  {"config",
16✔
1496
                                   {
16✔
1497
                                       {"authFunctionName", *config.custom_function_auth},
16✔
1498
                                       {"authFunctionId", function_name_to_id[*config.custom_function_auth]},
16✔
1499
                                   }}});
16✔
1500
    }
16✔
1501

1502
    if (config.enable_api_key_auth) {
387✔
1503
        auto all_auth_providers = auth_providers.get_json();
387✔
1504
        auto api_key_provider =
387✔
1505
            std::find_if(all_auth_providers.begin(), all_auth_providers.end(), [](const nlohmann::json& provider) {
387✔
1506
                return provider["type"] == "api-key";
387✔
1507
            });
387✔
1508
        REALM_ASSERT(api_key_provider != all_auth_providers.end());
387✔
1509
        std::string api_key_provider_id = (*api_key_provider)["_id"];
387✔
1510
        auto api_key_enable_resp = auth_providers[api_key_provider_id]["enable"].put("");
387✔
1511
        REALM_ASSERT(api_key_enable_resp.http_status_code >= 200 && api_key_enable_resp.http_status_code < 300);
387✔
1512
    }
387✔
1513

1514
    auto secrets = app["secrets"];
387✔
1515
    secrets.post_json({{"name", "BackingDB_uri"}, {"value", config.mongo_uri}});
387✔
1516
    secrets.post_json({{"name", "gcm"}, {"value", "gcm"}});
387✔
1517
    secrets.post_json({{"name", "customTokenKey"}, {"value", "My_very_confidential_secretttttt"}});
387✔
1518

1519
    if (config.enable_custom_token_auth) {
387✔
1520
        auth_providers.post_json(
16✔
1521
            {{"type", "custom-token"},
16✔
1522
             {"config",
16✔
1523
              {
16✔
1524
                  {"audience", nlohmann::json::array()},
16✔
1525
                  {"signingAlgorithm", "HS256"},
16✔
1526
                  {"useJWKURI", false},
16✔
1527
              }},
16✔
1528
             {"secret_config", {{"signingKeys", nlohmann::json::array({"customTokenKey"})}}},
16✔
1529
             {"disabled", false},
16✔
1530
             {"metadata_fields",
16✔
1531
              {{{"required", false}, {"name", "user_data.name"}, {"field_name", "name"}},
16✔
1532
               {{"required", true}, {"name", "user_data.occupation"}, {"field_name", "occupation"}},
16✔
1533
               {{"required", true}, {"name", "my_metadata.name"}, {"field_name", "anotherName"}}}}});
16✔
1534
    }
16✔
1535

1536
    auto services = app["services"];
387✔
1537
    static const std::string mongo_service_name = "BackingDB";
387✔
1538

1539
    nlohmann::json mongo_service_def = {
387✔
1540
        {"name", mongo_service_name},
387✔
1541
        {"type", "mongodb"},
387✔
1542
        {"config", {{"uri", config.mongo_uri}}},
387✔
1543
    };
387✔
1544
    nlohmann::json sync_config;
387✔
1545
    if (config.flx_sync_config) {
387✔
1546
        auto queryable_fields = nlohmann::json::array();
201✔
1547
        const auto& queryable_fields_src = config.flx_sync_config->queryable_fields;
201✔
1548
        std::copy(queryable_fields_src.begin(), queryable_fields_src.end(), std::back_inserter(queryable_fields));
201✔
1549
        auto asymmetric_tables = nlohmann::json::array();
201✔
1550
        for (const auto& obj_schema : config.schema) {
398✔
1551
            if (obj_schema.table_type == ObjectSchema::ObjectType::TopLevelAsymmetric) {
398✔
1552
                asymmetric_tables.emplace_back(obj_schema.name);
4✔
1553
            }
4✔
1554
        }
398✔
1555
        sync_config = nlohmann::json{{"database_name", config.mongo_dbname},
201✔
1556
                                     {"queryable_fields_names", queryable_fields},
201✔
1557
                                     {"asymmetric_tables", asymmetric_tables}};
201✔
1558
        mongo_service_def["config"]["flexible_sync"] = sync_config;
201✔
1559
    }
201✔
1560
    else {
186✔
1561
        sync_config = nlohmann::json{
186✔
1562
            {"database_name", config.mongo_dbname},
186✔
1563
            {"partition",
186✔
1564
             {
186✔
1565
                 {"key", config.partition_key.name},
186✔
1566
                 {"type", property_type_to_bson_type_str(config.partition_key.type)},
186✔
1567
                 {"required", !is_nullable(config.partition_key.type)},
186✔
1568
                 {"permissions",
186✔
1569
                  {
186✔
1570
                      {"read", true},
186✔
1571
                      {"write", true},
186✔
1572
                  }},
186✔
1573
             }},
186✔
1574
        };
186✔
1575
        mongo_service_def["config"]["sync"] = sync_config;
186✔
1576
    }
186✔
1577

1578
    auto create_mongo_service_resp = services.post_json(std::move(mongo_service_def));
387✔
1579
    std::string mongo_service_id = create_mongo_service_resp["_id"];
387✔
1580

1581
    auto default_rule = services[mongo_service_id]["default_rule"];
387✔
1582
    auto service_roles = nlohmann::json::array();
387✔
1583
    if (config.service_roles.empty()) {
387✔
1584
        service_roles.push_back(transform_service_role({"default"}));
381✔
1585
    }
381✔
1586
    else {
6✔
1587
        std::transform(config.service_roles.begin(), config.service_roles.end(), std::back_inserter(service_roles),
6✔
1588
                       transform_service_role);
6✔
1589
    }
6✔
1590

1591
    default_rule.post_json({{"roles", service_roles}});
387✔
1592

1593
    // No need for a draft because there are no breaking changes in the initial schema when the app is created.
1594
    bool use_draft = false;
387✔
1595
    session.create_schema(app_id, config, use_draft);
387✔
1596

1597
    // Enable sync after schema is created.
1598
    AdminAPISession::ServiceConfig service_config;
387✔
1599
    service_config.database_name = sync_config["database_name"];
387✔
1600
    if (config.flx_sync_config) {
387✔
1601
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Flexible;
201✔
1602
        service_config.queryable_field_names = sync_config["queryable_fields_names"];
201✔
1603
        service_config.asymmetric_tables = sync_config["asymmetric_tables"];
201✔
1604
    }
201✔
1605
    else {
186✔
1606
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
186✔
1607
        service_config.partition = sync_config["partition"];
186✔
1608
    }
186✔
1609
    session.enable_sync(app_id, mongo_service_id, service_config);
387✔
1610

1611
    app["sync"]["config"].put_json({{"development_mode_enabled", config.dev_mode_enabled}});
387✔
1612

1613
    auto rules = services[mongo_service_id]["rules"];
387✔
1614
    rules.post_json({
387✔
1615
        {"database", config.mongo_dbname},
387✔
1616
        {"collection", "UserData"},
387✔
1617
        {"roles",
387✔
1618
         {{{"name", "default"},
387✔
1619
           {"apply_when", nlohmann::json::object()},
387✔
1620
           {"document_filters",
387✔
1621
            {
387✔
1622
                {"read", true},
387✔
1623
                {"write", true},
387✔
1624
            }},
387✔
1625
           {"read", true},
387✔
1626
           {"write", true},
387✔
1627
           {"insert", true},
387✔
1628
           {"delete", true}}}},
387✔
1629
    });
387✔
1630

1631
    app["custom_user_data"].patch_json({
387✔
1632
        {"mongo_service_id", mongo_service_id},
387✔
1633
        {"enabled", true},
387✔
1634
        {"database_name", config.mongo_dbname},
387✔
1635
        {"collection_name", "UserData"},
387✔
1636
        {"user_id_field", "user_id"},
387✔
1637
    });
387✔
1638

1639
    services.post_json({
387✔
1640
        {"name", "gcm"},
387✔
1641
        {"type", "gcm"},
387✔
1642
        {"config",
387✔
1643
         {
387✔
1644
             {"senderId", "gcm"},
387✔
1645
         }},
387✔
1646
        {"secret_config",
387✔
1647
         {
387✔
1648
             {"apiKey", "gcm"},
387✔
1649
         }},
387✔
1650
        {"version", 1},
387✔
1651
    });
387✔
1652

1653
    // Wait for initial sync to complete, as connecting while this is happening
1654
    // causes various problems
1655
    bool any_sync_types = std::any_of(config.schema.begin(), config.schema.end(), [](auto& object_schema) {
439✔
1656
        return object_schema.table_type == ObjectSchema::ObjectType::TopLevel;
439✔
1657
    });
439✔
1658
    if (any_sync_types) {
387✔
1659
        // Increasing timeout due to occasional slow startup of the translator on baasaas
1660
        timed_sleeping_wait_for(
381✔
1661
            [&] {
1,433✔
1662
                return session.is_initial_sync_complete(app_id);
1,433✔
1663
            },
1,433✔
1664
            std::chrono::seconds(60), std::chrono::seconds(1));
381✔
1665
    }
381✔
1666

1667
    return {client_app_id, app_id, session, config};
387✔
1668
}
387✔
1669

1670
AppSession get_runtime_app_session()
1671
{
162✔
1672
    static const AppSession cached_app_session = [&] {
162✔
1673
        auto cached_app_session = create_app(default_app_config());
2✔
1674
        return cached_app_session;
2✔
1675
    }();
2✔
1676
    return cached_app_session;
162✔
1677
}
162✔
1678

1679

1680
#ifdef REALM_MONGODB_ENDPOINT
1681
TEST_CASE("app: baas admin api", "[sync][app][admin api][baas]") {
1682
    SECTION("embedded objects") {
1683
        Schema schema{{"top",
1684
                       {{"_id", PropertyType::String, true},
1685
                        {"location", PropertyType::Object | PropertyType::Nullable, "location"}}},
1686
                      {"location",
1687
                       ObjectSchema::ObjectType::Embedded,
1688
                       {{"coordinates", PropertyType::Double | PropertyType::Array}}}};
1689

1690
        auto test_app_config = minimal_app_config("test", schema);
1691
        create_app(test_app_config);
1692
    }
1693

1694
    SECTION("embedded object with array") {
1695
        Schema schema{
1696
            {"a",
1697
             {{"_id", PropertyType::String, true},
1698
              {"b_link", PropertyType::Object | PropertyType::Array | PropertyType::Nullable, "b"}}},
1699
            {"b",
1700
             ObjectSchema::ObjectType::Embedded,
1701
             {{"c_link", PropertyType::Object | PropertyType::Nullable, "c"}}},
1702
            {"c", {{"_id", PropertyType::String, true}, {"d_str", PropertyType::String}}},
1703
        };
1704
        auto test_app_config = minimal_app_config("test", schema);
1705
        create_app(test_app_config);
1706
    }
1707

1708
    SECTION("dictionaries") {
1709
        Schema schema{
1710
            {"a", {{"_id", PropertyType::String, true}, {"b_dict", PropertyType::Dictionary | PropertyType::String}}},
1711
        };
1712

1713
        auto test_app_config = minimal_app_config("test", schema);
1714
        create_app(test_app_config);
1715
    }
1716

1717
    SECTION("set") {
1718
        Schema schema{
1719
            {"a", {{"_id", PropertyType::String, true}, {"b_dict", PropertyType::Set | PropertyType::String}}},
1720
        };
1721

1722
        auto test_app_config = minimal_app_config("test", schema);
1723
        create_app(test_app_config);
1724
    }
1725
}
1726
#endif
1727

1728
} // namespace realm
1729

1730
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc