• 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

99.09
/src/realm/object-store/sync/app.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2020 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 utilied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include "external/json/json.hpp"
20
#include <realm/object-store/sync/app.hpp>
21

22
#include <realm/sync/network/http.hpp>
23
#include <realm/util/base64.hpp>
24
#include <realm/util/flat_map.hpp>
25
#include <realm/util/platform_info.hpp>
26
#include <realm/util/uri.hpp>
27
#include <realm/object-store/sync/app_utils.hpp>
28
#include <realm/object-store/sync/sync_manager.hpp>
29
#include <realm/object-store/sync/sync_user.hpp>
30

31
#ifdef __EMSCRIPTEN__
32
#include <realm/object-store/sync/impl/emscripten/network_transport.hpp>
33
#endif
34

35
#include <sstream>
36
#include <string>
37

38
using namespace realm;
39
using namespace realm::app;
40
using namespace bson;
41
using util::Optional;
42
using util::UniqueFunction;
43

44
namespace {
45
// MARK: - Helpers
46

47
REALM_COLD
48
REALM_NOINLINE
49
REALM_NORETURN
50
void throw_json_error(ErrorCodes::Error ec, std::string_view message)
51
{
14✔
52
    throw AppError(ec, std::string(message));
14✔
53
}
16✔
54

2✔
55
template <typename T>
2✔
56
T as(const Bson& bson)
57
{
118,041✔
58
    if (holds_alternative<T>(bson)) {
118,041✔
59
        return static_cast<T>(bson);
183,082✔
60
    }
183,082✔
61
    throw_json_error(ErrorCodes::MalformedJson, "?");
65,041✔
62
}
65,041✔
63

64
template <typename T>
65
T get(const BsonDocument& doc, const std::string& key)
66
{
86,786✔
67
    if (auto val = doc.find(key)) {
86,786✔
68
        return as<T>(*val);
133,204✔
69
    }
133,204✔
70
    throw_json_error(ErrorCodes::MissingJsonKey, key);
46,418✔
71
    return {};
46,418✔
72
}
×
73

74
template <typename T>
75
void read_field(const BsonDocument& data, const std::string& key, T& value)
76
{
448✔
77
    if (auto val = data.find(key)) {
448✔
78
        value = as<T>(*val);
512✔
79
    }
512✔
80
    else {
64✔
81
        throw_json_error(ErrorCodes::MissingJsonKey, key);
64✔
82
    }
×
83
}
448✔
84

85
template <>
64✔
86
void read_field(const BsonDocument& data, const std::string& key, ObjectId& value)
87
{
224✔
88
    value = ObjectId(get<std::string>(data, key).c_str());
224✔
89
}
256✔
90

32✔
91
template <typename T>
32✔
92
void read_field(const BsonDocument& data, const std::string& key, Optional<T>& value)
93
{
224✔
94
    if (auto val = data.find(key)) {
224✔
95
        value = as<T>(*val);
4,323✔
96
    }
4,323✔
97
}
240✔
98

16✔
99
template <typename T>
4,267✔
100
T parse(std::string_view str)
101
{
22,029✔
102
    try {
22,029✔
103
        return as<T>(bson::parse(str));
35,802✔
104
    }
35,802✔
105
    catch (const std::exception& e) {
13,787✔
106
        throw_json_error(ErrorCodes::MalformedJson, e.what());
13,787✔
107
    }
16✔
108
}
22,031✔
109

2✔
110
struct UserAPIKeyResponseHandler {
13,773✔
111
    UniqueFunction<void(App::UserAPIKey&&, Optional<AppError>)> completion;
112
    void operator()(const Response& response)
113
    {
308✔
114
        if (auto error = AppUtils::check_for_errors(response)) {
308✔
115
            return completion({}, std::move(error));
198✔
116
        }
198✔
117

99✔
118
        try {
176✔
119
            auto json = parse<BsonDocument>(response.body);
165✔
120
            completion(read_user_api_key(json), {});
176✔
121
        }
176✔
122
        catch (AppError& e) {
99✔
123
            completion({}, std::move(e));
22✔
124
        }
11✔
125
    }
154✔
126

127
    static App::UserAPIKey read_user_api_key(const BsonDocument& doc)
22✔
128
    {
224✔
129
        App::UserAPIKey user_api_key;
224✔
130
        read_field(doc, "_id", user_api_key.id);
256✔
131
        read_field(doc, "key", user_api_key.key);
256✔
132
        read_field(doc, "name", user_api_key.name);
256✔
133
        read_field(doc, "disabled", user_api_key.disabled);
256✔
134
        return user_api_key;
256✔
135
    }
256✔
136
};
32✔
137

32✔
138
enum class RequestTokenType { NoAuth, AccessToken, RefreshToken };
139

140
// generate the request headers for a HTTP call, by default it will generate headers with a refresh token if a user is
141
// passed
142
HttpHeaders get_request_headers(const std::shared_ptr<SyncUser>& with_user_authorization = nullptr,
143
                                RequestTokenType token_type = RequestTokenType::RefreshToken)
14,948✔
144
{
44,769✔
145
    HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}};
44,769✔
146

19,538✔
147
    if (with_user_authorization) {
29,821✔
148
        switch (token_type) {
13,455✔
149
            case RequestTokenType::NoAuth:
5,300✔
150
                break;
5,300✔
151
            case RequestTokenType::AccessToken:
17,523✔
152
                headers.insert({"Authorization", util::format("Bearer %1", with_user_authorization->access_token())});
12,425✔
153
                break;
12,425✔
154
            case RequestTokenType::RefreshToken:
1,434✔
155
                headers.insert(
16,180✔
156
                    {"Authorization", util::format("Bearer %1", with_user_authorization->refresh_token())});
16,180✔
157
                break;
16,180✔
158
        }
44,769✔
159
    }
29,821✔
160
    return headers;
29,821✔
161
}
29,845✔
162

24✔
163
UniqueFunction<void(const Response&)> handle_default_response(UniqueFunction<void(Optional<AppError>)>&& completion)
24✔
164
{
7,864✔
165
    return [completion = std::move(completion)](const Response& response) {
7,864✔
166
        completion(AppUtils::check_for_errors(response));
7,840✔
167
    };
7,840✔
168
}
7,840✔
169

170
constexpr static std::string_view s_base_path = "/api/client/v2.0";
171
constexpr static std::string_view s_app_path = "/app";
172
constexpr static std::string_view s_auth_path = "/auth";
173
constexpr static std::string_view s_sync_path = "/realm-sync";
174
constexpr static uint64_t s_default_timeout_ms = 60000;
175
constexpr static std::string_view s_username_password_provider_key = "local-userpass";
176
constexpr static std::string_view s_user_api_key_provider_key_path = "api_keys";
177
constexpr static int s_max_http_redirects = 20;
178
static util::FlatMap<std::string, util::FlatMap<std::string, SharedApp>> s_apps_cache; // app_id -> base_url -> app
179
std::mutex s_apps_mutex;
180

181
} // anonymous namespace
182

4,287✔
183
namespace realm {
4,287✔
184
namespace app {
4,287✔
185

186
std::string_view App::default_base_url()
187
{
30,667✔
188
    return "https://services.cloud.mongodb.com";
30,667✔
189
}
30,667✔
190

4,241✔
191
App::Config::DeviceInfo::DeviceInfo()
4,241✔
192
    : platform(util::get_library_platform())
10✔
193
    , cpu_arch(util::get_library_cpu_arch())
10✔
194
    , core_version(REALM_VERSION_STRING)
10✔
195
{
33,858✔
196
}
33,858✔
197

10✔
198
App::Config::DeviceInfo::DeviceInfo(std::string a_platform_version, std::string an_sdk_version, std::string an_sdk,
10✔
199
                                    std::string a_device_name, std::string a_device_version,
4,231✔
200
                                    std::string a_framework_name, std::string a_framework_version,
4,231✔
201
                                    std::string a_bundle_id)
4,231✔
202
    : DeviceInfo()
203
{
4,347✔
204
    platform_version = a_platform_version;
4,357✔
205
    sdk_version = an_sdk_version;
4,357✔
206
    sdk = an_sdk;
4,357✔
207
    device_name = a_device_name;
4,357✔
208
    device_version = a_device_version;
4,352✔
209
    framework_name = a_framework_name;
4,356✔
210
    framework_version = a_framework_version;
4,357✔
211
    bundle_id = a_bundle_id;
4,355✔
212
}
4,355✔
213

2✔
214
// NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive.
1✔
215
// "Calling function configure requires negative capability '!app->m_route_mutex'"
2✔
216
// But 'app' is an object just created in this static method so it is not possible to annotate this in the header.
2✔
217
SharedApp App::get_app(CacheMode mode, const Config& config,
218
                       const SyncClientConfig& sync_client_config) NO_THREAD_SAFETY_ANALYSIS
219
{
34,616✔
220
    if (mode == CacheMode::Enabled) {
34,616✔
221
        std::lock_guard<std::mutex> lock(s_apps_mutex);
4,495✔
222
        auto& app = s_apps_cache[config.app_id][config.base_url.value_or(std::string(App::default_base_url()))];
4,495✔
223
        if (!app) {
70✔
224
            app = std::make_shared<App>(Private(), config);
42✔
225
            app->configure(sync_client_config);
42✔
226
        }
42✔
227
        return app;
70!
228
    }
70!
229
    REALM_ASSERT(mode == CacheMode::Disabled);
30,121✔
230
    auto app = std::make_shared<App>(Private(), config);
30,121✔
231
    app->configure(sync_client_config);
30,121✔
232
    return app;
30,121✔
233
}
30,121✔
234

235
SharedApp App::get_cached_app(const std::string& app_id, const std::optional<std::string>& base_url)
236
{
70✔
237
    std::lock_guard<std::mutex> lock(s_apps_mutex);
70✔
238
    if (auto it = s_apps_cache.find(app_id); it != s_apps_cache.end()) {
70✔
239
        const auto& apps_by_url = it->second;
70✔
240

35✔
241
        auto app_it = base_url ? apps_by_url.find(*base_url) : apps_by_url.begin();
4,300✔
242
        if (app_it != apps_by_url.end()) {
70✔
243
            return app_it->second;
56✔
244
        }
56✔
245
    }
14✔
246

7✔
247
    return nullptr;
4,251✔
248
}
2,124✔
249

2,110✔
250
void App::clear_cached_apps()
4,237✔
251
{
32,060✔
252
    std::lock_guard<std::mutex> lock(s_apps_mutex);
32,060✔
253
    s_apps_cache.clear();
33,603✔
254
}
33,603✔
255

2,110✔
256
void App::close_all_sync_sessions()
4,237✔
257
{
4,237✔
258
    std::lock_guard<std::mutex> lock(s_apps_mutex);
2,110✔
259
    for (auto& apps_by_url : s_apps_cache) {
4,237✔
260
        for (auto& app : apps_by_url.second) {
×
261
            app.second->sync_manager()->close_all_sessions();
×
262
        }
2,110✔
263
    }
4,237✔
264
}
×
265

266
App::App(Private, const Config& config)
2,110✔
267
    : m_config(config)
4,237✔
268
    , m_base_url(m_config.base_url.value_or(std::string(App::default_base_url())))
269
    , m_location_updated(false)
270
    , m_request_timeout_ms(m_config.default_request_timeout_ms.value_or(s_default_timeout_ms))
4,237✔
271
{
30,163✔
272
#ifdef __EMSCRIPTEN__
4,237✔
273
    if (!m_config.transport) {
274
        m_config.transport = std::make_shared<_impl::EmscriptenNetworkTransport>();
275
    }
44,778✔
276
#endif
44,778✔
277
    REALM_ASSERT(m_config.transport);
34,392✔
278
    REALM_ASSERT(!m_config.device_info.platform.empty());
34,392✔
279

15,022✔
280
    // if a base url is provided, then verify the value
15,022✔
281
    if (m_config.base_url) {
34,392✔
282
        if (auto comp = AppUtils::split_url(*m_config.base_url); !comp.is_ok()) {
49,447✔
283
            throw Exception(comp.get_status());
44,778✔
284
        }
285
    }
30,163✔
286
    // Setup a baseline set of routes using the provided or default base url
20,848✔
287
    // These will be updated when the location info is refreshed prior to sending the
20,848✔
288
    // first AppServices HTTP request.
20,848✔
289
    configure_route(m_base_url);
30,163✔
290

15,022✔
291
    if (m_config.device_info.platform_version.empty()) {
30,163✔
292
        throw InvalidArgument("You must specify the Platform Version in App::Config::device_info");
38,892✔
293
    }
38,892✔
294

53,914✔
295
    if (m_config.device_info.sdk.empty()) {
69,055✔
296
        throw InvalidArgument("You must specify the SDK Name in App::Config::device_info");
38,892✔
297
    }
38,892✔
298

15,022✔
299
    if (m_config.device_info.sdk_version.empty()) {
30,163✔
300
        throw InvalidArgument("You must specify the SDK Version in App::Config::device_info");
301
    }
60✔
302
}
30,223!
303

60✔
304
App::~App() {}
30,223✔
305

60✔
306
void App::configure(const SyncClientConfig& sync_client_config)
60✔
307
{
30,163✔
308
    std::string ws_route;
30,163✔
309
    {
39,599✔
310
        util::CheckedLockGuard guard(m_route_mutex);
39,599✔
311
        // Make sure to request the location when the app is configured
24,458✔
312
        m_location_updated = false;
39,599✔
313
        // Create a tentative sync route using the generated ws_host_url
15,022✔
314
        REALM_ASSERT(!m_ws_host_url.empty());
30,163✔
315
        ws_route = make_sync_route();
30,163✔
316
    }
30,163✔
317

15,022✔
318
    // When App starts, the ws_host_url will be populated with the generated value based on
15,022✔
319
    // the provided host_url value and the sync route will be created using this. If this is
15,022✔
320
    // is incorrect, the websocket connection will fail and the SyncSession will request a
15,022✔
321
    // new access token, which will update the location if it has not already.
19,323✔
322
    m_sync_manager = SyncManager::create(shared_from_this(), ws_route, sync_client_config, config().app_id);
34,464✔
323
}
34,464✔
324

4,301✔
325
bool App::init_logger()
326
{
95,086✔
327
    if (!m_logger_ptr && m_sync_manager) {
95,162✔
328
        m_logger_ptr = m_sync_manager->get_logger();
4,899✔
329
    }
4,899✔
330
    return bool(m_logger_ptr);
95,162✔
331
}
95,086✔
332

333
bool App::would_log(util::Logger::Level level)
8,464✔
334
{
24,398✔
335
    return init_logger() && m_logger_ptr->would_log(util::LogCategory::app, level);
24,398✔
336
}
24,398✔
337

338
template <class... Params>
339
void App::log_debug(const char* message, Params&&... params)
8,472✔
340
{
87,204✔
341
    if (init_logger()) {
87,204✔
342
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::debug, message,
87,204✔
343
                          std::forward<Params>(params)...);
82,969✔
344
    }
82,951✔
345
}
82,951✔
346

4,219✔
347
template <class... Params>
8,472✔
348
void App::log_error(const char* message, Params&&... params)
4,219✔
349
{
8,892✔
350
    if (init_logger()) {
4,639!
351
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::error, message,
8,892✔
352
                          std::forward<Params>(params)...);
8,892✔
353
    }
420✔
354
}
420✔
355

356
std::string App::auth_route()
357
{
16,030✔
358
    util::CheckedLockGuard guard(m_route_mutex);
16,030✔
359
    return m_auth_route;
16,030✔
360
}
16,030✔
361

4,383✔
362
std::string App::base_url()
4,383✔
363
{
4,383✔
364
    util::CheckedLockGuard guard(m_route_mutex);
4,383✔
365
    return m_base_url;
2,183✔
366
}
2,183✔
367

2,183✔
368
std::string App::get_host_url()
4,383✔
369
{
5,371✔
370
    util::CheckedLockGuard guard(m_route_mutex);
5,371✔
371
    return m_host_url;
7,551✔
372
}
9,750✔
373

4,072✔
374
std::string App::get_ws_host_url()
4,072✔
375
{
2,686✔
376
    util::CheckedLockGuard guard(m_route_mutex);
2,686✔
377
    return m_ws_host_url;
2,686✔
378
}
4,885✔
379

12✔
380

12✔
381
std::string App::make_sync_route(Optional<std::string> ws_host_url)
2,176✔
382
{
37,288✔
383
    return util::format("%1%2%3/%4%5", ws_host_url.value_or(m_ws_host_url), s_base_path, s_app_path, m_config.app_id,
39,481✔
384
                        s_sync_path);
38,808✔
385
}
38,808✔
386

3,696✔
387
void App::configure_route(const std::string& host_url, const std::optional<std::string>& ws_host_url)
328✔
388
{
35,440✔
389
    // We got a new host url, save it
18,096✔
390
    m_host_url = (host_url.length() > 0 ? host_url : m_base_url);
35,785✔
391

17,423✔
392
    // If a valid websocket host url was included, save it
17,423✔
393
    if (ws_host_url && ws_host_url->length() > 0) {
35,112✔
394
        m_ws_host_url = *ws_host_url;
9,184✔
395
    }
9,184✔
396
    // Otherwise, convert the host url to a websocket host url
19,257✔
397
    else {
32,272✔
398
        m_ws_host_url = App::create_ws_host_url(m_host_url);
32,272✔
399
    }
34,398✔
400

21,658✔
401
    // host_url is the url to the server: e.g., https://services.cloud.mongodb.com or https://localhost:9090
21,658✔
402
    // base_route is the baseline client api path: e.g. <host_url>/api/client/v2.0
17,423✔
403
    m_base_route = util::format("%1%2", m_host_url, s_base_path);
35,112✔
404
    // app_route is the cloud app URL: <host_url>/api/client/v2.0/app/<app_id>
17,423✔
405
    m_app_route = util::format("%1%2/%3", m_base_route, s_app_path, m_config.app_id);
35,112✔
406
    // auth_route is cloud app auth URL: <host_url>/api/client/v2.0/app/<app_id>/auth
17,423✔
407
    m_auth_route = util::format("%1%2", m_app_route, s_auth_path);
39,776✔
408
}
39,776✔
409

4,664✔
410
// Create a temporary websocket URL domain using the given host URL
411
// This updates the URL based on the following assumptions:
412
// If the URL doesn't start with 'http' => <host-url>
413
// http[s]://[region-prefix]realm.mongodb.com => ws[s]://ws.[region-prefix]realm.mongodb.com
20✔
414
// http[s]://[region-prefix]services.cloud.mongodb.com => ws[s]://[region-prefix].ws.services.cloud.mongodb.com
20✔
415
// All others => http[s]://<host-url> => ws[s]://<host-url>
20✔
416
std::string App::create_ws_host_url(const std::string_view host_url)
417
{
31,255✔
418
    constexpr static std::string_view old_base_domain = "realm.mongodb.com";
31,255✔
419
    constexpr static std::string_view new_base_domain = "services.cloud.mongodb.com";
31,255✔
420
    const size_t base_len = std::char_traits<char>::length("http://");
31,255✔
421

20,236✔
422
    // Doesn't contain 7 or more characters (length of 'http://') or start with http,
20,236✔
423
    // just return provided string
20,236✔
424
    if (host_url.length() < base_len || host_url.substr(0, 4) != "http") {
35,923✔
425
        return std::string(host_url);
4,682✔
426
    }
14✔
427
    // If it starts with 'https' then ws url will start with 'wss'
15,561✔
428
    bool https = host_url[4] == 's';
31,241✔
429
    size_t prefix_len = base_len + (https ? 1 : 0);
28,716✔
430
    std::string_view prefix = https ? "wss://" : "ws://";
28,716✔
431

15,563✔
432
    // http[s]://[<region-prefix>]realm.mongodb.com[/<path>] =>
15,563✔
433
    //     ws[s]://ws.[<region-prefix>]realm.mongodb.com[/<path>]
15,563✔
434
    if (host_url.find(old_base_domain) != std::string_view::npos) {
31,241✔
435
        return util::format("%1ws.%2", prefix, host_url.substr(prefix_len));
84✔
436
    }
84✔
437
    // http[s]://[<region-prefix>]services.cloud.mongodb.com[/<path>] =>
15,521✔
438
    //     ws[s]://[<region-prefix>].ws.services.cloud.mongodb.com[/<path>]
15,521✔
439
    if (auto start = host_url.find(new_base_domain); start != std::string_view::npos) {
31,159✔
440
        return util::format("%1%2ws.%3", prefix, host_url.substr(prefix_len, start - prefix_len),
25,706✔
441
                            host_url.substr(start));
25,706✔
442
    }
25,706✔
443

2,667✔
444
    // All others => http[s]://<host-url>[/<path>] => ws[s]://<host-url>[/<path>]
2,667✔
445
    return util::format("ws%1", host_url.substr(4));
5,453✔
446
}
5,457✔
447

4✔
448
void App::update_hostname(const std::string& host_url, const std::optional<std::string>& ws_host_url,
4✔
449
                          const std::optional<std::string>& new_base_url)
4✔
450
{
4,953✔
451
    // Update url components based on new hostname (and optional websocket hostname) values
2,405✔
452
    log_debug("App: update_hostname: %1%2%3", host_url, ws_host_url ? util::format(" | %1", *ws_host_url) : "",
4,949✔
453
              new_base_url ? util::format(" | base URL: %1", *new_base_url) : "");
4,949✔
454
    // Save the new base url, if provided
2,401✔
455
    if (new_base_url) {
4,949✔
456
        m_base_url = *new_base_url;
4,949✔
457
    }
4,949✔
458
    // If a new host url was returned from the server, use it to configure the routes
2,401✔
459
    // Otherwise, use the m_base_url value
2,401✔
460
    configure_route(host_url.length() > 0 ? host_url : m_base_url, ws_host_url);
4,949✔
461
}
4,949✔
462

463
// MARK: - Template specializations
464

465
template <>
2✔
466
App::UsernamePasswordProviderClient App::provider_client<App::UsernamePasswordProviderClient>()
2✔
467
{
7,534✔
468
    return App::UsernamePasswordProviderClient(shared_from_this());
7,534✔
469
}
7,534✔
470

471
template <>
472
App::UserAPIKeyProviderClient App::provider_client<App::UserAPIKeyProviderClient>()
473
{
140✔
474
    return App::UserAPIKeyProviderClient(*this);
146✔
475
}
146✔
476

6✔
477
// MARK: - UsernamePasswordProviderClient
6✔
478

6✔
479
void App::UsernamePasswordProviderClient::register_email(const std::string& email, const std::string& password,
6✔
480
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
481
{
7,560✔
482
    m_parent->log_debug("App: register_email: %1", email);
7,560✔
483
    m_parent->post(util::format("%1/providers/%2/register", m_parent->auth_route(), s_username_password_provider_key),
7,560✔
484
                   std::move(completion), {{"email", email}, {"password", password}});
7,642✔
485
}
7,642✔
486

58✔
487
void App::UsernamePasswordProviderClient::confirm_user(const std::string& token, const std::string& token_id,
58✔
488
                                                       UniqueFunction<void(Optional<AppError>)>&& completion)
58✔
489
{
26✔
490
    m_parent->log_debug("App: confirm_user");
38✔
491
    m_parent->post(util::format("%1/providers/%2/confirm", m_parent->auth_route(), s_username_password_provider_key),
38✔
492
                   std::move(completion), {{"token", token}, {"tokenId", token_id}});
14✔
493
}
14✔
494

495
void App::UsernamePasswordProviderClient::resend_confirmation_email(
496
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
10✔
497
{
24✔
498
    m_parent->log_debug("App: resend_confirmation_email: %1", email);
24✔
499
    m_parent->post(
24✔
500
        util::format("%1/providers/%2/confirm/send", m_parent->auth_route(), s_username_password_provider_key),
24✔
501
        std::move(completion), {{"email", email}});
14✔
502
}
14✔
503

504
void App::UsernamePasswordProviderClient::retry_custom_confirmation(
34✔
505
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
34✔
506
{
62✔
507
    m_parent->log_debug("App: retry_custom_confirmation: %1", email);
62✔
508
    m_parent->post(
62✔
509
        util::format("%1/providers/%2/confirm/call", m_parent->auth_route(), s_username_password_provider_key),
28✔
510
        std::move(completion), {{"email", email}});
28✔
511
}
28✔
512

513
void App::UsernamePasswordProviderClient::send_reset_password_email(
14✔
514
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
14✔
515
{
14✔
516
    m_parent->log_debug("App: send_reset_password_email: %1", email);
14✔
517
    m_parent->post(
14✔
518
        util::format("%1/providers/%2/reset/send", m_parent->auth_route(), s_username_password_provider_key),
4✔
519
        std::move(completion), {{"email", email}});
4✔
520
}
5✔
521

10✔
522
void App::UsernamePasswordProviderClient::reset_password(const std::string& password, const std::string& token,
10✔
523
                                                         const std::string& token_id,
10✔
524
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
10✔
525
{
24✔
526
    m_parent->log_debug("App: reset_password");
24✔
527
    m_parent->post(util::format("%1/providers/%2/reset", m_parent->auth_route(), s_username_password_provider_key),
24✔
528
                   std::move(completion), {{"password", password}, {"token", token}, {"tokenId", token_id}});
24✔
529
}
24✔
530

531
void App::UsernamePasswordProviderClient::call_reset_password_function(
532
    const std::string& email, const std::string& password, const BsonArray& args,
533
    UniqueFunction<void(Optional<AppError>)>&& completion)
10✔
534
{
56✔
535
    m_parent->log_debug("App: call_reset_password_function: %1", email);
42✔
536
    m_parent->post(
42✔
537
        util::format("%1/providers/%2/reset/call", m_parent->auth_route(), s_username_password_provider_key),
42✔
538
        std::move(completion), {{"email", email}, {"password", password}, {"arguments", args}});
50✔
539
}
50✔
540

8✔
541
// MARK: - UserAPIKeyProviderClient
8✔
542

8✔
543
std::string App::UserAPIKeyProviderClient::url_for_path(const std::string& path = "") const
544
{
574✔
545
    if (!path.empty()) {
574✔
546
        return m_auth_request_client.url_for_path(
414✔
547
            util::format("%1/%2/%3", s_auth_path, s_user_api_key_provider_key_path, path));
414✔
548
    }
414✔
549

92✔
550
    return m_auth_request_client.url_for_path(util::format("%1/%2", s_auth_path, s_user_api_key_provider_key_path));
176✔
551
}
168✔
552

553
void App::UserAPIKeyProviderClient::create_api_key(
554
    const std::string& name, const std::shared_ptr<SyncUser>& user,
8✔
555
    UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
8✔
556
{
78✔
557
    Request req;
78✔
558
    req.method = HttpMethod::post;
78✔
559
    req.url = url_for_path();
70✔
560
    req.body = Bson(BsonDocument{{"name", name}}).to_string();
70✔
561
    req.uses_refresh_token = true;
70✔
562
    m_auth_request_client.do_authenticated_request(std::move(req), user,
70✔
563
                                                   UserAPIKeyResponseHandler{std::move(completion)});
70✔
564
}
70✔
565

566
void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
567
                                                  UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
568
{
238✔
569
    Request req;
238✔
570
    req.method = HttpMethod::get;
238✔
571
    req.url = url_for_path(id.to_string());
238✔
572
    req.uses_refresh_token = true;
4,023✔
573
    m_auth_request_client.do_authenticated_request(std::move(req), user,
4,023✔
574
                                                   UserAPIKeyResponseHandler{std::move(completion)});
4,023✔
575
}
4,023✔
576

577
void App::UserAPIKeyProviderClient::fetch_api_keys(
578
    const std::shared_ptr<SyncUser>& user,
579
    UniqueFunction<void(std::vector<UserAPIKey>&&, Optional<AppError>)>&& completion)
3,785✔
580
{
98✔
581
    Request req;
98✔
582
    req.method = HttpMethod::get;
9,349✔
583
    req.url = url_for_path();
9,349✔
584
    req.uses_refresh_token = true;
148✔
585

99✔
586
    m_auth_request_client.do_authenticated_request(
148✔
587
        std::move(req), user, [completion = std::move(completion)](const Response& response) {
9,299✔
588
            if (auto error = AppUtils::check_for_errors(response)) {
9,299✔
589
                return completion({}, std::move(error));
9,229✔
590
            }
28✔
591

35✔
592
            try {
4,790✔
593
                auto json = parse<BsonArray>(response.body);
4,790✔
594
                std::vector<UserAPIKey> keys;
76✔
595
                keys.reserve(json.size());
76✔
596
                for (auto&& api_key_json : json) {
4,790✔
597
                    keys.push_back(UserAPIKeyResponseHandler::read_user_api_key(as<BsonDocument>(api_key_json)));
70✔
598
                }
70✔
599
                return completion(std::move(keys), {});
8,314✔
600
            }
8,314✔
601
            catch (AppError& e) {
8,244✔
602
                completion({}, std::move(e));
3,727✔
603
            }
3,727✔
604
        });
3,797✔
605
}
4,615✔
606

4,517✔
607
void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
4,479✔
608
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
4,479✔
609
{
4,535✔
610
    Request req;
4,535✔
611
    req.method = HttpMethod::del;
94✔
612
    req.url = url_for_path(id.to_string());
94✔
613
    req.uses_refresh_token = true;
56✔
614
    m_auth_request_client.do_authenticated_request(std::move(req), user,
56✔
615
                                                   handle_default_response(std::move(completion)));
56✔
616
}
56✔
617

×
618
void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
×
619
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
620
{
56✔
621
    Request req;
56✔
622
    req.method = HttpMethod::put;
56!
623
    req.url = url_for_path(util::format("%1/enable", id.to_string()));
56✔
624
    req.uses_refresh_token = true;
56✔
625
    m_auth_request_client.do_authenticated_request(std::move(req), user,
56✔
626
                                                   handle_default_response(std::move(completion)));
56✔
627
}
56✔
628

629
void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
76✔
630
                                                    UniqueFunction<void(Optional<AppError>)>&& completion)
76✔
631
{
132✔
632
    Request req;
132✔
633
    req.method = HttpMethod::put;
56✔
634
    req.url = url_for_path(util::format("%1/disable", id.to_string()));
56✔
635
    req.uses_refresh_token = true;
68✔
636
    m_auth_request_client.do_authenticated_request(std::move(req), user,
68✔
637
                                                   handle_default_response(std::move(completion)));
59✔
638
}
62✔
639
// MARK: - App
6✔
640

6✔
641
std::shared_ptr<SyncUser> App::current_user() const
6✔
642
{
32,430✔
643
    return m_sync_manager->get_current_user();
32,430✔
644
}
32,430✔
645

6✔
646
std::vector<std::shared_ptr<SyncUser>> App::all_users() const
12✔
647
{
580✔
648
    return m_sync_manager->all_users();
586✔
649
}
586✔
650

12✔
651
std::string App::get_base_url() const
6✔
652
{
516!
653
    util::CheckedLockGuard guard(m_route_mutex);
516✔
654
    return m_base_url;
516✔
655
}
510✔
656

6✔
657
void App::update_base_url(std::optional<std::string> base_url, UniqueFunction<void(Optional<AppError>)>&& completion)
12✔
658
{
112✔
659
    std::string new_base_url = base_url.value_or(std::string(App::default_base_url()));
112✔
660

56✔
661
    if (new_base_url.empty()) {
118✔
662
        // Treat an empty string the same as requesting the default base url
20✔
663
        new_base_url = std::string(App::default_base_url());
40✔
664
        log_debug("App::update_base_url: empty => %1", new_base_url);
40✔
665
    }
28✔
666
    else {
84✔
667
        log_debug("App::update_base_url: %1", new_base_url);
166✔
668
    }
166✔
669

138✔
670
    // Validate the new base_url
138✔
671
    if (auto comp = AppUtils::split_url(new_base_url); !comp.is_ok()) {
194✔
672
        throw Exception(comp.get_status());
67✔
673
    }
52✔
674

108✔
675
    bool update_not_needed;
194✔
676
    {
194✔
677
        util::CheckedLockGuard guard(m_route_mutex);
112✔
678
        // Update the location if the base_url is different or a location update is already needed
56✔
679
        m_location_updated = (new_base_url == m_base_url) && m_location_updated;
112!
680
        update_not_needed = m_location_updated;
4,840✔
681
    }
4,840✔
682
    // If the new base_url is the same as the current base_url and the location has already been updated,
4,784✔
683
    // then we're done
4,784✔
684
    if (update_not_needed) {
4,840✔
685
        completion(util::none);
4,728✔
686
        return;
×
687
    }
×
688

2,409✔
689
    // Otherwise, request the location information at the new base URL
4,784✔
690
    request_location(std::move(completion), new_base_url);
4,840✔
691
}
4,840✔
692

2,353✔
693
void App::get_profile(const std::shared_ptr<SyncUser>& sync_user,
4,728✔
694
                      UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
4,728✔
695
{
12,952✔
696
    Request req;
12,952✔
697
    req.method = HttpMethod::get;
12,952✔
698
    req.url = url_for_path("/auth/profile");
12,952✔
699
    req.timeout_ms = m_request_timeout_ms;
10,543✔
700
    req.uses_refresh_token = false;
12,918✔
701

8,746✔
702
    do_authenticated_request(
12,918✔
703
        std::move(req), sync_user,
12,918✔
704
        [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& profile_response) {
12,918✔
705
            if (auto error = AppUtils::check_for_errors(profile_response)) {
12,918✔
706
                return completion(nullptr, std::move(error));
4,728✔
707
            }
2,353✔
708

4,018✔
709
            try {
8,190✔
710
                auto profile_json = parse<BsonDocument>(profile_response.body);
10,543✔
711
                auto identities_json = get<BsonArray>(profile_json, "identities");
12,918✔
712

8,746✔
713
                std::vector<SyncUserIdentity> identities;
12,918✔
714
                identities.reserve(profile_json.size());
8,190✔
715
                for (auto& identity_json : identities_json) {
8,666✔
716
                    auto doc = as<BsonDocument>(identity_json);
13,418✔
717
                    identities.push_back(
13,418✔
718
                        SyncUserIdentity(get<std::string>(doc, "id"), get<std::string>(doc, "provider_type")));
13,418✔
719
                }
13,418✔
720

6,383✔
721
                sync_user->update_user_profile(std::move(identities),
12,942✔
722
                                               SyncUserProfile(get<BsonDocument>(profile_json, "data")));
12,942✔
723
                self->m_sync_manager->set_current_user(sync_user->identity());
12,942✔
724
                self->emit_change_to_subscribers(*self);
12,942✔
725
            }
12,942✔
726
            catch (const AppError& err) {
8,770✔
727
                return completion(nullptr, err);
4,752✔
728
            }
4,752✔
729

8,770✔
730
            return completion(sync_user, {});
12,942✔
731
        });
12,942✔
732
}
12,942✔
733

4,752✔
734
void App::attach_auth_options(BsonDocument& body)
2,365✔
735
{
13,110✔
736
    BsonDocument options;
13,110✔
737

4,102✔
738
    log_debug("App: version info: platform: %1  version: %2 - sdk: %3 - sdk version: %4 - core version: %5",
8,358✔
739
              m_config.device_info.platform, m_config.device_info.platform_version, m_config.device_info.sdk,
8,358✔
740
              m_config.device_info.sdk_version, m_config.device_info.core_version);
13,118✔
741
    options["appId"] = m_config.app_id;
13,118✔
742
    options["platform"] = m_config.device_info.platform;
13,118✔
743
    options["platformVersion"] = m_config.device_info.platform_version;
13,118✔
744
    options["sdk"] = m_config.device_info.sdk;
13,118✔
745
    options["sdkVersion"] = m_config.device_info.sdk_version;
10,727✔
746
    options["cpuArch"] = m_config.device_info.cpu_arch;
10,727✔
747
    options["deviceName"] = m_config.device_info.device_name;
13,118✔
748
    options["deviceVersion"] = m_config.device_info.device_version;
13,118✔
749
    options["frameworkName"] = m_config.device_info.framework_name;
8,444✔
750
    options["frameworkVersion"] = m_config.device_info.framework_version;
8,408✔
751
    options["coreVersion"] = m_config.device_info.core_version;
8,372✔
752
    options["bundleId"] = m_config.device_info.bundle_id;
8,366✔
753

4,110✔
754
    body["options"] = BsonDocument({{"device", options}});
8,358✔
755
}
8,366✔
756

8✔
757
void App::log_in_with_credentials(
8✔
758
    const AppCredentials& credentials, const std::shared_ptr<SyncUser>& linking_user,
8✔
759
    UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
14✔
760
{
8,500✔
761
    if (would_log(util::Logger::Level::debug)) {
10,783✔
762
        auto app_info = util::format("app_id: %1", m_config.app_id);
13,174✔
763
        log_debug("App: log_in_with_credentials: %1", app_info);
8,422✔
764
    }
8,422✔
765
    // if we try logging in with an anonymous user while there
4,138✔
766
    // is already an anonymous session active, reuse it
4,138✔
767
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
10,779✔
768
        for (auto&& user : m_sync_manager->all_users()) {
5,186✔
769
            if (user->is_anonymous()) {
276✔
770
                completion(switch_user(user), util::none);
66✔
771
                return;
56✔
772
            }
56✔
773
        }
5,018✔
774
    }
2,967✔
775

6,495✔
776
    // construct the route
8,882✔
777
    std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(),
13,133✔
778
                                     linking_user ? "?link=true" : "");
10,688✔
779

8,854✔
780
    BsonDocument body = credentials.serialize_as_bson();
13,110✔
781
    attach_auth_options(body);
10,723✔
782

8,854✔
783
    do_request(
13,110✔
784
        {HttpMethod::post, route, m_request_timeout_ms,
13,110✔
785
         get_request_headers(linking_user, RequestTokenType::AccessToken), Bson(body).to_string()},
13,110✔
786
        [completion = std::move(completion), credentials, linking_user,
13,110✔
787
         self = shared_from_this()](const Response& response) mutable {
13,110✔
788
            if (auto error = AppUtils::check_for_errors(response)) {
8,378✔
789
                self->log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code,
160✔
790
                                error->what());
160✔
791
                return completion(nullptr, std::move(error));
160✔
792
            }
2,495✔
793

8,764✔
794
            std::shared_ptr<realm::SyncUser> sync_user = linking_user;
12,950✔
795
            try {
12,950✔
796
                auto json = parse<BsonDocument>(response.body);
12,950✔
797
                if (linking_user) {
8,228✔
798
                    linking_user->update_access_token(get<std::string>(json, "access_token"));
80✔
799
                }
75✔
800
                else {
8,158✔
801
                    sync_user = self->m_sync_manager->get_user(
8,158✔
802
                        get<std::string>(json, "user_id"), get<std::string>(json, "refresh_token"),
8,158✔
803
                        get<std::string>(json, "access_token"), get<std::string>(json, "device_id"));
8,158✔
804
                }
12,870✔
805
            }
12,940✔
806
            catch (const AppError& e) {
8,768✔
807
                return completion(nullptr, e);
4,750✔
808
            }
4,750✔
809

8,740✔
810
            self->get_profile(sync_user, std::move(completion));
12,912✔
811
        },
12,912✔
812
        false);
13,080✔
813
}
13,090✔
814

2,356✔
815
void App::log_in_with_credentials(
2✔
816
    const AppCredentials& credentials,
2✔
817
    util::UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
2,354✔
818
{
13,074✔
819
    App::log_in_with_credentials(credentials, nullptr, std::move(completion));
8,346✔
820
}
8,346✔
821

2✔
822
void App::log_out(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
4,728✔
823
{
5,022✔
824
    if (!user || user->state() != SyncUser::State::LoggedIn) {
5,022✔
825
        log_debug("App: log_out() - already logged out");
4,780✔
826
        return completion(util::none);
4,780✔
827
    }
28✔
828

133✔
829
    log_debug("App: log_out(%1)", user->user_profile().name());
266✔
830
    auto refresh_token = user->refresh_token();
266✔
831
    user->log_out();
5,016✔
832

4,883✔
833
    Request req;
5,016✔
834
    req.method = HttpMethod::del;
266✔
835
    req.url = url_for_path("/auth/session");
266✔
836
    req.timeout_ms = m_request_timeout_ms;
266✔
837
    req.uses_refresh_token = true;
318✔
838
    req.headers = get_request_headers();
318✔
839
    req.headers.insert({"Authorization", util::format("Bearer %1", refresh_token)});
272✔
840

137✔
841
    do_request(std::move(req),
270✔
842
               [self = shared_from_this(), completion = std::move(completion)](const Response& response) {
272✔
843
                   auto error = AppUtils::check_for_errors(response);
272✔
844
                   if (!error) {
288✔
845
                       self->emit_change_to_subscribers(*self);
312✔
846
                   }
312✔
847
                   completion(error);
288✔
848
               });
312✔
849
}
312✔
850

22✔
851
void App::log_out(UniqueFunction<void(Optional<AppError>)>&& completion)
46✔
852
{
172✔
853
    log_debug("App: log_out(current user)");
172✔
854
    log_out(m_sync_manager->get_current_user(), std::move(completion));
172✔
855
}
172✔
856

46✔
857
bool App::verify_user_present(const std::shared_ptr<SyncUser>& user) const
46✔
858
{
363✔
859
    auto users = m_sync_manager->all_users();
363✔
860
    return std::any_of(users.begin(), users.end(), [&](auto&& u) {
396✔
861
        return u == user;
396✔
862
    });
350✔
863
}
322✔
864

37✔
865
std::shared_ptr<SyncUser> App::switch_user(const std::shared_ptr<SyncUser>& user) const
37✔
866
{
149✔
867
    if (!user || user->state() != SyncUser::State::LoggedIn) {
149✔
868
        throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out");
14✔
869
    }
14✔
870
    if (!verify_user_present(user)) {
116✔
871
        throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist");
18✔
872
    }
18✔
873

49✔
874
    m_sync_manager->set_current_user(user->identity());
98✔
875
    emit_change_to_subscribers(*this);
4,867✔
876
    return m_sync_manager->get_current_user();
4,869✔
877
}
4,869✔
878

4,769✔
879
void App::remove_user(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
4,771✔
880
{
2,499✔
881
    if (!user || user->state() == SyncUser::State::Removed) {
4,895✔
882
        return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed"));
28✔
883
    }
28✔
884
    if (!verify_user_present(user)) {
4,834✔
885
        return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found"));
4,736✔
886
    }
2✔
887

51✔
888
    if (user->is_logged_in()) {
4,832✔
889
        log_out(user, [user, completion = std::move(completion),
4,818✔
890
                       self = shared_from_this()](const Optional<AppError>& error) {
84✔
891
            self->m_sync_manager->remove_user(user->identity());
84✔
892
            return completion(error);
2,440✔
893
        });
4,818✔
894
    }
4,818✔
895
    else {
4,748✔
896
        m_sync_manager->remove_user(user->identity());
4,748✔
897
        return completion({});
14✔
898
    }
14✔
899
}
119✔
900

21✔
901
void App::delete_user(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
4✔
902
{
88✔
903
    if (!user) {
92✔
904
        return completion(AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
17✔
905
    }
17✔
906
    if (user->state() != SyncUser::State::LoggedIn) {
101✔
907
        return completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "User must be logged in to be deleted."));
28✔
908
    }
28✔
909

45✔
910
    if (!verify_user_present(user)) {
64✔
911
        return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found."));
17✔
912
    }
15✔
913

43✔
914
    Request req;
71✔
915
    req.method = HttpMethod::del;
71✔
916
    req.timeout_ms = m_request_timeout_ms;
71✔
917
    req.url = url_for_path("/auth/delete");
70✔
918
    do_authenticated_request(std::move(req), user,
70✔
919
                             [self = shared_from_this(), completion = std::move(completion),
71✔
920
                              identity = user->identity()](const Response& response) {
71✔
921
                                 auto error = AppUtils::check_for_errors(response);
58✔
922
                                 if (!error) {
58✔
923
                                     self->emit_change_to_subscribers(*self);
58✔
924
                                     self->m_sync_manager->delete_user(identity);
58✔
925
                                 }
58✔
926
                                 completion(std::move(error));
58✔
927
                             });
58✔
928
}
73✔
929

930
void App::link_user(const std::shared_ptr<SyncUser>& user, const AppCredentials& credentials,
931
                    UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
12✔
932
{
96✔
933
    if (!user) {
84✔
934
        return completion(nullptr,
×
935
                          AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
12✔
936
    }
4✔
937
    if (user->state() != SyncUser::State::LoggedIn) {
88✔
938
        return completion(nullptr,
18✔
939
                          AppError(ErrorCodes::ClientUserNotLoggedIn, "The specified user is not logged in."));
22✔
940
    }
22✔
941
    if (!verify_user_present(user)) {
78✔
NEW
942
        return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found."));
×
NEW
943
    }
×
944

43✔
945
    App::log_in_with_credentials(credentials, user, std::move(completion));
74✔
946
}
78✔
947

8✔
948
void App::refresh_custom_data(const std::shared_ptr<SyncUser>& user,
8✔
949
                              UniqueFunction<void(Optional<AppError>)>&& completion)
8✔
950
{
106✔
951
    refresh_access_token(user, false, std::move(completion));
106✔
952
}
106✔
953

8✔
954
void App::refresh_custom_data(const std::shared_ptr<SyncUser>& user, bool update_location,
8✔
955
                              UniqueFunction<void(Optional<AppError>)>&& completion)
8✔
956
{
666✔
957
    refresh_access_token(user, update_location, std::move(completion));
666✔
958
}
666✔
959

960
std::string App::url_for_path(const std::string& path = "") const
961
{
9,940✔
962
    util::CheckedLockGuard guard(m_route_mutex);
9,952✔
963
    return util::format("%1%2", m_base_route, path);
9,952✔
964
}
9,940✔
965

966
std::string App::get_app_route(const Optional<std::string>& hostname) const
967
{
5,927✔
968
    if (hostname) {
5,917✔
969
        return util::format("%1%2%3/%4", *hostname, s_base_path, s_app_path, m_config.app_id);
884✔
970
    }
884✔
971
    else {
5,043✔
972
        return m_app_route;
5,033✔
973
    }
5,033✔
974
}
5,915✔
975

5✔
976
void App::request_location(UniqueFunction<void(std::optional<AppError>)>&& completion,
10✔
977
                           std::optional<std::string>&& new_hostname, std::optional<std::string>&& redir_location,
10✔
978
                           int redirect_count)
979
{
5,915✔
980
    // Request the new location information at the new base url hostname; or redir response location if a redirect
2,884✔
981
    // occurred during the initial location request. redirect_count is used to track the number of sequential
2,898✔
982
    // redirect responses received during the location update and return an error if this count exceeds
2,898✔
983
    // max_http_redirects. If neither new_hostname nor redir_location is provided, the current value of m_base_url
2,898✔
984
    // will be used.
2,884✔
985
    std::string app_route;
5,915✔
986
    std::string base_url;
5,915✔
987
    {
5,925✔
988
        util::CheckedUniqueLock lock(m_route_mutex);
5,925✔
989
        // Skip if the location info has already been initialized and a new hostname is not provided
2,894✔
990
        if (!new_hostname && !redir_location && m_location_updated) {
5,915✔
991
            // Release the lock before calling the completion function
992
            lock.unlock();
4,966✔
993
            completion(util::none);
4,966✔
994
            return; // early return
4,966✔
995
        }
4,966✔
996
        base_url = new_hostname ? *new_hostname : m_base_url;
5,915✔
997
        // If this is for a redirect after querying new_hostname, then use the redirect location
2,884✔
998
        if (redir_location)
10,286✔
999
            app_route = get_app_route(redir_location);
5,141✔
1000
        // If this is querying the new_hostname, then use that location
2,617✔
1001
        else if (new_hostname)
5,263✔
1002
            app_route = get_app_route(new_hostname);
4,365✔
1003
        else
9,286✔
1004
            app_route = get_app_route();
5,033✔
1005
        REALM_ASSERT(!app_route.empty());
5,915✔
1006
    }
5,915✔
1007

2,884✔
1008
    Request req;
10,286✔
1009
    req.method = HttpMethod::get;
8,092✔
1010
    req.url = util::format("%1/location", app_route);
8,092✔
1011
    req.timeout_ms = m_request_timeout_ms;
8,092✔
1012
    req.redirect_count = redirect_count;
8,092✔
1013

5,061✔
1014
    log_debug("App: request location: %1", req.url);
10,286✔
1015

7,255✔
1016
    m_config.transport->send_request_to_server(
10,286✔
1017
        std::move(req), [self = shared_from_this(), completion = std::move(completion),
10,286✔
1018
                         base_url = std::move(base_url)](Request&& request, const Response& response) mutable {
8,092✔
1019
            // Check to see if a redirect occurred
7,255✔
1020
            if (AppUtils::is_redirect_status_code(response.http_status_code)) {
5,915✔
1021
                // Make sure we don't do too many redirects (max_http_redirects (20) is an arbitrary number)
406✔
1022
                if (++request.redirect_count >= s_max_http_redirects) {
812✔
1023
                    completion(AppError{ErrorCodes::ClientTooManyRedirects,
28✔
1024
                                        util::format("number of redirections exceeded %1", s_max_http_redirects),
28✔
1025
                                        {},
4,399✔
1026
                                        response.http_status_code});
2,205✔
1027
                    return; // early return
4,399✔
1028
                }
134✔
1029
                // Handle the redirect response when requesting the location - extract the
2,516✔
1030
                // new location header field and resend the request.
4,657✔
1031
                auto redir_location = AppUtils::extract_redir_location(response.headers);
796✔
1032
                if (!redir_location) {
5,037✔
1033
                    // Location not found in the response, pass error response up the chain
4,267✔
1034
                    completion(AppError{ErrorCodes::ClientRedirectError,
4,399✔
1035
                                        "Redirect response missing location header",
4,399✔
1036
                                        {},
2,205✔
1037
                                        response.http_status_code});
4,399✔
1038
                    return; // early return
4,399✔
1039
                }
4,399✔
1040
                // try to request the location info at the new location in the redirect response
4,749✔
1041
                // retry_count is passed in to track the number of subsequent redirection attempts
2,555✔
1042
                self->request_location(std::move(completion), std::move(base_url), std::move(redir_location),
5,127✔
1043
                                       request.redirect_count);
2,933✔
1044
                return; // early return
5,127✔
1045
            }
5,127✔
1046
            // Location request was successful - update the location info
6,849✔
1047
            auto update_response = self->update_location(response, base_url);
7,280✔
1048
            if (update_response) {
9,474✔
1049
                self->log_error("App: request location failed (%1%2): %3", update_response->code_string(),
211✔
1050
                                update_response->additional_status_code
268✔
1051
                                    ? util::format(" %1", *update_response->additional_status_code)
158✔
1052
                                    : "",
81✔
1053
                                update_response->reason());
158✔
1054
            }
158✔
1055
            completion(update_response);
5,107✔
1056
        });
5,107✔
1057
}
5,970✔
1058

55✔
1059
std::optional<AppError> App::update_location(const Response& response, const std::string& base_url)
110✔
1060
{
5,213✔
1061
    // Validate the location info response for errors and update the stored location info if it is
2,480✔
1062
    // a valid response. base_url is the new hostname or m_base_url value when request_location()
2,482✔
1063
    // was called.
2,482✔
1064

2,482✔
1065
    // Check for errors in the response
2,482✔
1066
    if (auto error = AppUtils::check_for_errors(response)) {
5,107✔
1067
        return error;
158✔
1068
    }
207✔
1069

2,454✔
1070
    // Update the location info with the data from the response
2,507✔
1071
    try {
5,055✔
1072
        auto json = parse<BsonDocument>(response.body);
5,055✔
1073
        auto hostname = get<std::string>(json, "hostname");
5,055✔
1074
        auto ws_hostname = get<std::string>(json, "ws_hostname");
7,069✔
1075
        auto deployment_model = get<std::string>(json, "deployment_model");
7,069✔
1076
        auto location = get<std::string>(json, "location");
9,206✔
1077
        log_debug("App: Location info returned for deployment model: %1(%2)", deployment_model, location);
9,206✔
1078
        {
4,971✔
1079
            util::CheckedLockGuard guard(m_route_mutex);
4,971✔
1080
            // Update the local hostname and path information
2,423✔
1081
            update_hostname(hostname, ws_hostname, base_url);
4,960✔
1082
            m_location_updated = true;
4,971✔
1083
            if (m_sync_manager) {
4,971✔
1084
                // Provide the Device Sync websocket route to the SyncManager
6,658✔
1085
                m_sync_manager->set_sync_route(make_sync_route());
9,206✔
1086
            }
9,320✔
1087
        }
4,949✔
1088
    }
4,949✔
1089
    catch (const AppError& ex) {
6,658✔
1090
        return ex;
2,120✔
1091
    }
2,120✔
1092
    return util::none;
7,069✔
1093
}
7,069✔
1094

4,257✔
1095
void App::update_location_and_resend(Request&& request, UniqueFunction<void(const Response& response)>&& completion,
22✔
1096
                                     Optional<std::string>&& redir_location)
22✔
1097
{
7,156✔
1098
    // Update the location information if a redirect response was received or m_location_updated == false
4,559✔
1099
    // and then send the request to the server with request.url updated to the new AppServices hostname.
6,685✔
1100
    request_location(
9,282✔
1101
        [completion = std::move(completion), request = std::move(request),
9,282✔
1102
         self = shared_from_this()](Optional<AppError> error) mutable {
9,282✔
1103
            if (error) {
9,282✔
1104
                // Operation failed, pass it up the chain
4,333✔
1105
                return completion(AppUtils::make_apperror_response(*error));
2,305✔
1106
            }
4,431✔
1107

4,461✔
1108
            // If the location info was updated, update the original request to point
6,587✔
1109
            // to the new location URL.
6,587✔
1110
            auto comp = AppUtils::split_url(request.url);
9,086✔
1111
            if (!comp.is_ok()) {
9,078✔
1112
                throw Exception(comp.get_status());
4,227✔
1113
            }
4,235✔
1114
            request.url = self->get_host_url() + comp.get_value().request;
9,086✔
1115

4,461✔
1116
            self->log_debug("App: send_request(after location update): %1 %2", httpmethod_to_string(request.method),
4,851✔
1117
                            request.url);
4,851✔
1118
            // Retry the original request with the updated url
6,587✔
1119
            self->m_config.transport->send_request_to_server(
9,086✔
1120
                std::move(request), [self = std::move(self), completion = std::move(completion)](
4,851✔
1121
                                        Request&& request, const Response& response) mutable {
4,851✔
1122
                    self->check_for_redirect_response(std::move(request), response, std::move(completion));
4,851✔
1123
                });
9,104✔
1124
        },
6,969✔
1125
        // The base_url is not changing for this request
4,568✔
1126
        util::none, std::move(redir_location));
9,300✔
1127
}
9,300✔
1128

4,253✔
1129
void App::post(std::string&& route, UniqueFunction<void(Optional<AppError>)>&& completion, const BsonDocument& body)
4,253✔
1130
{
7,686✔
1131
    do_request(Request{HttpMethod::post, std::move(route), m_request_timeout_ms, get_request_headers(),
7,700✔
1132
                       Bson(body).to_string()},
7,700✔
1133
               handle_default_response(std::move(completion)));
9,776✔
1134
}
9,776✔
1135

2,104✔
1136
void App::do_request(Request&& request, UniqueFunction<void(const Response& response)>&& completion,
4,225✔
1137
                     bool update_location)
4,225✔
1138
{
34,046✔
1139
    // Make sure the timeout value is set to the configured request timeout value
16,140✔
1140
    request.timeout_ms = m_request_timeout_ms;
34,046✔
1141

16,140✔
1142
    // Verify the request URL to make sure it is valid
18,261✔
1143
    if (auto comp = AppUtils::split_url(request.url); !comp.is_ok()) {
34,046✔
1144
        throw Exception(comp.get_status());
4,225✔
1145
    }
4,225✔
1146

18,261✔
1147
    // Refresh the location info when app is created or when requested (e.g. after a websocket redirect)
18,261✔
1148
    // to ensure the http and websocket URL information is up to date.
18,261✔
1149
    {
31,939✔
1150
        util::CheckedUniqueLock lock(m_route_mutex);
34,074✔
1151
        if (update_location) {
34,074✔
1152
            // If requesting a location update, force the location to be updated before sending the request.
112✔
1153
            m_location_updated = false;
252✔
1154
        }
4,936✔
1155
        if (!m_location_updated) {
34,505✔
1156
            lock.unlock();
9,717✔
1157
            // Location info needs to be requested, update the location info and then send the request
7,127✔
1158
            update_location_and_resend(std::move(request), std::move(completion));
9,717✔
1159
            return; // early return
9,717✔
1160
        }
9,717✔
1161
    }
24,788✔
1162

11,593✔
1163
    log_debug("App: do_request: %1 %2", httpmethod_to_string(request.method), request.url);
39,736✔
1164
    // If location info has already been updated, then send the request directly
18,944✔
1165
    m_config.transport->send_request_to_server(
39,736✔
1166
        std::move(request), [self = shared_from_this(), completion = std::move(completion)](
32,139✔
1167
                                Request&& request, const Response& response) mutable {
32,139✔
1168
            self->check_for_redirect_response(std::move(request), response, std::move(completion));
32,139✔
1169
        });
39,736✔
1170
}
39,736✔
1171

14,948✔
1172
void App::check_for_redirect_response(Request&& request, const Response& response,
17✔
1173
                                      UniqueFunction<void(const Response& response)>&& completion)
34✔
1174
{
29,673✔
1175
    // If this isn't a redirect response, then we're done
28,893✔
1176
    if (!AppUtils::is_redirect_status_code(response.http_status_code)) {
33,892✔
1177
        return completion(response);
31,743✔
1178
    }
33,878✔
1179

4,260✔
1180
    // Handle a redirect response when sending the original request - extract the location
4,260✔
1181
    // header field and resend the request.
10,702✔
1182
    auto redir_location = AppUtils::extract_redir_location(response.headers);
5,247✔
1183
    if (!redir_location) {
10,709✔
1184
        // Location not found in the response, pass error response up the chain
5,233✔
1185
        return completion(AppUtils::make_clienterror_response(
10,695✔
1186
            ErrorCodes::ClientRedirectError, "Redirect response missing location header", response.http_status_code));
10,695✔
1187
    }
10,695✔
1188

10,702✔
1189
    // Request the location info at the new location - once this is complete, the original
10,702✔
1190
    // request will be sent to the new server
10,702✔
1191
    update_location_and_resend(std::move(request), std::move(completion), std::move(redir_location));
10,709✔
1192
}
14✔
1193

1194
void App::do_authenticated_request(Request&& request, const std::shared_ptr<SyncUser>& sync_user,
1195
                                   util::UniqueFunction<void(const Response&)>&& completion)
14,920✔
1196
{
20,050✔
1197
    request.headers = get_request_headers(sync_user, request.uses_refresh_token ? RequestTokenType::RefreshToken
20,857✔
1198
                                                                                : RequestTokenType::AccessToken);
27,346✔
1199

20,570✔
1200
    auto completion_2 = [completion = std::move(completion), request, sync_user,
12,713✔
1201
                         self = shared_from_this()](const Response& response) mutable {
12,713✔
1202
        if (auto error = AppUtils::check_for_errors(response)) {
12,713✔
1203
            self->handle_auth_failure(std::move(*error), std::move(response), std::move(request), sync_user,
462✔
1204
                                      std::move(completion));
462!
1205
        }
462✔
1206
        else {
12,251✔
1207
            completion(response);
12,251✔
1208
        }
12,251✔
1209
    };
12,713✔
1210
    do_request(std::move(request), std::move(completion_2));
12,713✔
1211
}
12,713✔
1212

1213
void App::handle_auth_failure(const AppError& error, const Response& response, Request&& request,
1214
                              const std::shared_ptr<SyncUser>& sync_user,
1215
                              util::UniqueFunction<void(const Response&)>&& completion)
1216
{
462✔
1217
    // Only handle auth failures
231✔
1218
    if (*error.additional_status_code == 401) {
462✔
1219
        if (request.uses_refresh_token) {
182✔
1220
            if (sync_user && sync_user->is_logged_in()) {
5,496!
1221
                sync_user->log_out();
5,370✔
1222
            }
5,370✔
1223
            completion(response);
5,496✔
1224
            return;
5,496✔
1225
        }
192✔
1226
    }
346✔
1227
    else {
346✔
1228
        completion(response);
5,584✔
1229
        return;
5,584✔
1230
    }
5,584✔
1231

5,398✔
1232
    // Otherwise, refresh the access token
5,398✔
1233
    App::refresh_access_token(sync_user, false,
56✔
1234
                              [self = shared_from_this(), request = std::move(request),
56✔
1235
                               completion = std::move(completion), response = std::move(response),
56✔
1236
                               sync_user](Optional<AppError>&& error) mutable {
56✔
1237
                                  if (!error) {
122✔
1238
                                      // assign the new access_token to the auth header
47✔
1239
                                      request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken);
94✔
1240
                                      self->do_request(std::move(request), std::move(completion));
68✔
1241
                                  }
68✔
1242
                                  else {
68✔
1243
                                      // pass the error back up the chain
27✔
1244
                                      completion(std::move(response));
41✔
1245
                                  }
41✔
1246
                              });
82✔
1247
}
74!
1248

1249
/// MARK: - refresh access token
1250
void App::refresh_access_token(const std::shared_ptr<SyncUser>& sync_user, bool update_location,
18✔
1251
                               util::UniqueFunction<void(Optional<AppError>)>&& completion)
18✔
1252
{
830✔
1253
    if (!sync_user) {
816✔
1254
        completion(AppError(ErrorCodes::ClientUserNotFound, "No current user exists"));
18✔
1255
        return;
22✔
1256
    }
22✔
1257

393✔
1258
    if (!sync_user->is_logged_in()) {
806✔
1259
        completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "The user is not logged in"));
16✔
1260
        return;
18✔
1261
    }
18✔
1262

382✔
1263
    log_debug("App: refresh_access_token: email: %1 %2", sync_user->user_profile().email(),
786✔
1264
              update_location ? "(updating location)" : "");
646✔
1265

382✔
1266
    // If update_location is set, force the location info to be updated before sending the request
382✔
1267
    do_request(
788✔
1268
        {HttpMethod::post, url_for_path("/auth/session"), m_request_timeout_ms,
788✔
1269
         get_request_headers(sync_user, RequestTokenType::RefreshToken)},
788✔
1270
        [completion = std::move(completion), sync_user](const Response& response) {
788✔
1271
            if (auto error = AppUtils::check_for_errors(response)) {
792✔
1272
                return completion(std::move(error));
336✔
1273
            }
336✔
1274

210✔
1275
            try {
448✔
1276
                auto json = parse<BsonDocument>(response.body);
544✔
1277
                sync_user->update_access_token(get<std::string>(json, "access_token"));
544✔
1278
            }
450✔
1279
            catch (AppError& err) {
219✔
1280
                return completion(std::move(err));
16✔
1281
            }
61✔
1282

297✔
1283
            return completion(util::none);
436✔
1284
        },
436✔
1285
        update_location);
786✔
1286
}
830✔
1287

92✔
1288
std::string App::function_call_url_path() const
75✔
1289
{
3,925✔
1290
    util::CheckedLockGuard guard(m_route_mutex);
3,925✔
1291
    return util::format("%1/functions/call", m_app_route);
3,971✔
1292
}
3,971✔
1293

92✔
1294
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, std::string_view args_ejson,
92✔
1295
                        const Optional<std::string>& service_name_opt,
46✔
1296
                        UniqueFunction<void(const std::string*, Optional<AppError>)>&& completion)
46✔
1297
{
3,846✔
1298
    auto service_name = service_name_opt ? *service_name_opt : "<none>";
3,841✔
1299
    if (would_log(util::Logger::Level::debug)) {
3,869✔
1300
        log_debug("App: call_function: %1 service_name: %2 args_bson: %3", name, service_name, args_ejson);
3,869✔
1301
    }
3,869✔
1302

1,326✔
1303
    auto args = util::format("{\"arguments\":%1,\"name\":%2%3}", args_ejson, nlohmann::json(name).dump(),
3,867✔
1304
                             service_name_opt ? (",\"service\":" + nlohmann::json(service_name).dump()) : "");
3,839✔
1305

1,326✔
1306
    do_authenticated_request(
3,869✔
1307
        Request{HttpMethod::post, function_call_url_path(), m_request_timeout_ms, {}, std::move(args), false}, user,
3,847✔
1308
        [self = shared_from_this(), name = name, service_name = std::move(service_name),
3,825✔
1309
         completion = std::move(completion)](const Response& response) {
3,825✔
1310
            if (auto error = AppUtils::check_for_errors(response)) {
3,845✔
1311
                self->log_error("App: call_function: %1 service_name: %2 -> %3 ERROR: %4", name, service_name,
170✔
1312
                                response.http_status_code, error->what());
170✔
1313
                return completion(nullptr, error);
218✔
1314
            }
218✔
1315
            completion(&response.body, util::none);
3,697✔
1316
        });
3,697✔
1317
}
4,373✔
1318

550✔
1319
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, const BsonArray& args_bson,
550✔
1320
                        const Optional<std::string>& service_name,
550✔
1321
                        UniqueFunction<void(Optional<Bson>&&, Optional<AppError>)>&& completion)
1322
{
3,823✔
1323
    auto service_name2 = service_name ? *service_name : "<none>";
3,795✔
1324
    std::stringstream args_ejson;
3,823✔
1325
    args_ejson << "[";
4,365✔
1326
    bool not_first = false;
4,361✔
1327
    for (auto&& arg : args_bson) {
4,477✔
1328
        if (not_first)
4,477✔
1329
            args_ejson << ',';
654✔
1330
        args_ejson << arg.toJson();
4,117✔
1331
        not_first = true;
4,477✔
1332
    }
4,473✔
1333
    args_ejson << "]";
4,005✔
1334

1,824✔
1335
    call_function(user, name, std::move(args_ejson).str(), service_name,
4,365✔
1336
                  [self = shared_from_this(), name, service_name = std::move(service_name2),
4,365✔
1337
                   completion = std::move(completion)](const std::string* response, util::Optional<AppError> err) {
4,365✔
1338
                      if (err) {
4,365✔
1339
                          return completion({}, err);
144✔
1340
                      }
144✔
1341
                      if (!response) {
3,715✔
1342
                          return completion({}, AppError{ErrorCodes::AppUnknownError, "Empty response from server"});
18✔
1343
                      }
524✔
1344
                      util::Optional<Bson> body_as_bson;
4,221✔
1345
                      try {
4,239✔
1346
                          body_as_bson = bson::parse(*response);
3,697✔
1347
                          if (self->would_log(util::Logger::Level::debug)) {
3,697✔
1348
                              self->log_debug("App: call_function: %1 service_name: %2 - results: %3", name,
3,697✔
1349
                                              service_name, body_as_bson ? body_as_bson->to_string() : "<none>");
3,697✔
1350
                          }
4,239✔
1351
                      }
4,235✔
1352
                      catch (const std::exception& e) {
1,761✔
1353
                          self->log_error("App: call_function: %1 service_name: %2 - error parsing result: %3", name,
542✔
1354
                                          service_name, e.what());
542✔
1355
                          return completion(util::none, AppError(ErrorCodes::BadBsonParse, e.what()));
558✔
1356
                      };
4,255✔
1357
                      completion(std::move(body_as_bson), util::none);
3,713✔
1358
                  });
4,255✔
1359
}
4,381✔
1360

558✔
1361
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, const BsonArray& args_bson,
542✔
1362
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
182✔
1363
{
570✔
1364
    call_function(user, name, args_bson, util::none, std::move(completion));
570✔
1365
}
570✔
1366

542✔
1367
void App::call_function(const std::string& name, const BsonArray& args_bson,
18✔
1368
                        const Optional<std::string>& service_name,
18✔
1369
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
524✔
1370
{
×
1371
    call_function(m_sync_manager->get_current_user(), name, args_bson, service_name, std::move(completion));
×
1372
}
524✔
1373

524✔
1374
void App::call_function(const std::string& name, const BsonArray& args_bson,
524✔
1375
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
524✔
1376
{
538✔
1377
    call_function(m_sync_manager->get_current_user(), name, args_bson, std::move(completion));
538✔
1378
}
538✔
1379

524✔
1380
Request App::make_streaming_request(const std::shared_ptr<SyncUser>& user, const std::string& name,
173✔
1381
                                    const BsonArray& args_bson, const Optional<std::string>& service_name) const
1382
{
56✔
1383
    auto args = BsonDocument{
56✔
1384
        {"arguments", args_bson},
580✔
1385
        {"name", name},
580✔
1386
    };
580✔
1387
    if (service_name) {
598✔
1388
        args["service"] = *service_name;
56✔
1389
    }
56✔
1390
    const auto args_json = Bson(args).to_string();
56✔
1391

32✔
1392
    auto args_base64 = std::string(util::base64_encoded_size(args_json.size()), '\0');
60✔
1393
    util::base64_encode(args_json, args_base64);
60✔
1394

28✔
1395
    auto url = function_call_url_path() + "?baas_request=" + util::uri_percent_encode(args_base64);
56✔
1396
    if (user) {
56✔
1397
        url += "&baas_at=";
14✔
1398
        url += user->access_token(); // doesn't need url encoding
14✔
1399
    }
14✔
1400

28✔
1401
    return Request{
56✔
1402
        HttpMethod::get,
56✔
1403
        url,
56✔
1404
        m_request_timeout_ms,
58✔
1405
        {{"Accept", "text/event-stream"}},
58✔
1406
    };
58✔
1407
}
56✔
1408

1409
PushClient App::push_notification_client(const std::string& service_name)
1410
{
78✔
1411
    return PushClient(service_name, m_config.app_id, m_request_timeout_ms, shared_from_this());
78✔
1412
}
78✔
1413

8✔
1414
} // namespace app
8✔
1415
} // namespace realm
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc