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

realm / realm-core / thomas.goyne_851

27 Feb 2025 12:51AM UTC coverage: 91.132% (+0.01%) from 91.119%
thomas.goyne_851

Pull #8072

Evergreen

tgoyne
Add some logging to a flakey audit test
Pull Request #8072: Enable automatic client reset handling for audit Realms

102776 of 181548 branches covered (56.61%)

77 of 79 new or added lines in 2 files covered. (97.47%)

81 existing lines in 14 files now uncovered.

217476 of 238639 relevant lines covered (91.13%)

5926136.9 hits per line

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

84.57
/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,875✔
43
    switch (type & ~PropertyType::Flags) {
5,875✔
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:
17✔
53
            return "date";
17✔
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,147✔
61
            return "long";
1,147✔
62
        case PropertyType::Object:
✔
63
            return "object";
×
64
        case PropertyType::ObjectId:
2,044✔
65
            return "objectId";
2,044✔
66
        case PropertyType::String:
2,437✔
67
            return "string";
2,437✔
68
        case PropertyType::LinkingObjects:
✔
69
            return "linkingObjects";
×
70
        default:
✔
71
            REALM_COMPILER_HINT_UNREACHABLE();
×
72
    }
5,875✔
73
}
5,875✔
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)
207✔
81
        , m_partition_key(partition_key)
207✔
82
        , m_mongo_service_name(service_name)
207✔
83
        , m_mongo_db_name(db_name)
207✔
84
        , m_is_flx_sync(is_flx_sync)
207✔
85
    {
433✔
86
    }
433✔
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,048✔
106
    nlohmann::json required = nlohmann::json::array();
2,048✔
107
    nlohmann::json properties = nlohmann::json::object();
2,048✔
108
    for (const auto& prop : obj_schema.persisted_properties) {
7,212✔
109
        if (include_prop && !include_prop(prop)) {
7,212✔
110
            continue;
1,571✔
111
        }
1,571✔
112
        if (clear_path) {
5,641✔
113
            m_current_path.clear();
5,523✔
114
        }
5,523✔
115
        properties.emplace(prop.name, property_to_jsonschema(prop));
5,641✔
116
        if (!is_nullable(prop.type) && !is_collection(prop.type)) {
5,641✔
117
            required.push_back(prop.name);
2,513✔
118
        }
2,513✔
119
    }
5,641✔
120

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

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

132
    if ((prop.type & ~PropertyType::Flags) == PropertyType::Object) {
5,797✔
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,235✔
169
        type_output = {{"bsonType", property_type_to_bson_type_str(prop.type)}};
5,235✔
170
    }
5,235✔
171

172
    if (is_array(prop.type)) {
5,797✔
173
        return nlohmann::json{{"bsonType", "array"}, {"items", type_output}};
276✔
174
    }
276✔
175
    if (is_set(prop.type)) {
5,521✔
176
        return nlohmann::json{{"bsonType", "array"}, {"uniqueItems", true}, {"items", type_output}};
4✔
177
    }
4✔
178
    if (is_dictionary(prop.type)) {
5,517✔
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,461✔
185
    return type_output;
5,461✔
186
}
5,517✔
187

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

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

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

220
    ~CurlGlobalGuard()
221
    {
17,384✔
222
        std::lock_guard<std::mutex> lk(m_mutex);
17,384✔
223
        if (--m_users == 0) {
17,384✔
224
            curl_global_cleanup();
17,384✔
225
        }
17,384✔
226
    }
17,384✔
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,317✔
243
    REALM_ASSERT(response);
14,317✔
244
    size_t realsize = size * nmemb;
14,317✔
245
    response->append(ptr, realsize);
14,317✔
246
    return realsize;
14,317✔
247
}
14,317✔
248

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

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

278
    return {};
1,420✔
279
}
1,616✔
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,384✔
287
    CurlGlobalGuard curl_global_guard;
17,384✔
288
    auto curl = curl_easy_init();
17,384✔
289
    if (!curl) {
17,384✔
290
        return app::Response{500, -1};
×
291
    }
×
292

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

299
    std::string response;
17,384✔
300
    app::HttpHeaders response_headers;
17,384✔
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,384✔
306

307
    /* Now specify the POST data */
308
    if (request.method == app::HttpMethod::post) {
17,384✔
309
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
8,326✔
310
    }
8,326✔
311
    else if (request.method == app::HttpMethod::put) {
9,058✔
312
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
2,178✔
313
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
2,178✔
314
    }
2,178✔
315
    else if (request.method == app::HttpMethod::patch) {
6,880✔
316
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
808✔
317
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
808✔
318
    }
808✔
319
    else if (request.method == app::HttpMethod::del) {
6,072✔
320
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
471✔
321
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
471✔
322
    }
471✔
323
    else if (request.method == app::HttpMethod::patch) {
5,601✔
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,384✔
329

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

335
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
17,384✔
336
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
17,384✔
337
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
17,384✔
338
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_cb);
17,384✔
339
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers);
17,384✔
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,384✔
347
    auto response_code = curl_easy_perform(curl);
17,384✔
348
    auto total_time = std::chrono::steady_clock::now() - start_time;
17,384✔
349

350
    auto logger = util::Logger::get_default_logger();
17,384✔
351
    if (response_code != CURLE_OK) {
17,384✔
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,384✔
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,384✔
372
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
17,384✔
373
    return {
17,384✔
374
        http_code,
17,384✔
375
        0, // binding_response_code
17,384✔
376
        std::move(response_headers),
17,384✔
377
        std::move(response),
17,384✔
378
    };
17,384✔
379
}
17,384✔
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);
×
393
            logger->info("Starting baasaas container with githash of %1", ref_spec);
×
394
        }
×
395
        else if (mode == StartMode::Branch) {
2✔
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) {
2✔
400
            url_path = util::format("startContainer?patchId=%1", ref_spec);
2✔
401
            logger->info("Starting baasaas container for patch id %1", ref_spec);
2✔
402
        }
2✔
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,205✔
449
        if (!m_http_endpoint.empty() || m_container_id.empty()) {
1,205✔
450
            return;
1,203✔
451
        }
1,203✔
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
    {
393✔
521
        poll();
393✔
522
        return m_http_endpoint;
393✔
523
    }
393✔
524

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

531
    const std::string& mongo_endpoint()
532
    {
393✔
533
        poll();
393✔
534
        return m_mongo_endpoint;
393✔
535
    }
393✔
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
{
397✔
596
    static std::optional<sync::RedirectingHttpServer> redirector;
397✔
597
    auto redirector_enabled = [&] {
397✔
598
        const static auto enabled_values = {"On", "on", "1"};
397✔
599
        auto enable_redirector = getenv_sv("ENABLE_BAAS_REDIRECTOR");
397✔
600
        return std::any_of(enabled_values.begin(), enabled_values.end(), [&](const auto val) {
811✔
601
            return val == enable_redirector;
811✔
602
        });
811✔
603
    };
397✔
604

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

609
    return redirector;
397✔
610
}
397✔
611

612
class BaasaasLauncher : public Catch::EventListenerBase {
613
public:
614
    static std::optional<Baasaas>& get_baasaas_holder()
615
    {
1,211✔
616
        static std::optional<Baasaas> global_baasaas = std::nullopt;
1,211✔
617
        return global_baasaas;
1,211✔
618
    }
1,211✔
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()) {
×
655
                throw std::runtime_error("Expected git hash in BAASAAS_REF_SPEC env variable, but it was empty");
×
656
            }
×
657
            mode = Baasaas::StartMode::GitHash;
×
658
        }
×
659
        else if (mode_spec == "patchid") {
2✔
660
            if (ref_spec.empty()) {
2✔
661
                throw std::runtime_error("Expected patch id in BAASAAS_REF_SPEC env variable, but it was empty");
×
662
            }
×
663
            mode = Baasaas::StartMode::PatchId;
2✔
664
        }
2✔
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,675✔
695
    return AdminAPIEndpoint(util::format("%1/%2", m_url, name), m_access_token);
19,675✔
696
}
19,675✔
697

698
app::Response AdminAPIEndpoint::do_request(app::Request request) const
699
{
12,384✔
700
    if (request.url.find('?') == std::string::npos) {
12,384✔
701
        request.url = util::format("%1?bypass_service_change=SyncSchemaVersionIncrease", request.url);
12,384✔
702
    }
12,384✔
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,384✔
707
    request.headers["Accept"] = "application/json";
12,384✔
708
    request.headers["Authorization"] = util::format("Bearer %1", m_access_token);
12,384✔
709
    return do_http_request(std::move(request));
12,384✔
710
}
12,384✔
711

712
app::Response AdminAPIEndpoint::get(const std::vector<std::pair<std::string, std::string>>& params) const
713
{
3,913✔
714
    app::Request req;
3,913✔
715
    req.method = app::HttpMethod::get;
3,913✔
716
    std::stringstream ss;
3,913✔
717
    bool needs_and = false;
3,913✔
718
    ss << m_url;
3,913✔
719
    if (!params.empty() && m_url.find('?') != std::string::npos) {
3,913!
720
        needs_and = true;
×
721
    }
×
722
    for (const auto& param : params) {
3,913✔
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,913✔
733
    return do_request(std::move(req));
3,913✔
734
}
3,913✔
735

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

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

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

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

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

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

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

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

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

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

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

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

835
    return AdminAPISession(std::move(admin_url), std::move(access_token), std::move(group_id));
395✔
836
}
395✔
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
{
393✔
891
    auto app_endpoint = apps()[app_id];
393✔
892
    auto resp = app_endpoint.del();
393✔
893
    REALM_ASSERT_EX(resp.http_status_code == 204, resp.http_status_code, resp.body);
393✔
894
}
393✔
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
{
433✔
950
    static const std::string mongo_service_name = "BackingDB";
433✔
951

952
    auto drafts = apps()[app_id]["drafts"];
433✔
953
    std::string draft_id;
433✔
954
    if (use_draft) {
433✔
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"];
433✔
960
    auto current_schema = schemas.get_json();
433✔
961
    auto target_schema = config.schema;
433✔
962

963
    std::unordered_map<std::string, std::string> current_schema_tables;
433✔
964
    for (const auto& schema : current_schema) {
433✔
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,387✔
971
        if (config.flx_sync_config) {
3,387✔
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,845✔
979
    };
3,387✔
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;
433✔
985
    BaasRuleBuilder rule_builder(target_schema, config.partition_key, mongo_service_name, config.mongo_dbname,
433✔
986
                                 static_cast<bool>(config.flx_sync_config));
433✔
987
    for (const auto& obj_schema : target_schema) {
1,026✔
988
        auto it = current_schema_tables.find(obj_schema.name);
1,026✔
989
        if (it != current_schema_tables.end()) {
1,026✔
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);
910✔
995
        auto schema_create_resp = schemas.post_json(schema_to_create);
910✔
996
        object_schema_to_create.push_back({schema_create_resp["_id"], &obj_schema});
910✔
997
    }
910✔
998

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

1006
    // Delete removed tables
1007
    for (const auto& table : current_schema_tables) {
433✔
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) {
433✔
1014
        drafts[draft_id]["deployment"].post_json({});
38✔
1015
    }
38✔
1016
}
433✔
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
{
401✔
1074
    if (config.mode == AdminAPISession::ServiceConfig::SyncMode::Flexible) {
401✔
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},
194✔
1090
                          {"partition", *config.partition},
194✔
1091
                          {"state", config.state},
194✔
1092
                          {"is_recovery_mode_disabled", config.recovery_is_disabled}};
194✔
1093
}
401✔
1094

1095
AdminAPIEndpoint AdminAPISession::service_config_endpoint(const std::string& app_id,
1096
                                                          const std::string& service_id) const
1097
{
443✔
1098
    return apps()[app_id]["services"][service_id]["config"];
443✔
1099
}
443✔
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
{
397✔
1126
    auto endpoint = service_config_endpoint(app_id, service_id);
397✔
1127
    sync_config.state = "enabled";
397✔
1128
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
397✔
1129
    return sync_config;
397✔
1130
}
397✔
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
{
126✔
1144
    std::vector<AdminAPISession::SchemaVersionInfo> ret;
126✔
1145
    auto endpoint = apps()[app_id]["sync"]["schemas"]["versions"];
126✔
1146
    auto res = endpoint.get_json();
126✔
1147
    for (auto&& version : res["versions"].get<std::vector<nlohmann::json>>()) {
178✔
1148
        SchemaVersionInfo info;
178✔
1149
        info.version_major = version["version_major"];
178✔
1150
        ret.push_back(std::move(info));
178✔
1151
    }
178✔
1152

1153
    return ret;
126✔
1154
}
126✔
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,542✔
1209
    auto progress_endpoint = apps()[app_id]["sync"]["progress"];
1,542✔
1210
    auto progress_result = progress_endpoint.get_json();
1,542✔
1211
    if (is_flx_sync) {
1,542✔
1212
        // accepting_clients key is only true in FLX after the first initial sync has completed
1213
        auto it = progress_result.find("accepting_clients");
733✔
1214
        return it != progress_result.end() && it->is_boolean() && it->get<bool>();
733✔
1215
    }
733✔
1216

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

1229
AdminAPISession::MigrationStatus AdminAPISession::get_migration_status(const std::string& app_id) const
1230
{
714✔
1231
    MigrationStatus status;
714✔
1232
    auto progress_endpoint = apps()[app_id]["sync"]["migration"];
714✔
1233
    auto progress_result = progress_endpoint.get_json();
714✔
1234
    auto errorMessage = progress_result["errorMessage"];
714✔
1235
    if (errorMessage.is_string() && !errorMessage.get<std::string>().empty()) {
714✔
1236
        throw Exception(Status{ErrorCodes::RuntimeError, errorMessage.get<std::string>()});
×
1237
    }
×
1238
    if (!progress_result["statusMessage"].is_string() || !progress_result["isMigrated"].is_boolean()) {
714✔
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>();
714✔
1245
    status.isMigrated = progress_result["isMigrated"].get<bool>();
714✔
1246
    status.isCancelable = progress_result["isCancelable"].get<bool>();
714✔
1247
    status.isRevertible = progress_result["isRevertible"].get<bool>();
714✔
1248
    status.complete = status.statusMessage.empty();
714✔
1249
    return status;
714✔
1250
}
714✔
1251

1252
AdminAPIEndpoint AdminAPISession::apps(APIFamily family) const
1253
{
5,512✔
1254
    switch (family) {
5,512✔
1255
        case APIFamily::Admin:
5,500✔
1256
            return AdminAPIEndpoint(util::format("%1/api/admin/v3.0/groups/%2/apps", m_base_url, m_group_id),
5,500✔
1257
                                    m_access_token);
5,500✔
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,512✔
1262
    REALM_UNREACHABLE();
1263
}
×
1264

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

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

1291
    auto& redirector = get_redirector(base_url);
393✔
1292
    if (redirector) {
393✔
1293
        return redirector->base_url();
188✔
1294
    }
188✔
1295
    return base_url;
205✔
1296
}
393✔
1297

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

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

1310

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

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

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

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

1338

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1589
    nlohmann::json mongo_service_def = {
395✔
1590
        {"name", mongo_service_name},
395✔
1591
        {"type", "mongodb"},
395✔
1592
        {"config", {{"uri", config.mongo_uri}}},
395✔
1593
    };
395✔
1594
    nlohmann::json sync_config;
395✔
1595
    if (config.flx_sync_config) {
395✔
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 {
190✔
1611
        sync_config = nlohmann::json{
190✔
1612
            {"database_name", config.mongo_dbname},
190✔
1613
            {"partition",
190✔
1614
             {
190✔
1615
                 {"key", config.partition_key.name},
190✔
1616
                 {"type", property_type_to_bson_type_str(config.partition_key.type)},
190✔
1617
                 {"required", !is_nullable(config.partition_key.type)},
190✔
1618
                 {"permissions",
190✔
1619
                  {
190✔
1620
                      {"read", true},
190✔
1621
                      {"write", true},
190✔
1622
                  }},
190✔
1623
             }},
190✔
1624
        };
190✔
1625
        mongo_service_def["config"]["sync"] = sync_config;
190✔
1626
    }
190✔
1627

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

1631
    auto default_rule = services[mongo_service_id]["default_rule"];
395✔
1632
    auto service_roles = nlohmann::json::array();
395✔
1633
    if (config.service_roles.empty()) {
395✔
1634
        service_roles.push_back(transform_service_role({"default"}));
389✔
1635
    }
389✔
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}});
395✔
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;
395✔
1645
    session.create_schema(app_id, config, use_draft);
395✔
1646

1647
    // Enable sync after schema is created.
1648
    AdminAPISession::ServiceConfig service_config;
395✔
1649
    service_config.database_name = sync_config["database_name"];
395✔
1650
    if (config.flx_sync_config) {
395✔
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 {
190✔
1656
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
190✔
1657
        service_config.partition = sync_config["partition"];
190✔
1658
    }
190✔
1659
    session.enable_sync(app_id, mongo_service_id, service_config);
395✔
1660

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

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

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

1689
    services.post_json({
395✔
1690
        {"name", "gcm"},
395✔
1691
        {"type", "gcm"},
395✔
1692
        {"config",
395✔
1693
         {
395✔
1694
             {"senderId", "gcm"},
395✔
1695
         }},
395✔
1696
        {"secret_config",
395✔
1697
         {
395✔
1698
             {"apiKey", "gcm"},
395✔
1699
         }},
395✔
1700
        {"version", 1},
395✔
1701
    });
395✔
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) {
447✔
1706
        return object_schema.table_type == ObjectSchema::ObjectType::TopLevel;
447✔
1707
    });
447✔
1708
    if (any_sync_types) {
395✔
1709
        // Increasing timeout due to occasional slow startup of the translator on baasaas
1710
        timed_sleeping_wait_for(
389✔
1711
            [&] {
1,446✔
1712
                return session.is_initial_sync_complete(app_id, config.flx_sync_config.has_value());
1,446✔
1713
            },
1,446✔
1714
            std::chrono::seconds(60), std::chrono::seconds(1));
389✔
1715
    }
389✔
1716

1717
    return {client_app_id, app_id, session, config};
395✔
1718
}
395✔
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