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

realm / realm-core / thomas.goyne_275

09 Apr 2024 03:33AM UTC coverage: 92.608% (+0.5%) from 92.088%
thomas.goyne_275

Pull #7300

Evergreen

tgoyne
Extract some duplicated code in PushClient
Pull Request #7300: Rework sync user handling and metadata storage

102672 of 194970 branches covered (52.66%)

3165 of 3247 new or added lines in 46 files covered. (97.47%)

34 existing lines in 9 files now uncovered.

249420 of 269329 relevant lines covered (92.61%)

45087511.34 hits per line

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

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

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

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

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

25
#if REALM_ENABLE_AUTH_TESTS
26

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

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

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

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

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

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

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

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

102
nlohmann::json BaasRuleBuilder::object_schema_to_jsonschema(const ObjectSchema& obj_schema,
103
                                                            const IncludePropCond& include_prop, bool clear_path)
104
{
14,524✔
105
    nlohmann::json required = nlohmann::json::array();
14,524✔
106
    nlohmann::json properties = nlohmann::json::object();
14,524✔
107
    for (const auto& prop : obj_schema.persisted_properties) {
50,952✔
108
        if (include_prop && !include_prop(prop)) {
50,952✔
109
            continue;
10,152✔
110
        }
10,152✔
111
        if (clear_path) {
40,800✔
112
            m_current_path.clear();
40,512✔
113
        }
40,512✔
114
        properties.emplace(prop.name, property_to_jsonschema(prop));
40,800✔
115
        if (!is_nullable(prop.type) && !is_collection(prop.type)) {
40,800✔
116
            required.push_back(prop.name);
18,074✔
117
        }
18,074✔
118
    }
40,800✔
119

7,030✔
120
    return {
14,524✔
121
        {"properties", properties},
14,524✔
122
        {"required", required},
14,524✔
123
        {"title", obj_schema.name},
14,524✔
124
    };
14,524✔
125
}
14,524✔
126

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

20,132✔
131
    if ((prop.type & ~PropertyType::Flags) == PropertyType::Object) {
41,952✔
132
        auto target_obj = m_schema.find(prop.object_type);
3,202✔
133
        REALM_ASSERT(target_obj != m_schema.end());
3,202✔
134

1,601✔
135
        if (target_obj->table_type == ObjectSchema::ObjectType::Embedded) {
3,202✔
136
            m_current_path.push_back(prop.name);
240✔
137
            if (is_collection(prop.type)) {
240✔
138
                m_current_path.push_back("[]");
80✔
139
            }
80✔
140

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

20,132✔
171
    if (is_array(prop.type)) {
41,952✔
172
        return nlohmann::json{{"bsonType", "array"}, {"items", type_output}};
2,226✔
173
    }
2,226✔
174
    if (is_set(prop.type)) {
39,726✔
175
        return nlohmann::json{{"bsonType", "array"}, {"uniqueItems", true}, {"items", type_output}};
32✔
176
    }
32✔
177
    if (is_dictionary(prop.type)) {
39,694✔
178
        return nlohmann::json{
80✔
179
            {"bsonType", "object"}, {"properties", nlohmann::json::object()}, {"additionalProperties", type_output}};
80✔
180
    }
80✔
181

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

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

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

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

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

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

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

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

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

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

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

5,931✔
277
    return {};
12,342✔
278
}
12,342✔
279

280
} // namespace
281

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

61,964✔
290
    struct curl_slist* list = nullptr;
129,914✔
291
    auto curl_cleanup = util::ScopeExit([&]() noexcept {
129,914✔
292
        curl_easy_cleanup(curl);
129,914✔
293
        curl_slist_free_all(list);
129,914✔
294
    });
129,914✔
295

61,964✔
296
    std::string response;
129,914✔
297
    app::HttpHeaders response_headers;
129,914✔
298

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

61,964✔
304
    /* Now specify the POST data */
61,964✔
305
    if (request.method == app::HttpMethod::post) {
129,914✔
306
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
63,693✔
307
    }
63,693✔
308
    else if (request.method == app::HttpMethod::put) {
66,221✔
309
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
15,378✔
310
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
15,378✔
311
    }
15,378✔
312
    else if (request.method == app::HttpMethod::patch) {
50,843✔
313
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
6,052✔
314
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
6,052✔
315
    }
6,052✔
316
    else if (request.method == app::HttpMethod::del) {
44,791✔
317
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
3,538✔
318
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
3,538✔
319
    }
3,538✔
320
    else if (request.method == app::HttpMethod::patch) {
41,253✔
321
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
×
322
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request.body.c_str());
×
323
    }
×
324

61,964✔
325
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, request.timeout_ms);
129,914✔
326

61,964✔
327
    for (auto header : request.headers) {
355,714✔
328
        auto header_str = util::format("%1: %2", header.first, header.second);
355,714✔
329
        list = curl_slist_append(list, header_str.c_str());
355,714✔
330
    }
355,714✔
331

61,964✔
332
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
129,914✔
333
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
129,914✔
334
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
129,914✔
335
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_cb);
129,914✔
336
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers);
129,914✔
337

61,964✔
338
#ifdef REALM_CURL_CACERTS
339
    auto ca_info = unquote_string(REALM_QUOTE(REALM_CURL_CACERTS));
340
    curl_easy_setopt(curl, CURLOPT_CAINFO, ca_info.c_str());
341
#endif
342

61,964✔
343
    auto start_time = std::chrono::steady_clock::now();
129,914✔
344
    auto response_code = curl_easy_perform(curl);
129,914✔
345
    auto total_time = std::chrono::steady_clock::now() - start_time;
129,914✔
346

61,964✔
347
    auto logger = util::Logger::get_default_logger();
129,914✔
348
    if (response_code != CURLE_OK) {
129,914✔
349
        logger->error("curl_easy_perform() failed when sending request to '%1' with body '%2': %3", request.url,
15✔
350
                      request.body, curl_easy_strerror(response_code));
15✔
351
    }
15✔
352
    if (logger->would_log(util::Logger::Level::trace)) {
129,914✔
353
        std::string coid = [&] {
×
354
            auto coid_header = response_headers.find("X-Appservices-Request-Id");
×
355
            if (coid_header == response_headers.end()) {
×
356
                return std::string{};
×
357
            }
×
358
            return util::format("BaaS Coid: \"%1\"", coid_header->second);
×
359
        }();
×
360

NEW
361
        logger->trace("Baas API %1 request to %2 took %3 %4\n", app::httpmethod_to_string(request.method),
×
NEW
362
                      request.url, std::chrono::duration_cast<std::chrono::milliseconds>(total_time), coid);
×
UNCOV
363
    }
×
364

61,964✔
365
    int http_code = 0;
129,914✔
366
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
129,914✔
367
    return {
129,914✔
368
        http_code,
129,914✔
369
        0, // binding_response_code
129,914✔
370
        std::move(response_headers),
129,914✔
371
        std::move(response),
129,914✔
372
    };
129,914✔
373
}
129,914✔
374

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

8✔
401
        auto resp = do_request(std::move(url_path), app::HttpMethod::post);
16✔
402
        m_container_id = resp["id"].get<std::string>();
16✔
403
        logger->info("Baasaas container started with id \"%1\"", m_container_id);
16✔
404
        auto lock_file = util::File(std::string{s_baasaas_lock_file_name}, util::File::mode_Write);
16✔
405
        lock_file.write(m_container_id);
16✔
406
    }
16✔
407

408
    explicit Baasaas(std::string api_key, std::string baasaas_instance_id)
409
        : m_api_key(std::move(api_key))
410
        , m_base_url(get_baasaas_base_url())
411
        , m_container_id(std::move(baasaas_instance_id))
412
        , m_externally_managed_instance(true)
413
    {
×
414
        auto logger = util::Logger::get_default_logger();
×
415
        logger->info("Using externally managed baasaas instance \"%1\"", m_container_id);
×
416
    }
×
417

418
    Baasaas(const Baasaas&) = delete;
419
    Baasaas(Baasaas&&) = delete;
420
    Baasaas& operator=(const Baasaas&) = delete;
421
    Baasaas& operator=(Baasaas&&) = delete;
422

423
    ~Baasaas()
424
    {
16✔
425
        stop();
16✔
426
    }
16✔
427

428
    void poll()
429
    {
9,292✔
430
        if (!m_http_endpoint.empty() || m_container_id.empty()) {
9,292✔
431
            return;
9,276✔
432
        }
9,276✔
433

8✔
434
        auto logger = util::Logger::get_default_logger();
16✔
435
        auto poll_start_at = std::chrono::system_clock::now();
16✔
436
        std::string http_endpoint;
16✔
437
        std::string mongo_endpoint;
16✔
438
        bool logged = false;
16✔
439
        while (std::chrono::system_clock::now() - poll_start_at < std::chrono::minutes(2) &&
67✔
440
               m_http_endpoint.empty()) {
67✔
441
            if (http_endpoint.empty()) {
67✔
442
                auto status_obj =
36✔
443
                    do_request(util::format("containerStatus?id=%1", m_container_id), app::HttpMethod::get);
36✔
444
                if (!status_obj["httpUrl"].is_null()) {
36✔
445
                    http_endpoint = status_obj["httpUrl"].get<std::string>();
16✔
446
                    mongo_endpoint = status_obj["mongoUrl"].get<std::string>();
16✔
447
                }
16✔
448
            }
36✔
449
            else {
31✔
450
                app::Request baas_req;
31✔
451
                baas_req.url = util::format("%1/api/private/v1.0/version", http_endpoint);
31✔
452
                baas_req.method = app::HttpMethod::get;
31✔
453
                baas_req.headers.insert_or_assign("Content-Type", "application/json");
31✔
454
                auto baas_resp = do_http_request(baas_req);
31✔
455
                if (baas_resp.http_status_code >= 200 && baas_resp.http_status_code < 300) {
31✔
456
                    m_http_endpoint = http_endpoint;
16✔
457
                    m_mongo_endpoint = mongo_endpoint;
16✔
458
                    break;
16✔
459
                }
16✔
460
            }
51✔
461

25✔
462
            if (!logged) {
51✔
463
                logger->info("Waiting for baasaas container \"%1\" to be ready", m_container_id);
16✔
464
                logged = true;
16✔
465
            }
16✔
466
            std::this_thread::sleep_for(std::chrono::seconds(3));
51✔
467
        }
51✔
468

8✔
469
        if (m_http_endpoint.empty()) {
16✔
470
            throw std::runtime_error(
×
471
                util::format("Failed to launch baasaas container %1 within 2 minutes", m_container_id));
×
472
        }
×
473
    }
16✔
474

475
    void stop()
476
    {
32✔
477
        if (m_externally_managed_instance) {
32✔
478
            return;
×
479
        }
×
480
        auto container_id = std::move(m_container_id);
32✔
481
        if (container_id.empty()) {
32✔
482
            return;
16✔
483
        }
16✔
484

8✔
485
        auto logger = util::Logger::get_default_logger();
16✔
486
        logger->info("Stopping baasaas container with id \"%1\"", container_id);
16✔
487
        do_request(util::format("stopContainer?id=%1", container_id), app::HttpMethod::post);
16✔
488
        auto lock_file = util::File(std::string{s_baasaas_lock_file_name}, util::File::mode_Write);
16✔
489
        lock_file.resize(0);
16✔
490
        lock_file.close();
16✔
491
        util::File::remove(lock_file.get_path());
16✔
492
    }
16✔
493

494
    const std::string& http_endpoint()
495
    {
6,306✔
496
        poll();
6,306✔
497
        return m_http_endpoint;
6,306✔
498
    }
6,306✔
499

500
    const std::string& mongo_endpoint()
501
    {
2,986✔
502
        poll();
2,986✔
503
        return m_mongo_endpoint;
2,986✔
504
    }
2,986✔
505

506
private:
507
    nlohmann::json do_request(std::string api_path, app::HttpMethod method)
508
    {
68✔
509
        app::Request request;
68✔
510

33✔
511
        request.url = util::format("%1/%2", m_base_url, api_path);
68✔
512
        request.method = method;
68✔
513
        request.headers.insert_or_assign("apiKey", m_api_key);
68✔
514
        request.headers.insert_or_assign("Content-Type", "application/json");
68✔
515
        auto response = do_http_request(request);
68✔
516
        REALM_ASSERT_EX(response.http_status_code >= 200 && response.http_status_code < 300,
68✔
517
                        util::format("Baasaas api response code: %1 Response body: %2", response.http_status_code,
68✔
518
                                     response.body));
68✔
519
        return nlohmann::json::parse(response.body);
68✔
520
    }
68✔
521

522
    static std::string get_baasaas_base_url()
523
    {
16✔
524
        auto env_value = getenv_sv("BAASAAS_BASE_URL");
16✔
525
        if (env_value.empty()) {
16✔
526
            // This is the current default endpoint for baasaas maintained by the sync team.
8✔
527
            // You can reach out for help in #appx-device-sync-internal if there are problems.
8✔
528
            return "https://us-east-1.aws.data.mongodb-api.com/app/baas-container-service-autzb/endpoint";
16✔
529
        }
16✔
530

531
        return unquote_string(env_value);
×
532
    }
×
533

534
    constexpr static std::string_view s_baasaas_lock_file_name = "baasaas_instance.lock";
535

536
    std::string m_api_key;
537
    std::string m_base_url;
538
    std::string m_container_id;
539
    bool m_externally_managed_instance;
540
    std::string m_http_endpoint;
541
    std::string m_mongo_endpoint;
542
};
543

544
class BaasaasLauncher : public Catch::EventListenerBase {
545
public:
546
    static std::optional<Baasaas>& get_baasaas_holder()
547
    {
9,340✔
548
        static std::optional<Baasaas> global_baasaas = std::nullopt;
9,340✔
549
        return global_baasaas;
9,340✔
550
    }
9,340✔
551

552
    using Catch::EventListenerBase::EventListenerBase;
553

554
    void testRunStarting(Catch::TestRunInfo const&) override
555
    {
32✔
556
        std::string_view api_key(getenv_sv("BAASAAS_API_KEY"));
32✔
557
        if (api_key.empty()) {
32✔
558
            return;
16✔
559
        }
16✔
560

8✔
561
        // Allow overriding the baas base url at runtime via an environment variable, even if BAASAAS_API_KEY
8✔
562
        // is also specified.
8✔
563
        if (!getenv_sv("BAAS_BASE_URL").empty()) {
16✔
564
            return;
×
565
        }
×
566

8✔
567
        // If we've started a baasaas container outside of running the tests, then use that instead of
8✔
568
        // figuring out how to start our own.
8✔
569
        if (auto baasaas_instance = getenv_sv("BAASAAS_INSTANCE_ID"); !baasaas_instance.empty()) {
16✔
570
            auto& baasaas_holder = get_baasaas_holder();
×
571
            REALM_ASSERT(!baasaas_holder);
×
572
            baasaas_holder.emplace(std::string{api_key}, std::string{baasaas_instance});
×
573
            return;
×
574
        }
×
575

8✔
576
        std::string_view ref_spec(getenv_sv("BAASAAS_REF_SPEC"));
16✔
577
        std::string_view mode_spec(getenv_sv("BAASAAS_START_MODE"));
16✔
578
        Baasaas::StartMode mode = Baasaas::StartMode::Default;
16✔
579
        if (mode_spec == "branch") {
16✔
580
            if (ref_spec.empty()) {
×
581
                throw std::runtime_error("Expected branch name in BAASAAS_REF_SPEC env variable, but it was empty");
×
582
            }
×
583
            mode = Baasaas::StartMode::Branch;
×
584
        }
×
585
        else if (mode_spec == "githash") {
16✔
586
            if (ref_spec.empty()) {
16✔
587
                throw std::runtime_error("Expected git hash in BAASAAS_REF_SPEC env variable, but it was empty");
×
588
            }
×
589
            mode = Baasaas::StartMode::GitHash;
16✔
590
        }
16✔
591
        else if (mode_spec == "patchid") {
×
592
            if (ref_spec.empty()) {
×
593
                throw std::runtime_error("Expected patch id in BAASAAS_REF_SPEC env variable, but it was empty");
×
594
            }
×
595
            mode = Baasaas::StartMode::PatchId;
×
596
        }
×
597
        else {
×
598
            if (!mode_spec.empty()) {
×
599
                throw std::runtime_error("Excepted BAASAAS_START_MODE to be \"githash\", \"patchid\", or \"branch\"");
×
600
            }
×
601
            ref_spec = {};
×
602
        }
×
603

8✔
604
        auto& baasaas_holder = get_baasaas_holder();
16✔
605
        REALM_ASSERT(!baasaas_holder);
16✔
606
        baasaas_holder.emplace(std::string{api_key}, mode, std::string{ref_spec});
16✔
607

8✔
608
        get_runtime_app_session();
16✔
609
    }
16✔
610

611
    void testRunEnded(Catch::TestRunStats const&) override
612
    {
32✔
613
        if (auto& baasaas_holder = get_baasaas_holder(); baasaas_holder.has_value()) {
32✔
614
            baasaas_holder->stop();
16✔
615
        }
16✔
616
    }
32✔
617
};
618

619
CATCH_REGISTER_LISTENER(BaasaasLauncher)
620

621
AdminAPIEndpoint AdminAPIEndpoint::operator[](StringData name) const
622
{
140,717✔
623
    return AdminAPIEndpoint(util::format("%1/%2", m_url, name), m_access_token);
140,717✔
624
}
140,717✔
625

626
app::Response AdminAPIEndpoint::do_request(app::Request request) const
627
{
90,800✔
628
    if (request.url.find('?') == std::string::npos) {
90,800✔
629
        request.url = util::format("%1?bypass_service_change=SyncSchemaVersionIncrease", request.url);
90,800✔
630
    }
90,800✔
631
    else {
×
632
        request.url = util::format("%1&bypass_service_change=SyncSchemaVersionIncrease", request.url);
×
633
    }
×
634
    request.headers["Content-Type"] = "application/json;charset=utf-8";
90,800✔
635
    request.headers["Accept"] = "application/json";
90,800✔
636
    request.headers["Authorization"] = util::format("Bearer %1", m_access_token);
90,800✔
637
    return do_http_request(std::move(request));
90,800✔
638
}
90,800✔
639

640
app::Response AdminAPIEndpoint::get(const std::vector<std::pair<std::string, std::string>>& params) const
641
{
27,790✔
642
    app::Request req;
27,790✔
643
    req.method = app::HttpMethod::get;
27,790✔
644
    std::stringstream ss;
27,790✔
645
    bool needs_and = false;
27,790✔
646
    ss << m_url;
27,790✔
647
    if (!params.empty() && m_url.find('?') != std::string::npos) {
27,790!
648
        needs_and = true;
×
649
    }
×
650
    for (const auto& param : params) {
13,694✔
651
        if (needs_and) {
×
652
            ss << "&";
×
653
        }
×
654
        else {
×
655
            ss << "?";
×
656
        }
×
657
        needs_and = true;
×
658
        ss << param.first << "=" << param.second;
×
659
    }
×
660
    req.url = ss.str();
27,790✔
661
    return do_request(std::move(req));
27,790✔
662
}
27,790✔
663

664
app::Response AdminAPIEndpoint::del() const
665
{
3,242✔
666
    app::Request req;
3,242✔
667
    req.method = app::HttpMethod::del;
3,242✔
668
    req.url = m_url;
3,242✔
669
    return do_request(std::move(req));
3,242✔
670
}
3,242✔
671

672
nlohmann::json AdminAPIEndpoint::get_json(const std::vector<std::pair<std::string, std::string>>& params) const
673
{
27,790✔
674
    auto resp = get(params);
27,790✔
675
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300,
27,790✔
676
                    util::format("url: %1, reply: %2", m_url, resp.body));
27,790✔
677
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
27,790✔
678
}
27,790✔
679

680
app::Response AdminAPIEndpoint::post(std::string body) const
681
{
38,530✔
682
    app::Request req;
38,530✔
683
    req.method = app::HttpMethod::post;
38,530✔
684
    req.url = m_url;
38,530✔
685
    req.body = std::move(body);
38,530✔
686
    return do_request(std::move(req));
38,530✔
687
}
38,530✔
688

689
nlohmann::json AdminAPIEndpoint::post_json(nlohmann::json body) const
690
{
38,354✔
691
    auto resp = post(body.dump());
38,354✔
692
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300, m_url, body.dump(), resp.body);
38,354✔
693
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
38,354✔
694
}
38,354✔
695

696
app::Response AdminAPIEndpoint::put(std::string body) const
697
{
15,186✔
698
    app::Request req;
15,186✔
699
    req.method = app::HttpMethod::put;
15,186✔
700
    req.url = m_url;
15,186✔
701
    req.body = std::move(body);
15,186✔
702
    return do_request(std::move(req));
15,186✔
703
}
15,186✔
704

705
nlohmann::json AdminAPIEndpoint::put_json(nlohmann::json body) const
706
{
12,120✔
707
    auto resp = put(body.dump());
12,120✔
708
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300,
12,120✔
709
                    util::format("url: %1 request: %2, reply: %3", m_url, body.dump(), resp.body));
12,120✔
710
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
11,504✔
711
}
12,120✔
712

713
app::Response AdminAPIEndpoint::patch(std::string body) const
714
{
6,052✔
715
    app::Request req;
6,052✔
716
    req.method = app::HttpMethod::patch;
6,052✔
717
    req.url = m_url;
6,052✔
718
    req.body = std::move(body);
6,052✔
719
    return do_request(std::move(req));
6,052✔
720
}
6,052✔
721

722
nlohmann::json AdminAPIEndpoint::patch_json(nlohmann::json body) const
723
{
6,052✔
724
    auto resp = patch(body.dump());
6,052✔
725
    REALM_ASSERT_EX(resp.http_status_code >= 200 && resp.http_status_code < 300,
6,052✔
726
                    util::format("url: %1 request: %2, reply: %3", m_url, body.dump(), resp.body));
6,052✔
727
    return nlohmann::json::parse(resp.body.empty() ? "{}" : resp.body);
6,052✔
728
}
6,052✔
729

730
AdminAPISession AdminAPISession::login(const AppCreateConfig& config)
731
{
3,002✔
732
    std::string admin_url = config.admin_url;
3,002✔
733
    nlohmann::json login_req_body{
3,002✔
734
        {"provider", "userpass"},
3,002✔
735
        {"username", config.admin_username},
3,002✔
736
        {"password", config.admin_password},
3,002✔
737
    };
3,002✔
738
    if (config.logger) {
3,002✔
739
        config.logger->trace("Logging into baas admin api: %1", admin_url);
3,002✔
740
    }
3,002✔
741
    app::Request auth_req{
3,002✔
742
        app::HttpMethod::post,
3,002✔
743
        util::format("%1/api/admin/v3.0/auth/providers/local-userpass/login", admin_url),
3,002✔
744
        60000, // 1 minute timeout
3,002✔
745
        {
3,002✔
746
            {"Content-Type", "application/json;charset=utf-8"},
3,002✔
747
            {"Accept", "application/json"},
3,002✔
748
        },
3,002✔
749
        login_req_body.dump(),
3,002✔
750
    };
3,002✔
751
    auto login_resp = do_http_request(std::move(auth_req));
3,002✔
752
    REALM_ASSERT_EX(login_resp.http_status_code == 200, login_resp.http_status_code, login_resp.body);
3,002✔
753
    auto login_resp_body = nlohmann::json::parse(login_resp.body);
3,002✔
754

1,433✔
755
    std::string access_token = login_resp_body["access_token"];
3,002✔
756

1,433✔
757
    AdminAPIEndpoint user_profile(util::format("%1/api/admin/v3.0/auth/profile", admin_url), access_token);
3,002✔
758
    auto profile_resp = user_profile.get_json();
3,002✔
759

1,433✔
760
    std::string group_id = profile_resp["roles"][0]["group_id"];
3,002✔
761

1,433✔
762
    return AdminAPISession(std::move(admin_url), std::move(access_token), std::move(group_id));
3,002✔
763
}
3,002✔
764

765
void AdminAPISession::revoke_user_sessions(const std::string& user_id, const std::string& app_id) const
766
{
32✔
767
    auto endpoint = apps()[app_id]["users"][user_id]["logout"];
32✔
768
    auto response = endpoint.put("");
32✔
769
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
32✔
770
}
32✔
771

772
void AdminAPISession::disable_user_sessions(const std::string& user_id, const std::string& app_id) const
773
{
16✔
774
    auto endpoint = apps()[app_id]["users"][user_id]["disable"];
16✔
775
    auto response = endpoint.put("");
16✔
776
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
16✔
777
}
16✔
778

779
void AdminAPISession::enable_user_sessions(const std::string& user_id, const std::string& app_id) const
780
{
16✔
781
    auto endpoint = apps()[app_id]["users"][user_id]["enable"];
16✔
782
    auto response = endpoint.put("");
16✔
783
    REALM_ASSERT_EX(response.http_status_code == 204, response.http_status_code, response.body);
16✔
784
}
16✔
785

786
// returns false for an invalid/expired access token
787
bool AdminAPISession::verify_access_token(const std::string& access_token, const std::string& app_id) const
788
{
176✔
789
    auto endpoint = apps()[app_id]["users"]["verify_token"];
176✔
790
    nlohmann::json request_body{
176✔
791
        {"token", access_token},
176✔
792
    };
176✔
793
    auto response = endpoint.post(request_body.dump());
176✔
794
    if (response.http_status_code == 200) {
176✔
795
        auto resp_json = nlohmann::json::parse(response.body.empty() ? "{}" : response.body);
128✔
796
        try {
128✔
797
            // if these fields are found, then the token is valid according to the server.
64✔
798
            // if it is invalid or expired then an error response is sent.
64✔
799
            int64_t issued_at = resp_json["iat"];
128✔
800
            int64_t expires_at = resp_json["exp"];
128✔
801
            return issued_at != 0 && expires_at != 0;
128✔
802
        }
128✔
803
        catch (...) {
×
804
            return false;
×
805
        }
×
806
    }
48✔
807
    return false;
48✔
808
}
48✔
809

810
void AdminAPISession::set_development_mode_to(const std::string& app_id, bool enable) const
811
{
192✔
812
    auto endpoint = apps()[app_id]["sync"]["config"];
192✔
813
    endpoint.put_json({{"development_mode_enabled", enable}});
192✔
814
}
192✔
815

816
void AdminAPISession::delete_app(const std::string& app_id) const
817
{
2,986✔
818
    auto app_endpoint = apps()[app_id];
2,986✔
819
    auto resp = app_endpoint.del();
2,986✔
820
    REALM_ASSERT_EX(resp.http_status_code == 204, resp.http_status_code, resp.body);
2,986✔
821
}
2,986✔
822

823
std::vector<AdminAPISession::Service> AdminAPISession::get_services(const std::string& app_id) const
824
{
544✔
825
    auto endpoint = apps()[app_id]["services"];
544✔
826
    auto response = endpoint.get_json();
544✔
827
    std::vector<AdminAPISession::Service> services;
544✔
828
    for (auto service : response) {
1,088✔
829
        services.push_back(
1,088✔
830
            {service["_id"], service["name"], service["type"], service["version"], service["last_modified"]});
1,088✔
831
    }
1,088✔
832
    return services;
544✔
833
}
544✔
834

835

836
std::vector<std::string> AdminAPISession::get_errors(const std::string& app_id) const
837
{
×
838
    auto endpoint = apps()[app_id]["logs"];
×
839
    auto response = endpoint.get_json({{"errors_only", "true"}});
×
840
    std::vector<std::string> errors;
×
841
    const auto& logs = response["logs"];
×
842
    std::transform(logs.begin(), logs.end(), std::back_inserter(errors), [](const auto& err) {
×
843
        return err["error"];
×
844
    });
×
845
    return errors;
×
846
}
×
847

848

849
AdminAPISession::Service AdminAPISession::get_sync_service(const std::string& app_id) const
850
{
544✔
851
    auto services = get_services(app_id);
544✔
852
    auto sync_service = std::find_if(services.begin(), services.end(), [&](auto s) {
544✔
853
        return s.type == "mongodb";
544✔
854
    });
544✔
855
    REALM_ASSERT(sync_service != services.end());
544✔
856
    return *sync_service;
544✔
857
}
544✔
858

859
void AdminAPISession::trigger_client_reset(const std::string& app_id, int64_t file_ident) const
860
{
1,232✔
861
    auto endpoint = apps(APIFamily::Admin)[app_id]["sync"]["force_reset"];
1,232✔
862
    endpoint.put_json(nlohmann::json{{"file_ident", file_ident}});
1,232✔
863
}
1,232✔
864

865
void AdminAPISession::migrate_to_flx(const std::string& app_id, const std::string& service_id,
866
                                     bool migrate_to_flx) const
867
{
240✔
868
    auto endpoint = apps()[app_id]["sync"]["migration"];
240✔
869
    endpoint.put_json(nlohmann::json{{"serviceId", service_id}, {"action", migrate_to_flx ? "start" : "rollback"}});
192✔
870
}
240✔
871

872
// Each breaking change bumps the schema version, so you can create a new version for each breaking change if
873
// 'use_draft' is false. Set 'use_draft' to true if you want all changes to the schema to be deployed at once
874
// resulting in only one schema version.
875
void AdminAPISession::create_schema(const std::string& app_id, const AppCreateConfig& config, bool use_draft) const
876
{
3,306✔
877
    static const std::string mongo_service_name = "BackingDB";
3,306✔
878

1,585✔
879
    auto drafts = apps()[app_id]["drafts"];
3,306✔
880
    std::string draft_id;
3,306✔
881
    if (use_draft) {
3,306✔
882
        auto draft_create_resp = drafts.post_json({});
304✔
883
        draft_id = draft_create_resp["_id"];
304✔
884
    }
304✔
885

1,585✔
886
    auto schemas = apps()[app_id]["schemas"];
3,306✔
887
    auto current_schema = schemas.get_json();
3,306✔
888
    auto target_schema = config.schema;
3,306✔
889

1,585✔
890
    std::unordered_map<std::string, std::string> current_schema_tables;
3,306✔
891
    for (const auto& schema : current_schema) {
2,025✔
892
        current_schema_tables[schema["metadata"]["collection"]] = schema["_id"];
880✔
893
    }
880✔
894

1,585✔
895
    // Add new tables
1,585✔
896

1,585✔
897
    auto pk_and_queryable_only = [&](const Property& prop) {
24,492✔
898
        if (config.flx_sync_config) {
24,492✔
899
            const auto& queryable_fields = config.flx_sync_config->queryable_fields;
9,680✔
900

4,792✔
901
            if (std::find(queryable_fields.begin(), queryable_fields.end(), prop.name) != queryable_fields.end()) {
9,680✔
902
                return true;
4,080✔
903
            }
4,080✔
904
        }
20,412✔
905
        return prop.name == "_id" || prop.name == config.partition_key.name;
20,412✔
906
    };
20,412✔
907

1,585✔
908
    // Create the schemas in two passes: first populate just the primary key and
1,585✔
909
    // partition key, then add the rest of the properties. This ensures that the
1,585✔
910
    // targets of links exist before adding the links.
1,585✔
911
    std::vector<std::pair<std::string, const ObjectSchema*>> object_schema_to_create;
3,306✔
912
    BaasRuleBuilder rule_builder(target_schema, config.partition_key, mongo_service_name, config.mongo_dbname,
3,306✔
913
                                 static_cast<bool>(config.flx_sync_config));
3,306✔
914
    for (const auto& obj_schema : target_schema) {
7,454✔
915
        auto it = current_schema_tables.find(obj_schema.name);
7,454✔
916
        if (it != current_schema_tables.end()) {
7,454✔
917
            object_schema_to_create.push_back({it->second, &obj_schema});
624✔
918
            continue;
624✔
919
        }
624✔
920

3,299✔
921
        auto schema_to_create = rule_builder.object_schema_to_baas_schema(obj_schema, pk_and_queryable_only);
6,830✔
922
        auto schema_create_resp = schemas.post_json(schema_to_create);
6,830✔
923
        object_schema_to_create.push_back({schema_create_resp["_id"], &obj_schema});
6,830✔
924
    }
6,830✔
925

1,585✔
926
    // Update existing tables (including the ones just created)
1,585✔
927
    for (const auto& [id, obj_schema] : object_schema_to_create) {
7,454✔
928
        auto schema_to_create = rule_builder.object_schema_to_baas_schema(*obj_schema, nullptr);
7,454✔
929
        schema_to_create["_id"] = id;
7,454✔
930
        schemas[id].put_json(schema_to_create);
7,454✔
931
    }
7,454✔
932

1,585✔
933
    // Delete removed tables
1,585✔
934
    for (const auto& table : current_schema_tables) {
2,025✔
935
        if (target_schema.find(table.first) == target_schema.end()) {
880✔
936
            schemas[table.second].del();
256✔
937
        }
256✔
938
    }
880✔
939

1,585✔
940
    if (use_draft) {
3,306✔
941
        drafts[draft_id]["deployment"].post_json({});
304✔
942
    }
304✔
943
}
3,306✔
944

945
static nlohmann::json convert_config(AdminAPISession::ServiceConfig config)
946
{
3,050✔
947
    if (config.mode == AdminAPISession::ServiceConfig::SyncMode::Flexible) {
3,050✔
948
        auto payload = nlohmann::json{{"database_name", config.database_name},
1,480✔
949
                                      {"state", config.state},
1,480✔
950
                                      {"is_recovery_mode_disabled", config.recovery_is_disabled}};
1,480✔
951
        if (config.queryable_field_names) {
1,480✔
952
            payload["queryable_fields_names"] = *config.queryable_field_names;
1,480✔
953
        }
1,480✔
954
        if (config.permissions) {
1,480✔
955
            payload["permissions"] = *config.permissions;
16✔
956
        }
16✔
957
        if (config.asymmetric_tables) {
1,480✔
958
            payload["asymmetric_tables"] = *config.asymmetric_tables;
1,464✔
959
        }
1,464✔
960
        return payload;
1,480✔
961
    }
1,480✔
962
    return nlohmann::json{{"database_name", config.database_name},
1,570✔
963
                          {"partition", *config.partition},
1,570✔
964
                          {"state", config.state},
1,570✔
965
                          {"is_recovery_mode_disabled", config.recovery_is_disabled}};
1,570✔
966
}
1,570✔
967

968
AdminAPIEndpoint AdminAPISession::service_config_endpoint(const std::string& app_id,
969
                                                          const std::string& service_id) const
970
{
3,386✔
971
    return apps()[app_id]["services"][service_id]["config"];
3,386✔
972
}
3,386✔
973

974
AdminAPISession::ServiceConfig AdminAPISession::disable_sync(const std::string& app_id, const std::string& service_id,
975
                                                             AdminAPISession::ServiceConfig sync_config) const
976
{
×
977
    auto endpoint = service_config_endpoint(app_id, service_id);
×
978
    if (sync_config.state != "") {
×
979
        sync_config.state = "";
×
980
        endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
×
981
    }
×
982
    return sync_config;
×
983
}
×
984

985
AdminAPISession::ServiceConfig AdminAPISession::pause_sync(const std::string& app_id, const std::string& service_id,
986
                                                           AdminAPISession::ServiceConfig sync_config) const
987
{
×
988
    auto endpoint = service_config_endpoint(app_id, service_id);
×
989
    if (sync_config.state != "disabled") {
×
990
        sync_config.state = "disabled";
×
991
        endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
×
992
    }
×
993
    return sync_config;
×
994
}
×
995

996
AdminAPISession::ServiceConfig AdminAPISession::enable_sync(const std::string& app_id, const std::string& service_id,
997
                                                            AdminAPISession::ServiceConfig sync_config) const
998
{
3,018✔
999
    auto endpoint = service_config_endpoint(app_id, service_id);
3,018✔
1000
    sync_config.state = "enabled";
3,018✔
1001
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
3,018✔
1002
    return sync_config;
3,018✔
1003
}
3,018✔
1004

1005
AdminAPISession::ServiceConfig AdminAPISession::set_disable_recovery_to(const std::string& app_id,
1006
                                                                        const std::string& service_id,
1007
                                                                        ServiceConfig sync_config, bool disable) const
1008
{
32✔
1009
    auto endpoint = service_config_endpoint(app_id, service_id);
32✔
1010
    sync_config.recovery_is_disabled = disable;
32✔
1011
    endpoint.patch_json({{sync_config.sync_service_name(), convert_config(sync_config)}});
32✔
1012
    return sync_config;
32✔
1013
}
32✔
1014

1015
std::vector<AdminAPISession::SchemaVersionInfo> AdminAPISession::get_schema_versions(const std::string& app_id) const
1016
{
953✔
1017
    std::vector<AdminAPISession::SchemaVersionInfo> ret;
953✔
1018
    auto endpoint = apps()[app_id]["sync"]["schemas"]["versions"];
953✔
1019
    auto res = endpoint.get_json();
953✔
1020
    for (auto&& version : res["versions"].get<std::vector<nlohmann::json>>()) {
1,374✔
1021
        SchemaVersionInfo info;
1,374✔
1022
        info.version_major = version["version_major"];
1,374✔
1023
        ret.push_back(std::move(info));
1,374✔
1024
    }
1,374✔
1025

484✔
1026
    return ret;
953✔
1027
}
953✔
1028

1029
AdminAPISession::ServiceConfig AdminAPISession::get_config(const std::string& app_id,
1030
                                                           const AdminAPISession::Service& service) const
1031
{
336✔
1032
    auto endpoint = service_config_endpoint(app_id, service.id);
336✔
1033
    auto response = endpoint.get_json();
336✔
1034
    AdminAPISession::ServiceConfig config;
336✔
1035
    if (response.contains("flexible_sync")) {
336✔
1036
        auto sync = response["flexible_sync"];
128✔
1037
        config.mode = AdminAPISession::ServiceConfig::SyncMode::Flexible;
128✔
1038
        config.state = sync["state"];
128✔
1039
        config.database_name = sync["database_name"];
128✔
1040
        config.permissions = sync["permissions"];
128✔
1041
        config.queryable_field_names = sync["queryable_fields_names"];
128✔
1042
        auto recovery_disabled = sync["is_recovery_mode_disabled"];
128✔
1043
        config.recovery_is_disabled = recovery_disabled.is_boolean() ? recovery_disabled.get<bool>() : false;
128✔
1044
    }
128✔
1045
    else if (response.contains("sync")) {
208✔
1046
        auto sync = response["sync"];
208✔
1047
        config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
208✔
1048
        config.state = sync["state"];
208✔
1049
        config.database_name = sync["database_name"];
208✔
1050
        config.partition = sync["partition"];
208✔
1051
        auto recovery_disabled = sync["is_recovery_mode_disabled"];
208✔
1052
        config.recovery_is_disabled = recovery_disabled.is_boolean() ? recovery_disabled.get<bool>() : false;
208✔
1053
    }
208✔
1054
    else {
×
1055
        throw std::runtime_error(util::format("Unsupported config format from server: %1", response));
×
1056
    }
×
1057
    return config;
336✔
1058
}
336✔
1059

1060
bool AdminAPISession::is_sync_enabled(const std::string& app_id) const
1061
{
240✔
1062
    auto sync_service = get_sync_service(app_id);
240✔
1063
    auto config = get_config(app_id, sync_service);
240✔
1064
    return config.state == "enabled";
240✔
1065
}
240✔
1066

1067
bool AdminAPISession::is_sync_terminated(const std::string& app_id) const
1068
{
×
1069
    auto sync_service = get_sync_service(app_id);
×
1070
    auto config = get_config(app_id, sync_service);
×
1071
    if (config.state == "enabled") {
×
1072
        return false;
×
1073
    }
×
1074
    auto state_endpoint = apps()[app_id]["sync"]["state"];
×
1075
    auto state_result = state_endpoint.get_json(
×
1076
        {{"sync_type", config.mode == ServiceConfig::SyncMode::Flexible ? "flexible" : "partition"}});
×
1077
    return state_result["state"].get<std::string>().empty();
×
1078
}
×
1079

1080
bool AdminAPISession::is_initial_sync_complete(const std::string& app_id) const
1081
{
10,640✔
1082
    auto progress_endpoint = apps()[app_id]["sync"]["progress"];
10,640✔
1083
    auto progress_result = progress_endpoint.get_json();
10,640✔
1084
    if (auto it = progress_result.find("progress"); it != progress_result.end() && it->is_object() && !it->empty()) {
10,640✔
1085
        for (auto& elem : *it) {
12,397✔
1086
            auto is_complete = elem["complete"];
12,397✔
1087
            if (!is_complete.is_boolean() || !is_complete.get<bool>()) {
12,397✔
1088
                return false;
3,002✔
1089
            }
3,002✔
1090
        }
12,397✔
1091
        return true;
5,175✔
1092
    }
3,932✔
1093
    return false;
3,932✔
1094
}
3,932✔
1095

1096
AdminAPISession::MigrationStatus AdminAPISession::get_migration_status(const std::string& app_id) const
1097
{
6,007✔
1098
    MigrationStatus status;
6,007✔
1099
    auto progress_endpoint = apps()[app_id]["sync"]["migration"];
6,007✔
1100
    auto progress_result = progress_endpoint.get_json();
6,007✔
1101
    auto errorMessage = progress_result["errorMessage"];
6,007✔
1102
    if (errorMessage.is_string() && !errorMessage.get<std::string>().empty()) {
6,007✔
1103
        throw Exception(Status{ErrorCodes::RuntimeError, errorMessage.get<std::string>()});
×
1104
    }
×
1105
    if (!progress_result["statusMessage"].is_string() || !progress_result["isMigrated"].is_boolean()) {
6,007✔
1106
        throw Exception(
×
1107
            Status{ErrorCodes::RuntimeError, util::format("Invalid result returned from migration status request: %1",
×
1108
                                                          progress_result.dump(4, 32, true))});
×
1109
    }
×
1110

3,077✔
1111
    status.statusMessage = progress_result["statusMessage"].get<std::string>();
6,007✔
1112
    status.isMigrated = progress_result["isMigrated"].get<bool>();
6,007✔
1113
    status.isCancelable = progress_result["isCancelable"].get<bool>();
6,007✔
1114
    status.isRevertible = progress_result["isRevertible"].get<bool>();
6,007✔
1115
    status.complete = status.statusMessage.empty();
6,007✔
1116
    return status;
6,007✔
1117
}
6,007✔
1118

1119
AdminAPIEndpoint AdminAPISession::apps(APIFamily family) const
1120
{
39,036✔
1121
    switch (family) {
39,036✔
1122
        case APIFamily::Admin:
39,036✔
1123
            return AdminAPIEndpoint(util::format("%1/api/admin/v3.0/groups/%2/apps", m_base_url, m_group_id),
39,036✔
1124
                                    m_access_token);
39,036✔
1125
        case APIFamily::Private:
✔
1126
            return AdminAPIEndpoint(util::format("%1/api/private/v1.0/groups/%2/apps", m_base_url, m_group_id),
×
1127
                                    m_access_token);
×
1128
    }
×
1129
    REALM_UNREACHABLE();
1130
}
×
1131

1132
realm::Schema get_default_schema()
1133
{
724✔
1134
    const auto dog_schema =
724✔
1135
        ObjectSchema("Dog", {realm::Property("_id", PropertyType::ObjectId | PropertyType::Nullable, true),
724✔
1136
                             realm::Property("breed", PropertyType::String | PropertyType::Nullable),
724✔
1137
                             realm::Property("name", PropertyType::String),
724✔
1138
                             realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
724✔
1139
    const auto cat_schema =
724✔
1140
        ObjectSchema("Cat", {realm::Property("_id", PropertyType::String | PropertyType::Nullable, true),
724✔
1141
                             realm::Property("breed", PropertyType::String | PropertyType::Nullable),
724✔
1142
                             realm::Property("name", PropertyType::String),
724✔
1143
                             realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
724✔
1144
    const auto person_schema =
724✔
1145
        ObjectSchema("Person", {realm::Property("_id", PropertyType::ObjectId | PropertyType::Nullable, true),
724✔
1146
                                realm::Property("age", PropertyType::Int),
724✔
1147
                                realm::Property("dogs", PropertyType::Object | PropertyType::Array, "Dog"),
724✔
1148
                                realm::Property("firstName", PropertyType::String),
724✔
1149
                                realm::Property("lastName", PropertyType::String),
724✔
1150
                                realm::Property("realm_id", PropertyType::String | PropertyType::Nullable)});
724✔
1151
    return realm::Schema({dog_schema, cat_schema, person_schema});
724✔
1152
}
724✔
1153

1154
std::string get_base_url()
1155
{
6,306✔
1156
    if (auto baas_url = getenv_sv("BAAS_BASE_URL"); !baas_url.empty()) {
6,306✔
1157
        return std::string{baas_url};
×
1158
    }
×
1159
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
6,306✔
1160
        return baasaas_holder->http_endpoint();
6,306✔
1161
    }
6,306✔
1162

1163
    return get_compile_time_base_url();
×
1164
}
×
1165

1166
std::string get_admin_url()
1167
{
2,986✔
1168
    if (auto baas_admin_url = getenv_sv("BAAS_ADMIN_URL"); !baas_admin_url.empty()) {
2,986✔
1169
        return std::string{baas_admin_url};
×
1170
    }
×
1171
    if (auto compile_url = get_compile_time_admin_url(); !compile_url.empty()) {
2,986✔
1172
        return compile_url;
×
1173
    }
×
1174

1,433✔
1175
    return get_base_url();
2,986✔
1176
}
2,986✔
1177

1178
std::string get_mongodb_server()
1179
{
2,986✔
1180
    if (auto baas_url = getenv_sv("BAAS_MONGO_URL"); !baas_url.empty()) {
2,986✔
1181
        return std::string{baas_url};
×
1182
    }
×
1183

1,433✔
1184
    if (auto& baasaas_holder = BaasaasLauncher::get_baasaas_holder(); baasaas_holder.has_value()) {
2,986✔
1185
        return baasaas_holder->mongo_endpoint();
2,986✔
1186
    }
2,986✔
1187
    return "mongodb://localhost:26000";
×
1188
}
×
1189

1190

1191
AppCreateConfig default_app_config()
1192
{
112✔
1193
    ObjectId id = ObjectId::gen();
112✔
1194
    std::string db_name = util::format("test_data_%1", id.to_string());
112✔
1195
    std::string app_url = get_base_url();
112✔
1196
    std::string admin_url = get_admin_url();
112✔
1197
    REALM_ASSERT(!app_url.empty());
112✔
1198
    REALM_ASSERT(!admin_url.empty());
112✔
1199

8✔
1200
    std::string update_user_data_func = util::format(R"(
112✔
1201
        exports = async function(data) {
112✔
1202
            const user = context.user;
112✔
1203
            const mongodb = context.services.get("BackingDB");
112✔
1204
            const userDataCollection = mongodb.db("%1").collection("UserData");
112✔
1205
            await userDataCollection.updateOne(
112✔
1206
                                               { "user_id": user.id },
112✔
1207
                                               { "$set": data },
112✔
1208
                                               { "upsert": true }
112✔
1209
                                               );
112✔
1210
            return true;
112✔
1211
        };
112✔
1212
    )",
112✔
1213
                                                     db_name);
112✔
1214

8✔
1215
    constexpr const char* sum_func = R"(
112✔
1216
        exports = function(...args) {
112✔
1217
            return args.reduce((a,b) => a + b, 0);
112✔
1218
        };
112✔
1219
    )";
112✔
1220

8✔
1221
    constexpr const char* confirm_func = R"(
112✔
1222
        exports = ({ token, tokenId, username }) => {
112✔
1223
            // process the confirm token, tokenId and username
112✔
1224
            if (username.includes("realm_tests_do_autoverify")) {
112✔
1225
              return { status: 'success' }
112✔
1226
            }
112✔
1227
            // do not confirm the user
112✔
1228
            return { status: 'fail' };
112✔
1229
        };
112✔
1230
    )";
112✔
1231

8✔
1232
    constexpr const char* auth_func = R"(
112✔
1233
        exports = (loginPayload) => {
112✔
1234
            return loginPayload["realmCustomAuthFuncUserId"];
112✔
1235
        };
112✔
1236
    )";
112✔
1237

8✔
1238
    constexpr const char* reset_func = R"(
112✔
1239
        exports = ({ token, tokenId, username, password }) => {
112✔
1240
            // process the reset token, tokenId, username and password
112✔
1241
            if (password.includes("realm_tests_do_reset")) {
112✔
1242
              return { status: 'success' };
112✔
1243
            }
112✔
1244
            // will not reset the password
112✔
1245
            return { status: 'fail' };
112✔
1246
        };
112✔
1247
    )";
112✔
1248

8✔
1249
    std::vector<AppCreateConfig::FunctionDef> funcs = {
112✔
1250
        {"updateUserData", update_user_data_func, false},
112✔
1251
        {"sumFunc", sum_func, false},
112✔
1252
        {"confirmFunc", confirm_func, false},
112✔
1253
        {"authFunc", auth_func, false},
112✔
1254
        {"resetFunc", reset_func, false},
112✔
1255
    };
112✔
1256

8✔
1257
    Property partition_key("realm_id", PropertyType::String | PropertyType::Nullable);
112✔
1258

8✔
1259
    AppCreateConfig::UserPassAuthConfig user_pass_config{
112✔
1260
        false,
112✔
1261
        "",
112✔
1262
        "confirmFunc",
112✔
1263
        "http://localhost/confirmEmail",
112✔
1264
        "resetFunc",
112✔
1265
        "",
112✔
1266
        "http://localhost/resetPassword",
112✔
1267
        true,
112✔
1268
        true,
112✔
1269
    };
112✔
1270

8✔
1271
    return AppCreateConfig{
112✔
1272
        "test",
112✔
1273
        std::move(app_url),
112✔
1274
        std::move(admin_url), // BAAS Admin API URL may be different
112✔
1275
        "unique_user@domain.com",
112✔
1276
        "password",
112✔
1277
        get_mongodb_server(),
112✔
1278
        db_name,
112✔
1279
        get_default_schema(),
112✔
1280
        std::move(partition_key),
112✔
1281
        false,                              // Dev mode disabled
112✔
1282
        util::none,                         // Default to no FLX sync config
112✔
1283
        std::move(funcs),                   // Add default functions
112✔
1284
        std::move(user_pass_config),        // enable basic user/pass auth
112✔
1285
        std::string{"authFunc"},            // custom auth function
112✔
1286
        true,                               // enable_api_key_auth
112✔
1287
        true,                               // enable_anonymous_auth
112✔
1288
        true,                               // enable_custom_token_auth
112✔
1289
        {},                                 // no service roles on the default rule
112✔
1290
        util::Logger::get_default_logger(), // provide the logger to the admin api
112✔
1291
    };
112✔
1292
}
112✔
1293

1294
AppCreateConfig minimal_app_config(const std::string& name, const Schema& schema)
1295
{
2,874✔
1296
    Property partition_key("realm_id", PropertyType::String | PropertyType::Nullable);
2,874✔
1297
    std::string app_url = get_base_url();
2,874✔
1298
    std::string admin_url = get_admin_url();
2,874✔
1299
    REALM_ASSERT(!app_url.empty());
2,874✔
1300
    REALM_ASSERT(!admin_url.empty());
2,874✔
1301

1,425✔
1302
    AppCreateConfig::UserPassAuthConfig user_pass_config{
2,874✔
1303
        true,  "Confirm", "", "http://example.com/confirmEmail", "", "Reset", "http://exmaple.com/resetPassword",
2,874✔
1304
        false, false,
2,874✔
1305
    };
2,874✔
1306

1,425✔
1307
    ObjectId id = ObjectId::gen();
2,874✔
1308
    return AppCreateConfig{
2,874✔
1309
        name,
2,874✔
1310
        std::move(app_url),
2,874✔
1311
        std::move(admin_url), // BAAS Admin API URL may be different
2,874✔
1312
        "unique_user@domain.com",
2,874✔
1313
        "password",
2,874✔
1314
        get_mongodb_server(),
2,874✔
1315
        util::format("test_data_%1_%2", name, id.to_string()),
2,874✔
1316
        schema,
2,874✔
1317
        std::move(partition_key),
2,874✔
1318
        false,                              // Dev mode disabled
2,874✔
1319
        util::none,                         // no FLX sync config
2,874✔
1320
        {},                                 // no functions
2,874✔
1321
        std::move(user_pass_config),        // enable basic user/pass auth
2,874✔
1322
        util::none,                         // disable custom auth
2,874✔
1323
        true,                               // enable api key auth
2,874✔
1324
        true,                               // enable anonymous auth
2,874✔
1325
        false,                              // enable_custom_token_auth
2,874✔
1326
        {},                                 // no service roles on the default rule
2,874✔
1327
        util::Logger::get_default_logger(), // provide the logger to the admin api
2,874✔
1328
    };
2,874✔
1329
}
2,874✔
1330

1331
AppSession create_app(const AppCreateConfig& config)
1332
{
3,002✔
1333
    auto session = AdminAPISession::login(config);
3,002✔
1334
    auto create_app_resp = session.apps().post_json(nlohmann::json{{"name", config.app_name}});
3,002✔
1335
    std::string app_id = create_app_resp["_id"];
3,002✔
1336
    std::string client_app_id = create_app_resp["client_app_id"];
3,002✔
1337

1,433✔
1338
    auto app = session.apps()[app_id];
3,002✔
1339

1,433✔
1340
    auto functions = app["functions"];
3,002✔
1341
    std::unordered_map<std::string, std::string> function_name_to_id;
3,002✔
1342
    for (const auto& func : config.functions) {
2,033✔
1343
        auto create_func_resp = functions.post_json({
640✔
1344
            {"name", func.name},
640✔
1345
            {"private", func.is_private},
640✔
1346
            {"can_evaluate", nlohmann::json::object()},
640✔
1347
            {"source", func.source},
640✔
1348
        });
640✔
1349
        function_name_to_id.insert({func.name, create_func_resp["_id"]});
640✔
1350
    }
640✔
1351

1,433✔
1352
    auto auth_providers = app["auth_providers"];
3,002✔
1353
    if (config.enable_anonymous_auth) {
3,002✔
1354
        auth_providers.post_json({{"type", "anon-user"}});
3,002✔
1355
    }
3,002✔
1356
    if (config.user_pass_auth) {
3,002✔
1357
        auto user_pass_config_obj = nlohmann::json{
3,002✔
1358
            {"autoConfirm", config.user_pass_auth->auto_confirm},
3,002✔
1359
            {"confirmEmailSubject", config.user_pass_auth->confirm_email_subject},
3,002✔
1360
            {"emailConfirmationUrl", config.user_pass_auth->email_confirmation_url},
3,002✔
1361
            {"resetPasswordSubject", config.user_pass_auth->reset_password_subject},
3,002✔
1362
            {"resetPasswordUrl", config.user_pass_auth->reset_password_url},
3,002✔
1363
        };
3,002✔
1364
        if (!config.user_pass_auth->confirmation_function_name.empty()) {
3,002✔
1365
            const auto& confirm_func_name = config.user_pass_auth->confirmation_function_name;
128✔
1366
            user_pass_config_obj.emplace("confirmationFunctionName", confirm_func_name);
128✔
1367
            user_pass_config_obj.emplace("confirmationFunctionId", function_name_to_id[confirm_func_name]);
128✔
1368
            user_pass_config_obj.emplace("runConfirmationFunction", config.user_pass_auth->run_confirmation_function);
128✔
1369
        }
128✔
1370
        if (!config.user_pass_auth->reset_function_name.empty()) {
3,002✔
1371
            const auto& reset_func_name = config.user_pass_auth->reset_function_name;
128✔
1372
            user_pass_config_obj.emplace("resetFunctionName", reset_func_name);
128✔
1373
            user_pass_config_obj.emplace("resetFunctionId", function_name_to_id[reset_func_name]);
128✔
1374
            user_pass_config_obj.emplace("runResetFunction", config.user_pass_auth->run_reset_function);
128✔
1375
        }
128✔
1376
        auth_providers.post_json({{"type", "local-userpass"}, {"config", std::move(user_pass_config_obj)}});
3,002✔
1377
    }
3,002✔
1378
    if (config.custom_function_auth) {
3,002✔
1379
        auth_providers.post_json({{"type", "custom-function"},
128✔
1380
                                  {"config",
128✔
1381
                                   {
128✔
1382
                                       {"authFunctionName", *config.custom_function_auth},
128✔
1383
                                       {"authFunctionId", function_name_to_id[*config.custom_function_auth]},
128✔
1384
                                   }}});
128✔
1385
    }
128✔
1386

1,433✔
1387
    if (config.enable_api_key_auth) {
3,002✔
1388
        auto all_auth_providers = auth_providers.get_json();
3,002✔
1389
        auto api_key_provider =
3,002✔
1390
            std::find_if(all_auth_providers.begin(), all_auth_providers.end(), [](const nlohmann::json& provider) {
3,002✔
1391
                return provider["type"] == "api-key";
3,002✔
1392
            });
3,002✔
1393
        REALM_ASSERT(api_key_provider != all_auth_providers.end());
3,002✔
1394
        std::string api_key_provider_id = (*api_key_provider)["_id"];
3,002✔
1395
        auto api_key_enable_resp = auth_providers[api_key_provider_id]["enable"].put("");
3,002✔
1396
        REALM_ASSERT(api_key_enable_resp.http_status_code >= 200 && api_key_enable_resp.http_status_code < 300);
3,002✔
1397
    }
3,002✔
1398

1,433✔
1399
    auto secrets = app["secrets"];
3,002✔
1400
    secrets.post_json({{"name", "BackingDB_uri"}, {"value", config.mongo_uri}});
3,002✔
1401
    secrets.post_json({{"name", "gcm"}, {"value", "gcm"}});
3,002✔
1402
    secrets.post_json({{"name", "customTokenKey"}, {"value", "My_very_confidential_secretttttt"}});
3,002✔
1403

1,433✔
1404
    if (config.enable_custom_token_auth) {
3,002✔
1405
        auth_providers.post_json(
128✔
1406
            {{"type", "custom-token"},
128✔
1407
             {"config",
128✔
1408
              {
128✔
1409
                  {"audience", nlohmann::json::array()},
128✔
1410
                  {"signingAlgorithm", "HS256"},
128✔
1411
                  {"useJWKURI", false},
128✔
1412
              }},
128✔
1413
             {"secret_config", {{"signingKeys", nlohmann::json::array({"customTokenKey"})}}},
128✔
1414
             {"disabled", false},
128✔
1415
             {"metadata_fields",
128✔
1416
              {{{"required", false}, {"name", "user_data.name"}, {"field_name", "name"}},
128✔
1417
               {{"required", true}, {"name", "user_data.occupation"}, {"field_name", "occupation"}},
128✔
1418
               {{"required", true}, {"name", "my_metadata.name"}, {"field_name", "anotherName"}}}}});
128✔
1419
    }
128✔
1420

1,433✔
1421
    auto services = app["services"];
3,002✔
1422
    static const std::string mongo_service_name = "BackingDB";
3,002✔
1423

1,433✔
1424
    nlohmann::json mongo_service_def = {
3,002✔
1425
        {"name", mongo_service_name},
3,002✔
1426
        {"type", "mongodb"},
3,002✔
1427
        {"config", {{"uri", config.mongo_uri}}},
3,002✔
1428
    };
3,002✔
1429
    nlohmann::json sync_config;
3,002✔
1430
    if (config.flx_sync_config) {
3,002✔
1431
        auto queryable_fields = nlohmann::json::array();
1,464✔
1432
        const auto& queryable_fields_src = config.flx_sync_config->queryable_fields;
1,464✔
1433
        std::copy(queryable_fields_src.begin(), queryable_fields_src.end(), std::back_inserter(queryable_fields));
1,464✔
1434
        auto asymmetric_tables = nlohmann::json::array();
1,464✔
1435
        for (const auto& obj_schema : config.schema) {
2,648✔
1436
            if (obj_schema.table_type == ObjectSchema::ObjectType::TopLevelAsymmetric) {
2,648✔
1437
                asymmetric_tables.emplace_back(obj_schema.name);
32✔
1438
            }
32✔
1439
        }
2,648✔
1440
        sync_config = nlohmann::json{{"database_name", config.mongo_dbname},
1,464✔
1441
                                     {"queryable_fields_names", queryable_fields},
1,464✔
1442
                                     {"asymmetric_tables", asymmetric_tables}};
1,464✔
1443
        mongo_service_def["config"]["flexible_sync"] = sync_config;
1,464✔
1444
    }
1,464✔
1445
    else {
1,538✔
1446
        sync_config = nlohmann::json{
1,538✔
1447
            {"database_name", config.mongo_dbname},
1,538✔
1448
            {"partition",
1,538✔
1449
             {
1,538✔
1450
                 {"key", config.partition_key.name},
1,538✔
1451
                 {"type", property_type_to_bson_type_str(config.partition_key.type)},
1,538✔
1452
                 {"required", !is_nullable(config.partition_key.type)},
1,538✔
1453
                 {"permissions",
1,538✔
1454
                  {
1,538✔
1455
                      {"read", true},
1,538✔
1456
                      {"write", true},
1,538✔
1457
                  }},
1,538✔
1458
             }},
1,538✔
1459
        };
1,538✔
1460
        mongo_service_def["config"]["sync"] = sync_config;
1,538✔
1461
    }
1,538✔
1462

1,433✔
1463
    auto create_mongo_service_resp = services.post_json(std::move(mongo_service_def));
3,002✔
1464
    std::string mongo_service_id = create_mongo_service_resp["_id"];
3,002✔
1465

1,433✔
1466
    auto default_rule = services[mongo_service_id]["default_rule"];
3,002✔
1467
    auto service_roles = nlohmann::json::array();
3,002✔
1468
    if (config.service_roles.empty()) {
3,002✔
1469
        service_roles = nlohmann::json::array({{{"name", "default"},
2,954✔
1470
                                                {"apply_when", nlohmann::json::object()},
2,954✔
1471
                                                {"document_filters",
2,954✔
1472
                                                 {
2,954✔
1473
                                                     {"read", true},
2,954✔
1474
                                                     {"write", true},
2,954✔
1475
                                                 }},
2,954✔
1476
                                                {"read", true},
2,954✔
1477
                                                {"write", true},
2,954✔
1478
                                                {"insert", true},
2,954✔
1479
                                                {"delete", true}}});
2,954✔
1480
    }
2,954✔
1481
    else {
48✔
1482
        std::transform(config.service_roles.begin(), config.service_roles.end(), std::back_inserter(service_roles),
48✔
1483
                       [](const AppCreateConfig::ServiceRole& role_def) {
48✔
1484
                           nlohmann::json ret{
48✔
1485
                               {"name", role_def.name},
48✔
1486
                               {"apply_when", role_def.apply_when},
48✔
1487
                               {"document_filters",
48✔
1488
                                {
48✔
1489
                                    {"read", role_def.document_filters.read},
48✔
1490
                                    {"write", role_def.document_filters.write},
48✔
1491
                                }},
48✔
1492
                               {"insert", role_def.insert_filter},
48✔
1493
                               {"delete", role_def.delete_filter},
48✔
1494
                               {"read", role_def.read},
48✔
1495
                               {"write", role_def.write},
48✔
1496
                           };
48✔
1497
                           return ret;
48✔
1498
                       });
48✔
1499
    }
48✔
1500

1,433✔
1501
    default_rule.post_json({{"roles", service_roles}});
3,002✔
1502

1,433✔
1503
    // No need for a draft because there are no breaking changes in the initial schema when the app is created.
1,433✔
1504
    bool use_draft = false;
3,002✔
1505
    session.create_schema(app_id, config, use_draft);
3,002✔
1506

1,433✔
1507
    // Enable sync after schema is created.
1,433✔
1508
    AdminAPISession::ServiceConfig service_config;
3,002✔
1509
    service_config.database_name = sync_config["database_name"];
3,002✔
1510
    if (config.flx_sync_config) {
3,002✔
1511
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Flexible;
1,464✔
1512
        service_config.queryable_field_names = sync_config["queryable_fields_names"];
1,464✔
1513
        service_config.asymmetric_tables = sync_config["asymmetric_tables"];
1,464✔
1514
    }
1,464✔
1515
    else {
1,538✔
1516
        service_config.mode = AdminAPISession::ServiceConfig::SyncMode::Partitioned;
1,538✔
1517
        service_config.partition = sync_config["partition"];
1,538✔
1518
    }
1,538✔
1519
    session.enable_sync(app_id, mongo_service_id, service_config);
3,002✔
1520

1,433✔
1521
    app["sync"]["config"].put_json({{"development_mode_enabled", config.dev_mode_enabled}});
3,002✔
1522

1,433✔
1523
    auto rules = services[mongo_service_id]["rules"];
3,002✔
1524
    rules.post_json({
3,002✔
1525
        {"database", config.mongo_dbname},
3,002✔
1526
        {"collection", "UserData"},
3,002✔
1527
        {"roles",
3,002✔
1528
         {{{"name", "default"},
3,002✔
1529
           {"apply_when", nlohmann::json::object()},
3,002✔
1530
           {"document_filters",
3,002✔
1531
            {
3,002✔
1532
                {"read", true},
3,002✔
1533
                {"write", true},
3,002✔
1534
            }},
3,002✔
1535
           {"read", true},
3,002✔
1536
           {"write", true},
3,002✔
1537
           {"insert", true},
3,002✔
1538
           {"delete", true}}}},
3,002✔
1539
    });
3,002✔
1540

1,433✔
1541
    app["custom_user_data"].patch_json({
3,002✔
1542
        {"mongo_service_id", mongo_service_id},
3,002✔
1543
        {"enabled", true},
3,002✔
1544
        {"database_name", config.mongo_dbname},
3,002✔
1545
        {"collection_name", "UserData"},
3,002✔
1546
        {"user_id_field", "user_id"},
3,002✔
1547
    });
3,002✔
1548

1,433✔
1549
    services.post_json({
3,002✔
1550
        {"name", "gcm"},
3,002✔
1551
        {"type", "gcm"},
3,002✔
1552
        {"config",
3,002✔
1553
         {
3,002✔
1554
             {"senderId", "gcm"},
3,002✔
1555
         }},
3,002✔
1556
        {"secret_config",
3,002✔
1557
         {
3,002✔
1558
             {"apiKey", "gcm"},
3,002✔
1559
         }},
3,002✔
1560
        {"version", 1},
3,002✔
1561
    });
3,002✔
1562

1,433✔
1563
    // Wait for initial sync to complete, as connecting while this is happening
1,433✔
1564
    // causes various problems
1,433✔
1565
    bool any_sync_types = std::any_of(config.schema.begin(), config.schema.end(), [](auto& object_schema) {
3,066✔
1566
        return object_schema.table_type == ObjectSchema::ObjectType::TopLevel;
3,066✔
1567
    });
3,066✔
1568
    if (any_sync_types) {
3,002✔
1569
        timed_sleeping_wait_for(
2,954✔
1570
            [&] {
9,888✔
1571
                return session.is_initial_sync_complete(app_id);
9,888✔
1572
            },
9,888✔
1573
            std::chrono::seconds(30), std::chrono::seconds(1));
2,954✔
1574
    }
2,954✔
1575

1,433✔
1576
    return {client_app_id, app_id, session, config};
3,002✔
1577
}
3,002✔
1578

1579
AppSession get_runtime_app_session()
1580
{
1,462✔
1581
    static const AppSession cached_app_session = [&] {
739✔
1582
        auto cached_app_session = create_app(default_app_config());
16✔
1583
        return cached_app_session;
16✔
1584
    }();
16✔
1585
    return cached_app_session;
1,462✔
1586
}
1,462✔
1587

1588

1589
#ifdef REALM_MONGODB_ENDPOINT
1590
TEST_CASE("app: baas admin api", "[sync][app][admin api][baas]") {
1591
    SECTION("embedded objects") {
1592
        Schema schema{{"top",
1593
                       {{"_id", PropertyType::String, true},
1594
                        {"location", PropertyType::Object | PropertyType::Nullable, "location"}}},
1595
                      {"location",
1596
                       ObjectSchema::ObjectType::Embedded,
1597
                       {{"coordinates", PropertyType::Double | PropertyType::Array}}}};
1598

1599
        auto test_app_config = minimal_app_config("test", schema);
1600
        create_app(test_app_config);
1601
    }
1602

1603
    SECTION("embedded object with array") {
1604
        Schema schema{
1605
            {"a",
1606
             {{"_id", PropertyType::String, true},
1607
              {"b_link", PropertyType::Object | PropertyType::Array | PropertyType::Nullable, "b"}}},
1608
            {"b",
1609
             ObjectSchema::ObjectType::Embedded,
1610
             {{"c_link", PropertyType::Object | PropertyType::Nullable, "c"}}},
1611
            {"c", {{"_id", PropertyType::String, true}, {"d_str", PropertyType::String}}},
1612
        };
1613
        auto test_app_config = minimal_app_config("test", schema);
1614
        create_app(test_app_config);
1615
    }
1616

1617
    SECTION("dictionaries") {
1618
        Schema schema{
1619
            {"a", {{"_id", PropertyType::String, true}, {"b_dict", PropertyType::Dictionary | PropertyType::String}}},
1620
        };
1621

1622
        auto test_app_config = minimal_app_config("test", schema);
1623
        create_app(test_app_config);
1624
    }
1625

1626
    SECTION("set") {
1627
        Schema schema{
1628
            {"a", {{"_id", PropertyType::String, true}, {"b_dict", PropertyType::Set | PropertyType::String}}},
1629
        };
1630

1631
        auto test_app_config = minimal_app_config("test", schema);
1632
        create_app(test_app_config);
1633
    }
1634
}
1635
#endif
1636

1637
} // namespace realm
1638

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