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

realm / realm-core / 2544

05 Aug 2024 04:04PM UTC coverage: 91.108%. Remained the same
2544

push

Evergreen

web-flow
Only track pending client resets done by the same core version (#7944)

If the previous attempt at performing a client reset was done with a different
core version then we should retry the client reset as the new version may have
fixed a bug that made the previous attempt fail (or may be a downgrade to a
version before when the bug was introduced). This also simplifies the tracking
as it means that we don't need to be able to read trackers created by different
versions.

This also means that we can freely change the schema of the table, which this
takes advantage of to drop the unused primary key and make the error required,
as we never actually stored null and the code reading it would have crashed if
it encountered a null error.

102728 of 181534 branches covered (56.59%)

138 of 153 new or added lines in 10 files covered. (90.2%)

59 existing lines in 11 files now uncovered.

216763 of 237918 relevant lines covered (91.11%)

5998814.86 hits per line

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

84.34
/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
#include <util/sync/redirect_server.hpp>
21

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

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

26
#if REALM_ENABLE_AUTH_TESTS
27

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

278
    return {};
1,405✔
279
}
1,600✔
280

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

283
} // namespace
284

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

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

299
    std::string response;
17,229✔
300
    app::HttpHeaders response_headers;
17,229✔
301

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

307
    /* Now specify the POST data */
308
    if (request.method == app::HttpMethod::post) {
17,229✔
309
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
8,237✔
310
    }
8,237✔
311
    else if (request.method == app::HttpMethod::put) {
8,992✔
312
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
2,164✔
313
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
2,164✔
314
    }
2,164✔
315
    else if (request.method == app::HttpMethod::patch) {
6,828✔
316
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
800✔
317
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
800✔
318
    }
800✔
319
    else if (request.method == app::HttpMethod::del) {
6,028✔
320
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
465✔
321
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
465✔
322
    }
465✔
323
    else if (request.method == app::HttpMethod::patch) {
5,563✔
324
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
×
325
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
×
326
    }
×
327

328
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, request.timeout_ms);
17,229✔
329

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

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

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

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

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

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

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

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

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

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

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

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

447
    void poll()
448
    {
1,193✔
449
        if (!m_http_endpoint.empty() || m_container_id.empty()) {
1,193✔
450
            return;
1,191✔
451
        }
1,191✔
452

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

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

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

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

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

519
    const std::string admin_endpoint()
520
    {
389✔
521
        poll();
389✔
522
        return m_http_endpoint;
389✔
523
    }
389✔
524

525
    std::string http_endpoint()
526
    {
415✔
527
        poll();
415✔
528
        return m_http_endpoint;
415✔
529
    }
415✔
530

531
    const std::string& mongo_endpoint()
532
    {
389✔
533
        poll();
389✔
534
        return m_mongo_endpoint;
389✔
535
    }
389✔
536

537
private:
538
    std::pair<nlohmann::json, std::string> do_request(std::string api_path, app::HttpMethod method)
539
    {
8✔
540
        app::Request request;
8✔
541

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

564
    std::string baas_coid_from_response(const app::Response& resp)
565
    {
8✔
566
        if (auto it = resp.headers.find(g_baas_coid_header_name); it != resp.headers.end()) {
8✔
567
            return it->second;
8✔
568
        }
8✔
569
        return "<not found>";
×
570
    }
8✔
571

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

581
        return unquote_string(env_value);
×
582
    }
2✔
583

584
    constexpr static std::string_view s_baasaas_lock_file_name = "baasaas_instance.lock";
585

586
    std::string m_api_key;
587
    std::string m_base_url;
588
    std::string m_container_id;
589
    bool m_externally_managed_instance;
590
    std::string m_http_endpoint;
591
    std::string m_mongo_endpoint;
592
};
593

594
static std::optional<sync::RedirectingHttpServer>& get_redirector(const std::string& base_url)
595
{
393✔
596
    static std::optional<sync::RedirectingHttpServer> redirector;
393✔
597
    auto redirector_enabled = [&] {
393✔
598
        const static auto enabled_values = {"On", "on", "1"};
393✔
599
        auto enable_redirector = getenv_sv("ENABLE_BAAS_REDIRECTOR");
393✔
600
        return std::any_of(enabled_values.begin(), enabled_values.end(), [&](const auto val) {
801✔
601
            return val == enable_redirector;
801✔
602
        });
801✔
603
    };
393✔
604

605
    if (redirector_enabled() && !redirector && !base_url.empty()) {
393✔
606
        redirector.emplace(base_url, util::Logger::get_default_logger());
1✔
607
    }
1✔
608

609
    return redirector;
393✔
610
}
393✔
611

612
class BaasaasLauncher : public Catch::EventListenerBase {
613
public:
614
    static std::optional<Baasaas>& get_baasaas_holder()
615
    {
1,199✔
616
        static std::optional<Baasaas> global_baasaas = std::nullopt;
1,199✔
617
        return global_baasaas;
1,199✔
618
    }
1,199✔
619

620
    using Catch::EventListenerBase::EventListenerBase;
621

622
    void testRunStarting(Catch::TestRunInfo const&) override
623
    {
4✔
624
        std::string_view api_key(getenv_sv("BAASAAS_API_KEY"));
4✔
625
        if (api_key.empty()) {
4✔
626
            return;
2✔
627
        }
2✔
628

629
        // Allow overriding the baas base url at runtime via an environment variable, even if BAASAAS_API_KEY
630
        // is also specified.
631
        if (!getenv_sv("BAAS_BASE_URL").empty()) {
2✔
632
            return;
×
633
        }
×
634

635
        // If we've started a baasaas container outside of running the tests, then use that instead of
636
        // figuring out how to start our own.
637
        if (auto baasaas_instance = getenv_sv("BAASAAS_INSTANCE_ID"); !baasaas_instance.empty()) {
2✔
638
            auto& baasaas_holder = get_baasaas_holder();
×
639
            REALM_ASSERT(!baasaas_holder);
×
640
            baasaas_holder.emplace(std::string{api_key}, std::string{baasaas_instance});
×
641
            return;
×
642
        }
×
643

644
        std::string_view ref_spec(getenv_sv("BAASAAS_REF_SPEC"));
2✔
645
        std::string_view mode_spec(getenv_sv("BAASAAS_START_MODE"));
2✔
646
        Baasaas::StartMode mode = Baasaas::StartMode::Default;
2✔
647
        if (mode_spec == "branch") {
2✔
648
            if (ref_spec.empty()) {
×
649
                throw std::runtime_error("Expected branch name in BAASAAS_REF_SPEC env variable, but it was empty");
×
650
            }
×
651
            mode = Baasaas::StartMode::Branch;
×
652
        }
×
653
        else if (mode_spec == "githash") {
2✔
654
            if (ref_spec.empty()) {
2✔
655
                throw std::runtime_error("Expected git hash in BAASAAS_REF_SPEC env variable, but it was empty");
×
656
            }
×
657
            mode = Baasaas::StartMode::GitHash;
2✔
658
        }
2✔
659
        else if (mode_spec == "patchid") {
×
660
            if (ref_spec.empty()) {
×
661
                throw std::runtime_error("Expected patch id in BAASAAS_REF_SPEC env variable, but it was empty");
×
662
            }
×
663
            mode = Baasaas::StartMode::PatchId;
×
664
        }
×
665
        else {
×
666
            if (!mode_spec.empty()) {
×
667
                throw std::runtime_error("Expected BAASAAS_START_MODE to be \"githash\", \"patchid\", or \"branch\"");
×
668
            }
×
669
            ref_spec = {};
×
670
        }
×
671

672
        auto& baasaas_holder = get_baasaas_holder();
2✔
673
        REALM_ASSERT(!baasaas_holder);
2✔
674
        baasaas_holder.emplace(std::string{api_key}, mode, std::string{ref_spec});
2✔
675

676
        get_runtime_app_session();
2✔
677
    }
2✔
678

679
    void testRunEnded(Catch::TestRunStats const&) override
680
    {
4✔
681
        if (auto& redirector = get_redirector({})) {
4✔
682
            redirector = std::nullopt;
1✔
683
        }
1✔
684

685
        if (auto& baasaas_holder = get_baasaas_holder()) {
4✔
686
            baasaas_holder->stop();
2✔
687
        }
2✔
688
    }
4✔
689
};
690

691
CATCH_REGISTER_LISTENER(BaasaasLauncher)
692

693
AdminAPIEndpoint AdminAPIEndpoint::operator[](StringData name) const
694
{
19,521✔
695
    return AdminAPIEndpoint(util::format("%1/%2", m_url, name), m_access_token);
19,521✔
696
}
19,521✔
697

698
app::Response AdminAPIEndpoint::do_request(app::Request request) const
699
{
12,274✔
700
    if (request.url.find('?') == std::string::npos) {
12,274✔
701
        request.url = util::format("%1?bypass_service_change=SyncSchemaVersionIncrease", request.url);
12,274✔
702
    }
12,274✔
703
    else {
×
704
        request.url = util::format("%1&bypass_service_change=SyncSchemaVersionIncrease", request.url);
×
705
    }
×
706
    request.headers["Content-Type"] = "application/json;charset=utf-8";
12,274✔
707
    request.headers["Accept"] = "application/json";
12,274✔
708
    request.headers["Authorization"] = util::format("Bearer %1", m_access_token);
12,274✔
709
    return do_http_request(std::move(request));
12,274✔
710
}
12,274✔
711

712
app::Response AdminAPIEndpoint::get(const std::vector<std::pair<std::string, std::string>>& params) const
713
{
3,889✔
714
    app::Request req;
3,889✔
715
    req.method = app::HttpMethod::get;
3,889✔
716
    std::stringstream ss;
3,889✔
717
    bool needs_and = false;
3,889✔
718
    ss << m_url;
3,889✔
719
    if (!params.empty() && m_url.find('?') != std::string::npos) {
3,889!
720
        needs_and = true;
×
721
    }
×
722
    for (const auto& param : params) {
3,889✔
723
        if (needs_and) {
×
724
            ss << "&";
×
725
        }
×
726
        else {
×
727
            ss << "?";
×
728
        }
×
729
        needs_and = true;
×
730
        ss << param.first << "=" << param.second;
×
731
    }
×
732
    req.url = ss.str();
3,889✔
733
    return do_request(std::move(req));
3,889✔
734
}
3,889✔
735

736
app::Response AdminAPIEndpoint::del() const
737
{
421✔
738
    app::Request req;
421✔
739
    req.method = app::HttpMethod::del;
421✔
740
    req.url = m_url;
421✔
741
    return do_request(std::move(req));
421✔
742
}
421✔
743

744
nlohmann::json AdminAPIEndpoint::get_json(const std::vector<std::pair<std::string, std::string>>& params) const
745
{
3,889✔
746
    auto resp = get(params);
3,889✔
747
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, resp.http_status_code,
3,889✔
748
                    resp.body);
3,889✔
749
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
3,889✔
750
}
3,889✔
751

752
app::Response AdminAPIEndpoint::post(std::string body) const
753
{
5,024✔
754
    app::Request req;
5,024✔
755
    req.method = app::HttpMethod::post;
5,024✔
756
    req.url = m_url;
5,024✔
757
    req.body = std::move(body);
5,024✔
758
    return do_request(std::move(req));
5,024✔
759
}
5,024✔
760

761
nlohmann::json AdminAPIEndpoint::post_json(nlohmann::json body) const
762
{
5,002✔
763
    auto resp = post(body.dump());
5,002✔
764
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(),
5,002✔
765
                    resp.http_status_code, resp.body);
5,002✔
766
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
5,002✔
767
}
5,002✔
768

769
app::Response AdminAPIEndpoint::put(std::string body) const
770
{
2,140✔
771
    app::Request req;
2,140✔
772
    req.method = app::HttpMethod::put;
2,140✔
773
    req.url = m_url;
2,140✔
774
    req.body = std::move(body);
2,140✔
775
    return do_request(std::move(req));
2,140✔
776
}
2,140✔
777

778
nlohmann::json AdminAPIEndpoint::put_json(nlohmann::json body) const
779
{
1,741✔
780
    auto resp = put(body.dump());
1,741✔
781
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(),
1,741✔
782
                    resp.http_status_code, resp.body);
1,741✔
783
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
1,741✔
784
}
1,741✔
785

786
app::Response AdminAPIEndpoint::patch(std::string body) const
787
{
800✔
788
    app::Request req;
800✔
789
    req.method = app::HttpMethod::patch;
800✔
790
    req.url = m_url;
800✔
791
    req.body = std::move(body);
800✔
792
    return do_request(std::move(req));
800✔
793
}
800✔
794

795
nlohmann::json AdminAPIEndpoint::patch_json(nlohmann::json body) const
796
{
800✔
797
    auto resp = patch(body.dump());
800✔
798
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(),
800✔
799
                    resp.http_status_code, resp.body);
800✔
800
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
800✔
801
}
800✔
802

803
AdminAPISession AdminAPISession::login(const AppCreateConfig& config)
804
{
391✔
805
    std::string admin_url = config.admin_url;
391✔
806
    nlohmann::json login_req_body{
391✔
807
        {"provider", "userpass"},
391✔
808
        {"username", config.admin_username},
391✔
809
        {"password", config.admin_password},
391✔
810
    };
391✔
811
    if (config.logger) {
391✔
812
        config.logger->trace("Logging into baas admin api: %1", admin_url);
391✔
813
    }
391✔
814
    app::Request auth_req{
391✔
815
        app::HttpMethod::post,
391✔
816
        util::format("%1/api/admin/v3.0/auth/providers/local-userpass/login", admin_url),
391✔
817
        60000, // 1 minute timeout
391✔
818
        {
391✔
819
            {"Content-Type", "application/json;charset=utf-8"},
391✔
820
            {"Accept", "application/json"},
391✔
821
        },
391✔
822
        login_req_body.dump(),
391✔
823
    };
391✔
824
    auto login_resp = do_http_request(std::move(auth_req));
391✔
825
    REALM_ASSERT_EX(login_resp.http_status_code == 200, login_resp.http_status_code, login_resp.body);
391✔
826
    auto login_resp_body = nlohmann::json::parse(login_resp.body);
391✔
827

828
    std::string access_token = login_resp_body["access_token"];
391✔
829

830
    AdminAPIEndpoint user_profile(util::format("%1/api/admin/v3.0/auth/profile", admin_url), access_token);
391✔
831
    auto profile_resp = user_profile.get_json();
391✔
832

833
    std::string group_id = profile_resp["roles"][0]["group_id"];
391✔
834

835
    return AdminAPISession(std::move(admin_url), std::move(access_token), std::move(group_id));
391✔
836
}
391✔
837

838
void AdminAPISession::revoke_user_sessions(const std::string& user_id, const std::string& app_id) const
839
{
4✔
840
    auto endpoint = apps()[app_id]["users"][user_id]["logout"];
4✔
841
    auto response = endpoint.put("");
4✔
842
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
4✔
843
}
4✔
844

845
void AdminAPISession::disable_user_sessions(const std::string& user_id, const std::string& app_id) const
846
{
2✔
847
    auto endpoint = apps()[app_id]["users"][user_id]["disable"];
2✔
848
    auto response = endpoint.put("");
2✔
849
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
2✔
850
}
2✔
851

852
void AdminAPISession::enable_user_sessions(const std::string& user_id, const std::string& app_id) const
853
{
2✔
854
    auto endpoint = apps()[app_id]["users"][user_id]["enable"];
2✔
855
    auto response = endpoint.put("");
2✔
856
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
2✔
857
}
2✔
858

859
// returns false for an invalid/expired access token
860
bool AdminAPISession::verify_access_token(const std::string& access_token, const std::string& app_id) const
861
{
22✔
862
    auto endpoint = apps()[app_id]["users"]["verify_token"];
22✔
863
    nlohmann::json request_body{
22✔
864
        {"token", access_token},
22✔
865
    };
22✔
866
    auto response = endpoint.post(request_body.dump());
22✔
867
    if (response.http_status_code == 200) {
22✔
868
        auto resp_json = nlohmann::json::parse(response.body.empty() ? "{}" : response.body);
16✔
869
        try {
16✔
870
            // if these fields are found, then the token is valid according to the server.
871
            // if it is invalid or expired then an error response is sent.
872
            int64_t issued_at = resp_json["iat"];
16✔
873
            int64_t expires_at = resp_json["exp"];
16✔
874
            return issued_at != 0 && expires_at != 0;
16✔
875
        }
16✔
876
        catch (...) {
16✔
877
            return false;
×
878
        }
×
879
    }
16✔
880
    return false;
6✔
881
}
22✔
882

883
void AdminAPISession::set_development_mode_to(const std::string& app_id, bool enable) const
884
{
24✔
885
    auto endpoint = apps()[app_id]["sync"]["config"];
24✔
886
    endpoint.put_json({{"development_mode_enabled", enable}});
24✔
887
}
24✔
888

889
void AdminAPISession::delete_app(const std::string& app_id) const
890
{
389✔
891
    auto app_endpoint = apps()[app_id];
389✔
892
    auto resp = app_endpoint.del();
389✔
893
    REALM_ASSERT_EX(resp.http_status_code == 204, resp.http_status_code, resp.body);
389✔
894
}
389✔
895

896
std::vector<AdminAPISession::Service> AdminAPISession::get_services(const std::string& app_id) const
897
{
216✔
898
    auto endpoint = apps()[app_id]["services"];
216✔
899
    auto response = endpoint.get_json();
216✔
900
    std::vector<AdminAPISession::Service> services;
216✔
901
    for (auto service : response) {
432✔
902
        services.push_back(
432✔
903
            {service["_id"], service["name"], service["type"], service["version"], service["last_modified"]});
432✔
904
    }
432✔
905
    return services;
216✔
906
}
216✔
907

908

909
std::vector<std::string> AdminAPISession::get_errors(const std::string& app_id) const
910
{
×
911
    auto endpoint = apps()[app_id]["logs"];
×
912
    auto response = endpoint.get_json({{"errors_only", "true"}});
×
913
    std::vector<std::string> errors;
×
914
    const auto& logs = response["logs"];
×
915
    std::transform(logs.begin(), logs.end(), std::back_inserter(errors), [](const auto& err) {
×
916
        return err["error"];
×
917
    });
×
918
    return errors;
×
919
}
×
920

921

922
AdminAPISession::Service AdminAPISession::get_sync_service(const std::string& app_id) const
923
{
216✔
924
    auto services = get_services(app_id);
216✔
925
    auto sync_service = std::find_if(services.begin(), services.end(), [&](auto s) {
216✔
926
        return s.type == "mongodb";
216✔
927
    });
216✔
928
    REALM_ASSERT(sync_service != services.end());
216✔
929
    return *sync_service;
216✔
930
}
216✔
931

932
void AdminAPISession::trigger_client_reset(const std::string& app_id, int64_t file_ident) const
933
{
178✔
934
    auto endpoint = apps(APIFamily::Admin)[app_id]["sync"]["force_reset"];
178✔
935
    endpoint.put_json(nlohmann::json{{"file_ident", file_ident}});
178✔
936
}
178✔
937

938
void AdminAPISession::migrate_to_flx(const std::string& app_id, const std::string& service_id,
939
                                     bool migrate_to_flx) const
940
{
30✔
941
    auto endpoint = apps()[app_id]["sync"]["migration"];
30✔
942
    endpoint.put_json(nlohmann::json{{"serviceId", service_id}, {"action", migrate_to_flx ? "start" : "rollback"}});
30✔
943
}
30✔
944

945
// Each breaking change bumps the schema version, so you can create a new version for each breaking change if
946
// 'use_draft' is false. Set 'use_draft' to true if you want all changes to the schema to be deployed at once
947
// resulting in only one schema version.
948
void AdminAPISession::create_schema(const std::string& app_id, const AppCreateConfig& config, bool use_draft) const
949
{
429✔
950
    static const std::string mongo_service_name = "BackingDB";
429✔
951

952
    auto drafts = apps()[app_id]["drafts"];
429✔
953
    std::string draft_id;
429✔
954
    if (use_draft) {
429✔
955
        auto draft_create_resp = drafts.post_json({});
38✔
956
        draft_id = draft_create_resp["_id"];
38✔
957
    }
38✔
958

959
    auto schemas = apps()[app_id]["schemas"];
429✔
960
    auto current_schema = schemas.get_json();
429✔
961
    auto target_schema = config.schema;
429✔
962

963
    std::unordered_map<std::string, std::string> current_schema_tables;
429✔
964
    for (const auto& schema : current_schema) {
429✔
965
        current_schema_tables[schema["metadata"]["collection"]] = schema["_id"];
148✔
966
    }
148✔
967

968
    // Add new tables
969

970
    auto pk_and_queryable_only = [&](const Property& prop) {
3,367✔
971
        if (config.flx_sync_config) {
3,367✔
972
            const auto& queryable_fields = config.flx_sync_config->queryable_fields;
1,537✔
973

974
            if (std::find(queryable_fields.begin(), queryable_fields.end(), prop.name) != queryable_fields.end()) {
1,537✔
975
                return true;
542✔
976
            }
542✔
977
        }
1,537✔
978
        return prop.name == "_id" || prop.name == config.partition_key.name;
2,825✔
979
    };
3,367✔
980

981
    // Create the schemas in two passes: first populate just the primary key and
982
    // partition key, then add the rest of the properties. This ensures that the
983
    // targets of links exist before adding the links.
984
    std::vector<std::pair<std::string, const ObjectSchema*>> object_schema_to_create;
429✔
985
    BaasRuleBuilder rule_builder(target_schema, config.partition_key, mongo_service_name, config.mongo_dbname,
429✔
986
                                 static_cast<bool>(config.flx_sync_config));
429✔
987
    for (const auto& obj_schema : target_schema) {
1,020✔
988
        auto it = current_schema_tables.find(obj_schema.name);
1,020✔
989
        if (it != current_schema_tables.end()) {
1,020✔
990
            object_schema_to_create.push_back({it->second, &obj_schema});
116✔
991
            continue;
116✔
992
        }
116✔
993

994
        auto schema_to_create = rule_builder.object_schema_to_baas_schema(obj_schema, pk_and_queryable_only);
904✔
995
        auto schema_create_resp = schemas.post_json(schema_to_create);
904✔
996
        object_schema_to_create.push_back({schema_create_resp["_id"], &obj_schema});
904✔
997
    }
904✔
998

999
    // Update existing tables (including the ones just created)
1000
    for (const auto& [id, obj_schema] : object_schema_to_create) {
1,020✔
1001
        auto schema_to_create = rule_builder.object_schema_to_baas_schema(*obj_schema, nullptr);
1,020✔
1002
        schema_to_create["_id"] = id;
1,020✔
1003
        schemas[id].put_json(schema_to_create);
1,020✔
1004
    }
1,020✔
1005

1006
    // Delete removed tables
1007
    for (const auto& table : current_schema_tables) {
429✔
1008
        if (target_schema.find(table.first) == target_schema.end()) {
148✔
1009
            schemas[table.second].del();
32✔
1010
        }
32✔
1011
    }
148✔
1012

1013
    if (use_draft) {
429✔
1014
        drafts[draft_id]["deployment"].post_json({});
38✔
1015
    }
38✔
1016
}
429✔
1017

1018
bool AdminAPISession::set_feature_flag(const std::string& app_id, const std::string& flag_name, bool enable) const
1019
{
×
1020
    auto features = apps(APIFamily::Private)[app_id]["features"];
×
1021
    auto flag_response =
×
1022
        features.post_json(nlohmann::json{{"action", enable ? "enable" : "disable"}, {"feature_flags", {flag_name}}});
×
1023
    return flag_response.empty();
×
1024
}
×
1025

1026
bool AdminAPISession::get_feature_flag(const std::string& app_id, const std::string& flag_name) const
1027
{
×
1028
    auto features = apps(APIFamily::Private)[app_id]["features"];
×
1029
    auto response = features.get_json();
×
1030
    if (auto feature_list = response["enabled"]; !feature_list.empty()) {
×
1031
        return std::find_if(feature_list.begin(), feature_list.end(), [&flag_name](const auto& feature) {
×
1032
                   return feature == flag_name;
×
1033
               }) != feature_list.end();
×
1034
    }
×
1035
    return false;
×
1036
}
×
1037

1038
nlohmann::json AdminAPISession::get_default_rule(const std::string& app_id) const
1039
{
50✔
1040
    auto baas_sync_service = get_sync_service(app_id);
50✔
1041
    auto rule_endpoint = apps()[app_id]["services"][baas_sync_service.id]["default_rule"];
50✔
1042
    auto rule = rule_endpoint.get_json();
50✔
1043
    return rule;
50✔
1044
}
50✔
1045

1046
bool AdminAPISession::update_default_rule(const std::string& app_id, nlohmann::json rule_json) const
1047
{
98✔
1048
    if (auto id = rule_json.find("_id");
98✔
1049
        id == rule_json.end() || !id->is_string() || id->get<std::string>().empty()) {
98✔
1050
        return false;
×
1051
    }
×
1052

1053
    auto baas_sync_service = get_sync_service(app_id);
98✔
1054
    auto rule_endpoint = apps()[app_id]["services"][baas_sync_service.id]["default_rule"];
98✔
1055
    auto response = rule_endpoint.put_json(rule_json);
98✔
1056
    return response.empty();
98✔
1057
}
98✔
1058

1059
nlohmann::json AdminAPISession::get_app_settings(const std::string& app_id) const
1060
{
×
1061
    auto settings_endpoint = apps(APIFamily::Private)[app_id]["settings"];
×
1062
    return settings_endpoint.get_json();
×
1063
}
×
1064

1065
bool AdminAPISession::patch_app_settings(const std::string& app_id, nlohmann::json&& json) const
1066
{
12✔
1067
    auto settings_endpoint = apps(APIFamily::Private)[app_id]["settings"];
12✔
1068
    auto response = settings_endpoint.patch_json(std::move(json));
12✔
1069
    return response.empty();
12✔
1070
}
12✔
1071

1072
static nlohmann::json convert_config(AdminAPISession::ServiceConfig config)
1073
{
397✔
1074
    if (config.mode == AdminAPISession::ServiceConfig::SyncMode::Flexible) {
397✔
1075
        auto payload = nlohmann::json{{"database_name", config.database_name},
207✔
1076
                                      {"state", config.state},
207✔
1077
                                      {"is_recovery_mode_disabled", config.recovery_is_disabled}};
207✔
1078
        if (config.queryable_field_names) {
207✔
1079
            payload["queryable_fields_names"] = *config.queryable_field_names;
207✔
1080
        }
207✔
1081
        if (config.permissions) {
207✔
1082
            payload["permissions"] = *config.permissions;
2✔
1083
        }
2✔
1084
        if (config.asymmetric_tables) {
207✔
1085
            payload["asymmetric_tables"] = *config.asymmetric_tables;
205✔
1086
        }
205✔
1087
        return payload;
207✔
1088
    }
207✔
1089
    return nlohmann::json{{"database_name", config.database_name},
190✔
1090
                          {"partition", *config.partition},
190✔
1091
                          {"state", config.state},
190✔
1092
                          {"is_recovery_mode_disabled", config.recovery_is_disabled}};
190✔
1093
}
397✔
1094

1095
AdminAPIEndpoint AdminAPISession::service_config_endpoint(const std::string& app_id,
1096
                                                          const std::string& service_id) const
1097
{
439✔
1098
    return apps()[app_id]["services"][service_id]["config"];
439✔
1099
}
439✔
1100

1101
AdminAPISession::ServiceConfig AdminAPISession::disable_sync(const std::string& app_id, const std::string& service_id,
1102
                                                             AdminAPISession::ServiceConfig sync_config) const
1103
{
×
1104
    auto endpoint = service_config_endpoint(app_id, service_id);
×
1105
    if (sync_config.state != "") {
×
1106
        sync_config.state = "";
×
1107
        endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
×
1108
    }
×
1109
    return sync_config;
×
1110
}
×
1111

1112
AdminAPISession::ServiceConfig AdminAPISession::pause_sync(const std::string& app_id, const std::string& service_id,
1113
                                                           AdminAPISession::ServiceConfig sync_config) const
1114
{
×
1115
    auto endpoint = service_config_endpoint(app_id, service_id);
×
1116
    if (sync_config.state != "disabled") {
×
1117
        sync_config.state = "disabled";
×
1118
        endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
×
1119
    }
×
1120
    return sync_config;
×
1121
}
×
1122

1123
AdminAPISession::ServiceConfig AdminAPISession::enable_sync(const std::string& app_id, const std::string& service_id,
1124
                                                            AdminAPISession::ServiceConfig sync_config) const
1125
{
393✔
1126
    auto endpoint = service_config_endpoint(app_id, service_id);
393✔
1127
    sync_config.state = "enabled";
393✔
1128
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
393✔
1129
    return sync_config;
393✔
1130
}
393✔
1131

1132
AdminAPISession::ServiceConfig AdminAPISession::set_disable_recovery_to(const std::string& app_id,
1133
                                                                        const std::string& service_id,
1134
                                                                        ServiceConfig sync_config, bool disable) const
1135
{
4✔
1136
    auto endpoint = service_config_endpoint(app_id, service_id);
4✔
1137
    sync_config.recovery_is_disabled = disable;
4✔
1138
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
4✔
1139
    return sync_config;
4✔
1140
}
4✔
1141

1142
std::vector<AdminAPISession::SchemaVersionInfo> AdminAPISession::get_schema_versions(const std::string& app_id) const
1143
{
106✔
1144
    std::vector<AdminAPISession::SchemaVersionInfo> ret;
106✔
1145
    auto endpoint = apps()[app_id]["sync"]["schemas"]["versions"];
106✔
1146
    auto res = endpoint.get_json();
106✔
1147
    for (auto&& version : res["versions"].get<std::vector<nlohmann::json>>()) {
157✔
1148
        SchemaVersionInfo info;
157✔
1149
        info.version_major = version["version_major"];
157✔
1150
        ret.push_back(std::move(info));
157✔
1151
    }
157✔
1152

1153
    return ret;
106✔
1154
}
106✔
1155

1156
AdminAPISession::ServiceConfig AdminAPISession::get_config(const std::string& app_id,
1157
                                                           const AdminAPISession::Service& service) const
1158
{
42✔
1159
    auto endpoint = service_config_endpoint(app_id, service.id);
42✔
1160
    auto response = endpoint.get_json();
42✔
1161
    AdminAPISession::ServiceConfig config;
42✔
1162
    if (response.contains("flexible_sync")) {
42✔
1163
        auto sync = response["flexible_sync"];
16✔
1164
        config.mode = AdminAPISession::ServiceConfig::SyncMode::Flexible;
16✔
1165
        config.state = sync["state"];
16✔
1166
        config.database_name = sync["database_name"];
16✔
1167
        config.permissions = sync["permissions"];
16✔
1168
        config.queryable_field_names = sync["queryable_fields_names"];
16✔
1169
        auto recovery_disabled = sync["is_recovery_mode_disabled"];
16✔
1170
        config.recovery_is_disabled = recovery_disabled.is_boolean() ? recovery_disabled.get<bool>() : false;
16✔
1171
    }
16✔
1172
    else if (response.contains("sync")) {
26✔
1173
        auto sync = response["sync"];
26✔
1174
        config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
26✔
1175
        config.state = sync["state"];
26✔
1176
        config.database_name = sync["database_name"];
26✔
1177
        config.partition = sync["partition"];
26✔
1178
        auto recovery_disabled = sync["is_recovery_mode_disabled"];
26✔
1179
        config.recovery_is_disabled = recovery_disabled.is_boolean() ? recovery_disabled.get<bool>() : false;
26✔
1180
    }
26✔
1181
    else {
×
1182
        throw std::runtime_error(util::format("Unsupported config format from server: %1", response));
×
1183
    }
×
1184
    return config;
42✔
1185
}
42✔
1186

1187
bool AdminAPISession::is_sync_enabled(const std::string& app_id) const
1188
{
30✔
1189
    auto sync_service = get_sync_service(app_id);
30✔
1190
    auto config = get_config(app_id, sync_service);
30✔
1191
    return config.state == "enabled";
30✔
1192
}
30✔
1193

1194
bool AdminAPISession::is_sync_terminated(const std::string& app_id) const
1195
{
×
1196
    auto sync_service = get_sync_service(app_id);
×
1197
    auto config = get_config(app_id, sync_service);
×
1198
    if (config.state == "enabled") {
×
1199
        return false;
×
1200
    }
×
1201
    auto state_endpoint = apps()[app_id]["sync"]["state"];
×
1202
    auto state_result = state_endpoint.get_json(
×
1203
        {{"sync_type", config.mode == ServiceConfig::SyncMode::Flexible ? "flexible" : "partition"}});
×
1204
    return state_result["state"].get<std::string>().empty();
×
1205
}
×
1206

1207
bool AdminAPISession::is_initial_sync_complete(const std::string& app_id, bool is_flx_sync) const
1208
{
1,533✔
1209
    auto progress_endpoint = apps()[app_id]["sync"]["progress"];
1,533✔
1210
    auto progress_result = progress_endpoint.get_json();
1,533✔
1211
    if (is_flx_sync) {
1,533✔
1212
        // accepting_clients key is only true in FLX after the first initial sync has completed
1213
        auto it = progress_result.find("accepting_clients");
737✔
1214
        return it != progress_result.end() && it->is_boolean() && it->get<bool>();
737✔
1215
    }
737✔
1216

1217
    if (auto it = progress_result.find("progress"); it != progress_result.end() && it->is_object() && !it->empty()) {
796✔
1218
        for (auto& elem : *it) {
1,108✔
1219
            auto is_complete = elem["complete"];
1,108✔
1220
            if (!is_complete.is_boolean() || !is_complete.get<bool>()) {
1,108✔
1221
                return false;
262✔
1222
            }
262✔
1223
        }
1,108✔
1224
        return true;
282✔
1225
    }
544✔
1226
    return false;
252✔
1227
}
796✔
1228

1229
AdminAPISession::MigrationStatus AdminAPISession::get_migration_status(const std::string& app_id) const
1230
{
731✔
1231
    MigrationStatus status;
731✔
1232
    auto progress_endpoint = apps()[app_id]["sync"]["migration"];
731✔
1233
    auto progress_result = progress_endpoint.get_json();
731✔
1234
    auto errorMessage = progress_result["errorMessage"];
731✔
1235
    if (errorMessage.is_string() && !errorMessage.get<std::string>().empty()) {
731✔
1236
        throw Exception(Status{ErrorCodes::RuntimeError, errorMessage.get<std::string>()});
×
1237
    }
×
1238
    if (!progress_result["statusMessage"].is_string() || !progress_result["isMigrated"].is_boolean()) {
731✔
1239
        throw Exception(
×
1240
            Status{ErrorCodes::RuntimeError, util::format("Invalid result returned from migration status request: %1",
×
1241
                                                          progress_result.dump(4, 32, true))});
×
1242
    }
×
1243

1244
    status.statusMessage = progress_result["statusMessage"].get<std::string>();
731✔
1245
    status.isMigrated = progress_result["isMigrated"].get<bool>();
731✔
1246
    status.isCancelable = progress_result["isCancelable"].get<bool>();
731✔
1247
    status.isRevertible = progress_result["isRevertible"].get<bool>();
731✔
1248
    status.complete = status.statusMessage.empty();
731✔
1249
    return status;
731✔
1250
}
731✔
1251

1252
AdminAPIEndpoint AdminAPISession::apps(APIFamily family) const
1253
{
5,476✔
1254
    switch (family) {
5,476✔
1255
        case APIFamily::Admin:
5,464✔
1256
            return AdminAPIEndpoint(util::format("%1/api/admin/v3.0/groups/%2/apps", m_base_url, m_group_id),
5,464✔
1257
                                    m_access_token);
5,464✔
1258
        case APIFamily::Private:
12✔
1259
            return AdminAPIEndpoint(util::format("%1/api/private/v1.0/groups/%2/apps", m_base_url, m_group_id),
12✔
1260
                                    m_access_token);
12✔
1261
    }
5,476✔
1262
    REALM_UNREACHABLE();
1263
}
×
1264

1265
realm::Schema get_default_schema()
1266
{
82✔
1267
    const auto dog_schema =
82✔
1268
        ObjectSchema("Dog", {realm::Property("_id", PropertyType::ObjectId | PropertyType::Nullable, true),
82✔
1269
                             realm::Property("breed", PropertyType::String | PropertyType::Nullable),
82✔
1270
                             realm::Property("name", PropertyType::String),
82✔
1271
                             realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
82✔
1272
    const auto cat_schema =
82✔
1273
        ObjectSchema("Cat", {realm::Property("_id", PropertyType::String | PropertyType::Nullable, true),
82✔
1274
                             realm::Property("breed", PropertyType::String | PropertyType::Nullable),
82✔
1275
                             realm::Property("name", PropertyType::String),
82✔
1276
                             realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
82✔
1277
    const auto person_schema =
82✔
1278
        ObjectSchema("Person", {realm::Property("_id", PropertyType::ObjectId | PropertyType::Nullable, true),
82✔
1279
                                realm::Property("age", PropertyType::Int),
82✔
1280
                                realm::Property("dogs", PropertyType::Object | PropertyType::Array, "Dog"),
82✔
1281
                                realm::Property("firstName", PropertyType::String),
82✔
1282
                                realm::Property("lastName", PropertyType::String),
82✔
1283
                                realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
82✔
1284
    return realm::Schema({dog_schema, cat_schema, person_schema});
82✔
1285
}
82✔
1286

1287
std::string get_base_url()
1288
{
389✔
1289
    auto base_url = get_real_base_url();
389✔
1290

1291
    auto& redirector = get_redirector(base_url);
389✔
1292
    if (redirector) {
389✔
1293
        return redirector->base_url();
187✔
1294
    }
187✔
1295
    return base_url;
202✔
1296
}
389✔
1297

1298
std::string get_real_base_url()
1299
{
415✔
1300
    if (auto baas_url = getenv_sv("BAAS_BASE_URL"); !baas_url.empty()) {
415✔
1301
        return std::string{baas_url};
×
1302
    }
×
1303
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
415✔
1304
        return baasaas_holder->http_endpoint();
415✔
1305
    }
415✔
1306

1307
    return get_compile_time_base_url();
×
1308
}
415✔
1309

1310

1311
std::string get_admin_url()
1312
{
389✔
1313
    if (auto baas_admin_url = getenv_sv("BAAS_ADMIN_URL"); !baas_admin_url.empty()) {
389✔
1314
        return std::string{baas_admin_url};
×
1315
    }
×
1316
    if (auto compile_url = get_compile_time_admin_url(); !compile_url.empty()) {
389✔
1317
        return compile_url;
×
1318
    }
×
1319
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
389✔
1320
        return baasaas_holder->admin_endpoint();
389✔
1321
    }
389✔
1322

1323
    return get_real_base_url();
×
1324
}
389✔
1325

1326
std::string get_mongodb_server()
1327
{
389✔
1328
    if (auto baas_url = getenv_sv("BAAS_MONGO_URL"); !baas_url.empty()) {
389✔
1329
        return std::string{baas_url};
×
1330
    }
×
1331

1332
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
389✔
1333
        return baasaas_holder->mongo_endpoint();
389✔
1334
    }
389✔
1335
    return "mongodb://localhost:26000";
×
1336
}
389✔
1337

1338

1339
AppCreateConfig default_app_config()
1340
{
14✔
1341
    ObjectId id = ObjectId::gen();
14✔
1342
    std::string db_name = util::format("test_data_%1", id.to_string());
14✔
1343
    std::string app_url = get_base_url();
14✔
1344
    std::string admin_url = get_admin_url();
14✔
1345
    REALM_ASSERT(!app_url.empty());
14✔
1346
    REALM_ASSERT(!admin_url.empty());
14✔
1347

1348
    std::string update_user_data_func = util::format(R"(
14✔
1349
        exports = async function(data) {
14✔
1350
            const user = context.user;
14✔
1351
            const mongodb = context.services.get("BackingDB");
14✔
1352
            const userDataCollection = mongodb.db("%1").collection("UserData");
14✔
1353
            await userDataCollection.updateOne(
14✔
1354
                                               { "user_id": user.id },
14✔
1355
                                               { "$set": data },
14✔
1356
                                               { "upsert": true }
14✔
1357
                                               );
14✔
1358
            return true;
14✔
1359
        };
14✔
1360
    )",
14✔
1361
                                                     db_name);
14✔
1362

1363
    constexpr const char* sum_func = R"(
14✔
1364
        exports = function(...args) {
14✔
1365
            return args.reduce((a,b) => a + b, 0);
14✔
1366
        };
14✔
1367
    )";
14✔
1368

1369
    constexpr const char* confirm_func = R"(
14✔
1370
        exports = ({ token, tokenId, username }) => {
14✔
1371
            // process the confirm token, tokenId and username
14✔
1372
            if (username.includes("realm_tests_do_autoverify")) {
14✔
1373
              return { status: 'success' }
14✔
1374
            }
14✔
1375
            // do not confirm the user
14✔
1376
            return { status: 'fail' };
14✔
1377
        };
14✔
1378
    )";
14✔
1379

1380
    constexpr const char* auth_func = R"(
14✔
1381
        exports = (loginPayload) => {
14✔
1382
            return loginPayload["realmCustomAuthFuncUserId"];
14✔
1383
        };
14✔
1384
    )";
14✔
1385

1386
    constexpr const char* reset_func = R"(
14✔
1387
        exports = ({ token, tokenId, username, password }) => {
14✔
1388
            // process the reset token, tokenId, username and password
14✔
1389
            if (password.includes("realm_tests_do_reset")) {
14✔
1390
              return { status: 'success' };
14✔
1391
            }
14✔
1392
            // will not reset the password
14✔
1393
            return { status: 'fail' };
14✔
1394
        };
14✔
1395
    )";
14✔
1396

1397
    std::vector<AppCreateConfig::FunctionDef> funcs = {
14✔
1398
        {"updateUserData", update_user_data_func, false},
14✔
1399
        {"sumFunc", sum_func, false},
14✔
1400
        {"confirmFunc", confirm_func, false},
14✔
1401
        {"authFunc", auth_func, false},
14✔
1402
        {"resetFunc", reset_func, false},
14✔
1403
    };
14✔
1404

1405
    Property partition_key("realm_id", PropertyType::String | PropertyType::Nullable);
14✔
1406

1407
    AppCreateConfig::UserPassAuthConfig user_pass_config{
14✔
1408
        false,
14✔
1409
        "",
14✔
1410
        "confirmFunc",
14✔
1411
        "http://localhost/confirmEmail",
14✔
1412
        "resetFunc",
14✔
1413
        "",
14✔
1414
        "http://localhost/resetPassword",
14✔
1415
        true,
14✔
1416
        true,
14✔
1417
    };
14✔
1418

1419
    return AppCreateConfig{
14✔
1420
        "test",
14✔
1421
        std::move(app_url),
14✔
1422
        std::move(admin_url), // BAAS Admin API URL may be different
14✔
1423
        "unique_user@domain.com",
14✔
1424
        "password",
14✔
1425
        get_mongodb_server(),
14✔
1426
        db_name,
14✔
1427
        get_default_schema(),
14✔
1428
        std::move(partition_key),
14✔
1429
        false,                              // Dev mode disabled
14✔
1430
        util::none,                         // Default to no FLX sync config
14✔
1431
        std::move(funcs),                   // Add default functions
14✔
1432
        std::move(user_pass_config),        // enable basic user/pass auth
14✔
1433
        std::string{"authFunc"},            // custom auth function
14✔
1434
        true,                               // enable_api_key_auth
14✔
1435
        true,                               // enable_anonymous_auth
14✔
1436
        true,                               // enable_custom_token_auth
14✔
1437
        {},                                 // no service roles on the default rule
14✔
1438
        util::Logger::get_default_logger(), // provide the logger to the admin api
14✔
1439
    };
14✔
1440
}
14✔
1441

1442
AppCreateConfig minimal_app_config(const std::string& name, const Schema& schema)
1443
{
375✔
1444
    Property partition_key("realm_id", PropertyType::String | PropertyType::Nullable);
375✔
1445
    std::string app_url = get_base_url();
375✔
1446
    std::string admin_url = get_admin_url();
375✔
1447
    REALM_ASSERT(!app_url.empty());
375✔
1448
    REALM_ASSERT(!admin_url.empty());
375✔
1449

1450
    AppCreateConfig::UserPassAuthConfig user_pass_config{
375✔
1451
        true,  "Confirm", "", "http://example.com/confirmEmail", "", "Reset", "http://exmaple.com/resetPassword",
375✔
1452
        false, false,
375✔
1453
    };
375✔
1454

1455
    ObjectId id = ObjectId::gen();
375✔
1456
    return AppCreateConfig{
375✔
1457
        name,
375✔
1458
        std::move(app_url),
375✔
1459
        std::move(admin_url), // BAAS Admin API URL may be different
375✔
1460
        "unique_user@domain.com",
375✔
1461
        "password",
375✔
1462
        get_mongodb_server(),
375✔
1463
        util::format("test_data_%1_%2", name, id.to_string()),
375✔
1464
        schema,
375✔
1465
        std::move(partition_key),
375✔
1466
        false,                              // Dev mode disabled
375✔
1467
        util::none,                         // no FLX sync config
375✔
1468
        {},                                 // no functions
375✔
1469
        std::move(user_pass_config),        // enable basic user/pass auth
375✔
1470
        util::none,                         // disable custom auth
375✔
1471
        true,                               // enable api key auth
375✔
1472
        true,                               // enable anonymous auth
375✔
1473
        false,                              // enable_custom_token_auth
375✔
1474
        {},                                 // no service roles on the default rule
375✔
1475
        util::Logger::get_default_logger(), // provide the logger to the admin api
375✔
1476
    };
375✔
1477
}
375✔
1478

1479
nlohmann::json transform_service_role(const AppCreateConfig::ServiceRole& role_def)
1480
{
405✔
1481
    return {
405✔
1482
        {"name", role_def.name},
405✔
1483
        {"apply_when", role_def.apply_when},
405✔
1484
        {"document_filters",
405✔
1485
         {
405✔
1486
             {"read", role_def.document_filters.read},
405✔
1487
             {"write", role_def.document_filters.write},
405✔
1488
         }},
405✔
1489
        {"insert", role_def.insert_filter},
405✔
1490
        {"delete", role_def.delete_filter},
405✔
1491
        {"read", role_def.read},
405✔
1492
        {"write", role_def.write},
405✔
1493
    };
405✔
1494
}
405✔
1495

1496
AppSession create_app(const AppCreateConfig& config)
1497
{
391✔
1498
    auto session = AdminAPISession::login(config);
391✔
1499
    auto create_app_resp = session.apps().post_json(nlohmann::json{{"name", config.app_name}});
391✔
1500
    std::string app_id = create_app_resp["_id"];
391✔
1501
    std::string client_app_id = create_app_resp["client_app_id"];
391✔
1502

1503
    auto app = session.apps()[app_id];
391✔
1504

1505
    auto functions = app["functions"];
391✔
1506
    std::unordered_map<std::string, std::string> function_name_to_id;
391✔
1507
    for (const auto& func : config.functions) {
391✔
1508
        auto create_func_resp = functions.post_json({
80✔
1509
            {"name", func.name},
80✔
1510
            {"private", func.is_private},
80✔
1511
            {"can_evaluate", nlohmann::json::object()},
80✔
1512
            {"source", func.source},
80✔
1513
        });
80✔
1514
        function_name_to_id.insert({func.name, create_func_resp["_id"]});
80✔
1515
    }
80✔
1516

1517
    auto auth_providers = app["auth_providers"];
391✔
1518
    if (config.enable_anonymous_auth) {
391✔
1519
        auth_providers.post_json({{"type", "anon-user"}});
391✔
1520
    }
391✔
1521
    if (config.user_pass_auth) {
391✔
1522
        auto user_pass_config_obj = nlohmann::json{
391✔
1523
            {"autoConfirm", config.user_pass_auth->auto_confirm},
391✔
1524
            {"confirmEmailSubject", config.user_pass_auth->confirm_email_subject},
391✔
1525
            {"emailConfirmationUrl", config.user_pass_auth->email_confirmation_url},
391✔
1526
            {"resetPasswordSubject", config.user_pass_auth->reset_password_subject},
391✔
1527
            {"resetPasswordUrl", config.user_pass_auth->reset_password_url},
391✔
1528
        };
391✔
1529
        if (!config.user_pass_auth->confirmation_function_name.empty()) {
391✔
1530
            const auto& confirm_func_name = config.user_pass_auth->confirmation_function_name;
16✔
1531
            user_pass_config_obj.emplace("confirmationFunctionName", confirm_func_name);
16✔
1532
            user_pass_config_obj.emplace("confirmationFunctionId", function_name_to_id[confirm_func_name]);
16✔
1533
            user_pass_config_obj.emplace("runConfirmationFunction", config.user_pass_auth->run_confirmation_function);
16✔
1534
        }
16✔
1535
        if (!config.user_pass_auth->reset_function_name.empty()) {
391✔
1536
            const auto& reset_func_name = config.user_pass_auth->reset_function_name;
16✔
1537
            user_pass_config_obj.emplace("resetFunctionName", reset_func_name);
16✔
1538
            user_pass_config_obj.emplace("resetFunctionId", function_name_to_id[reset_func_name]);
16✔
1539
            user_pass_config_obj.emplace("runResetFunction", config.user_pass_auth->run_reset_function);
16✔
1540
        }
16✔
1541
        auth_providers.post_json({{"type", "local-userpass"}, {"config", std::move(user_pass_config_obj)}});
391✔
1542
    }
391✔
1543
    if (config.custom_function_auth) {
391✔
1544
        auth_providers.post_json({{"type", "custom-function"},
16✔
1545
                                  {"config",
16✔
1546
                                   {
16✔
1547
                                       {"authFunctionName", *config.custom_function_auth},
16✔
1548
                                       {"authFunctionId", function_name_to_id[*config.custom_function_auth]},
16✔
1549
                                   }}});
16✔
1550
    }
16✔
1551

1552
    if (config.enable_api_key_auth) {
391✔
1553
        auto all_auth_providers = auth_providers.get_json();
391✔
1554
        auto api_key_provider =
391✔
1555
            std::find_if(all_auth_providers.begin(), all_auth_providers.end(), [](const nlohmann::json& provider) {
391✔
1556
                return provider["type"] == "api-key";
391✔
1557
            });
391✔
1558
        REALM_ASSERT(api_key_provider != all_auth_providers.end());
391✔
1559
        std::string api_key_provider_id = (*api_key_provider)["_id"];
391✔
1560
        auto api_key_enable_resp = auth_providers[api_key_provider_id]["enable"].put("");
391✔
1561
        REALM_ASSERT(api_key_enable_resp.http_status_code >= 200 && api_key_enable_resp.http_status_code < 300);
391✔
1562
    }
391✔
1563

1564
    auto secrets = app["secrets"];
391✔
1565
    secrets.post_json({{"name", "BackingDB_uri"}, {"value", config.mongo_uri}});
391✔
1566
    secrets.post_json({{"name", "gcm"}, {"value", "gcm"}});
391✔
1567
    secrets.post_json({{"name", "customTokenKey"}, {"value", "My_very_confidential_secretttttt"}});
391✔
1568

1569
    if (config.enable_custom_token_auth) {
391✔
1570
        auth_providers.post_json(
16✔
1571
            {{"type", "custom-token"},
16✔
1572
             {"config",
16✔
1573
              {
16✔
1574
                  {"audience", nlohmann::json::array()},
16✔
1575
                  {"signingAlgorithm", "HS256"},
16✔
1576
                  {"useJWKURI", false},
16✔
1577
              }},
16✔
1578
             {"secret_config", {{"signingKeys", nlohmann::json::array({"customTokenKey"})}}},
16✔
1579
             {"disabled", false},
16✔
1580
             {"metadata_fields",
16✔
1581
              {{{"required", false}, {"name", "user_data.name"}, {"field_name", "name"}},
16✔
1582
               {{"required", true}, {"name", "user_data.occupation"}, {"field_name", "occupation"}},
16✔
1583
               {{"required", true}, {"name", "my_metadata.name"}, {"field_name", "anotherName"}}}}});
16✔
1584
    }
16✔
1585

1586
    auto services = app["services"];
391✔
1587
    static const std::string mongo_service_name = "BackingDB";
391✔
1588

1589
    nlohmann::json mongo_service_def = {
391✔
1590
        {"name", mongo_service_name},
391✔
1591
        {"type", "mongodb"},
391✔
1592
        {"config", {{"uri", config.mongo_uri}}},
391✔
1593
    };
391✔
1594
    nlohmann::json sync_config;
391✔
1595
    if (config.flx_sync_config) {
391✔
1596
        auto queryable_fields = nlohmann::json::array();
205✔
1597
        const auto& queryable_fields_src = config.flx_sync_config->queryable_fields;
205✔
1598
        std::copy(queryable_fields_src.begin(), queryable_fields_src.end(), std::back_inserter(queryable_fields));
205✔
1599
        auto asymmetric_tables = nlohmann::json::array();
205✔
1600
        for (const auto& obj_schema : config.schema) {
402✔
1601
            if (obj_schema.table_type == ObjectSchema::ObjectType::TopLevelAsymmetric) {
402✔
1602
                asymmetric_tables.emplace_back(obj_schema.name);
4✔
1603
            }
4✔
1604
        }
402✔
1605
        sync_config = nlohmann::json{{"database_name", config.mongo_dbname},
205✔
1606
                                     {"queryable_fields_names", queryable_fields},
205✔
1607
                                     {"asymmetric_tables", asymmetric_tables}};
205✔
1608
        mongo_service_def["config"]["flexible_sync"] = sync_config;
205✔
1609
    }
205✔
1610
    else {
186✔
1611
        sync_config = nlohmann::json{
186✔
1612
            {"database_name", config.mongo_dbname},
186✔
1613
            {"partition",
186✔
1614
             {
186✔
1615
                 {"key", config.partition_key.name},
186✔
1616
                 {"type", property_type_to_bson_type_str(config.partition_key.type)},
186✔
1617
                 {"required", !is_nullable(config.partition_key.type)},
186✔
1618
                 {"permissions",
186✔
1619
                  {
186✔
1620
                      {"read", true},
186✔
1621
                      {"write", true},
186✔
1622
                  }},
186✔
1623
             }},
186✔
1624
        };
186✔
1625
        mongo_service_def["config"]["sync"] = sync_config;
186✔
1626
    }
186✔
1627

1628
    auto create_mongo_service_resp = services.post_json(std::move(mongo_service_def));
391✔
1629
    std::string mongo_service_id = create_mongo_service_resp["_id"];
391✔
1630

1631
    auto default_rule = services[mongo_service_id]["default_rule"];
391✔
1632
    auto service_roles = nlohmann::json::array();
391✔
1633
    if (config.service_roles.empty()) {
391✔
1634
        service_roles.push_back(transform_service_role({"default"}));
385✔
1635
    }
385✔
1636
    else {
6✔
1637
        std::transform(config.service_roles.begin(), config.service_roles.end(), std::back_inserter(service_roles),
6✔
1638
                       transform_service_role);
6✔
1639
    }
6✔
1640

1641
    default_rule.post_json({{"roles", service_roles}});
391✔
1642

1643
    // No need for a draft because there are no breaking changes in the initial schema when the app is created.
1644
    bool use_draft = false;
391✔
1645
    session.create_schema(app_id, config, use_draft);
391✔
1646

1647
    // Enable sync after schema is created.
1648
    AdminAPISession::ServiceConfig service_config;
391✔
1649
    service_config.database_name = sync_config["database_name"];
391✔
1650
    if (config.flx_sync_config) {
391✔
1651
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Flexible;
205✔
1652
        service_config.queryable_field_names = sync_config["queryable_fields_names"];
205✔
1653
        service_config.asymmetric_tables = sync_config["asymmetric_tables"];
205✔
1654
    }
205✔
1655
    else {
186✔
1656
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
186✔
1657
        service_config.partition = sync_config["partition"];
186✔
1658
    }
186✔
1659
    session.enable_sync(app_id, mongo_service_id, service_config);
391✔
1660

1661
    app["sync"]["config"].put_json({{"development_mode_enabled", config.dev_mode_enabled}});
391✔
1662

1663
    auto rules = services[mongo_service_id]["rules"];
391✔
1664
    rules.post_json({
391✔
1665
        {"database", config.mongo_dbname},
391✔
1666
        {"collection", "UserData"},
391✔
1667
        {"roles",
391✔
1668
         {{{"name", "default"},
391✔
1669
           {"apply_when", nlohmann::json::object()},
391✔
1670
           {"document_filters",
391✔
1671
            {
391✔
1672
                {"read", true},
391✔
1673
                {"write", true},
391✔
1674
            }},
391✔
1675
           {"read", true},
391✔
1676
           {"write", true},
391✔
1677
           {"insert", true},
391✔
1678
           {"delete", true}}}},
391✔
1679
    });
391✔
1680

1681
    app["custom_user_data"].patch_json({
391✔
1682
        {"mongo_service_id", mongo_service_id},
391✔
1683
        {"enabled", true},
391✔
1684
        {"database_name", config.mongo_dbname},
391✔
1685
        {"collection_name", "UserData"},
391✔
1686
        {"user_id_field", "user_id"},
391✔
1687
    });
391✔
1688

1689
    services.post_json({
391✔
1690
        {"name", "gcm"},
391✔
1691
        {"type", "gcm"},
391✔
1692
        {"config",
391✔
1693
         {
391✔
1694
             {"senderId", "gcm"},
391✔
1695
         }},
391✔
1696
        {"secret_config",
391✔
1697
         {
391✔
1698
             {"apiKey", "gcm"},
391✔
1699
         }},
391✔
1700
        {"version", 1},
391✔
1701
    });
391✔
1702

1703
    // Wait for initial sync to complete, as connecting while this is happening
1704
    // causes various problems
1705
    bool any_sync_types = std::any_of(config.schema.begin(), config.schema.end(), [](auto& object_schema) {
443✔
1706
        return object_schema.table_type == ObjectSchema::ObjectType::TopLevel;
443✔
1707
    });
443✔
1708
    if (any_sync_types) {
391✔
1709
        // Increasing timeout due to occasional slow startup of the translator on baasaas
1710
        timed_sleeping_wait_for(
385✔
1711
            [&] {
1,437✔
1712
                return session.is_initial_sync_complete(app_id, config.flx_sync_config.has_value());
1,437✔
1713
            },
1,437✔
1714
            std::chrono::seconds(60), std::chrono::seconds(1));
385✔
1715
    }
385✔
1716

1717
    return {client_app_id, app_id, session, config};
391✔
1718
}
391✔
1719

1720
AppSession get_runtime_app_session()
1721
{
164✔
1722
    static const AppSession cached_app_session = [&] {
164✔
1723
        auto cached_app_session = create_app(default_app_config());
2✔
1724
        return cached_app_session;
2✔
1725
    }();
2✔
1726
    return cached_app_session;
164✔
1727
}
164✔
1728

1729

1730
#ifdef REALM_MONGODB_ENDPOINT
1731
TEST_CASE("app: baas admin api", "[sync][app][admin api][baas]") {
1732
    SECTION("embedded objects") {
1733
        Schema schema{{"top",
1734
                       {{"_id", PropertyType::String, true},
1735
                        {"location", PropertyType::Object | PropertyType::Nullable, "location"}}},
1736
                      {"location",
1737
                       ObjectSchema::ObjectType::Embedded,
1738
                       {{"coordinates", PropertyType::Double | PropertyType::Array}}}};
1739

1740
        auto test_app_config = minimal_app_config("test", schema);
1741
        create_app(test_app_config);
1742
    }
1743

1744
    SECTION("embedded object with array") {
1745
        Schema schema{
1746
            {"a",
1747
             {{"_id", PropertyType::String, true},
1748
              {"b_link", PropertyType::Object | PropertyType::Array | PropertyType::Nullable, "b"}}},
1749
            {"b",
1750
             ObjectSchema::ObjectType::Embedded,
1751
             {{"c_link", PropertyType::Object | PropertyType::Nullable, "c"}}},
1752
            {"c", {{"_id", PropertyType::String, true}, {"d_str", PropertyType::String}}},
1753
        };
1754
        auto test_app_config = minimal_app_config("test", schema);
1755
        create_app(test_app_config);
1756
    }
1757

1758
    SECTION("dictionaries") {
1759
        Schema schema{
1760
            {"a", {{"_id", PropertyType::String, true}, {"b_dict", PropertyType::Dictionary | PropertyType::String}}},
1761
        };
1762

1763
        auto test_app_config = minimal_app_config("test", schema);
1764
        create_app(test_app_config);
1765
    }
1766

1767
    SECTION("set") {
1768
        Schema schema{
1769
            {"a", {{"_id", PropertyType::String, true}, {"b_dict", PropertyType::Set | PropertyType::String}}},
1770
        };
1771

1772
        auto test_app_config = minimal_app_config("test", schema);
1773
        create_app(test_app_config);
1774
    }
1775
}
1776
#endif
1777

1778
} // namespace realm
1779

1780
#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

© 2025 Coveralls, Inc