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

realm / realm-core / michael.wilkersonbarker_1387

07 Sep 2024 05:17AM UTC coverage: 91.103% (-0.005%) from 91.108%
michael.wilkersonbarker_1387

Pull #8011

Evergreen

michael-wb
Fixed refresh access token test
Pull Request #8011: RCORE-2253 Redirected user authenticated app requests cause user to be logged out and location is not updated

102888 of 181598 branches covered (56.66%)

222 of 258 new or added lines in 5 files covered. (86.05%)

65 existing lines in 19 files now uncovered.

217392 of 238621 relevant lines covered (91.1%)

5668504.84 hits per line

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

98.22
/test/object-store/sync/app.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2016 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 "collection_fixtures.hpp"
20
#include "util/sync/baas_admin_api.hpp"
21
#include "util/sync/redirect_server.hpp"
22
#include "util/sync/sync_test_utils.hpp"
23
#include "util/test_path.hpp"
24
#include "util/unit_test_transport.hpp"
25
#include "util/test_path.hpp"
26

27
#include <realm/object-store/impl/object_accessor_impl.hpp>
28
#include <realm/object-store/sync/app_credentials.hpp>
29
#include <realm/object-store/sync/app_utils.hpp>
30
#include <realm/object-store/sync/async_open_task.hpp>
31
#include <realm/object-store/sync/generic_network_transport.hpp>
32
#include <realm/object-store/sync/mongo_client.hpp>
33
#include <realm/object-store/sync/mongo_collection.hpp>
34
#include <realm/object-store/sync/mongo_database.hpp>
35
#include <realm/object-store/sync/sync_session.hpp>
36
#include <realm/object-store/sync/sync_user.hpp>
37
#include <realm/object-store/thread_safe_reference.hpp>
38
#include <realm/object-store/util/uuid.hpp>
39
#include <realm/sync/network/default_socket.hpp>
40
#include <realm/sync/network/websocket.hpp>
41
#include <realm/sync/noinst/server/access_token.hpp>
42
#include <realm/util/base64.hpp>
43
#include <realm/util/overload.hpp>
44
#include <realm/util/platform_info.hpp>
45
#include <realm/util/future.hpp>
46
#include <realm/util/uri.hpp>
47

48
#include <catch2/catch_all.hpp>
49
#include <external/json/json.hpp>
50
#include <external/mpark/variant.hpp>
51

52
#include <condition_variable>
53
#include <iostream>
54
#include <list>
55
#include <mutex>
56

57
using namespace realm;
58
using namespace realm::app;
59
using util::any_cast;
60
using util::Optional;
61

62
using namespace std::string_view_literals;
63
using namespace std::literals::string_literals;
64
using namespace std::chrono_literals;
65
using namespace Catch::Matchers;
66

67

68
namespace {
69
std::shared_ptr<User> log_in(std::shared_ptr<App> app, AppCredentials credentials = AppCredentials::anonymous())
70
{
94✔
71
    if (auto transport = dynamic_cast<UnitTestTransport*>(app->config().transport.get())) {
94✔
72
        transport->set_provider_type(credentials.provider_as_string());
56✔
73
    }
56✔
74
    std::shared_ptr<User> user;
94✔
75
    app->log_in_with_credentials(credentials, [&](std::shared_ptr<User> user_arg, Optional<AppError> error) {
94✔
76
        REQUIRE_FALSE(error);
94!
77
        REQUIRE(user_arg);
94!
78
        user = std::move(user_arg);
94✔
79
    });
94✔
80
    REQUIRE(user);
94!
81
    return user;
94✔
82
}
94✔
83

84
AppError failed_log_in(std::shared_ptr<App> app, AppCredentials credentials = AppCredentials::anonymous())
85
{
16✔
86
    Optional<AppError> err;
16✔
87
    app->log_in_with_credentials(credentials, [&](std::shared_ptr<User> user, Optional<AppError> error) {
16✔
88
        REQUIRE(error);
16!
89
        REQUIRE_FALSE(user);
16!
90
        err = error;
16✔
91
    });
16✔
92
    REQUIRE(err);
16!
93
    return *err;
16✔
94
}
16✔
95

96
} // namespace
97

98
namespace realm {
99
class TestHelper {
100
public:
101
    static DBRef get_db(Realm& realm)
102
    {
2✔
103
        return Realm::Internal::get_db(realm);
2✔
104
    }
2✔
105
};
106
} // namespace realm
107

108
static const std::string profile_0_name = "Ursus americanus Ursus boeckhi";
109
static const std::string profile_0_first_name = "Ursus americanus";
110
static const std::string profile_0_last_name = "Ursus boeckhi";
111
static const std::string profile_0_email = "Ursus ursinus";
112
static const std::string profile_0_picture_url = "Ursus malayanus";
113
static const std::string profile_0_gender = "Ursus thibetanus";
114
static const std::string profile_0_birthday = "Ursus americanus";
115
static const std::string profile_0_min_age = "Ursus maritimus";
116
static const std::string profile_0_max_age = "Ursus arctos";
117

118
static const nlohmann::json profile_0 = {
119
    {"name", profile_0_name},         {"first_name", profile_0_first_name},   {"last_name", profile_0_last_name},
120
    {"email", profile_0_email},       {"picture_url", profile_0_picture_url}, {"gender", profile_0_gender},
121
    {"birthday", profile_0_birthday}, {"min_age", profile_0_min_age},         {"max_age", profile_0_max_age}};
122

123
static nlohmann::json user_json(std::string access_token, std::string user_id = random_string(15))
124
{
4✔
125
    return {{"access_token", access_token},
4✔
126
            {"refresh_token", access_token},
4✔
127
            {"user_id", user_id},
4✔
128
            {"device_id", "Panda Bear"}};
4✔
129
}
4✔
130

131
static nlohmann::json user_profile_json(std::string user_id = random_string(15),
132
                                        std::string identity_0_id = "Ursus arctos isabellinus",
133
                                        std::string identity_1_id = "Ursus arctos horribilis",
134
                                        std::string provider_type = "anon-user")
135
{
2✔
136
    return {{"user_id", user_id},
2✔
137
            {"identities",
2✔
138
             {{{"id", identity_0_id}, {"provider_type", provider_type}},
2✔
139
              {{"id", identity_1_id}, {"provider_type", "lol_wut"}}}},
2✔
140
            {"data", profile_0}};
2✔
141
}
2✔
142

143
static const std::string good_access_token =
144
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
145
    "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2RldklkIjoi"
146
    "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU0M2Rk"
147
    "Y2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s";
148

149
static const std::string good_access_token2 =
150
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
151
    "eyJleHAiOjE1ODkzMDE3MjAsImlhdCI6MTU4NDExODcyMCwiaXNzIjoiNWU2YmJiYzBhNmI3ZGZkM2UyNTA0OGI3Iiwic3RpdGNoX2RldklkIjoi"
152
    "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU2YmJi"
153
    "YzBhNmI3ZGZkM2UyNTA0OGIzIiwidHlwIjoiYWNjZXNzIn0.eSX4QMjIOLbdOYOPzQrD_racwLUk1HGFgxtx2a34k80";
154

155
#if REALM_ENABLE_AUTH_TESTS
156

157
#include <realm/util/sha_crypto.hpp>
158

159
static std::string create_jwt(const std::string& appId)
160
{
2✔
161
    nlohmann::json header = {{"alg", "HS256"}, {"typ", "JWT"}};
2✔
162
    nlohmann::json payload = {{"aud", appId}, {"sub", "someUserId"}, {"exp", 1961896476}};
2✔
163

164
    payload["user_data"]["name"] = "Foo Bar";
2✔
165
    payload["user_data"]["occupation"] = "firefighter";
2✔
166

167
    payload["my_metadata"]["name"] = "Bar Foo";
2✔
168
    payload["my_metadata"]["occupation"] = "stock analyst";
2✔
169

170
    std::string header_str = header.dump();
2✔
171
    std::string payload_str = payload.dump();
2✔
172

173
    std::string encoded_header;
2✔
174
    encoded_header.resize(util::base64_encoded_size(header_str.length()));
2✔
175
    util::base64_encode(header_str, encoded_header);
2✔
176

177
    std::string encoded_payload;
2✔
178
    encoded_payload.resize(util::base64_encoded_size(payload_str.length()));
2✔
179
    util::base64_encode(payload_str, encoded_payload);
2✔
180

181
    // Remove padding characters.
182
    while (encoded_header.back() == '=')
2✔
183
        encoded_header.pop_back();
×
184
    while (encoded_payload.back() == '=')
6✔
185
        encoded_payload.pop_back();
4✔
186

187
    std::string jwtPayload = encoded_header + "." + encoded_payload;
2✔
188

189
    std::array<char, 32> hmac;
2✔
190
    unsigned char key[] = "My_very_confidential_secretttttt";
2✔
191
    util::hmac_sha256(util::unsafe_span_cast<uint8_t>(jwtPayload), util::unsafe_span_cast<uint8_t>(hmac),
2✔
192
                      util::Span<uint8_t, 32>(key, 32));
2✔
193

194
    std::string signature;
2✔
195
    signature.resize(util::base64_encoded_size(hmac.size()));
2✔
196
    util::base64_encode(hmac, signature);
2✔
197
    while (signature.back() == '=')
4✔
198
        signature.pop_back();
2✔
199
    std::replace(signature.begin(), signature.end(), '+', '-');
2✔
200
    std::replace(signature.begin(), signature.end(), '/', '_');
2✔
201

202
    return jwtPayload + "." + signature;
2✔
203
}
2✔
204

205
// MARK: - Verify AppError with all error codes
206
TEST_CASE("app: verify app error codes", "[sync][app][local]") {
2✔
207
    auto error_codes = ErrorCodes::get_error_list();
2✔
208
    std::vector<std::pair<int, std::string>> http_status_codes = {
2✔
209
        {0, ""},
2✔
210
        {100, "http error code considered fatal: some http error. Informational: 100"},
2✔
211
        {200, ""},
2✔
212
        {300, "http error code considered fatal: some http error. Redirection: 300"},
2✔
213
        {400, "http error code considered fatal: some http error. Client Error: 400"},
2✔
214
        {500, "http error code considered fatal: some http error. Server Error: 500"},
2✔
215
        {600, "http error code considered fatal: some http error. Unknown HTTP Error: 600"}};
2✔
216

217
    auto make_http_error = [](std::optional<std::string_view> error_code, int http_status = 500,
2✔
218
                              std::optional<std::string_view> error = "some error",
2✔
219
                              std::optional<std::string_view> link = "http://dummy-link/") -> app::Response {
328✔
220
        nlohmann::json body;
328✔
221
        if (error_code) {
328✔
222
            body["error_code"] = *error_code;
324✔
223
        }
324✔
224
        if (error) {
328✔
225
            body["error"] = *error;
324✔
226
        }
324✔
227
        if (link) {
328✔
228
            body["link"] = *link;
326✔
229
        }
326✔
230

231
        return {
328✔
232
            http_status,
328✔
233
            0,
328✔
234
            {{"Content-Type", "application/json"}},
328✔
235
            body.empty() ? "{}" : body.dump(),
328✔
236
        };
328✔
237
    };
328✔
238

239
    auto validate_json_body = [](std::string body, std::optional<std::string_view> error_code,
2✔
240
                                 std::optional<std::string_view> error = "some error",
2✔
241
                                 std::optional<std::string_view> logs_link = "http://dummy-link/") -> bool {
326✔
242
        if (body.empty()) {
326✔
243
            return false;
×
244
        }
×
245
        try {
326✔
246
            auto json_body = nlohmann::json::parse(body);
326✔
247
            // If provided, check the error_code value against the 'error_code' value in the json body
248
            auto code = json_body.find("error_code");
326✔
249
            if (error_code && !error_code->empty()) {
326✔
250
                if (code == json_body.end() || code->get<std::string>() != *error_code) {
322✔
251
                    return false;
×
252
                }
×
253
            }
322✔
254
            // If not provided, it's an error if the value is included in the json body
255
            else if (code != json_body.end()) {
4✔
256
                return false;
×
257
            }
×
258
            // If provided, check the message value against the 'error' value in the json body
259
            auto message = json_body.find("error");
326✔
260
            if (error && !error->empty()) {
326✔
261
                if (message == json_body.end() || message->get<std::string>() != *error) {
326✔
262
                    return false;
×
263
                }
×
264
            }
326✔
265
            // If not provided, it's an error if the value is included in the json body
266
            else if (message != json_body.end()) {
×
267
                return false;
×
268
            }
×
269
            // If provided, check the logs_link value against the 'link' value in the json body
270
            auto link = json_body.find("link");
326✔
271
            if (logs_link && !logs_link->empty()) {
326✔
272
                if (link == json_body.end() || link->get<std::string>() != *logs_link) {
324✔
273
                    return false;
×
274
                }
×
275
            }
324✔
276
            // If not provided, it's an error if the value is included in the json body
277
            else if (link != json_body.end()) {
2✔
278
                return false;
×
279
            }
×
280
        }
326✔
281
        catch (const nlohmann::json::exception&) {
326✔
282
            // It's also a failure if parsing the json body throws an exception
283
            return false;
×
284
        }
×
285
        return true;
326✔
286
    };
326✔
287

288
    // Success responses
289
    app::Response response = {200, 0, {}, ""};
2✔
290
    auto app_error = AppUtils::check_for_errors(response);
2✔
291
    REQUIRE(!app_error);
2!
292

293
    response = {0, 0, {}, ""};
2✔
294
    app_error = AppUtils::check_for_errors(response);
2✔
295
    REQUIRE(!app_error);
2!
296

297
    // Empty error code
298
    response = make_http_error("");
2✔
299
    app_error = AppUtils::check_for_errors(response);
2✔
300
    REQUIRE(app_error);
2!
301
    REQUIRE(app_error->code() == ErrorCodes::AppUnknownError);
2!
302
    REQUIRE(app_error->code_string() == "AppUnknownError");
2!
303
    REQUIRE(app_error->server_error.empty());
2!
304
    REQUIRE(app_error->reason() == "some error");
2!
305
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
306
    REQUIRE(*app_error->additional_status_code == 500);
2!
307

308
    // Re-compose back into a Response
309
    auto err_response = AppUtils::make_apperror_response(*app_error);
2✔
310
    REQUIRE(err_response.http_status_code == 500);
2!
311
    REQUIRE(!err_response.body.empty());
2!
312
    REQUIRE(validate_json_body(err_response.body, ""));
2!
313
    REQUIRE(!err_response.client_error_code);
2!
314
    REQUIRE(err_response.custom_status_code == 0);
2!
315
    auto ct = AppUtils::find_header("content-type", err_response.headers);
2✔
316
    REQUIRE(ct);
2!
317
    REQUIRE(ct->second == "application/json");
2!
318

319
    // Missing error code
320
    response = make_http_error(std::nullopt);
2✔
321
    app_error = AppUtils::check_for_errors(response);
2✔
322
    REQUIRE(app_error);
2!
323
    REQUIRE(app_error->code() == ErrorCodes::AppUnknownError);
2!
324
    REQUIRE(app_error->code_string() == "AppUnknownError");
2!
325
    REQUIRE(app_error->server_error.empty());
2!
326
    REQUIRE(app_error->reason() == "some error");
2!
327
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
328
    REQUIRE(*app_error->additional_status_code == 500);
2!
329

330
    // Re-compose back into a Response
331
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
332
    REQUIRE(err_response.http_status_code == 500);
2!
333
    REQUIRE(!err_response.body.empty());
2!
334
    REQUIRE(validate_json_body(err_response.body, std::nullopt));
2!
335
    REQUIRE(!err_response.client_error_code);
2!
336
    REQUIRE(err_response.custom_status_code == 0);
2!
337
    ct = AppUtils::find_header("content-type", err_response.headers);
2✔
338
    REQUIRE(ct);
2!
339
    REQUIRE(ct->second == "application/json");
2!
340

341
    // Missing error message
342
    response = make_http_error("InvalidParameter", 404, std::nullopt);
2✔
343
    app_error = AppUtils::check_for_errors(response);
2✔
344
    REQUIRE(app_error);
2!
345
    REQUIRE(app_error->code() == ErrorCodes::InvalidParameter);
2!
346
    REQUIRE(app_error->code_string() == "InvalidParameter");
2!
347
    REQUIRE(app_error->server_error == "InvalidParameter");
2!
348
    REQUIRE(app_error->reason() == "no error message");
2!
349
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
350
    REQUIRE(*app_error->additional_status_code == 404);
2!
351

352
    // Re-compose back into a Response
353
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
354
    REQUIRE(err_response.http_status_code == 404);
2!
355
    REQUIRE(!err_response.body.empty());
2!
356
    REQUIRE(validate_json_body(err_response.body, "InvalidParameter", "no error message"));
2!
357
    REQUIRE(!err_response.client_error_code);
2!
358
    REQUIRE(err_response.custom_status_code == 0);
2!
359
    ct = AppUtils::find_header("content-type", err_response.headers);
2✔
360
    REQUIRE(ct);
2!
361
    REQUIRE(ct->second == "application/json");
2!
362

363
    // Missing logs link
364
    response = make_http_error("InvalidParameter", 403, "some error occurred", std::nullopt);
2✔
365
    app_error = AppUtils::check_for_errors(response);
2✔
366
    REQUIRE(app_error);
2!
367
    REQUIRE(app_error->code() == ErrorCodes::InvalidParameter);
2!
368
    REQUIRE(app_error->code_string() == "InvalidParameter");
2!
369
    REQUIRE(app_error->server_error == "InvalidParameter");
2!
370
    REQUIRE(app_error->reason() == "some error occurred");
2!
371
    REQUIRE(app_error->link_to_server_logs == "");
2!
372
    REQUIRE(*app_error->additional_status_code == 403);
2!
373

374
    // Re-compose back into a Response
375
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
376
    REQUIRE(err_response.http_status_code == 403);
2!
377
    REQUIRE(!err_response.body.empty());
2!
378
    REQUIRE(validate_json_body(err_response.body, "InvalidParameter", "some error occurred", std::nullopt));
2!
379
    REQUIRE(!err_response.client_error_code);
2!
380
    REQUIRE(err_response.custom_status_code == 0);
2!
381
    ct = AppUtils::find_header("content-type", err_response.headers);
2✔
382
    REQUIRE(ct);
2!
383
    REQUIRE(ct->second == "application/json");
2!
384

385
    // Missing error code and error message with success http status
386
    response = make_http_error(std::nullopt, 200, std::nullopt);
2✔
387
    app_error = AppUtils::check_for_errors(response);
2✔
388
    REQUIRE(!app_error);
2!
389

390
    for (auto [name, error] : error_codes) {
320✔
391
        // All error codes should not cause an exception
392
        if (error != ErrorCodes::HTTPError && error != ErrorCodes::OK) {
320✔
393
            response = make_http_error(name);
316✔
394
            app_error = AppUtils::check_for_errors(response);
316✔
395
            REQUIRE(app_error);
316!
396
            if (ErrorCodes::error_categories(error).test(ErrorCategory::app_error)) {
316✔
397
                REQUIRE(app_error->code() == error);
130!
398
                REQUIRE(app_error->code_string() == name);
130!
399
            }
130✔
400
            else {
186✔
401
                REQUIRE(app_error->code() == ErrorCodes::AppServerError);
186!
402
                REQUIRE(app_error->code_string() == "AppServerError");
186!
403
            }
186✔
404
            REQUIRE(app_error->server_error == name);
316!
405
            REQUIRE(app_error->reason() == "some error");
316!
406
            REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
316!
407
            REQUIRE(app_error->additional_status_code);
316!
408
            REQUIRE(*app_error->additional_status_code == 500);
316!
409

410
            // Re-compose back into a Response
411
            err_response = AppUtils::make_apperror_response(*app_error);
316✔
412
            REQUIRE(err_response.http_status_code == 500);
316!
413
            REQUIRE(!err_response.body.empty());
316!
414
            REQUIRE(validate_json_body(err_response.body, name));
316!
415
            REQUIRE(!err_response.client_error_code);
316!
416
            REQUIRE(err_response.custom_status_code == 0);
316!
417
            ct = AppUtils::find_header("content-type", err_response.headers);
316✔
418
            REQUIRE(ct);
316!
419
            REQUIRE(ct->second == "application/json");
316!
420
        }
316✔
421
    }
320✔
422

423
    response = make_http_error("AppErrorMissing", 404);
2✔
424
    app_error = AppUtils::check_for_errors(response);
2✔
425
    REQUIRE(app_error);
2!
426
    REQUIRE(app_error->code() == ErrorCodes::AppServerError);
2!
427
    REQUIRE(app_error->code_string() == "AppServerError");
2!
428
    REQUIRE(app_error->server_error == "AppErrorMissing");
2!
429
    REQUIRE(app_error->reason() == "some error");
2!
430
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
431
    REQUIRE(app_error->additional_status_code);
2!
432
    REQUIRE(*app_error->additional_status_code == 404);
2!
433

434
    // Re-compose back into a Response
435
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
436
    REQUIRE(err_response.http_status_code == 404);
2!
437
    REQUIRE(!err_response.body.empty());
2!
438
    REQUIRE(validate_json_body(err_response.body, "AppErrorMissing"));
2!
439
    REQUIRE(!err_response.client_error_code);
2!
440
    REQUIRE(err_response.custom_status_code == 0);
2!
441
    ct = AppUtils::find_header("content-type", err_response.headers);
2✔
442
    REQUIRE(ct);
2!
443
    REQUIRE(ct->second == "application/json");
2!
444

445
    // HTTPError with different status values
446
    for (auto [status, message] : http_status_codes) {
14✔
447
        response = {
14✔
448
            status,
14✔
449
            0,
14✔
450
            {},
14✔
451
            "some http error",
14✔
452
        };
14✔
453
        app_error = AppUtils::check_for_errors(response);
14✔
454
        if (message.empty()) {
14✔
455
            REQUIRE(!app_error);
4!
456
            continue;
4✔
457
        }
4✔
458
        REQUIRE(app_error);
10!
459
        REQUIRE(app_error->code() == ErrorCodes::HTTPError);
10!
460
        REQUIRE(app_error->code_string() == "HTTPError");
10!
461
        REQUIRE(app_error->server_error.empty());
10!
462
        REQUIRE(app_error->reason() == message);
10!
463
        REQUIRE(app_error->link_to_server_logs.empty());
10!
464
        REQUIRE(app_error->additional_status_code);
10!
465
        REQUIRE(*app_error->additional_status_code == status);
10!
466

467
        // Recompose back into a Response
468
        err_response = AppUtils::make_apperror_response(*app_error);
10✔
469
        REQUIRE(err_response.http_status_code == status);
10!
470
        REQUIRE(err_response.body == "some http error");
10!
471
        REQUIRE(!err_response.client_error_code);
10!
472
        REQUIRE(err_response.custom_status_code == 0);
10!
473
        REQUIRE(err_response.headers.empty());
10!
474
    }
10✔
475

476
    // Missing error code and error message with fatal http status
477
    response = {
2✔
478
        501,
2✔
479
        0,
2✔
480
        {},
2✔
481
        "",
2✔
482
    };
2✔
483
    app_error = AppUtils::check_for_errors(response);
2✔
484
    REQUIRE(app_error);
2!
485
    REQUIRE(app_error->code() == ErrorCodes::HTTPError);
2!
486
    REQUIRE(app_error->code_string() == "HTTPError");
2!
487
    REQUIRE(app_error->server_error.empty());
2!
488
    REQUIRE(app_error->reason() == "http error code considered fatal. Server Error: 501");
2!
489
    REQUIRE(app_error->link_to_server_logs.empty());
2!
490
    REQUIRE(app_error->additional_status_code);
2!
491
    REQUIRE(*app_error->additional_status_code == 501);
2!
492

493
    // Re-compose back into a Response
494
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
495
    REQUIRE(err_response.http_status_code == 501);
2!
496
    REQUIRE(err_response.body.empty());
2!
497
    REQUIRE(!err_response.client_error_code);
2!
498
    REQUIRE(err_response.custom_status_code == 0);
2!
499
    REQUIRE(err_response.headers.empty());
2!
500

501
    // Missing error code and error message contains period with redirect http status
502
    response = {
2✔
503
        308,
2✔
504
        0,
2✔
505
        {},
2✔
506
        "some http error. ocurred",
2✔
507
    };
2✔
508
    app_error = AppUtils::check_for_errors(response);
2✔
509
    REQUIRE(app_error);
2!
510
    REQUIRE(app_error->code() == ErrorCodes::HTTPError);
2!
511
    REQUIRE(app_error->code_string() == "HTTPError");
2!
512
    REQUIRE(app_error->server_error.empty());
2!
513
    REQUIRE(app_error->reason() == "http error code considered fatal: some http error. ocurred. Redirection: 308");
2!
514
    REQUIRE(app_error->link_to_server_logs.empty());
2!
515
    REQUIRE(app_error->additional_status_code);
2!
516
    REQUIRE(*app_error->additional_status_code == 308);
2!
517

518
    // Re-compose back into a Response
519
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
520
    REQUIRE(err_response.http_status_code == 308);
2!
521
    REQUIRE(err_response.body == "some http error. ocurred");
2!
522
    REQUIRE(!err_response.client_error_code);
2!
523
    REQUIRE(err_response.custom_status_code == 0);
2!
524
    REQUIRE(err_response.headers.empty());
2!
525

526
    // Valid client error code, with body, but no json
527
    app::Response client_response = {
2✔
528
        501,
2✔
529
        0,
2✔
530
        {},
2✔
531
        "Some error occurred",
2✔
532
        ErrorCodes::BadBsonParse, // client_error_code
2✔
533
    };
2✔
534
    app_error = AppUtils::check_for_errors(client_response);
2✔
535
    REQUIRE(app_error);
2!
536
    REQUIRE(app_error->code() == ErrorCodes::BadBsonParse);
2!
537
    REQUIRE(app_error->code_string() == "BadBsonParse");
2!
538
    REQUIRE(app_error->server_error.empty());
2!
539
    REQUIRE(app_error->reason() == "Some error occurred");
2!
540
    REQUIRE(app_error->link_to_server_logs.empty());
2!
541
    REQUIRE(app_error->additional_status_code);
2!
542
    REQUIRE(*app_error->additional_status_code == 501);
2!
543

544
    // Re-compose back into a Response
545
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
546
    REQUIRE(err_response.http_status_code == 501);
2!
547
    REQUIRE(err_response.body == "Some error occurred");
2!
548
    REQUIRE(err_response.client_error_code == ErrorCodes::BadBsonParse);
2!
549
    REQUIRE(err_response.custom_status_code == 0);
2!
550
    REQUIRE(err_response.headers.empty());
2!
551

552
    // Same response with client error code, but no body
553
    client_response.body = "";
2✔
554
    app_error = AppUtils::check_for_errors(client_response);
2✔
555
    REQUIRE(app_error);
2!
556
    REQUIRE(app_error->reason() == "client error code value considered fatal");
2!
557

558
    // Re-compose back into a Response
559
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
560
    REQUIRE(err_response.http_status_code == 501);
2!
561
    REQUIRE(err_response.body == "client error code value considered fatal");
2!
562
    REQUIRE(err_response.client_error_code == ErrorCodes::BadBsonParse);
2!
563
    REQUIRE(err_response.custom_status_code == 0);
2!
564
    REQUIRE(err_response.headers.empty());
2!
565

566
    // Valid custom status code, with body, but no json
567
    app::Response custom_response = {501,
2✔
568
                                     4999, // custom_status_code
2✔
569
                                     {},
2✔
570
                                     "Some custom error occurred"};
2✔
571
    app_error = AppUtils::check_for_errors(custom_response);
2✔
572
    REQUIRE(app_error);
2!
573
    REQUIRE(app_error->code() == ErrorCodes::CustomError);
2!
574
    REQUIRE(app_error->code_string() == "CustomError");
2!
575
    REQUIRE(app_error->server_error.empty());
2!
576
    REQUIRE(app_error->reason() == "Some custom error occurred");
2!
577
    REQUIRE(app_error->link_to_server_logs.empty());
2!
578
    REQUIRE(app_error->additional_status_code);
2!
579
    REQUIRE(*app_error->additional_status_code == 4999);
2!
580

581
    // Re-compose back into a Response
582
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
583
    REQUIRE(err_response.http_status_code == 0);
2!
584
    REQUIRE(err_response.body == "Some custom error occurred");
2!
585
    REQUIRE(!err_response.client_error_code);
2!
586
    REQUIRE(err_response.custom_status_code == 4999);
2!
587
    REQUIRE(err_response.headers.empty());
2!
588

589
    // Same response with custom status code, but no body
590
    custom_response.body = "";
2✔
591
    app_error = AppUtils::check_for_errors(custom_response);
2✔
592
    REQUIRE(app_error);
2!
593
    REQUIRE(app_error->reason() == "non-zero custom status code considered fatal");
2!
594

595
    // Re-compose back into a Response
596
    err_response = AppUtils::make_apperror_response(*app_error);
2✔
597
    REQUIRE(err_response.http_status_code == 0);
2!
598
    REQUIRE(err_response.body == "non-zero custom status code considered fatal");
2!
599
    REQUIRE(!err_response.client_error_code);
2!
600
    REQUIRE(err_response.custom_status_code == 4999);
2!
601
    REQUIRE(err_response.headers.empty());
2!
602
}
2✔
603

604
// MARK: - Verify generic app utils helper functions
605
TEST_CASE("app: verify app utils helpers", "[sync][app][local]") {
4✔
606
    SECTION("find_header") {
4✔
607
        std::map<std::string, std::string> headers1 = {{"header1", "header1-value"},
2✔
608
                                                       {"HEADER2", "header2-value"},
2✔
609
                                                       {"HeAdEr3", "header3-value"},
2✔
610
                                                       {"header@4", "header4-value"}};
2✔
611

612
        std::map<std::string, std::string> headers2 = {
2✔
613
            {"", "no-key-value"},
2✔
614
            {"header1", "header1-value"},
2✔
615
        };
2✔
616

617
        CHECK(AppUtils::find_header("", headers1) == nullptr);
2!
618
        CHECK(AppUtils::find_header("header", headers1) == nullptr);
2!
619
        CHECK(AppUtils::find_header("header*4", headers1) == nullptr);
2!
620
        CHECK(AppUtils::find_header("header5", headers1) == nullptr);
2!
621
        auto value = AppUtils::find_header("header1", headers1);
2✔
622
        CHECK(value != nullptr);
2!
623
        CHECK(value->first == "header1");
2!
624
        CHECK(value->second == "header1-value");
2!
625
        value = AppUtils::find_header("HEADER1", headers1);
2✔
626
        CHECK(value != nullptr);
2!
627
        CHECK(value->first == "header1");
2!
628
        CHECK(value->second == "header1-value");
2!
629
        value = AppUtils::find_header("header2", headers1);
2✔
630
        CHECK(value != nullptr);
2!
631
        CHECK(value->first == "HEADER2");
2!
632
        CHECK(value->second == "header2-value");
2!
633
        value = AppUtils::find_header("hEaDeR2", headers1);
2✔
634
        CHECK(value != nullptr);
2!
635
        CHECK(value->first == "HEADER2");
2!
636
        CHECK(value->second == "header2-value");
2!
637
        value = AppUtils::find_header("HEADER3", headers1);
2✔
638
        CHECK(value != nullptr);
2!
639
        CHECK(value->first == "HeAdEr3");
2!
640
        CHECK(value->second == "header3-value");
2!
641
        value = AppUtils::find_header("header3", headers1);
2✔
642
        CHECK(value != nullptr);
2!
643
        CHECK(value->first == "HeAdEr3");
2!
644
        CHECK(value->second == "header3-value");
2!
645
        value = AppUtils::find_header("HEADER@4", headers1);
2✔
646
        CHECK(value != nullptr);
2!
647
        CHECK(value->first == "header@4");
2!
648
        CHECK(value->second == "header4-value");
2!
649
        value = AppUtils::find_header("", headers2);
2✔
650
        CHECK(value != nullptr);
2!
651
        CHECK(value->first == "");
2!
652
        CHECK(value->second == "no-key-value");
2!
653
        value = AppUtils::find_header("HeAdEr1", headers2);
2✔
654
        CHECK(value != nullptr);
2!
655
        CHECK(value->first == "header1");
2!
656
        CHECK(value->second == "header1-value");
2!
657
    }
2✔
658

659
    SECTION("is_success_status_code") {
4✔
660
        CHECK(AppUtils::is_success_status_code(0));
2!
661
        for (int code = 200; code < 300; code++) {
202✔
662
            CHECK(AppUtils::is_success_status_code(code));
200!
663
        }
200✔
664
        CHECK(!AppUtils::is_success_status_code(1));
2!
665
        CHECK(!AppUtils::is_success_status_code(199));
2!
666
        CHECK(!AppUtils::is_success_status_code(300));
2!
667
        CHECK(!AppUtils::is_success_status_code(99999));
2!
668
    }
2✔
669
}
4✔
670

671
// MARK: - Login with Credentials Tests
672

673
TEST_CASE("app: login_with_credentials integration", "[sync][app][user][baas]") {
2✔
674
    SECTION("login") {
2✔
675
        TestAppSession session;
2✔
676
        auto app = session.app();
2✔
677
        app->log_out([](auto) {});
2✔
678

679
        int subscribe_processed = 0;
2✔
680

681
        auto token = app->subscribe([&subscribe_processed](auto&) {
4✔
682
            subscribe_processed++;
4✔
683
        });
4✔
684

685
        REQUIRE_FALSE(app->current_user());
2!
686
        auto user = log_in(app);
2✔
687
        CHECK(!user->device_id().empty());
2!
688
        CHECK(user->has_device_id());
2!
689
        REQUIRE(app->current_user());
2!
690
        CHECK(subscribe_processed == 1);
2!
691

692
        bool processed = false;
2✔
693
        app->log_out([&](auto error) {
2✔
694
            REQUIRE_FALSE(error);
2!
695
            processed = true;
2✔
696
        });
2✔
697
        REQUIRE_FALSE(app->current_user());
2!
698
        CHECK(processed);
2!
699
        CHECK(subscribe_processed == 2);
2!
700

701
        app->unsubscribe(token);
2✔
702
    }
2✔
703
}
2✔
704

705
// MARK: - UsernamePasswordProviderClient Tests
706

707
TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][baas]") {
26✔
708
    const std::string base_url = get_real_base_url();
26✔
709
    AutoVerifiedEmailCredentials creds;
26✔
710
    auto email = creds.email;
26✔
711
    auto password = creds.password;
26✔
712

713
    TestAppSession session;
26✔
714
    auto app = session.app();
26✔
715
    auto client = app->provider_client<App::UsernamePasswordProviderClient>();
26✔
716

717
    bool processed = false;
26✔
718

719
    client.register_email(email, password, [&](Optional<AppError> error) {
26✔
720
        CAPTURE(email);
26✔
721
        CAPTURE(password);
26✔
722
        REQUIRE_FALSE(error); // first registration success
26!
723
    });
26✔
724

725
    SECTION("double registration should fail") {
26✔
726
        client.register_email(email, password, [&](Optional<AppError> error) {
2✔
727
            // Error returned states the account has already been created
728
            REQUIRE(error);
2!
729
            CHECK(error->reason() == "name already in use");
2!
730
            CHECK(error->code() == ErrorCodes::AccountNameInUse);
2!
731
            CHECK(!error->link_to_server_logs.empty());
2!
732
            CHECK_THAT(error->link_to_server_logs, ContainsSubstring(base_url));
2✔
733
            processed = true;
2✔
734
        });
2✔
735
        CHECK(processed);
2!
736
    }
2✔
737

738
    SECTION("double registration should fail") {
26✔
739
        // the server registration function will reject emails that do not contain "realm_tests_do_autoverify"
740
        std::string email_to_reject = util::format("%1@%2.com", random_string(10), random_string(10));
2✔
741
        client.register_email(email_to_reject, password, [&](Optional<AppError> error) {
2✔
742
            REQUIRE(error);
2!
743
            CHECK(error->reason() == util::format("failed to confirm user \"%1\"", email_to_reject));
2!
744
            CHECK(error->code() == ErrorCodes::BadRequest);
2!
745
            processed = true;
2✔
746
        });
2✔
747
        CHECK(processed);
2!
748
    }
2✔
749

750
    SECTION("can login with registered account") {
26✔
751
        auto user = log_in(app, creds);
2✔
752
        CHECK(user->user_profile().email() == email);
2!
753
    }
2✔
754

755
    SECTION("cannot login with wrong password") {
26✔
756
        app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"),
2✔
757
                                     [&](std::shared_ptr<User> user, Optional<AppError> error) {
2✔
758
                                         CHECK(!user);
2!
759
                                         REQUIRE(error);
2!
760
                                         REQUIRE(error->code() == ErrorCodes::InvalidPassword);
2!
761
                                         processed = true;
2✔
762
                                     });
2✔
763
        CHECK(processed);
2!
764
    }
2✔
765

766
    SECTION("confirm user") {
26✔
767
        client.confirm_user("a_token", "a_token_id", [&](Optional<AppError> error) {
2✔
768
            REQUIRE(error);
2!
769
            CHECK(error->reason() == "invalid token data");
2!
770
            processed = true;
2✔
771
        });
2✔
772
        CHECK(processed);
2!
773
    }
2✔
774

775
    SECTION("resend confirmation email") {
26✔
776
        client.resend_confirmation_email(email, [&](Optional<AppError> error) {
2✔
777
            REQUIRE(error);
2!
778
            CHECK(error->reason() == "already confirmed");
2!
779
            processed = true;
2✔
780
        });
2✔
781
        CHECK(processed);
2!
782
    }
2✔
783

784
    SECTION("reset password invalid tokens") {
26✔
785
        client.reset_password(password, "token_sample", "token_id_sample", [&](Optional<AppError> error) {
2✔
786
            REQUIRE(error);
2!
787
            CHECK(error->reason() == "invalid token data");
2!
788
            CHECK(!error->link_to_server_logs.empty());
2!
789
            CHECK_THAT(error->link_to_server_logs, ContainsSubstring(base_url));
2✔
790
            processed = true;
2✔
791
        });
2✔
792
        CHECK(processed);
2!
793
    }
2✔
794

795
    SECTION("reset password function success") {
26✔
796
        // the imported test app will accept password reset if the password contains "realm_tests_do_reset" via a
797
        // function
798
        std::string accepted_new_password = util::format("realm_tests_do_reset%1", random_string(10));
2✔
799
        client.call_reset_password_function(email, accepted_new_password, {}, [&](Optional<AppError> error) {
2✔
800
            REQUIRE_FALSE(error);
2!
801
            processed = true;
2✔
802
        });
2✔
803
        CHECK(processed);
2!
804
    }
2✔
805

806
    SECTION("reset password function failure") {
26✔
807
        std::string rejected_password = util::format("%1", random_string(10));
2✔
808
        client.call_reset_password_function(email, rejected_password, {"foo", "bar"}, [&](Optional<AppError> error) {
2✔
809
            REQUIRE(error);
2!
810
            CHECK(error->reason() == util::format("failed to reset password for user \"%1\"", email));
2!
811
            CHECK(error->is_service_error());
2!
812
            processed = true;
2✔
813
        });
2✔
814
        CHECK(processed);
2!
815
    }
2✔
816

817
    SECTION("reset password function for invalid user fails") {
26✔
818
        client.call_reset_password_function(util::format("%1@%2.com", random_string(5), random_string(5)), password,
2✔
819
                                            {"foo", "bar"}, [&](Optional<AppError> error) {
2✔
820
                                                REQUIRE(error);
2!
821
                                                CHECK(error->reason() == "user not found");
2!
822
                                                CHECK(error->is_service_error());
2!
823
                                                CHECK(error->code() == ErrorCodes::UserNotFound);
2!
824
                                                processed = true;
2✔
825
                                            });
2✔
826
        CHECK(processed);
2!
827
    }
2✔
828

829
    SECTION("retry custom confirmation") {
26✔
830
        client.retry_custom_confirmation(email, [&](Optional<AppError> error) {
2✔
831
            REQUIRE(error);
2!
832
            CHECK(error->reason() == "already confirmed");
2!
833
            processed = true;
2✔
834
        });
2✔
835
        CHECK(processed);
2!
836
    }
2✔
837

838
    SECTION("retry custom confirmation for invalid user fails") {
26✔
839
        client.retry_custom_confirmation(util::format("%1@%2.com", random_string(5), random_string(5)),
2✔
840
                                         [&](Optional<AppError> error) {
2✔
841
                                             REQUIRE(error);
2!
842
                                             CHECK(error->reason() == "user not found");
2!
843
                                             CHECK(error->is_service_error());
2!
844
                                             CHECK(error->code() == ErrorCodes::UserNotFound);
2!
845
                                             processed = true;
2✔
846
                                         });
2✔
847
        CHECK(processed);
2!
848
    }
2✔
849

850
    SECTION("log in, remove, log in") {
26✔
851
        app->remove_user(app->current_user(), [](auto) {});
2✔
852
        CHECK(app->all_users().size() == 0);
2!
853
        CHECK(app->current_user() == nullptr);
2!
854

855
        auto user = log_in(app, AppCredentials::username_password(email, password));
2✔
856
        CHECK(user->user_profile().email() == email);
2!
857
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
858

859
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
860
            REQUIRE_FALSE(error);
2!
861
        });
2✔
862
        CHECK(user->state() == SyncUser::State::Removed);
2!
863

864
        log_in(app, AppCredentials::username_password(email, password));
2✔
865
        CHECK(user->state() == SyncUser::State::Removed);
2!
866
        CHECK(app->current_user() != user);
2!
867
        user = app->current_user();
2✔
868
        CHECK(user->user_profile().email() == email);
2!
869
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
870

871
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
872
            REQUIRE(!error);
2!
873
            CHECK(app->all_users().size() == 0);
2!
874
            processed = true;
2✔
875
        });
2✔
876

877
        CHECK(user->state() == SyncUser::State::Removed);
2!
878
        CHECK(processed);
2!
879
        CHECK(app->all_users().size() == 0);
2!
880
    }
2✔
881
}
26✔
882

883
// MARK: - UserAPIKeyProviderClient Tests
884

885
TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baas]") {
6✔
886
    TestAppSession session;
6✔
887
    auto app = session.app();
6✔
888
    auto client = app->provider_client<App::UserAPIKeyProviderClient>();
6✔
889

890
    bool processed = false;
6✔
891
    App::UserAPIKey api_key;
6✔
892

893
    SECTION("api-key") {
6✔
894
        std::shared_ptr<User> logged_in_user = app->current_user();
2✔
895
        auto api_key_name = util::format("%1", random_string(15));
2✔
896
        client.create_api_key(api_key_name, logged_in_user,
2✔
897
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
898
                                  REQUIRE_FALSE(error);
2!
899
                                  CHECK(user_api_key.name == api_key_name);
2!
900
                                  api_key = user_api_key;
2✔
901
                              });
2✔
902

903
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
904
            REQUIRE_FALSE(error);
2!
905
            CHECK(user_api_key.name == api_key_name);
2!
906
            CHECK(user_api_key.id == api_key.id);
2!
907
        });
2✔
908

909
        client.fetch_api_keys(logged_in_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
910
            CHECK(api_keys.size() == 1);
2!
911
            for (auto key : api_keys) {
2✔
912
                CHECK(key.id.to_string() == api_key.id.to_string());
2!
913
                CHECK(api_key.name == api_key_name);
2!
914
                CHECK(key.id == api_key.id);
2!
915
            }
2✔
916
            REQUIRE_FALSE(error);
2!
917
        });
2✔
918

919
        client.enable_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
920
            REQUIRE_FALSE(error);
2!
921
        });
2✔
922

923
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
924
            REQUIRE_FALSE(error);
2!
925
            CHECK(user_api_key.disabled == false);
2!
926
            CHECK(user_api_key.name == api_key_name);
2!
927
            CHECK(user_api_key.id == api_key.id);
2!
928
        });
2✔
929

930
        client.disable_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
931
            REQUIRE_FALSE(error);
2!
932
        });
2✔
933

934
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
935
            REQUIRE_FALSE(error);
2!
936
            CHECK(user_api_key.disabled == true);
2!
937
            CHECK(user_api_key.name == api_key_name);
2!
938
        });
2✔
939

940
        client.delete_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
941
            REQUIRE_FALSE(error);
2!
942
        });
2✔
943

944
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
945
            CHECK(user_api_key.name == "");
2!
946
            CHECK(error);
2!
947
            processed = true;
2✔
948
        });
2✔
949

950
        CHECK(processed);
2!
951
    }
2✔
952

953
    SECTION("api-key without a user") {
6✔
954
        std::shared_ptr<User> no_user = nullptr;
2✔
955
        auto api_key_name = util::format("%1", random_string(15));
2✔
956
        client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
957
            REQUIRE(error);
2!
958
            CHECK(error->is_service_error());
2!
959
            CHECK(error->reason() == "must authenticate first");
2!
960
            CHECK(user_api_key.name == "");
2!
961
        });
2✔
962

963
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
964
            REQUIRE(error);
2!
965
            CHECK(error->is_service_error());
2!
966
            CHECK(error->reason() == "must authenticate first");
2!
967
            CHECK(user_api_key.name == "");
2!
968
        });
2✔
969

970
        client.fetch_api_keys(no_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
971
            REQUIRE(error);
2!
972
            CHECK(error->is_service_error());
2!
973
            CHECK(error->reason() == "must authenticate first");
2!
974
            CHECK(api_keys.size() == 0);
2!
975
        });
2✔
976

977
        client.enable_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
978
            REQUIRE(error);
2!
979
            CHECK(error->is_service_error());
2!
980
            CHECK(error->reason() == "must authenticate first");
2!
981
        });
2✔
982

983
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
984
            REQUIRE(error);
2!
985
            CHECK(error->is_service_error());
2!
986
            CHECK(error->reason() == "must authenticate first");
2!
987
            CHECK(user_api_key.name == "");
2!
988
        });
2✔
989

990
        client.disable_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
991
            REQUIRE(error);
2!
992
            CHECK(error->is_service_error());
2!
993
            CHECK(error->reason() == "must authenticate first");
2!
994
        });
2✔
995

996
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
997
            REQUIRE(error);
2!
998
            CHECK(error->is_service_error());
2!
999
            CHECK(error->reason() == "must authenticate first");
2!
1000
            CHECK(user_api_key.name == "");
2!
1001
        });
2✔
1002

1003
        client.delete_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
1004
            REQUIRE(error);
2!
1005
            CHECK(error->is_service_error());
2!
1006
            CHECK(error->reason() == "must authenticate first");
2!
1007
        });
2✔
1008

1009
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1010
            CHECK(user_api_key.name == "");
2!
1011
            REQUIRE(error);
2!
1012
            CHECK(error->is_service_error());
2!
1013
            CHECK(error->reason() == "must authenticate first");
2!
1014
            processed = true;
2✔
1015
        });
2✔
1016
        CHECK(processed);
2!
1017
    }
2✔
1018

1019
    SECTION("api-key against the wrong user") {
6✔
1020
        std::shared_ptr<User> first_user = app->current_user();
2✔
1021
        create_user_and_log_in(app);
2✔
1022
        std::shared_ptr<User> second_user = app->current_user();
2✔
1023
        REQUIRE(first_user != second_user);
2!
1024
        auto api_key_name = util::format("%1", random_string(15));
2✔
1025
        App::UserAPIKey api_key;
2✔
1026
        App::UserAPIKeyProviderClient provider = app->provider_client<App::UserAPIKeyProviderClient>();
2✔
1027

1028
        provider.create_api_key(api_key_name, first_user,
2✔
1029
                                [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1030
                                    REQUIRE_FALSE(error);
2!
1031
                                    CHECK(user_api_key.name == api_key_name);
2!
1032
                                    api_key = user_api_key;
2✔
1033
                                });
2✔
1034

1035
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1036
            REQUIRE_FALSE(error);
2!
1037
            CHECK(user_api_key.name == api_key_name);
2!
1038
            CHECK(user_api_key.id.to_string() == user_api_key.id.to_string());
2!
1039
        });
2✔
1040

1041
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1042
            REQUIRE(error);
2!
1043
            CHECK(error->reason() == "API key not found");
2!
1044
            CHECK(error->is_service_error());
2!
1045
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1046
            CHECK(user_api_key.name == "");
2!
1047
        });
2✔
1048

1049
        provider.fetch_api_keys(first_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
1050
            CHECK(api_keys.size() == 1);
2!
1051
            for (auto api_key : api_keys) {
2✔
1052
                CHECK(api_key.name == api_key_name);
2!
1053
            }
2✔
1054
            REQUIRE_FALSE(error);
2!
1055
        });
2✔
1056

1057
        provider.fetch_api_keys(second_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
1058
            CHECK(api_keys.size() == 0);
2!
1059
            REQUIRE_FALSE(error);
2!
1060
        });
2✔
1061

1062
        provider.enable_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
1063
            REQUIRE_FALSE(error);
2!
1064
        });
2✔
1065

1066
        provider.enable_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
1067
            REQUIRE(error);
2!
1068
            CHECK(error->reason() == "API key not found");
2!
1069
            CHECK(error->is_service_error());
2!
1070
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1071
        });
2✔
1072

1073
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1074
            REQUIRE_FALSE(error);
2!
1075
            CHECK(user_api_key.disabled == false);
2!
1076
            CHECK(user_api_key.name == api_key_name);
2!
1077
        });
2✔
1078

1079
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1080
            REQUIRE(error);
2!
1081
            CHECK(user_api_key.name == "");
2!
1082
            CHECK(error->reason() == "API key not found");
2!
1083
            CHECK(error->is_service_error());
2!
1084
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1085
        });
2✔
1086

1087
        provider.disable_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
1088
            REQUIRE_FALSE(error);
2!
1089
        });
2✔
1090

1091
        provider.disable_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
1092
            REQUIRE(error);
2!
1093
            CHECK(error->reason() == "API key not found");
2!
1094
            CHECK(error->is_service_error());
2!
1095
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1096
        });
2✔
1097

1098
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1099
            REQUIRE_FALSE(error);
2!
1100
            CHECK(user_api_key.disabled == true);
2!
1101
            CHECK(user_api_key.name == api_key_name);
2!
1102
        });
2✔
1103

1104
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1105
            REQUIRE(error);
2!
1106
            CHECK(user_api_key.name == "");
2!
1107
            CHECK(error->reason() == "API key not found");
2!
1108
            CHECK(error->is_service_error());
2!
1109
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1110
        });
2✔
1111

1112
        provider.delete_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
1113
            REQUIRE(error);
2!
1114
            CHECK(error->reason() == "API key not found");
2!
1115
            CHECK(error->is_service_error());
2!
1116
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1117
        });
2✔
1118

1119
        provider.delete_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
1120
            REQUIRE_FALSE(error);
2!
1121
        });
2✔
1122

1123
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1124
            CHECK(user_api_key.name == "");
2!
1125
            REQUIRE(error);
2!
1126
            CHECK(error->reason() == "API key not found");
2!
1127
            CHECK(error->is_service_error());
2!
1128
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1129
            processed = true;
2✔
1130
        });
2✔
1131

1132
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1133
            CHECK(user_api_key.name == "");
2!
1134
            REQUIRE(error);
2!
1135
            CHECK(error->reason() == "API key not found");
2!
1136
            CHECK(error->is_service_error());
2!
1137
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1138
            processed = true;
2✔
1139
        });
2✔
1140

1141
        CHECK(processed);
2!
1142
    }
2✔
1143
}
6✔
1144

1145
// MARK: - Auth Providers Function Tests
1146

1147
TEST_CASE("app: auth providers function integration", "[sync][app][user][baas]") {
2✔
1148
    TestAppSession session;
2✔
1149
    auto app = session.app();
2✔
1150

1151
    SECTION("auth providers function integration") {
2✔
1152
        bson::BsonDocument function_params{{"realmCustomAuthFuncUserId", "123456"}};
2✔
1153
        auto credentials = AppCredentials::function(function_params);
2✔
1154
        auto user = log_in(app, credentials);
2✔
1155
        REQUIRE(user->identities()[0].provider_type == IdentityProviderFunction);
2!
1156
    }
2✔
1157
}
2✔
1158

1159
// MARK: - Link User Tests
1160

1161
TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") {
8✔
1162
    TestAppSession session;
8✔
1163
    auto app = session.app();
8✔
1164
    auto user = log_in(app);
8✔
1165

1166
    AutoVerifiedEmailCredentials creds;
8✔
1167
    app->provider_client<App::UsernamePasswordProviderClient>().register_email(creds.email, creds.password,
8✔
1168
                                                                               [&](Optional<AppError> error) {
8✔
1169
                                                                                   REQUIRE_FALSE(error);
8!
1170
                                                                               });
8✔
1171

1172
    SECTION("anonymous users are reused before they are linked to an identity") {
8✔
1173
        REQUIRE(user == log_in(app));
2!
1174
    }
2✔
1175

1176
    SECTION("linking a user adds that identity to the user") {
8✔
1177
        REQUIRE(user->identities().size() == 1);
2!
1178
        CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous);
2!
1179

1180
        app->link_user(user, creds, [&](std::shared_ptr<User> user2, Optional<AppError> error) {
2✔
1181
            REQUIRE_FALSE(error);
2!
1182
            REQUIRE(user == user2);
2!
1183
            REQUIRE(user->identities().size() == 2);
2!
1184
            CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous);
2!
1185
            CHECK(user->identities()[1].provider_type == IdentityProviderUsernamePassword);
2!
1186
        });
2✔
1187
    }
2✔
1188

1189
    SECTION("linking an identity makes the user no longer returned by anonymous logins") {
8✔
1190
        app->link_user(user, creds, [&](std::shared_ptr<User>, Optional<AppError> error) {
2✔
1191
            REQUIRE_FALSE(error);
2!
1192
        });
2✔
1193
        auto user2 = log_in(app);
2✔
1194
        REQUIRE(user != user2);
2!
1195
    }
2✔
1196

1197
    SECTION("existing users are reused when logging in via linked identities") {
8✔
1198
        app->link_user(user, creds, [](std::shared_ptr<User>, Optional<AppError> error) {
2✔
1199
            REQUIRE_FALSE(error);
2!
1200
        });
2✔
1201
        app->log_out([](auto error) {
2✔
1202
            REQUIRE_FALSE(error);
2!
1203
        });
2✔
1204
        REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
1205
        // Should give us the same user instance despite logging in with a
1206
        // different identity
1207
        REQUIRE(user == log_in(app, creds));
2!
1208
        REQUIRE(user->state() == SyncUser::State::LoggedIn);
2!
1209
    }
2✔
1210
}
8✔
1211

1212
// MARK: - Delete User Tests
1213

1214
TEST_CASE("app: delete anonymous user integration", "[sync][app][user][baas]") {
2✔
1215
    TestAppSession session;
2✔
1216
    auto app = session.app();
2✔
1217

1218
    SECTION("delete user expect success") {
2✔
1219
        CHECK(app->all_users().size() == 1);
2!
1220

1221
        // Log in user 1
1222
        auto user_a = app->current_user();
2✔
1223
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
1224
        app->delete_user(user_a, [&](Optional<app::AppError> error) {
2✔
1225
            REQUIRE_FALSE(error);
2!
1226
            // a logged out anon user will be marked as Removed, not LoggedOut
1227
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
1228
        });
2✔
1229
        CHECK(app->all_users().empty());
2!
1230
        CHECK(app->current_user() == nullptr);
2!
1231

1232
        app->delete_user(user_a, [&](Optional<app::AppError> error) {
2✔
1233
            CHECK(error->reason() == "User must be logged in to be deleted.");
2!
1234
            CHECK(app->all_users().size() == 0);
2!
1235
        });
2✔
1236

1237
        // Log in user 2
1238
        auto user_b = log_in(app);
2✔
1239
        CHECK(app->current_user() == user_b);
2!
1240
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
1241
        CHECK(app->all_users().size() == 1);
2!
1242

1243
        app->delete_user(user_b, [&](Optional<app::AppError> error) {
2✔
1244
            REQUIRE_FALSE(error);
2!
1245
            CHECK(app->all_users().size() == 0);
2!
1246
        });
2✔
1247

1248
        CHECK(app->current_user() == nullptr);
2!
1249

1250
        // check both handles are no longer valid
1251
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
1252
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
1253
    }
2✔
1254
}
2✔
1255

1256
TEST_CASE("app: delete user with credentials integration", "[sync][app][user][baas]") {
2✔
1257
    TestAppSession session;
2✔
1258
    auto app = session.app();
2✔
1259
    app->remove_user(app->current_user(), [](auto) {});
2✔
1260

1261
    SECTION("log in and delete") {
2✔
1262
        CHECK(app->all_users().size() == 0);
2!
1263
        CHECK(app->current_user() == nullptr);
2!
1264

1265
        auto credentials = create_user_and_log_in(app);
2✔
1266
        auto user = app->current_user();
2✔
1267

1268
        CHECK(app->current_user() == user);
2!
1269
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
1270
        app->delete_user(user, [&](Optional<app::AppError> error) {
2✔
1271
            REQUIRE_FALSE(error);
2!
1272
            CHECK(app->all_users().size() == 0);
2!
1273
        });
2✔
1274
        CHECK(user->state() == SyncUser::State::Removed);
2!
1275
        CHECK(app->current_user() == nullptr);
2!
1276

1277
        app->log_in_with_credentials(credentials, [](std::shared_ptr<User> user, util::Optional<AppError> error) {
2✔
1278
            CHECK(!user);
2!
1279
            REQUIRE(error);
2!
1280
            REQUIRE(error->code() == ErrorCodes::InvalidPassword);
2!
1281
        });
2✔
1282
        CHECK(app->current_user() == nullptr);
2!
1283

1284
        CHECK(app->all_users().size() == 0);
2!
1285
        app->delete_user(user, [](Optional<app::AppError> err) {
2✔
1286
            CHECK(err->code() > 0);
2!
1287
        });
2✔
1288

1289
        CHECK(app->current_user() == nullptr);
2!
1290
        CHECK(app->all_users().size() == 0);
2!
1291
        CHECK(user->state() == SyncUser::State::Removed);
2!
1292
    }
2✔
1293
}
2✔
1294

1295
// MARK: - Call Function Tests
1296

1297
TEST_CASE("app: call function", "[sync][app][function][baas]") {
2✔
1298
    TestAppSession session;
2✔
1299
    auto app = session.app();
2✔
1300

1301
    bson::BsonArray toSum(5);
2✔
1302
    std::iota(toSum.begin(), toSum.end(), static_cast<int64_t>(1));
2✔
1303
    const auto checkFn = [](Optional<int64_t>&& sum, Optional<AppError>&& error) {
4✔
1304
        REQUIRE(!error);
4!
1305
        CHECK(*sum == 15);
4!
1306
    };
4✔
1307
    app->call_function<int64_t>("sumFunc", toSum, checkFn);
2✔
1308
    app->call_function<int64_t>(app->current_user(), "sumFunc", toSum, checkFn);
2✔
1309
}
2✔
1310

1311
// MARK: - Remote Mongo Client Tests
1312

1313
TEST_CASE("app: remote mongo client", "[sync][app][mongo][baas]") {
16✔
1314
    TestAppSession session;
16✔
1315
    auto app = session.app();
16✔
1316

1317
    auto remote_client = app->current_user()->mongo_client("BackingDB");
16✔
1318
    auto app_session = get_runtime_app_session();
16✔
1319
    auto db = remote_client.db(app_session.config.mongo_dbname);
16✔
1320
    auto dog_collection = db["Dog"];
16✔
1321
    auto cat_collection = db["Cat"];
16✔
1322
    auto person_collection = db["Person"];
16✔
1323

1324
    bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}};
16✔
1325

1326
    bson::BsonDocument dog_document2{{"name", "bob"}, {"breed", "french bulldog"}};
16✔
1327

1328
    auto dog3_object_id = ObjectId::gen();
16✔
1329
    bson::BsonDocument dog_document3{
16✔
1330
        {"_id", dog3_object_id},
16✔
1331
        {"name", "petunia"},
16✔
1332
        {"breed", "french bulldog"},
16✔
1333
    };
16✔
1334

1335
    auto cat_id_string = random_string(10);
16✔
1336
    bson::BsonDocument cat_document{
16✔
1337
        {"_id", cat_id_string},
16✔
1338
        {"name", "luna"},
16✔
1339
        {"breed", "scottish fold"},
16✔
1340
    };
16✔
1341

1342
    bson::BsonDocument person_document{
16✔
1343
        {"firstName", "John"},
16✔
1344
        {"lastName", "Johnson"},
16✔
1345
        {"age", 30},
16✔
1346
    };
16✔
1347

1348
    bson::BsonDocument person_document2{
16✔
1349
        {"firstName", "Bob"},
16✔
1350
        {"lastName", "Johnson"},
16✔
1351
        {"age", 30},
16✔
1352
    };
16✔
1353

1354
    bson::BsonDocument bad_document{{"bad", "value"}};
16✔
1355

1356
    dog_collection.delete_many(dog_document, [&](uint64_t, Optional<AppError> error) {
16✔
1357
        REQUIRE_FALSE(error);
16!
1358
    });
16✔
1359

1360
    dog_collection.delete_many(dog_document2, [&](uint64_t, Optional<AppError> error) {
16✔
1361
        REQUIRE_FALSE(error);
16!
1362
    });
16✔
1363

1364
    dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
16✔
1365
        REQUIRE_FALSE(error);
16!
1366
    });
16✔
1367

1368
    dog_collection.delete_many(person_document, [&](uint64_t, Optional<AppError> error) {
16✔
1369
        REQUIRE_FALSE(error);
16!
1370
    });
16✔
1371

1372
    dog_collection.delete_many(person_document2, [&](uint64_t, Optional<AppError> error) {
16✔
1373
        REQUIRE_FALSE(error);
16!
1374
    });
16✔
1375

1376
    SECTION("insert") {
16✔
1377
        bool processed = false;
2✔
1378
        ObjectId dog_object_id;
2✔
1379
        ObjectId dog2_object_id;
2✔
1380

1381
        dog_collection.insert_one_bson(bad_document, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1382
            CHECK(error);
2!
1383
            CHECK(!bson);
2!
1384
        });
2✔
1385

1386
        dog_collection.insert_one_bson(dog_document3, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1387
            REQUIRE_FALSE(error);
2!
1388
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1389
            CHECK(static_cast<ObjectId>(bson["insertedId"]) == dog3_object_id);
2!
1390
        });
2✔
1391

1392
        cat_collection.insert_one_bson(cat_document, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1393
            REQUIRE_FALSE(error);
2!
1394
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1395
            CHECK(static_cast<std::string>(bson["insertedId"]) == cat_id_string);
2!
1396
        });
2✔
1397

1398
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1399
            REQUIRE_FALSE(error);
2!
1400
        });
2✔
1401

1402
        cat_collection.delete_one(cat_document, [&](uint64_t, Optional<AppError> error) {
2✔
1403
            REQUIRE_FALSE(error);
2!
1404
        });
2✔
1405

1406
        dog_collection.insert_one(bad_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1407
            CHECK(error);
2!
1408
            CHECK(!object_id);
2!
1409
        });
2✔
1410

1411
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1412
            REQUIRE_FALSE(error);
2!
1413
            CHECK((*object_id).to_string() != "");
2!
1414
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1415
        });
2✔
1416

1417
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1418
            REQUIRE_FALSE(error);
2!
1419
            CHECK((*object_id).to_string() != "");
2!
1420
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1421
        });
2✔
1422

1423
        dog_collection.insert_one(dog_document3, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1424
            REQUIRE_FALSE(error);
2!
1425
            CHECK(object_id->type() == bson::Bson::Type::ObjectId);
2!
1426
            CHECK(static_cast<ObjectId>(*object_id) == dog3_object_id);
2!
1427
        });
2✔
1428

1429
        cat_collection.insert_one(cat_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1430
            REQUIRE_FALSE(error);
2!
1431
            CHECK(object_id->type() == bson::Bson::Type::String);
2!
1432
            CHECK(static_cast<std::string>(*object_id) == cat_id_string);
2!
1433
        });
2✔
1434

1435
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id, dog3_object_id});
2✔
1436
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1437
            REQUIRE_FALSE(error);
2!
1438
            CHECK((*object_id).to_string() != "");
2!
1439
        });
2✔
1440

1441
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1442
            REQUIRE_FALSE(error);
2!
1443
        });
2✔
1444

1445
        cat_collection.delete_one(cat_document, [&](uint64_t, Optional<AppError> error) {
2✔
1446
            REQUIRE_FALSE(error);
2!
1447
        });
2✔
1448

1449
        bson::BsonArray documents{
2✔
1450
            dog_document,
2✔
1451
            dog_document2,
2✔
1452
            dog_document3,
2✔
1453
        };
2✔
1454

1455
        dog_collection.insert_many_bson(documents, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1456
            REQUIRE_FALSE(error);
2!
1457
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1458
            auto insertedIds = static_cast<bson::BsonArray>(bson["insertedIds"]);
2✔
1459
        });
2✔
1460

1461
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1462
            REQUIRE_FALSE(error);
2!
1463
        });
2✔
1464

1465
        dog_collection.insert_many(documents, [&](bson::BsonArray inserted_docs, Optional<AppError> error) {
2✔
1466
            REQUIRE_FALSE(error);
2!
1467
            CHECK(inserted_docs.size() == 3);
2!
1468
            CHECK(inserted_docs[0].type() == bson::Bson::Type::ObjectId);
2!
1469
            CHECK(inserted_docs[1].type() == bson::Bson::Type::ObjectId);
2!
1470
            CHECK(inserted_docs[2].type() == bson::Bson::Type::ObjectId);
2!
1471
            CHECK(static_cast<ObjectId>(inserted_docs[2]) == dog3_object_id);
2!
1472
            processed = true;
2✔
1473
        });
2✔
1474

1475
        CHECK(processed);
2!
1476
    }
2✔
1477

1478
    SECTION("find") {
16✔
1479
        bool processed = false;
2✔
1480

1481
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1482
            REQUIRE_FALSE(error);
2!
1483
            CHECK((*document_array).size() == 0);
2!
1484
        });
2✔
1485

1486
        dog_collection.find_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1487
            REQUIRE_FALSE(error);
2!
1488
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 0);
2!
1489
        });
2✔
1490

1491
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1492
            REQUIRE_FALSE(error);
2!
1493
            CHECK(!document);
2!
1494
        });
2✔
1495

1496
        dog_collection.find_one_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1497
            REQUIRE_FALSE(error);
2!
1498
            CHECK((!bson || bson::holds_alternative<util::None>(*bson)));
2!
1499
        });
2✔
1500

1501
        ObjectId dog_object_id;
2✔
1502
        ObjectId dog2_object_id;
2✔
1503

1504
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1505
            REQUIRE_FALSE(error);
2!
1506
            CHECK((*object_id).to_string() != "");
2!
1507
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1508
        });
2✔
1509

1510
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1511
            REQUIRE_FALSE(error);
2!
1512
            CHECK((*object_id).to_string() != "");
2!
1513
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1514
        });
2✔
1515

1516
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id});
2✔
1517
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1518
            REQUIRE_FALSE(error);
2!
1519
            CHECK((*object_id).to_string() != "");
2!
1520
        });
2✔
1521

1522
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1523
            REQUIRE_FALSE(error);
2!
1524
            CHECK((*documents).size() == 1);
2!
1525
        });
2✔
1526

1527
        dog_collection.find_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1528
            REQUIRE_FALSE(error);
2!
1529
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 1);
2!
1530
        });
2✔
1531

1532
        person_collection.find(person_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1533
            REQUIRE_FALSE(error);
2!
1534
            CHECK((*documents).size() == 1);
2!
1535
        });
2✔
1536

1537
        MongoCollection::FindOptions options{
2✔
1538
            2,                                                         // document limit
2✔
1539
            Optional<bson::BsonDocument>({{"name", 1}, {"breed", 1}}), // project
2✔
1540
            Optional<bson::BsonDocument>({{"breed", 1}})               // sort
2✔
1541
        };
2✔
1542

1543
        dog_collection.find(dog_document, options,
2✔
1544
                            [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1545
                                REQUIRE_FALSE(error);
2!
1546
                                CHECK((*document_array).size() == 1);
2!
1547
                            });
2✔
1548

1549
        dog_collection.find({{"name", "fido"}}, options,
2✔
1550
                            [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1551
                                REQUIRE_FALSE(error);
2!
1552
                                CHECK((*document_array).size() == 1);
2!
1553
                                auto king_charles = static_cast<bson::BsonDocument>((*document_array)[0]);
2✔
1554
                                CHECK(king_charles["breed"] == "king charles");
2!
1555
                            });
2✔
1556

1557
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1558
            REQUIRE_FALSE(error);
2!
1559
            auto name = (*document)["name"];
2✔
1560
            CHECK(name == "fido");
2!
1561
        });
2✔
1562

1563
        dog_collection.find_one(dog_document, options,
2✔
1564
                                [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1565
                                    REQUIRE_FALSE(error);
2!
1566
                                    auto name = (*document)["name"];
2✔
1567
                                    CHECK(name == "fido");
2!
1568
                                });
2✔
1569

1570
        dog_collection.find_one_bson(dog_document, options, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1571
            REQUIRE_FALSE(error);
2!
1572
            auto name = (static_cast<bson::BsonDocument>(*bson))["name"];
2✔
1573
            CHECK(name == "fido");
2!
1574
        });
2✔
1575

1576
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1577
            REQUIRE_FALSE(error);
2!
1578
            CHECK((*documents).size() == 1);
2!
1579
        });
2✔
1580

1581
        dog_collection.find_one_and_delete(dog_document,
2✔
1582
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1583
                                               REQUIRE_FALSE(error);
2!
1584
                                               REQUIRE(document);
2!
1585
                                           });
2✔
1586

1587
        dog_collection.find_one_and_delete({{}},
2✔
1588
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1589
                                               REQUIRE_FALSE(error);
2!
1590
                                               REQUIRE(document);
2!
1591
                                           });
2✔
1592

1593
        dog_collection.find_one_and_delete({{"invalid", "key"}},
2✔
1594
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1595
                                               REQUIRE_FALSE(error);
2!
1596
                                               CHECK(!document);
2!
1597
                                           });
2✔
1598

1599
        dog_collection.find_one_and_delete_bson({{"invalid", "key"}}, {},
2✔
1600
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1601
                                                    REQUIRE_FALSE(error);
2!
1602
                                                    CHECK((!bson || bson::holds_alternative<util::None>(*bson)));
2!
1603
                                                });
2✔
1604

1605
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1606
            REQUIRE_FALSE(error);
2!
1607
            CHECK((*documents).size() == 0);
2!
1608
            processed = true;
2✔
1609
        });
2✔
1610

1611
        CHECK(processed);
2!
1612
    }
2✔
1613

1614
    SECTION("count and aggregate") {
16✔
1615
        bool processed = false;
2✔
1616

1617
        ObjectId dog_object_id;
2✔
1618
        ObjectId dog2_object_id;
2✔
1619

1620
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1621
            REQUIRE_FALSE(error);
2!
1622
            CHECK((*object_id).to_string() != "");
2!
1623
        });
2✔
1624

1625
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1626
            REQUIRE_FALSE(error);
2!
1627
            CHECK((*object_id).to_string() != "");
2!
1628
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1629
        });
2✔
1630

1631
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1632
            REQUIRE_FALSE(error);
2!
1633
            CHECK((*object_id).to_string() != "");
2!
1634
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1635
        });
2✔
1636

1637
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id});
2✔
1638
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1639
            REQUIRE_FALSE(error);
2!
1640
            CHECK((*object_id).to_string() != "");
2!
1641
        });
2✔
1642

1643
        bson::BsonDocument match{{"$match", bson::BsonDocument({{"name", "fido"}})}};
2✔
1644

1645
        bson::BsonDocument group{{"$group", bson::BsonDocument({{"_id", "$name"}})}};
2✔
1646

1647
        bson::BsonArray pipeline{match, group};
2✔
1648

1649
        dog_collection.aggregate(pipeline, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1650
            REQUIRE_FALSE(error);
2!
1651
            CHECK((*documents).size() == 1);
2!
1652
        });
2✔
1653

1654
        dog_collection.aggregate_bson(pipeline, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1655
            REQUIRE_FALSE(error);
2!
1656
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 1);
2!
1657
        });
2✔
1658

1659
        dog_collection.count({{"breed", "king charles"}}, [&](uint64_t count, Optional<AppError> error) {
2✔
1660
            REQUIRE_FALSE(error);
2!
1661
            CHECK(count == 2);
2!
1662
        });
2✔
1663

1664
        dog_collection.count_bson({{"breed", "king charles"}}, 0,
2✔
1665
                                  [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1666
                                      REQUIRE_FALSE(error);
2!
1667
                                      CHECK(static_cast<int64_t>(*bson) == 2);
2!
1668
                                  });
2✔
1669

1670
        dog_collection.count({{"breed", "french bulldog"}}, [&](uint64_t count, Optional<AppError> error) {
2✔
1671
            REQUIRE_FALSE(error);
2!
1672
            CHECK(count == 1);
2!
1673
        });
2✔
1674

1675
        dog_collection.count({{"breed", "king charles"}}, 1, [&](uint64_t count, Optional<AppError> error) {
2✔
1676
            REQUIRE_FALSE(error);
2!
1677
            CHECK(count == 1);
2!
1678
        });
2✔
1679

1680
        person_collection.count(
2✔
1681
            {{"firstName", "John"}, {"lastName", "Johnson"}, {"age", bson::BsonDocument({{"$gt", 25}})}}, 1,
2✔
1682
            [&](uint64_t count, Optional<AppError> error) {
2✔
1683
                REQUIRE_FALSE(error);
2!
1684
                CHECK(count == 1);
2!
1685
                processed = true;
2✔
1686
            });
2✔
1687

1688
        CHECK(processed);
2!
1689
    }
2✔
1690

1691
    SECTION("find and update") {
16✔
1692
        bool processed = false;
2✔
1693

1694
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1695
            Optional<bson::BsonDocument>({{"name", 1}, {"breed", 1}}), // project
2✔
1696
            Optional<bson::BsonDocument>({{"name", 1}}),               // sort,
2✔
1697
            true,                                                      // upsert
2✔
1698
            true                                                       // return new doc
2✔
1699
        };
2✔
1700

1701
        dog_collection.find_one_and_update(dog_document, dog_document2,
2✔
1702
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1703
                                               REQUIRE_FALSE(error);
2!
1704
                                               CHECK(!document);
2!
1705
                                           });
2✔
1706

1707
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1708
            REQUIRE_FALSE(error);
2!
1709
            CHECK((*object_id).to_string() != "");
2!
1710
        });
2✔
1711

1712
        dog_collection.find_one_and_update(dog_document, dog_document2, find_and_modify_options,
2✔
1713
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1714
                                               REQUIRE_FALSE(error);
2!
1715
                                               auto breed = static_cast<std::string>((*document)["breed"]);
2✔
1716
                                               CHECK(breed == "french bulldog");
2!
1717
                                           });
2✔
1718

1719
        dog_collection.find_one_and_update(dog_document2, dog_document, find_and_modify_options,
2✔
1720
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1721
                                               REQUIRE_FALSE(error);
2!
1722
                                               auto breed = static_cast<std::string>((*document)["breed"]);
2✔
1723
                                               CHECK(breed == "king charles");
2!
1724
                                           });
2✔
1725

1726
        dog_collection.find_one_and_update_bson(dog_document, dog_document2, find_and_modify_options,
2✔
1727
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1728
                                                    REQUIRE_FALSE(error);
2!
1729
                                                    auto breed = static_cast<std::string>(
2✔
1730
                                                        static_cast<bson::BsonDocument>(*bson)["breed"]);
2✔
1731
                                                    CHECK(breed == "french bulldog");
2!
1732
                                                });
2✔
1733

1734
        dog_collection.find_one_and_update_bson(dog_document2, dog_document, find_and_modify_options,
2✔
1735
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1736
                                                    REQUIRE_FALSE(error);
2!
1737
                                                    auto breed = static_cast<std::string>(
2✔
1738
                                                        static_cast<bson::BsonDocument>(*bson)["breed"]);
2✔
1739
                                                    CHECK(breed == "king charles");
2!
1740
                                                });
2✔
1741

1742
        dog_collection.find_one_and_update({{"name", "invalid name"}}, {{"name", "some name"}},
2✔
1743
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1744
                                               REQUIRE_FALSE(error);
2!
1745
                                               CHECK(!document);
2!
1746
                                               processed = true;
2✔
1747
                                           });
2✔
1748
        CHECK(processed);
2!
1749
        processed = false;
2✔
1750

1751
        dog_collection.find_one_and_update({{"name", "invalid name"}}, {{}}, find_and_modify_options,
2✔
1752
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1753
                                               REQUIRE(error);
2!
1754
                                               CHECK(error->reason() == "insert not permitted");
2!
1755
                                               CHECK(!document);
2!
1756
                                               processed = true;
2✔
1757
                                           });
2✔
1758
        CHECK(processed);
2!
1759
    }
2✔
1760

1761
    SECTION("update") {
16✔
1762
        bool processed = false;
2✔
1763
        ObjectId dog_object_id;
2✔
1764

1765
        dog_collection.update_one(dog_document, dog_document2, true,
2✔
1766
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1767
                                      REQUIRE_FALSE(error);
2!
1768
                                      CHECK((*result.upserted_id).to_string() != "");
2!
1769
                                  });
2✔
1770

1771
        dog_collection.update_one(dog_document2, dog_document,
2✔
1772
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1773
                                      REQUIRE_FALSE(error);
2!
1774
                                      CHECK(!result.upserted_id);
2!
1775
                                  });
2✔
1776

1777
        cat_collection.update_one({}, cat_document, true,
2✔
1778
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1779
                                      REQUIRE_FALSE(error);
2!
1780
                                      CHECK(result.upserted_id->type() == bson::Bson::Type::String);
2!
1781
                                      CHECK(result.upserted_id == cat_id_string);
2!
1782
                                  });
2✔
1783

1784
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1785
            REQUIRE_FALSE(error);
2!
1786
        });
2✔
1787

1788
        cat_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1789
            REQUIRE_FALSE(error);
2!
1790
        });
2✔
1791

1792
        dog_collection.update_one_bson(dog_document, dog_document2, true,
2✔
1793
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1794
                                           REQUIRE_FALSE(error);
2!
1795
                                           auto upserted_id = static_cast<bson::BsonDocument>(*bson)["upsertedId"];
2✔
1796

1797
                                           REQUIRE(upserted_id.type() == bson::Bson::Type::ObjectId);
2!
1798
                                       });
2✔
1799

1800
        dog_collection.update_one_bson(dog_document2, dog_document, true,
2✔
1801
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1802
                                           REQUIRE_FALSE(error);
2!
1803
                                           auto document = static_cast<bson::BsonDocument>(*bson);
2✔
1804
                                           auto foundUpsertedId = document.find("upsertedId");
2✔
1805
                                           REQUIRE(!foundUpsertedId);
2!
1806
                                       });
2✔
1807

1808
        cat_collection.update_one_bson({}, cat_document, true,
2✔
1809
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1810
                                           REQUIRE_FALSE(error);
2!
1811
                                           auto upserted_id = static_cast<bson::BsonDocument>(*bson)["upsertedId"];
2✔
1812
                                           REQUIRE(upserted_id.type() == bson::Bson::Type::String);
2!
1813
                                           REQUIRE(upserted_id == cat_id_string);
2!
1814
                                       });
2✔
1815

1816
        person_document["dogs"] = bson::BsonArray();
2✔
1817
        bson::BsonDocument person_document_copy = bson::BsonDocument(person_document);
2✔
1818
        person_document_copy["dogs"] = bson::BsonArray({dog_object_id});
2✔
1819
        person_collection.update_one(person_document, person_document, true,
2✔
1820
                                     [&](MongoCollection::UpdateResult, Optional<AppError> error) {
2✔
1821
                                         REQUIRE_FALSE(error);
2!
1822
                                         processed = true;
2✔
1823
                                     });
2✔
1824

1825
        CHECK(processed);
2!
1826
    }
2✔
1827

1828
    SECTION("update many") {
16✔
1829
        bool processed = false;
2✔
1830

1831
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1832
            REQUIRE_FALSE(error);
2!
1833
            CHECK((*object_id).to_string() != "");
2!
1834
        });
2✔
1835

1836
        dog_collection.update_many(dog_document2, dog_document, true,
2✔
1837
                                   [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1838
                                       REQUIRE_FALSE(error);
2!
1839
                                       CHECK((*result.upserted_id).to_string() != "");
2!
1840
                                   });
2✔
1841

1842
        dog_collection.update_many(dog_document2, dog_document,
2✔
1843
                                   [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1844
                                       REQUIRE_FALSE(error);
2!
1845
                                       CHECK(!result.upserted_id);
2!
1846
                                       processed = true;
2✔
1847
                                   });
2✔
1848

1849
        CHECK(processed);
2!
1850
    }
2✔
1851

1852
    SECTION("find and replace") {
16✔
1853
        bool processed = false;
2✔
1854
        ObjectId dog_object_id;
2✔
1855
        ObjectId person_object_id;
2✔
1856

1857
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1858
            Optional<bson::BsonDocument>({{"name", "fido"}}), // project
2✔
1859
            Optional<bson::BsonDocument>({{"name", 1}}),      // sort,
2✔
1860
            true,                                             // upsert
2✔
1861
            true                                              // return new doc
2✔
1862
        };
2✔
1863

1864
        dog_collection.find_one_and_replace(dog_document, dog_document2,
2✔
1865
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1866
                                                REQUIRE_FALSE(error);
2!
1867
                                                CHECK(!document);
2!
1868
                                            });
2✔
1869

1870
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1871
            REQUIRE_FALSE(error);
2!
1872
            CHECK((*object_id).to_string() != "");
2!
1873
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1874
        });
2✔
1875

1876
        dog_collection.find_one_and_replace(dog_document, dog_document2,
2✔
1877
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1878
                                                REQUIRE_FALSE(error);
2!
1879
                                                auto name = static_cast<std::string>((*document)["name"]);
2✔
1880
                                                CHECK(name == "fido");
2!
1881
                                            });
2✔
1882

1883
        dog_collection.find_one_and_replace(dog_document2, dog_document, find_and_modify_options,
2✔
1884
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1885
                                                REQUIRE_FALSE(error);
2!
1886
                                                auto name = static_cast<std::string>((*document)["name"]);
2✔
1887
                                                CHECK(static_cast<std::string>(name) == "fido");
2!
1888
                                            });
2✔
1889

1890
        person_document["dogs"] = bson::BsonArray({dog_object_id});
2✔
1891
        person_document2["dogs"] = bson::BsonArray({dog_object_id});
2✔
1892
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1893
            REQUIRE_FALSE(error);
2!
1894
            CHECK((*object_id).to_string() != "");
2!
1895
            person_object_id = static_cast<ObjectId>(*object_id);
2✔
1896
        });
2✔
1897

1898
        MongoCollection::FindOneAndModifyOptions person_find_and_modify_options{
2✔
1899
            Optional<bson::BsonDocument>({{"firstName", 1}}), // project
2✔
1900
            Optional<bson::BsonDocument>({{"firstName", 1}}), // sort,
2✔
1901
            false,                                            // upsert
2✔
1902
            true                                              // return new doc
2✔
1903
        };
2✔
1904

1905
        person_collection.find_one_and_replace(person_document, person_document2,
2✔
1906
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1907
                                                   REQUIRE_FALSE(error);
2!
1908
                                                   auto name = static_cast<std::string>((*document)["firstName"]);
2✔
1909
                                                   // Should return the old document
1910
                                                   CHECK(name == "John");
2!
1911
                                                   processed = true;
2✔
1912
                                               });
2✔
1913

1914
        person_collection.find_one_and_replace(person_document2, person_document, person_find_and_modify_options,
2✔
1915
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1916
                                                   REQUIRE_FALSE(error);
2!
1917
                                                   auto name = static_cast<std::string>((*document)["firstName"]);
2✔
1918
                                                   // Should return new document, Bob -> John
1919
                                                   CHECK(name == "John");
2!
1920
                                               });
2✔
1921

1922
        person_collection.find_one_and_replace({{"invalid", "item"}}, {{}},
2✔
1923
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1924
                                                   // If a document is not found then null will be returned for the
1925
                                                   // document and no error will be returned
1926
                                                   REQUIRE_FALSE(error);
2!
1927
                                                   CHECK(!document);
2!
1928
                                               });
2✔
1929

1930
        person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, person_find_and_modify_options,
2✔
1931
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1932
                                                   REQUIRE_FALSE(error);
2!
1933
                                                   CHECK(!document);
2!
1934
                                                   processed = true;
2✔
1935
                                               });
2✔
1936

1937
        CHECK(processed);
2!
1938
    }
2✔
1939

1940
    SECTION("delete") {
16✔
1941

1942
        bool processed = false;
2✔
1943

1944
        bson::BsonArray documents;
2✔
1945
        documents.push_back(dog_document);
2✔
1946
        documents.push_back(dog_document);
2✔
1947
        documents.push_back(dog_document);
2✔
1948

1949
        dog_collection.insert_many(documents, [&](bson::BsonArray inserted_docs, Optional<AppError> error) {
2✔
1950
            REQUIRE_FALSE(error);
2!
1951
            CHECK(inserted_docs.size() == 3);
2!
1952
        });
2✔
1953

1954
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1955
            Optional<bson::BsonDocument>({{"name", "fido"}}), // project
2✔
1956
            Optional<bson::BsonDocument>({{"name", 1}}),      // sort,
2✔
1957
            true,                                             // upsert
2✔
1958
            true                                              // return new doc
2✔
1959
        };
2✔
1960

1961
        dog_collection.delete_one(dog_document, [&](uint64_t deleted_count, Optional<AppError> error) {
2✔
1962
            REQUIRE_FALSE(error);
2!
1963
            CHECK(deleted_count >= 1);
2!
1964
        });
2✔
1965

1966
        dog_collection.delete_many(dog_document, [&](uint64_t deleted_count, Optional<AppError> error) {
2✔
1967
            REQUIRE_FALSE(error);
2!
1968
            CHECK(deleted_count >= 1);
2!
1969
            processed = true;
2✔
1970
        });
2✔
1971

1972
        person_collection.delete_many_bson(person_document, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1973
            REQUIRE_FALSE(error);
2!
1974
            CHECK(static_cast<int32_t>(static_cast<bson::BsonDocument>(*bson)["deletedCount"]) >= 1);
2!
1975
            processed = true;
2✔
1976
        });
2✔
1977

1978
        CHECK(processed);
2!
1979
    }
2✔
1980
}
16✔
1981

1982
// MARK: - Push Notifications Tests
1983

1984
TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") {
8✔
1985
    TestAppSession session;
8✔
1986
    auto app = session.app();
8✔
1987
    std::shared_ptr<User> sync_user = app->current_user();
8✔
1988

1989
    SECTION("register") {
8✔
1990
        bool processed;
2✔
1991

1992
        app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
1993
            REQUIRE_FALSE(error);
2!
1994
            processed = true;
2✔
1995
        });
2✔
1996

1997
        CHECK(processed);
2!
1998
    }
2✔
1999
    /*
2000
        // FIXME: It seems this test fails when the two register_device calls are invoked too quickly,
2001
        // The error returned will be 'Device not found' on the second register_device call.
2002
        SECTION("register twice") {
2003
            // registering the same device twice should not result in an error
2004
            bool processed;
2005

2006
            app->push_notification_client("gcm").register_device("hello",
2007
                                                                 sync_user,
2008
                                                                 [&](Optional<AppError> error) {
2009
                REQUIRE_FALSE(error);
2010
            });
2011

2012
            app->push_notification_client("gcm").register_device("hello",
2013
                                                                 sync_user,
2014
                                                                 [&](Optional<AppError> error) {
2015
                REQUIRE_FALSE(error);
2016
                processed = true;
2017
            });
2018

2019
            CHECK(processed);
2020
        }
2021
    */
2022
    SECTION("deregister") {
8✔
2023
        bool processed;
2✔
2024

2025
        app->push_notification_client("gcm").deregister_device(sync_user, [&](Optional<AppError> error) {
2✔
2026
            REQUIRE_FALSE(error);
2!
2027
            processed = true;
2✔
2028
        });
2✔
2029
        CHECK(processed);
2!
2030
    }
2✔
2031

2032
    SECTION("register with unavailable service") {
8✔
2033
        bool processed;
2✔
2034

2035
        app->push_notification_client("gcm_blah").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
2036
            REQUIRE(error);
2!
2037
            CHECK(error->reason() == "service not found: 'gcm_blah'");
2!
2038
            processed = true;
2✔
2039
        });
2✔
2040
        CHECK(processed);
2!
2041
    }
2✔
2042

2043
    SECTION("register with logged out user") {
8✔
2044
        bool processed;
2✔
2045

2046
        app->log_out([=](Optional<AppError> error) {
2✔
2047
            REQUIRE_FALSE(error);
2!
2048
        });
2✔
2049

2050
        app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
2051
            REQUIRE(error);
2!
2052
            processed = true;
2✔
2053
        });
2✔
2054

2055
        app->push_notification_client("gcm").register_device("hello", nullptr, [&](Optional<AppError> error) {
2✔
2056
            REQUIRE(error);
2!
2057
            processed = true;
2✔
2058
        });
2✔
2059

2060
        CHECK(processed);
2!
2061
    }
2✔
2062
}
8✔
2063

2064
// MARK: - Token refresh
2065

2066
TEST_CASE("app: token refresh", "[sync][app][token][baas]") {
2✔
2067
    TestAppSession session;
2✔
2068
    auto app = session.app();
2✔
2069
    std::shared_ptr<User> sync_user = app->current_user();
2✔
2070
    sync_user->update_data_for_testing([](UserData& data) {
2✔
2071
        data.access_token = RealmJWT(ENCODE_FAKE_JWT("fake_access_token"));
2✔
2072
    });
2✔
2073

2074
    auto remote_client = app->current_user()->mongo_client("BackingDB");
2✔
2075
    auto app_session = get_runtime_app_session();
2✔
2076
    auto db = remote_client.db(app_session.config.mongo_dbname);
2✔
2077
    auto dog_collection = db["Dog"];
2✔
2078
    bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}};
2✔
2079

2080
    SECTION("access token should refresh") {
2✔
2081
        /*
2082
         Expected sequence of events:
2083
         - `find_one` tries to hit the server with a bad access token
2084
         - Server returns an error because of the bad token, error should be something like:
2085
            {\"error\":\"json: cannot unmarshal array into Go value of type map[string]interface
2086
         {}\",\"link\":\"http://localhost:9090/groups/5f84167e776aa0f9dc27081a/apps/5f841686776aa0f9dc270876/logs?co_id=5f844c8c776aa0f9dc273db6\"}
2087
            http_status_code = 401
2088
            custom_status_code = 0
2089
         - App::handle_auth_failure is then called and an attempt to refresh the access token will be peformed.
2090
         - If the token refresh was successful, the original request will retry and we should expect no error in the
2091
         callback of `find_one`
2092
         */
2093
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument>, Optional<AppError> error) {
2✔
2094
            REQUIRE_FALSE(error);
2!
2095
        });
2✔
2096
    }
2✔
2097
}
2✔
2098

2099
// MARK: - Sync Tests
2100

2101
TEST_CASE("app: mixed lists with object links", "[sync][pbs][app][links][baas]") {
2✔
2102
    const std::string valid_pk_name = "_id";
2✔
2103

2104
    Schema schema{
2✔
2105
        {"TopLevel",
2✔
2106
         {
2✔
2107
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2108
             {"mixed_array", PropertyType::Mixed | PropertyType::Array | PropertyType::Nullable},
2✔
2109
         }},
2✔
2110
        {"Target",
2✔
2111
         {
2✔
2112
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2113
             {"value", PropertyType::Int},
2✔
2114
         }},
2✔
2115
    };
2✔
2116

2117
    auto server_app_config = minimal_app_config("set_new_embedded_object", schema);
2✔
2118
    auto app_session = create_app(server_app_config);
2✔
2119
    auto partition = random_string(100);
2✔
2120

2121
    auto obj_id = ObjectId::gen();
2✔
2122
    auto target_id = ObjectId::gen();
2✔
2123
    auto mixed_list_values = AnyVector{
2✔
2124
        Mixed{int64_t(1234)},
2✔
2125
        Mixed{},
2✔
2126
        Mixed{target_id},
2✔
2127
    };
2✔
2128
    {
2✔
2129
        TestAppSession test_session(app_session, {}, DeleteApp{false});
2✔
2130
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2131
        auto realm = Realm::get_shared_realm(config);
2✔
2132

2133
        CppContext c(realm);
2✔
2134
        realm->begin_transaction();
2✔
2135
        auto target_obj = Object::create(
2✔
2136
            c, realm, "Target", std::any(AnyDict{{valid_pk_name, target_id}, {"value", static_cast<int64_t>(1234)}}));
2✔
2137
        mixed_list_values.push_back(Mixed(target_obj.get_obj().get_link()));
2✔
2138

2139
        Object::create(c, realm, "TopLevel",
2✔
2140
                       std::any(AnyDict{
2✔
2141
                           {valid_pk_name, obj_id},
2✔
2142
                           {"mixed_array", mixed_list_values},
2✔
2143
                       }),
2✔
2144
                       CreatePolicy::ForceCreate);
2✔
2145
        realm->commit_transaction();
2✔
2146
        CHECK(!wait_for_upload(*realm));
2!
2147
    }
2✔
2148

2149
    {
2✔
2150
        TestAppSession test_session(app_session);
2✔
2151
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2152
        auto realm = Realm::get_shared_realm(config);
2✔
2153

2154
        CHECK(!wait_for_download(*realm));
2!
2155
        CppContext c(realm);
2✔
2156
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{obj_id});
2✔
2157
        auto list = util::any_cast<List&&>(obj.get_property_value<std::any>(c, "mixed_array"));
2✔
2158
        for (size_t idx = 0; idx < list.size(); ++idx) {
10✔
2159
            Mixed mixed = list.get_any(idx);
8✔
2160
            if (idx == 3) {
8✔
2161
                CHECK(mixed.is_type(type_TypedLink));
2!
2162
                auto link = mixed.get<ObjLink>();
2✔
2163
                auto link_table = realm->read_group().get_table(link.get_table_key());
2✔
2164
                CHECK(link_table->get_name() == "class_Target");
2!
2165
                auto link_obj = link_table->get_object(link.get_obj_key());
2✔
2166
                CHECK(link_obj.get_primary_key() == target_id);
2!
2167
            }
2✔
2168
            else {
6✔
2169
                CHECK(mixed == util::any_cast<Mixed>(mixed_list_values[idx]));
6!
2170
            }
6✔
2171
        }
8✔
2172
    }
2✔
2173
}
2✔
2174

2175
TEST_CASE("app: roundtrip values", "[sync][pbs][app][baas]") {
2✔
2176
    const std::string valid_pk_name = "_id";
2✔
2177

2178
    Schema schema{
2✔
2179
        {"TopLevel",
2✔
2180
         {
2✔
2181
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2182
             {"decimal", PropertyType::Decimal | PropertyType::Nullable},
2✔
2183
         }},
2✔
2184
    };
2✔
2185

2186
    auto server_app_config = minimal_app_config("roundtrip_values", schema);
2✔
2187
    auto app_session = create_app(server_app_config);
2✔
2188
    auto partition = random_string(100);
2✔
2189

2190
    Decimal128 large_significand = Decimal128(70) / Decimal128(1.09);
2✔
2191
    auto obj_id = ObjectId::gen();
2✔
2192
    {
2✔
2193
        TestAppSession test_session(app_session, {}, DeleteApp{false});
2✔
2194
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2195
        auto realm = Realm::get_shared_realm(config);
2✔
2196

2197
        CppContext c(realm);
2✔
2198
        realm->begin_transaction();
2✔
2199
        Object::create(c, realm, "TopLevel",
2✔
2200
                       util::Any(AnyDict{
2✔
2201
                           {valid_pk_name, obj_id},
2✔
2202
                           {"decimal", large_significand},
2✔
2203
                       }),
2✔
2204
                       CreatePolicy::ForceCreate);
2✔
2205
        realm->commit_transaction();
2✔
2206
        CHECK(!wait_for_upload(*realm, std::chrono::seconds(600)));
2!
2207
    }
2✔
2208

2209
    {
2✔
2210
        TestAppSession test_session(app_session);
2✔
2211
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2212
        auto realm = Realm::get_shared_realm(config);
2✔
2213

2214
        CHECK(!wait_for_download(*realm));
2!
2215
        CppContext c(realm);
2✔
2216
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", util::Any{obj_id});
2✔
2217
        auto val = obj.get_column_value<Decimal128>("decimal");
2✔
2218
        CHECK(val == large_significand);
2!
2219
    }
2✔
2220
}
2✔
2221

2222
TEST_CASE("app: upgrade from local to synced realm", "[sync][pbs][app][upgrade][baas]") {
4✔
2223
    const std::string valid_pk_name = "_id";
4✔
2224

2225
    Schema schema{
4✔
2226
        {"origin",
4✔
2227
         {{valid_pk_name, PropertyType::Int, Property::IsPrimary{true}},
4✔
2228
          {"link", PropertyType::Object | PropertyType::Nullable, "target"},
4✔
2229
          {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"}}},
4✔
2230
        {"target",
4✔
2231
         {{valid_pk_name, PropertyType::String, Property::IsPrimary{true}},
4✔
2232
          {"value", PropertyType::Int},
4✔
2233
          {"name", PropertyType::String}}},
4✔
2234
        {"other_origin",
4✔
2235
         {{valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
2236
          {"array", PropertyType::Array | PropertyType::Object, "other_target"}}},
4✔
2237
        {"other_target",
4✔
2238
         {{valid_pk_name, PropertyType::UUID, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
4✔
2239
        {"embedded", ObjectSchema::ObjectType::Embedded, {{"name", PropertyType::String | PropertyType::Nullable}}},
4✔
2240
    };
4✔
2241

2242
    /*             Create local realm             */
2243
    TestFile local_config;
4✔
2244
    local_config.schema = schema;
4✔
2245
    auto local_realm = Realm::get_shared_realm(local_config);
4✔
2246
    {
4✔
2247
        auto origin = local_realm->read_group().get_table("class_origin");
4✔
2248
        auto target = local_realm->read_group().get_table("class_target");
4✔
2249
        auto other_origin = local_realm->read_group().get_table("class_other_origin");
4✔
2250
        auto other_target = local_realm->read_group().get_table("class_other_target");
4✔
2251

2252
        local_realm->begin_transaction();
4✔
2253
        auto o = target->create_object_with_primary_key("Foo").set("name", "Egon");
4✔
2254
        // 'embedded_link' property is null.
2255
        origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
2256
        // 'embedded_link' property is not null.
2257
        auto obj = origin->create_object_with_primary_key(42);
4✔
2258
        auto col_key = origin->get_column_key("embedded_link");
4✔
2259
        obj.create_and_set_linked_object(col_key);
4✔
2260
        other_target->create_object_with_primary_key(UUID("3b241101-e2bb-4255-8caf-4136c566a961"));
4✔
2261
        other_origin->create_object_with_primary_key(ObjectId::gen());
4✔
2262
        local_realm->commit_transaction();
4✔
2263
    }
4✔
2264

2265
    /* Create a synced realm and upload some data */
2266
    auto server_app_config = minimal_app_config("upgrade_from_local", schema);
4✔
2267
    TestAppSession test_session(create_app(server_app_config));
4✔
2268
    auto partition = random_string(100);
4✔
2269
    auto user1 = test_session.app()->current_user();
4✔
2270
    SyncTestFile config1(user1, partition, schema);
4✔
2271

2272
    auto r1 = Realm::get_shared_realm(config1);
4✔
2273

2274
    auto origin = r1->read_group().get_table("class_origin");
4✔
2275
    auto target = r1->read_group().get_table("class_target");
4✔
2276
    auto other_origin = r1->read_group().get_table("class_other_origin");
4✔
2277
    auto other_target = r1->read_group().get_table("class_other_target");
4✔
2278

2279
    r1->begin_transaction();
4✔
2280
    auto o = target->create_object_with_primary_key("Baa").set("name", "Børge");
4✔
2281
    origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
2282
    other_target->create_object_with_primary_key(UUID("01234567-89ab-cdef-edcb-a98765432101"));
4✔
2283
    other_origin->create_object_with_primary_key(ObjectId::gen());
4✔
2284
    r1->commit_transaction();
4✔
2285
    CHECK(!wait_for_upload(*r1));
4!
2286

2287
    /* Copy local realm data over in a synced one*/
2288
    create_user_and_log_in(test_session.app());
4✔
2289
    auto user2 = test_session.app()->current_user();
4✔
2290
    REQUIRE(user1 != user2);
4!
2291

2292
    SyncTestFile config2(user1, partition, schema);
4✔
2293

2294
    SharedRealm r2;
4✔
2295
    SECTION("Copy before connecting to server") {
4✔
2296
        local_realm->convert(config2);
2✔
2297
        r2 = Realm::get_shared_realm(config2);
2✔
2298
    }
2✔
2299

2300
    SECTION("Open synced realm first") {
4✔
2301
        r2 = Realm::get_shared_realm(config2);
2✔
2302
        CHECK(!wait_for_download(*r2));
2!
2303
        local_realm->convert(config2);
2✔
2304
        CHECK(!wait_for_upload(*r2));
2!
2305
    }
2✔
2306

2307
    CHECK(!wait_for_download(*r2));
4!
2308
    advance_and_notify(*r2);
4✔
2309
    Group& g = r2->read_group();
4✔
2310
    // g.to_json(std::cout);
2311
    REQUIRE(g.get_table("class_origin")->size() == 2);
4!
2312
    REQUIRE(g.get_table("class_target")->size() == 2);
4!
2313
    REQUIRE(g.get_table("class_other_origin")->size() == 2);
4!
2314
    REQUIRE(g.get_table("class_other_target")->size() == 2);
4!
2315

2316
    CHECK(!wait_for_upload(*r2));
4!
2317
    CHECK(!wait_for_download(*r1));
4!
2318
    advance_and_notify(*r1);
4✔
2319
    // r1->read_group().to_json(std::cout);
2320
}
4✔
2321

2322
TEST_CASE("app: set new embedded object", "[sync][pbs][app][baas]") {
2✔
2323
    const std::string valid_pk_name = "_id";
2✔
2324

2325
    Schema schema{
2✔
2326
        {"TopLevel",
2✔
2327
         {
2✔
2328
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2329
             {"array_of_objs", PropertyType::Object | PropertyType::Array, "TopLevel_array_of_objs"},
2✔
2330
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"},
2✔
2331
             {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable,
2✔
2332
              "TopLevel_embedded_dict"},
2✔
2333
         }},
2✔
2334
        {"TopLevel_array_of_objs",
2✔
2335
         ObjectSchema::ObjectType::Embedded,
2✔
2336
         {
2✔
2337
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2338
         }},
2✔
2339
        {"TopLevel_embedded_obj",
2✔
2340
         ObjectSchema::ObjectType::Embedded,
2✔
2341
         {
2✔
2342
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2343
         }},
2✔
2344
        {"TopLevel_embedded_dict",
2✔
2345
         ObjectSchema::ObjectType::Embedded,
2✔
2346
         {
2✔
2347
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2348
         }},
2✔
2349
    };
2✔
2350

2351
    auto server_app_config = minimal_app_config("set_new_embedded_object", schema);
2✔
2352
    TestAppSession test_session(create_app(server_app_config));
2✔
2353
    auto partition = random_string(100);
2✔
2354

2355
    auto array_of_objs_id = ObjectId::gen();
2✔
2356
    auto embedded_obj_id = ObjectId::gen();
2✔
2357
    auto dict_obj_id = ObjectId::gen();
2✔
2358

2359
    {
2✔
2360
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2361
        auto realm = Realm::get_shared_realm(config);
2✔
2362

2363
        CppContext c(realm);
2✔
2364
        realm->begin_transaction();
2✔
2365
        auto array_of_objs =
2✔
2366
            Object::create(c, realm, "TopLevel",
2✔
2367
                           std::any(AnyDict{
2✔
2368
                               {valid_pk_name, array_of_objs_id},
2✔
2369
                               {"array_of_objs", AnyVector{AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}},
2✔
2370
                           }),
2✔
2371
                           CreatePolicy::ForceCreate);
2✔
2372

2373
        auto embedded_obj =
2✔
2374
            Object::create(c, realm, "TopLevel",
2✔
2375
                           std::any(AnyDict{
2✔
2376
                               {valid_pk_name, embedded_obj_id},
2✔
2377
                               {"embedded_obj", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}},
2✔
2378
                           }),
2✔
2379
                           CreatePolicy::ForceCreate);
2✔
2380

2381
        auto dict_obj = Object::create(
2✔
2382
            c, realm, "TopLevel",
2✔
2383
            std::any(AnyDict{
2✔
2384
                {valid_pk_name, dict_obj_id},
2✔
2385
                {"embedded_dict", AnyDict{{"foo", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}},
2✔
2386
            }),
2✔
2387
            CreatePolicy::ForceCreate);
2✔
2388

2389
        realm->commit_transaction();
2✔
2390
        {
2✔
2391
            realm->begin_transaction();
2✔
2392
            embedded_obj.set_property_value(c, "embedded_obj",
2✔
2393
                                            std::any(AnyDict{{
2✔
2394
                                                "array",
2✔
2395
                                                AnyVector{INT64_C(3), INT64_C(4)},
2✔
2396
                                            }}),
2✔
2397
                                            CreatePolicy::UpdateAll);
2✔
2398
            realm->commit_transaction();
2✔
2399
        }
2✔
2400

2401
        {
2✔
2402
            realm->begin_transaction();
2✔
2403
            List array(array_of_objs, array_of_objs.get_object_schema().property_for_name("array_of_objs"));
2✔
2404
            CppContext c2(realm, &array.get_object_schema());
2✔
2405
            array.set(c2, 0, std::any{AnyDict{{"array", AnyVector{INT64_C(5), INT64_C(6)}}}});
2✔
2406
            realm->commit_transaction();
2✔
2407
        }
2✔
2408

2409
        {
2✔
2410
            realm->begin_transaction();
2✔
2411
            object_store::Dictionary dict(dict_obj, dict_obj.get_object_schema().property_for_name("embedded_dict"));
2✔
2412
            CppContext c2(realm, &dict.get_object_schema());
2✔
2413
            dict.insert(c2, "foo", std::any{AnyDict{{"array", AnyVector{INT64_C(7), INT64_C(8)}}}});
2✔
2414
            realm->commit_transaction();
2✔
2415
        }
2✔
2416
        CHECK(!wait_for_upload(*realm));
2!
2417
    }
2✔
2418

2419
    {
2✔
2420
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2421
        auto realm = Realm::get_shared_realm(config);
2✔
2422

2423
        CHECK(!wait_for_download(*realm));
2!
2424
        CppContext c(realm);
2✔
2425
        {
2✔
2426
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{embedded_obj_id});
2✔
2427
            auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
2428
            auto array_list = util::any_cast<List&&>(embedded_obj.get_property_value<std::any>(c, "array"));
2✔
2429
            CHECK(array_list.size() == 2);
2!
2430
            CHECK(array_list.get<int64_t>(0) == int64_t(3));
2!
2431
            CHECK(array_list.get<int64_t>(1) == int64_t(4));
2!
2432
        }
2✔
2433

2434
        {
2✔
2435
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{array_of_objs_id});
2✔
2436
            auto embedded_list = util::any_cast<List&&>(obj.get_property_value<std::any>(c, "array_of_objs"));
2✔
2437
            CppContext c2(realm, &embedded_list.get_object_schema());
2✔
2438
            auto embedded_array_obj = util::any_cast<Object&&>(embedded_list.get(c2, 0));
2✔
2439
            auto array_list = util::any_cast<List&&>(embedded_array_obj.get_property_value<std::any>(c2, "array"));
2✔
2440
            CHECK(array_list.size() == 2);
2!
2441
            CHECK(array_list.get<int64_t>(0) == int64_t(5));
2!
2442
            CHECK(array_list.get<int64_t>(1) == int64_t(6));
2!
2443
        }
2✔
2444

2445
        {
2✔
2446
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{dict_obj_id});
2✔
2447
            object_store::Dictionary dict(obj, obj.get_object_schema().property_for_name("embedded_dict"));
2✔
2448
            CppContext c2(realm, &dict.get_object_schema());
2✔
2449
            auto embedded_obj = util::any_cast<Object&&>(dict.get(c2, "foo"));
2✔
2450
            auto array_list = util::any_cast<List&&>(embedded_obj.get_property_value<std::any>(c2, "array"));
2✔
2451
            CHECK(array_list.size() == 2);
2!
2452
            CHECK(array_list.get<int64_t>(0) == int64_t(7));
2!
2453
            CHECK(array_list.get<int64_t>(1) == int64_t(8));
2!
2454
        }
2✔
2455
    }
2✔
2456
}
2✔
2457

2458
TEST_CASE("app: make distributable client file", "[sync][pbs][app][baas]") {
2✔
2459
    TestAppSession session;
2✔
2460
    auto app = session.app();
2✔
2461

2462
    auto schema = get_default_schema();
2✔
2463
    SyncTestFile original_config(app->current_user(), bson::Bson("foo"), schema);
2✔
2464
    create_user_and_log_in(app);
2✔
2465
    SyncTestFile target_config(app->current_user(), bson::Bson("foo"), schema);
2✔
2466

2467
    // Create realm file without client file id
2468
    {
2✔
2469
        auto realm = Realm::get_shared_realm(original_config);
2✔
2470

2471
        // Write some data
2472
        realm->begin_transaction();
2✔
2473
        CppContext c;
2✔
2474
        Object::create(c, realm, "Person",
2✔
2475
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2476
                                               {"age", INT64_C(64)},
2✔
2477
                                               {"firstName", std::string("Paul")},
2✔
2478
                                               {"lastName", std::string("McCartney")}}));
2✔
2479
        realm->commit_transaction();
2✔
2480
        wait_for_upload(*realm);
2✔
2481
        wait_for_download(*realm);
2✔
2482

2483
        realm->convert(target_config);
2✔
2484

2485
        // Write some additional data
2486
        realm->begin_transaction();
2✔
2487
        Object::create(c, realm, "Dog",
2✔
2488
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2489
                                               {"breed", std::string("stabyhoun")},
2✔
2490
                                               {"name", std::string("albert")},
2✔
2491
                                               {"realm_id", std::string("foo")}}));
2✔
2492
        realm->commit_transaction();
2✔
2493
        wait_for_upload(*realm);
2✔
2494
    }
2✔
2495
    // Starting a new session based on the copy
2496
    {
2✔
2497
        auto realm = Realm::get_shared_realm(target_config);
2✔
2498
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2499
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 0);
2!
2500

2501
        // Should be able to download the object created in the source Realm
2502
        // after writing the copy
2503
        wait_for_download(*realm);
2✔
2504
        realm->refresh();
2✔
2505
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2506
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1);
2!
2507

2508
        // Check that we can continue committing to this realm
2509
        realm->begin_transaction();
2✔
2510
        CppContext c;
2✔
2511
        Object::create(c, realm, "Dog",
2✔
2512
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2513
                                               {"breed", std::string("bulldog")},
2✔
2514
                                               {"name", std::string("fido")},
2✔
2515
                                               {"realm_id", std::string("foo")}}));
2✔
2516
        realm->commit_transaction();
2✔
2517
        wait_for_upload(*realm);
2✔
2518
    }
2✔
2519
    // Original Realm should be able to read the object which was written to the copy
2520
    {
2✔
2521
        auto realm = Realm::get_shared_realm(original_config);
2✔
2522
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2523
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1);
2!
2524

2525
        wait_for_download(*realm);
2✔
2526
        realm->refresh();
2✔
2527
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2528
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 2);
2!
2529
    }
2✔
2530
}
2✔
2531

2532
TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
38✔
2533
    auto logger = util::Logger::get_default_logger();
38✔
2534

2535
    const auto schema = get_default_schema();
38✔
2536

2537
    auto get_dogs = [](SharedRealm r) -> Results {
44✔
2538
        wait_for_upload(*r, std::chrono::seconds(10));
44✔
2539
        wait_for_download(*r, std::chrono::seconds(10));
44✔
2540
        return Results(r, r->read_group().get_table("class_Dog"));
44✔
2541
    };
44✔
2542

2543
    auto create_one_dog = [](SharedRealm r) {
38✔
2544
        r->begin_transaction();
16✔
2545
        CppContext c;
16✔
2546
        Object::create(c, r, "Dog",
16✔
2547
                       std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
16✔
2548
                                        {"breed", std::string("bulldog")},
16✔
2549
                                        {"name", std::string("fido")}}),
16✔
2550
                       CreatePolicy::ForceCreate);
16✔
2551
        r->commit_transaction();
16✔
2552
    };
16✔
2553

2554
    TestAppSession session;
38✔
2555
    auto app = session.app();
38✔
2556
    const auto partition = random_string(100);
38✔
2557

2558
    SECTION("Add Objects") {
38✔
2559
        {
2✔
2560
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2561
            auto r = Realm::get_shared_realm(config);
2✔
2562

2563
            REQUIRE(get_dogs(r).size() == 0);
2!
2564
            create_one_dog(r);
2✔
2565
            REQUIRE(get_dogs(r).size() == 1);
2!
2566
        }
2✔
2567

2568
        {
2✔
2569
            create_user_and_log_in(app);
2✔
2570
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2571
            auto r = Realm::get_shared_realm(config);
2✔
2572
            Results dogs = get_dogs(r);
2✔
2573
            REQUIRE(dogs.size() == 1);
2!
2574
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2575
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2576
        }
2✔
2577
    }
2✔
2578

2579
    SECTION("MemOnly durability") {
38✔
2580
        {
2✔
2581
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2582
            config.in_memory = true;
2✔
2583
            config.encryption_key = std::vector<char>();
2✔
2584

2585
            REQUIRE(config.options().durability == DBOptions::Durability::MemOnly);
2!
2586
            auto r = Realm::get_shared_realm(config);
2✔
2587

2588
            REQUIRE(get_dogs(r).size() == 0);
2!
2589
            create_one_dog(r);
2✔
2590
            REQUIRE(get_dogs(r).size() == 1);
2!
2591
        }
2✔
2592

2593
        {
2✔
2594
            create_user_and_log_in(app);
2✔
2595
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2596
            config.in_memory = true;
2✔
2597
            config.encryption_key = std::vector<char>();
2✔
2598
            auto r = Realm::get_shared_realm(config);
2✔
2599
            Results dogs = get_dogs(r);
2✔
2600
            REQUIRE(dogs.size() == 1);
2!
2601
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2602
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2603
        }
2✔
2604
    }
2✔
2605

2606
    SECTION("Fast clock on client") {
38✔
2607
        {
2✔
2608
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2609
            auto r = Realm::get_shared_realm(config);
2✔
2610

2611
            REQUIRE(get_dogs(r).size() == 0);
2!
2612
            create_one_dog(r);
2✔
2613
            REQUIRE(get_dogs(r).size() == 1);
2!
2614
        }
2✔
2615

2616
        auto transport = std::make_shared<HookedTransport<>>();
2✔
2617
        TestAppSession hooked_session(session.app_session(), {transport}, DeleteApp{false});
2✔
2618
        auto app = hooked_session.app();
2✔
2619
        std::shared_ptr<User> user = app->current_user();
2✔
2620
        REQUIRE(user);
2!
2621
        REQUIRE(!user->access_token_refresh_required());
2!
2622
        // Make the User behave as if the client clock is 31 minutes fast, so the token looks expired locally
2623
        // (access tokens have an lifetime of 30 minutes today).
2624
        user->set_seconds_to_adjust_time_for_testing(31 * 60);
2✔
2625
        REQUIRE(user->access_token_refresh_required());
2!
2626

2627
        // This assumes that we make an http request for the new token while
2628
        // already in the WaitingForAccessToken state.
2629
        bool seen_waiting_for_access_token = false;
2✔
2630
        transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
2631
            auto user = app->current_user();
2✔
2632
            REQUIRE(user);
2!
2633
            for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) {
2✔
2634
                // Prior to the fix for #4941, this callback would be called from an infinite loop, always in the
2635
                // WaitingForAccessToken state.
2636
                if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2637
                    REQUIRE(!seen_waiting_for_access_token);
2!
2638
                    seen_waiting_for_access_token = true;
2✔
2639
                }
2✔
2640
            }
2✔
2641
            return std::nullopt;
2✔
2642
        };
2✔
2643
        SyncTestFile config(user, partition, schema);
2✔
2644
        auto r = Realm::get_shared_realm(config);
2✔
2645
        REQUIRE(seen_waiting_for_access_token);
2!
2646
        Results dogs = get_dogs(r);
2✔
2647
        REQUIRE(dogs.size() == 1);
2!
2648
        REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2649
        REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2650
    }
2✔
2651

2652
    SECTION("Expired Tokens") {
38✔
2653
        sync::AccessToken token;
8✔
2654
        {
8✔
2655
            std::shared_ptr<User> user = app->current_user();
8✔
2656
            SyncTestFile config(user, partition, schema);
8✔
2657
            auto r = Realm::get_shared_realm(config);
8✔
2658

2659
            REQUIRE(get_dogs(r).size() == 0);
8!
2660
            create_one_dog(r);
8✔
2661

2662
            REQUIRE(get_dogs(r).size() == 1);
8!
2663
            sync::AccessToken::ParseError error_state = realm::sync::AccessToken::ParseError::none;
8✔
2664
            sync::AccessToken::parse(user->access_token(), token, error_state, nullptr);
8✔
2665
            REQUIRE(error_state == sync::AccessToken::ParseError::none);
8!
2666
            REQUIRE(token.timestamp);
8!
2667
            REQUIRE(token.expires);
8!
2668
            REQUIRE(token.timestamp < token.expires);
8!
2669
            std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
8✔
2670
            token.expires = std::chrono::system_clock::to_time_t(now - 30s);
8✔
2671
            REQUIRE(token.expired(now));
8!
2672
        }
8✔
2673

2674
        auto transport = std::make_shared<HookedTransport<>>();
8✔
2675
        TestAppSession hooked_session(session.app_session(), {transport}, DeleteApp{false});
8✔
2676
        auto app = hooked_session.app();
8✔
2677
        std::shared_ptr<User> user = app->current_user();
8✔
2678
        REQUIRE(user);
8!
2679
        REQUIRE(!user->access_token_refresh_required());
8!
2680
        // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client.
2681
        user->update_data_for_testing([&token](UserData& data) {
8✔
2682
            data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", token.expires, token.timestamp));
8✔
2683
        });
8✔
2684
        REQUIRE(user->access_token_refresh_required());
8!
2685

2686
        SECTION("Expired Access Token is Refreshed") {
8✔
2687
            // This assumes that we make an http request for the new token while
2688
            // already in the WaitingForAccessToken state.
2689
            bool seen_waiting_for_access_token = false;
2✔
2690
            transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
2691
                auto user = app->current_user();
2✔
2692
                REQUIRE(user);
2!
2693
                for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) {
2✔
2694
                    if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2695
                        REQUIRE(!seen_waiting_for_access_token);
2!
2696
                        seen_waiting_for_access_token = true;
2✔
2697
                    }
2✔
2698
                }
2✔
2699
                return std::nullopt;
2✔
2700
            };
2✔
2701
            SyncTestFile config(user, partition, schema);
2✔
2702
            auto r = Realm::get_shared_realm(config);
2✔
2703
            REQUIRE(seen_waiting_for_access_token);
2!
2704
            Results dogs = get_dogs(r);
2✔
2705
            REQUIRE(dogs.size() == 1);
2!
2706
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2707
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2708
        }
2✔
2709

2710
        SECTION("User is logged out if the refresh request is denied") {
8✔
2711
            REQUIRE(user->is_logged_in());
2!
2712
            size_t hook_count = 0;
2✔
2713
            transport->response_hook = [&](const Request& request, Response& response) {
2✔
2714
                auto user = app->current_user();
2✔
2715
                if (hook_count++ == 0) {
2✔
2716
                    // the initial request should have a current user and log it out
2717
                    REQUIRE(user);
2!
2718
                    REQUIRE(user->is_logged_in());
2!
2719
                }
2✔
2720
                else {
×
2721
                    INFO(request.url);
×
2722
                    REQUIRE(!user);
×
2723
                }
×
2724
                // simulate the server denying the refresh
2725
                if (request.url.find("/session") != std::string::npos) {
2✔
2726
                    response.http_status_code = 401;
2✔
2727
                    response.body = "fake: refresh token could not be refreshed";
2✔
2728
                }
2✔
2729
            };
2✔
2730
            SyncTestFile config(user, partition, schema);
2✔
2731
            std::atomic<bool> sync_error_handler_called{false};
2✔
2732
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2733
                sync_error_handler_called.store(true);
2✔
2734
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
2!
2735
                REQUIRE_THAT(std::string{error.status.reason()},
2✔
2736
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
2✔
2737
            };
2✔
2738
            auto r = Realm::get_shared_realm(config);
2✔
2739
            timed_wait_for([&] {
3✔
2740
                return sync_error_handler_called.load();
3✔
2741
            });
3✔
2742
            // the failed refresh logs out the user
2743
            REQUIRE(!user->is_logged_in());
2!
2744
        }
2✔
2745

2746
        SECTION("User is left logged out if logged out while the refresh is in progress") {
8✔
2747
            REQUIRE(user->is_logged_in());
2!
2748
            transport->request_hook = [&](const Request&) -> std::optional<Response> {
4✔
2749
                user->log_out();
4✔
2750
                return std::nullopt;
4✔
2751
            };
4✔
2752
            SyncTestFile config(user, partition, schema);
2✔
2753
            std::atomic<bool> sync_error_handler_called{false};
2✔
2754
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2755
                sync_error_handler_called.store(true);
2✔
2756
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
2!
2757
                REQUIRE_THAT(std::string{error.status.reason()},
2✔
2758
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
2✔
2759
            };
2✔
2760
            auto r = Realm::get_shared_realm(config);
2✔
2761
            timed_wait_for([&] {
3✔
2762
                return sync_error_handler_called.load();
3✔
2763
            });
3✔
2764
            REQUIRE_FALSE(user->is_logged_in());
2!
2765
            REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
2766
        }
2✔
2767

2768
        SECTION("Requests that receive an error are retried on a backoff") {
8✔
2769
            using namespace std::chrono;
2✔
2770
            std::vector<time_point<steady_clock>> response_times;
2✔
2771
            std::atomic<bool> did_receive_valid_token{false};
2✔
2772
            constexpr size_t num_error_responses = 6;
2✔
2773

2774
            transport->response_hook = [&](const Request& request, Response& response) {
12✔
2775
                // simulate the server experiencing an internal server error
2776
                if (request.url.find("/session") != std::string::npos) {
12✔
2777
                    if (response_times.size() >= num_error_responses) {
12✔
2778
                        did_receive_valid_token.store(true);
2✔
2779
                        return;
2✔
2780
                    }
2✔
2781
                    response.http_status_code = 500;
10✔
2782
                }
10✔
2783
            };
12✔
2784
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
12✔
2785
                if (!did_receive_valid_token.load() && request.url.find("/session") != std::string::npos) {
12✔
2786
                    response_times.push_back(steady_clock::now());
12✔
2787
                }
12✔
2788
                return std::nullopt;
12✔
2789
            };
12✔
2790
            SyncTestFile config(user, partition, schema);
2✔
2791
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2792
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
×
2793
                REQUIRE_THAT(std::string{error.status.reason()},
×
2794
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
×
2795
            };
×
2796
            auto r = Realm::get_shared_realm(config);
2✔
2797
            create_one_dog(r);
2✔
2798
            timed_wait_for(
2✔
2799
                [&] {
38,986,011✔
2800
                    return did_receive_valid_token.load();
38,986,011✔
2801
                },
38,986,011✔
2802
                30s);
2✔
2803
            REQUIRE(user->is_logged_in());
2!
2804
            REQUIRE(response_times.size() >= num_error_responses);
2!
2805
            std::vector<uint64_t> delay_times;
2✔
2806
            for (size_t i = 1; i < response_times.size(); ++i) {
12✔
2807
                delay_times.push_back(duration_cast<milliseconds>(response_times[i] - response_times[i - 1]).count());
10✔
2808
            }
10✔
2809

2810
            // sync delays start at 1000ms minus a random number of up to 25%.
2811
            // the subsequent delay is double the previous one minus a random 25% again.
2812
            // this calculation happens in Connection::initiate_reconnect_wait()
2813
            bool increasing_delay = true;
2✔
2814
            for (size_t i = 1; i < delay_times.size(); ++i) {
10✔
2815
                if (delay_times[i - 1] >= delay_times[i]) {
8✔
2816
                    increasing_delay = false;
×
2817
                }
×
2818
            }
8✔
2819
            // fail if the first delay isn't longer than half a second
2820
            if (delay_times.size() <= 1 || delay_times[1] < 500) {
2✔
2821
                increasing_delay = false;
×
2822
            }
×
2823
            if (!increasing_delay) {
2✔
2824
                std::cerr << "delay times are not increasing: ";
×
2825
                for (auto& delay : delay_times) {
×
2826
                    std::cerr << delay << ", ";
×
2827
                }
×
2828
                std::cerr << std::endl;
×
2829
            }
×
2830
            REQUIRE(increasing_delay);
2!
2831
        }
2✔
2832
    }
8✔
2833

2834
    SECTION("Invalid refresh token") {
38✔
2835
        auto& app_session = session.app_session();
8✔
2836
        std::mutex mtx;
8✔
2837
        auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr<User> user, Realm::Config config) {
8✔
2838
            REQUIRE(user);
6!
2839
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
6!
2840

2841
            // requesting a new access token fails because the refresh token used for this request is revoked
2842
            user->refresh_custom_data([&](Optional<AppError> error) {
6✔
2843
                REQUIRE(error);
6!
2844
                REQUIRE(error->additional_status_code == 401);
6!
2845
                REQUIRE(error->code() == ErrorCodes::InvalidSession);
6!
2846
            });
6✔
2847

2848
            // Set a bad access token. This will force a request for a new access token when the sync session opens
2849
            // this is only necessary because the server doesn't actually revoke previously issued access tokens
2850
            // instead allowing their session to time out as normal. So this simulates the access token expiring.
2851
            // see:
2852
            // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386
2853
            user->update_data_for_testing([](UserData& data) {
6✔
2854
                data.access_token = RealmJWT(encode_fake_jwt("fake_access_token"));
6✔
2855
            });
6✔
2856
            REQUIRE(!app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
6!
2857

2858
            auto [sync_error_promise, sync_error] = util::make_promise_future<SyncError>();
6✔
2859
            config.sync_config->error_handler =
6✔
2860
                [promise = util::CopyablePromiseHolder(std::move(sync_error_promise))](std::shared_ptr<SyncSession>,
6✔
2861
                                                                                       SyncError error) mutable {
6✔
2862
                    promise.get_promise().emplace_value(std::move(error));
6✔
2863
                };
6✔
2864

2865
            auto transport = static_cast<SynchronousTestTransport*>(session.transport());
6✔
2866
            transport->block(); // don't let the token refresh happen until we're ready for it
6✔
2867
            auto r = Realm::get_shared_realm(config);
6✔
2868
            auto session = app->sync_manager()->get_existing_session(config.path);
6✔
2869
            REQUIRE(user->is_logged_in());
6!
2870
            REQUIRE(!sync_error.is_ready());
6!
2871
            {
6✔
2872
                std::atomic<bool> called{false};
6✔
2873
                session->wait_for_upload_completion([&](Status stat) {
6✔
2874
                    std::lock_guard lock(mtx);
6✔
2875
                    called.store(true);
6✔
2876
                    REQUIRE(stat.code() == ErrorCodes::InvalidSession);
6!
2877
                });
6✔
2878
                transport->unblock();
6✔
2879
                timed_wait_for([&] {
28,821✔
2880
                    return called.load();
28,821✔
2881
                });
28,821✔
2882
                std::lock_guard lock(mtx);
6✔
2883
                REQUIRE(called);
6!
2884
            }
6✔
2885

2886
            auto sync_error_res = wait_for_future(std::move(sync_error)).get();
6✔
2887
            REQUIRE(sync_error_res.status == ErrorCodes::AuthError);
6!
2888
            REQUIRE_THAT(std::string{sync_error_res.status.reason()},
6✔
2889
                         Catch::Matchers::StartsWith("Unable to refresh the user access token"));
6✔
2890

2891
            // the failed refresh logs out the user
2892
            std::lock_guard lock(mtx);
6✔
2893
            REQUIRE(!user->is_logged_in());
6!
2894
        };
6✔
2895

2896
        SECTION("Disabled user results in a sync error") {
8✔
2897
            auto creds = create_user_and_log_in(app);
2✔
2898
            auto user = app->current_user();
2✔
2899
            REQUIRE(user);
2!
2900
            SyncTestFile config(user, partition, schema);
2✔
2901
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
2902
            app_session.admin_api.disable_user_sessions(app->current_user()->user_id(), app_session.server_app_id);
2✔
2903

2904
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2905

2906
            // logging in again doesn't fix things while the account is disabled
2907
            auto error = failed_log_in(app, creds);
2✔
2908
            REQUIRE(error.code() == ErrorCodes::UserDisabled);
2!
2909

2910
            // admin enables user sessions again which should allow the session to continue
2911
            app_session.admin_api.enable_user_sessions(user->user_id(), app_session.server_app_id);
2✔
2912

2913
            // logging in now works properly
2914
            log_in(app, creds);
2✔
2915

2916
            // still referencing the same user
2917
            REQUIRE(user == app->current_user());
2!
2918
            REQUIRE(user->is_logged_in());
2!
2919

2920
            {
2✔
2921
                // check that there are no errors initiating a session now by making sure upload/download succeeds
2922
                auto r = Realm::get_shared_realm(config);
2✔
2923
                Results dogs = get_dogs(r);
2✔
2924
            }
2✔
2925
        }
2✔
2926

2927
        SECTION("Revoked refresh token results in a sync error") {
8✔
2928
            auto creds = create_user_and_log_in(app);
2✔
2929
            auto user = app->current_user();
2✔
2930
            SyncTestFile config(user, partition, schema);
2✔
2931
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
2932
            app_session.admin_api.revoke_user_sessions(user->user_id(), app_session.server_app_id);
2✔
2933
            // revoking a user session only affects the refresh token, so the access token should still continue to
2934
            // work.
2935
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
2936

2937
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2938

2939
            // logging in again succeeds and generates a new and valid refresh token
2940
            log_in(app, creds);
2✔
2941

2942
            // still referencing the same user and now the user is logged in
2943
            REQUIRE(user == app->current_user());
2!
2944
            REQUIRE(user->is_logged_in());
2!
2945

2946
            // new requests for an access token succeed again
2947
            user->refresh_custom_data([&](Optional<AppError> error) {
2✔
2948
                REQUIRE_FALSE(error);
2!
2949
            });
2✔
2950

2951
            {
2✔
2952
                // check that there are no errors initiating a new sync session by making sure upload/download
2953
                // succeeds
2954
                auto r = Realm::get_shared_realm(config);
2✔
2955
                Results dogs = get_dogs(r);
2✔
2956
            }
2✔
2957
        }
2✔
2958

2959
        SECTION("Revoked refresh token on an anonymous user results in a sync error") {
8✔
2960
            app->current_user()->log_out();
2✔
2961
            auto anon_user = log_in(app);
2✔
2962
            REQUIRE(app->current_user() == anon_user);
2!
2963
            SyncTestFile config(anon_user, partition, schema);
2✔
2964
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
2965
            app_session.admin_api.revoke_user_sessions(anon_user->user_id(), app_session.server_app_id);
2✔
2966
            // revoking a user session only affects the refresh token, so the access token should still continue to
2967
            // work.
2968
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
2969

2970
            verify_error_on_sync_with_invalid_refresh_token(anon_user, config);
2✔
2971

2972
            // the user has been logged out, and current user is reset
2973
            REQUIRE(!app->current_user());
2!
2974
            REQUIRE(!anon_user->is_logged_in());
2!
2975
            REQUIRE(anon_user->state() == SyncUser::State::Removed);
2!
2976

2977
            // new requests for an access token do not work for anon users
2978
            anon_user->refresh_custom_data([&](Optional<AppError> error) {
2✔
2979
                REQUIRE(error);
2!
2980
                REQUIRE(error->reason() ==
2!
2981
                        util::format("Cannot initiate a refresh on user '%1' because the user has been removed",
2✔
2982
                                     anon_user->user_id()));
2✔
2983
            });
2✔
2984

2985
            REQUIRE_EXCEPTION(
2✔
2986
                Realm::get_shared_realm(config), ClientUserNotFound,
2✔
2987
                util::format("Cannot start a sync session for user '%1' because this user has been removed.",
2✔
2988
                             anon_user->user_id()));
2✔
2989
        }
2✔
2990

2991
        SECTION("Opening a Realm with a removed email user results produces an exception") {
8✔
2992
            auto creds = create_user_and_log_in(app);
2✔
2993
            auto email_user = app->current_user();
2✔
2994
            const std::string user_ident = email_user->user_id();
2✔
2995
            REQUIRE(email_user);
2!
2996
            SyncTestFile config(email_user, partition, schema);
2✔
2997
            REQUIRE(email_user->is_logged_in());
2!
2998
            {
2✔
2999
                // sync works on a valid user
3000
                auto r = Realm::get_shared_realm(config);
2✔
3001
                Results dogs = get_dogs(r);
2✔
3002
            }
2✔
3003
            app->remove_user(email_user, [](util::Optional<AppError> err) {
2✔
3004
                REQUIRE(!err);
2!
3005
            });
2✔
3006
            REQUIRE_FALSE(email_user->is_logged_in());
2!
3007
            REQUIRE(email_user->state() == SyncUser::State::Removed);
2!
3008

3009
            // should not be able to open a synced Realm with an invalid user
3010
            REQUIRE_EXCEPTION(
2✔
3011
                Realm::get_shared_realm(config), ClientUserNotFound,
2✔
3012
                util::format("Cannot start a sync session for user '%1' because this user has been removed.",
2✔
3013
                             user_ident));
2✔
3014

3015
            std::shared_ptr<User> new_user_instance = log_in(app, creds);
2✔
3016
            // the previous instance is still invalid
3017
            REQUIRE_FALSE(email_user->is_logged_in());
2!
3018
            REQUIRE(email_user->state() == SyncUser::State::Removed);
2!
3019
            // but the new instance will work and has the same server issued ident
3020
            REQUIRE(new_user_instance);
2!
3021
            REQUIRE(new_user_instance->is_logged_in());
2!
3022
            REQUIRE(new_user_instance->user_id() == user_ident);
2!
3023
            {
2✔
3024
                // sync works again if the same user is logged back in
3025
                config.sync_config->user = new_user_instance;
2✔
3026
                auto r = Realm::get_shared_realm(config);
2✔
3027
                Results dogs = get_dogs(r);
2✔
3028
            }
2✔
3029
        }
2✔
3030
    }
8✔
3031

3032
    SECTION("large write transactions which would be too large if batched") {
38✔
3033
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3034

3035
        std::mutex mutex;
2✔
3036
        bool done = false;
2✔
3037
        auto r = Realm::get_shared_realm(config);
2✔
3038
        r->sync_session()->pause();
2✔
3039

3040
        // Create 26 MB worth of dogs in 26 transactions, which should work but
3041
        // will result in an error from the server if the changesets are batched
3042
        // for upload.
3043
        CppContext c;
2✔
3044
        for (auto i = 'a'; i < 'z'; ++i) {
52✔
3045
            r->begin_transaction();
50✔
3046
            Object::create(c, r, "Dog",
50✔
3047
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
50✔
3048
                                            {"breed", std::string("bulldog")},
50✔
3049
                                            {"name", random_string(1024 * 1024)}}),
50✔
3050
                           CreatePolicy::ForceCreate);
50✔
3051
            r->commit_transaction();
50✔
3052
        }
50✔
3053
        r->sync_session()->wait_for_upload_completion([&](Status status) {
2✔
3054
            std::lock_guard lk(mutex);
2✔
3055
            REQUIRE(status.is_ok());
2!
3056
            done = true;
2✔
3057
        });
2✔
3058
        r->sync_session()->resume();
2✔
3059

3060
        // If we haven't gotten an error in more than 5 minutes, then something has gone wrong
3061
        // and we should fail the test.
3062
        timed_wait_for(
2✔
3063
            [&] {
6,270,711✔
3064
                std::lock_guard lk(mutex);
6,270,711✔
3065
                return done;
6,270,711✔
3066
            },
6,270,711✔
3067
            std::chrono::minutes(5));
2✔
3068
    }
2✔
3069

3070
    SECTION("too large sync message error handling") {
38✔
3071
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3072

3073
        auto pf = util::make_promise_future<SyncError>();
2✔
3074
        config.sync_config->error_handler =
2✔
3075
            [sp = util::CopyablePromiseHolder(std::move(pf.promise))](auto, SyncError error) mutable {
2✔
3076
                sp.get_promise().emplace_value(std::move(error));
2✔
3077
            };
2✔
3078
        auto r = Realm::get_shared_realm(config);
2✔
3079

3080
        // Create 26 MB worth of dogs in a single transaction - this should all get put into one changeset
3081
        // and get uploaded at once, which for now is an error on the server.
3082
        r->begin_transaction();
2✔
3083
        CppContext c;
2✔
3084
        for (auto i = 'a'; i < 'z'; ++i) {
52✔
3085
            Object::create(c, r, "Dog",
50✔
3086
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
50✔
3087
                                            {"breed", std::string("bulldog")},
50✔
3088
                                            {"name", random_string(1024 * 1024)}}),
50✔
3089
                           CreatePolicy::ForceCreate);
50✔
3090
        }
50✔
3091
        r->commit_transaction();
2✔
3092

3093
#if defined(TEST_TIMEOUT_EXTRA) && TEST_TIMEOUT_EXTRA > 0
3094
        // It may take 30 minutes to transfer 16MB at 10KB/s
3095
        auto delay = std::chrono::minutes(35);
3096
#else
3097
        auto delay = std::chrono::minutes(5);
2✔
3098
#endif
2✔
3099

3100
        auto error = wait_for_future(std::move(pf.future), delay).get();
2✔
3101
        REQUIRE(error.status == ErrorCodes::LimitExceeded);
2!
3102
        REQUIRE(error.status.reason() ==
2!
3103
                "Sync websocket closed because the server received a message that was too large: "
2✔
3104
                "read limited at 16777217 bytes");
2✔
3105
        REQUIRE(error.is_client_reset_requested());
2!
3106
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
2!
3107
    }
2✔
3108

3109
    SECTION("freezing realm does not resume session") {
38✔
3110
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3111
        auto realm = Realm::get_shared_realm(config);
2✔
3112
        wait_for_download(*realm);
2✔
3113

3114
        auto state = realm->sync_session()->state();
2✔
3115
        REQUIRE(state == SyncSession::State::Active);
2!
3116

3117
        realm->sync_session()->pause();
2✔
3118
        state = realm->sync_session()->state();
2✔
3119
        REQUIRE(state == SyncSession::State::Paused);
2!
3120

3121
        realm->read_group();
2✔
3122

3123
        {
2✔
3124
            auto frozen = realm->freeze();
2✔
3125
            REQUIRE(realm->sync_session() == realm->sync_session());
2!
3126
            REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused);
2!
3127
        }
2✔
3128

3129
        {
2✔
3130
            auto frozen = Realm::get_frozen_realm(config, realm->read_transaction_version());
2✔
3131
            REQUIRE(realm->sync_session() == realm->sync_session());
2!
3132
            REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused);
2!
3133
        }
2✔
3134
    }
2✔
3135

3136
    SECTION("pausing a session does not hold the DB open") {
38✔
3137
        auto logger = util::Logger::get_default_logger();
2✔
3138
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3139
        DBRef dbref;
2✔
3140
        std::shared_ptr<SyncSession> sync_sess_ext_ref;
2✔
3141
        {
2✔
3142
            auto realm = Realm::get_shared_realm(config);
2✔
3143
            wait_for_download(*realm);
2✔
3144

3145
            auto state = realm->sync_session()->state();
2✔
3146
            REQUIRE(state == SyncSession::State::Active);
2!
3147

3148
            sync_sess_ext_ref = realm->sync_session()->external_reference();
2✔
3149
            dbref = TestHelper::get_db(*realm);
2✔
3150
            // An active PBS realm should have one ref each for:
3151
            // - RealmCoordinator
3152
            // - SyncSession
3153
            // - MigrationStore
3154
            // - SessionWrapper
3155
            // - local dbref
3156
            logger->trace("DBRef ACTIVE use count: %1", dbref.use_count());
2✔
3157
            REQUIRE(dbref.use_count() >= 5);
2!
3158

3159
            realm->sync_session()->pause();
2✔
3160
            state = realm->sync_session()->state();
2✔
3161
            REQUIRE(state == SyncSession::State::Paused);
2!
3162
            logger->trace("DBRef PAUSING called use count: %1", dbref.use_count());
2✔
3163
        }
2✔
3164

3165
        // Closing the realm should leave one ref each for:
3166
        // - SyncSession
3167
        // - MigrationStore
3168
        // - local dbref
3169
        REQUIRE_THAT(
2✔
3170
            [&] {
2✔
3171
                logger->trace("DBRef PAUSED use count: %1", dbref.use_count());
2✔
3172
                return dbref.use_count() < 4;
2✔
3173
            },
2✔
3174
            ReturnsTrueWithinTimeLimit{});
2✔
3175

3176
        // Releasing the external reference should leave one ref for:
3177
        // - local dbref
3178
        sync_sess_ext_ref.reset();
2✔
3179
        REQUIRE_THAT(
2✔
3180
            [&] {
2✔
3181
                logger->trace("DBRef TEARDOWN use count: %1", dbref.use_count());
2✔
3182
                return dbref.use_count() == 1;
2✔
3183
            },
2✔
3184
            ReturnsTrueWithinTimeLimit{});
2✔
3185
    }
2✔
3186

3187
    SECTION("validation") {
38✔
3188
        SyncTestFile config(app->current_user(), partition, schema);
6✔
3189

3190
        SECTION("invalid partition error handling") {
6✔
3191
            config.sync_config->partition_value = "not a bson serialized string";
2✔
3192
            std::atomic<bool> error_did_occur = false;
2✔
3193
            config.sync_config->error_handler = [&error_did_occur](std::shared_ptr<SyncSession>, SyncError error) {
2✔
3194
                CHECK(error.status.reason().find(
2!
3195
                          "Illegal Realm path (BIND): serialized partition 'not a bson serialized "
2✔
3196
                          "string' is invalid") != std::string::npos);
2✔
3197
                error_did_occur.store(true);
2✔
3198
            };
2✔
3199
            auto r = Realm::get_shared_realm(config);
2✔
3200
            auto session = app->sync_manager()->get_existing_session(r->config().path);
2✔
3201
            timed_wait_for([&] {
14,322✔
3202
                return error_did_occur.load();
14,322✔
3203
            });
14,322✔
3204
            REQUIRE(error_did_occur.load());
2!
3205
        }
2✔
3206

3207
        SECTION("invalid pk schema error handling") {
6✔
3208
            const std::string invalid_pk_name = "my_primary_key";
2✔
3209
            auto it = config.schema->find("Dog");
2✔
3210
            REQUIRE(it != config.schema->end());
2!
3211
            REQUIRE(it->primary_key_property());
2!
3212
            REQUIRE(it->primary_key_property()->name == "_id");
2!
3213
            it->primary_key_property()->name = invalid_pk_name;
2✔
3214
            it->primary_key = invalid_pk_name;
2✔
3215
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3216
                                      "The primary key property on a synchronized Realm must be named '_id' but "
2✔
3217
                                      "found 'my_primary_key' for type 'Dog'");
2✔
3218
        }
2✔
3219

3220
        SECTION("missing pk schema error handling") {
6✔
3221
            auto it = config.schema->find("Dog");
2✔
3222
            REQUIRE(it != config.schema->end());
2!
3223
            REQUIRE(it->primary_key_property());
2!
3224
            it->primary_key_property()->is_primary = false;
2✔
3225
            it->primary_key = "";
2✔
3226
            REQUIRE(!it->primary_key_property());
2!
3227
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3228
                                      "There must be a primary key property named '_id' on a synchronized "
2✔
3229
                                      "Realm but none was found for type 'Dog'");
2✔
3230
        }
2✔
3231
    }
6✔
3232

3233
    SECTION("get_file_ident") {
38✔
3234
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3235
        config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
3236
        auto r = Realm::get_shared_realm(config);
2✔
3237
        wait_for_download(*r);
2✔
3238

3239
        auto first_ident = r->sync_session()->get_file_ident();
2✔
3240
        REQUIRE(first_ident.ident != 0);
2!
3241
        REQUIRE(first_ident.salt != 0);
2!
3242

3243
        reset_utils::trigger_client_reset(session.app_session(), r);
2✔
3244
        r->sync_session()->restart_session();
2✔
3245
        wait_for_download(*r);
2✔
3246

3247
        REQUIRE(first_ident.ident != r->sync_session()->get_file_ident().ident);
2!
3248
        REQUIRE(first_ident.salt != r->sync_session()->get_file_ident().salt);
2!
3249
    }
2✔
3250
}
38✔
3251

3252
TEST_CASE("app: network transport handles redirection", "[sync][app][baas]") {
4✔
3253
    auto logger = util::Logger::get_default_logger();
4✔
3254
    auto redirector = sync::RedirectingHttpServer(get_real_base_url(), logger);
4✔
3255

3256
    std::mutex counter_mutex;
4✔
3257
    int error_count = 0;
4✔
3258
    int location_count = 0;
4✔
3259
    int redirect_count = 0;
4✔
3260
    int wsredirect_count = 0;
4✔
3261
    using RedirectEvent = sync::RedirectingHttpServer::Event;
4✔
3262
    redirector.set_event_hook([&](RedirectEvent event, std::optional<std::string> message) {
22✔
3263
        std::lock_guard lk(counter_mutex);
22✔
3264
        switch (event) {
22✔
3265
            case RedirectEvent::location:
10✔
3266
                location_count++;
10✔
3267
                logger->trace("Redirector event: location - count: %1", location_count);
10✔
3268
                return;
10✔
3269
            case RedirectEvent::redirect:
12✔
3270
                redirect_count++;
12✔
3271
                logger->trace("Redirector event: redirect - count: %1", redirect_count);
12✔
3272
                return;
12✔
NEW
3273
            case RedirectEvent::ws_redirect:
✔
NEW
3274
                wsredirect_count++;
×
NEW
3275
                logger->trace("Redirector event: ws_redirect - count: %1", wsredirect_count);
×
NEW
3276
                return;
×
NEW
3277
            case RedirectEvent::error:
✔
NEW
3278
                error_count++;
×
NEW
3279
                logger->trace("Redirect server received error: %1", message.value_or("unknown error"));
×
NEW
3280
                return;
×
3281
        }
22✔
3282
    });
22✔
3283

3284
    auto reset_counters = [&] {
10✔
3285
        std::lock_guard lk(counter_mutex);
10✔
3286
        error_count = 0;
10✔
3287
        location_count = 0;
10✔
3288
        redirect_count = 0;
10✔
3289
        wsredirect_count = 0;
10✔
3290
    };
10✔
3291

3292
    auto check_counters = [&](int locations, int redirects, int wsredirects, int errors) {
14✔
3293
        std::lock_guard lk(counter_mutex);
14✔
3294
        REQUIRE(location_count == locations);
14!
3295
        REQUIRE(redirect_count == redirects);
14!
3296
        REQUIRE(wsredirect_count == wsredirects);
14!
3297
        REQUIRE(error_count == errors);
14!
3298
    };
14✔
3299

3300
    // Make sure the location response points to the actual server
3301
    redirector.force_http_redirect(false);
4✔
3302
    redirector.force_websocket_redirect(false);
4✔
3303

3304
    auto tas_config = TestAppSession::Config{};
4✔
3305
    tas_config.base_url = redirector.base_url();
4✔
3306

3307
    // Since this test defines its own RedirectingHttpServer, the app session doesn't
3308
    // need to be retrieved at the beginning of the test to ensure the redirect server
3309
    // is initialized.
3310
    TestAppSession session{get_runtime_app_session(), tas_config, DeleteApp{false}};
4✔
3311
    auto app = session.app();
4✔
3312

3313
    // We should have already requested the location when the user was logged in
3314
    // during the session constructor.
3315
    auto user1_a = app->current_user();
4✔
3316
    REQUIRE(user1_a);
4!
3317
    // Expected location requested 1 time for the original location request,
3318
    // all others 0 since location request prior to login hits actual server
3319
    check_counters(1, 0, 0, 0);
4✔
3320
    REQUIRE(app->get_base_url() == redirector.base_url());
4!
3321
    REQUIRE(app->get_host_url() == redirector.server_url());
4!
3322

3323
    SECTION("Appservices requests are redirected") {
4✔
3324
        // Switch the location to use the redirector's address for http requests which will
3325
        // return redirect responses to redirect the request to the actual server
3326
        redirector.force_http_redirect(true);
2✔
3327
        redirector.force_websocket_redirect(false);
2✔
3328
        reset_counters();
2✔
3329
        // Reset the location flag and the cached location info so the app will request
3330
        // the location from the original base URL again upon the next appservices request.
3331
        app->reset_location_for_testing();
2✔
3332
        // Email registration should complete successfully
3333
        AutoVerifiedEmailCredentials creds;
2✔
3334
        {
2✔
3335
            auto pf = util::make_promise_future<void>();
2✔
3336
            app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3337
                creds.email, creds.password,
2✔
3338
                [promise = util::CopyablePromiseHolder<void>(std::move(pf.promise))](
2✔
3339
                    util::Optional<app::AppError> error) mutable {
2✔
3340
                    if (error) {
2✔
NEW
3341
                        promise.get_promise().set_error(error->to_status());
×
NEW
3342
                        return;
×
NEW
3343
                    }
×
3344
                    promise.get_promise().emplace_value();
2✔
3345
                });
2✔
3346
            REQUIRE(pf.future.get_no_throw().is_ok());
2!
3347
        }
2✔
3348
        // Login should fail since the profile request does not complete successfully due
3349
        // to the authorization headers being stripped from the redirected request
3350
        REQUIRE_FALSE(session.log_in_user(creds).is_ok());
2!
3351
        // Since the login failed, the original user1 is still the App's current user
3352
        auto user1_b = app->current_user();
2✔
3353
        REQUIRE(user1_b->is_logged_in());
2!
3354
        REQUIRE(user1_a == user1_b);
2!
3355
        // Expected location requested 2 times: once for register and after first profile
3356
        // attempt fails; there are 4 redirects: register, login, get profile, and refresh
3357
        // token
3358
        check_counters(2, 4, 0, 0);
2✔
3359
        REQUIRE(app->get_base_url() == redirector.base_url());
2!
3360
        REQUIRE(app->get_host_url() == redirector.base_url());
2!
3361

3362
        // Revert the location to point to the actual server's address so the login
3363
        // will complete successfully.
3364
        redirector.force_http_redirect(false);
2✔
3365
        redirector.force_websocket_redirect(false);
2✔
3366
        reset_counters();
2✔
3367
        // Log in will refresh the location prior to performing the login
3368
        auto result = session.log_in_user(creds);
2✔
3369
        REQUIRE(result.is_ok());
2!
3370
        // Since the log in completed successfully, app's current user was updated to
3371
        // the new user.
3372
        auto user3 = result.get_value();
2✔
3373
        REQUIRE(user3);
2!
3374
        REQUIRE(user3->is_logged_in());
2!
3375
        REQUIRE(user3 == app->current_user());
2!
3376
        REQUIRE(user3 != user1_b);
2!
3377
        // Expected location requested 1 time for location after first profile attempt
3378
        // fails; and two redirects: login and the first profile attempt
3379
        check_counters(1, 2, 0, 0);
2✔
3380
        REQUIRE(app->get_base_url() == redirector.base_url());
2!
3381
        REQUIRE(app->get_host_url() == redirector.server_url());
2!
3382
    }
2✔
3383

3384
    SECTION("Websocket connection returns redirection") {
4✔
3385
        auto get_dogs = [](SharedRealm r) -> Results {
6✔
3386
            wait_for_upload(*r, std::chrono::seconds(10));
6✔
3387
            wait_for_download(*r, std::chrono::seconds(10));
6✔
3388
            return Results(r, r->read_group().get_table("class_Dog"));
6✔
3389
        };
6✔
3390

3391
        auto create_one_dog = [](SharedRealm r) {
2✔
3392
            r->begin_transaction();
2✔
3393
            CppContext c;
2✔
3394
            Object::create(c, r, "Dog",
2✔
3395
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
3396
                                            {"breed", std::string("bulldog")},
2✔
3397
                                            {"name", std::string("fido")}}),
2✔
3398
                           CreatePolicy::ForceCreate);
2✔
3399
            r->commit_transaction();
2✔
3400
        };
2✔
3401

3402
        const auto schema = get_default_schema();
2✔
3403
        const auto partition = random_string(100);
2✔
3404
        // This websocket connection is not using redirection. Should connect
3405
        // directly to the actual server
3406
        {
2✔
3407
            reset_counters();
2✔
3408
            SyncTestFile config(user1_a, partition, schema);
2✔
3409
            auto r = Realm::get_shared_realm(config);
2✔
3410
            REQUIRE(get_dogs(r).size() == 0);
2!
3411
            create_one_dog(r);
2✔
3412
            REQUIRE(get_dogs(r).size() == 1);
2!
3413
            // The redirect server is not expected to be used...
3414
            check_counters(0, 0, 0, 0);
2✔
3415
        }
2✔
3416
        // Switch the location to use the redirector's address for websocket requests which will
3417
        // return the 4003 redirect close code, forcing app to update the location and refresh
3418
        // the access token.
3419
        redirector.force_websocket_redirect(true);
2✔
3420
        // Since app uses the hostname value returned from the last location response to create
3421
        // the server URL for requesting the location, the first location request (due to the
3422
        // location_updated flag being reset) needs to return the redirect server for both
3423
        // hostname and ws_hostname. When the location is requested a second time due to the
3424
        // login request, the location response should include the actual server for the
3425
        // hostname (so the login is successful) and the redirect server for the ws_hostname
3426
        // so the websocket initially connects to the redirect server.
3427
        redirector.force_http_redirect(true);
2✔
3428
        {
2✔
3429
            redirector.set_event_hook([&](RedirectEvent event, std::optional<std::string> message) {
12✔
3430
                std::lock_guard lk(counter_mutex);
12✔
3431
                switch (event) {
12✔
3432
                    case RedirectEvent::location:
4✔
3433
                        location_count++;
4✔
3434
                        logger->trace("Redirector event: location - count: %1", location_count);
4✔
3435
                        if (location_count == 1)
4✔
3436
                            // No longer sending redirect server as location hostname value
3437
                            redirector.force_http_redirect(false);
2✔
3438
                        return;
4✔
3439
                    case RedirectEvent::redirect:
6✔
3440
                        redirect_count++;
6✔
3441
                        logger->trace("Redirector event: redirect - count: %1", redirect_count);
6✔
3442
                        return;
6✔
3443
                    case RedirectEvent::ws_redirect:
2✔
3444
                        wsredirect_count++;
2✔
3445
                        logger->trace("Redirector event: ws_redirect - count: %1", wsredirect_count);
2✔
3446
                        return;
2✔
NEW
3447
                    case RedirectEvent::error:
✔
NEW
3448
                        error_count++;
×
NEW
3449
                        logger->trace("Redirect server received error: %1", message.value_or("unknown error"));
×
NEW
3450
                        return;
×
3451
                }
12✔
3452
            });
12✔
3453
        }
2✔
3454
        {
2✔
3455
            reset_counters();
2✔
3456
            // Reset the location flag and the cached location info so the app will request
3457
            // the location from the original base URL again upon the next appservices request.
3458
            app->reset_location_for_testing();
2✔
3459
            // Create a new user and log in to update the location info
3460
            // and start with a new realm
3461
            auto result = session.create_user_and_log_in();
2✔
3462
            REQUIRE(result.is_ok());
2!
3463
            // The location should have been requested twice; before register email and after
3464
            // first profile attempt fails; and three redirects: register email, login, and
3465
            // first profile attempt.
3466
            // NOTE: The ws_hostname still points to the redirect server
3467
            check_counters(2, 3, 0, 0);
2✔
3468
            reset_counters();
2✔
3469
            SyncTestFile config(app->current_user(), partition, schema);
2✔
3470
            auto r = Realm::get_shared_realm(config);
2✔
3471
            Results dogs = get_dogs(r);
2✔
3472
            REQUIRE(dogs.size() == 1);
2!
3473
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
3474
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
3475
            // The websocket should have redirected one time - the location update hits the
3476
            // actual server since the hostname points to its URL after the location update
3477
            // during user log in.
3478
            check_counters(0, 0, 1, 0);
2✔
3479
        }
2✔
3480
    }
2✔
3481
}
4✔
3482

3483
TEST_CASE("app: sync logs contain baas coid", "[sync][app][baas]") {
2✔
3484
    class InMemoryLogger : public util::Logger {
2✔
3485
    public:
2✔
3486
        void do_log(const util::LogCategory& cat, Level level, const std::string& msg) final
2✔
3487
        {
138✔
3488
            auto formatted_line = util::format("%1 %2 %3", cat.get_name(), level, msg);
138✔
3489
            std::lock_guard lk(mtx);
138✔
3490
            log_messages.emplace_back(std::move(formatted_line));
138✔
3491
        }
138✔
3492

3493
        std::vector<std::string> get_log_messages()
2✔
3494
        {
2✔
3495
            std::lock_guard lk(mtx);
2✔
3496
            std::vector<std::string> ret;
2✔
3497
            std::swap(ret, log_messages);
2✔
3498
            return ret;
2✔
3499
        }
2✔
3500

3501
        std::mutex mtx;
2✔
3502
        std::vector<std::string> log_messages;
2✔
3503
    };
2✔
3504

3505
    auto in_mem_logger = std::make_shared<InMemoryLogger>();
2✔
3506
    in_mem_logger->set_level_threshold(InMemoryLogger::Level::all);
2✔
3507
    TestAppSession::Config session_config;
2✔
3508
    session_config.logger = in_mem_logger;
2✔
3509
    TestAppSession app_session(get_runtime_app_session(), session_config, DeleteApp{false});
2✔
3510

3511
    const auto partition = random_string(100);
2✔
3512
    SyncTestFile config(app_session.app()->current_user(), partition, util::none);
2✔
3513
    auto realm = successfully_async_open_realm(config);
2✔
3514
    auto sync_session = realm->sync_session();
2✔
3515
    auto coid = SyncSession::OnlyForTesting::get_appservices_connection_id(*sync_session);
2✔
3516

3517
    auto transition_log_msg =
2✔
3518
        util::format("Connection[1] Connected to app services with request id: \"%1\". Further log entries for this "
2✔
3519
                     "connection will be prefixed with \"Connection[1:%1]\" instead of \"Connection[1]\"",
2✔
3520
                     coid);
2✔
3521
    auto bind_send_msg = util::format("Connection[1:%1] Session[1]: Sending: BIND", coid);
2✔
3522
    auto ping_send_msg = util::format("Connection[1:%1] Will emit a ping in", coid);
2✔
3523

3524
    auto log_messages = in_mem_logger->get_log_messages();
2✔
3525
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(transition_log_msg)));
2✔
3526
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(bind_send_msg)));
2✔
3527
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(ping_send_msg)));
2✔
3528
}
2✔
3529

3530

3531
TEST_CASE("app: trailing slash in base url", "[sync][app]") {
2✔
3532
    auto logger = util::Logger::get_default_logger();
2✔
3533

3534
    const auto schema = get_default_schema();
2✔
3535

3536
    SyncServer server({});
2✔
3537
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
2✔
3538
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
2✔
3539
    OfflineAppSession::Config oas_config(transport);
2✔
3540
    oas_config.base_url = util::format("http://localhost:%1/", server.port());
2✔
3541
    oas_config.socket_provider = socket_provider;
2✔
3542
    OfflineAppSession oas(oas_config);
2✔
3543
    AutoVerifiedEmailCredentials creds;
2✔
3544
    auto app = oas.app();
2✔
3545
    const auto partition = random_string(100);
2✔
3546

3547
    transport->request_hook = [&](const Request& req) -> std::optional<Response> {
8✔
3548
        if (req.url.find("/location") == std::string::npos) {
8✔
3549
            return std::nullopt;
6✔
3550
        }
6✔
3551

3552
        REQUIRE(req.url == util::format("http://localhost:%1/api/client/v2.0/app/app_id/location", server.port()));
2!
3553
        return Response{
2✔
3554
            200,
2✔
3555
            0,
2✔
3556
            {},
2✔
3557
            nlohmann::json(nlohmann::json::object({
2✔
3558
                               {"hostname", util::format("http://localhost:%1", server.port())},
2✔
3559
                               {"ws_hostname", util::format("ws://localhost:%1", server.port())},
2✔
3560
                               {"sync_route", util::format("ws://localhost:%1/realm-sync", server.port())},
2✔
3561
                           }))
2✔
3562
                .dump(),
2✔
3563
        };
2✔
3564
    };
2✔
3565

3566
    SyncTestFile realm_config(oas, "test");
2✔
3567

3568
    auto r = Realm::get_shared_realm(realm_config);
2✔
3569
    REQUIRE(!wait_for_download(*r));
2!
3570
}
2✔
3571

3572
TEST_CASE("app: redirect handling", "[sync][pbs][app]") {
6✔
3573
    auto logger = util::Logger::get_default_logger();
6✔
3574

3575
    const auto schema = get_default_schema();
6✔
3576

3577
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
6✔
3578
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
6✔
3579
    OfflineAppSession::Config oas_config(transport);
6✔
3580
    oas_config.base_url = "http://original.invalid:9090";
6✔
3581
    oas_config.socket_provider = socket_provider;
6✔
3582
    OfflineAppSession oas(oas_config);
6✔
3583
    AutoVerifiedEmailCredentials creds;
6✔
3584
    auto app = oas.app();
6✔
3585
    const auto partition = random_string(100);
6✔
3586

3587
    SECTION("server in maintenance reports error") {
6✔
3588
        transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
3589
            nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"},
2✔
3590
                                                {"error", "This service is currently undergoing maintenance"},
2✔
3591
                                                {"link", "https://link.to/server_logs"}};
2✔
3592
            return Response{500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()};
2✔
3593
        };
2✔
3594

3595
        app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password),
2✔
3596
                                     [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
3597
                                         REQUIRE(!user);
2!
3598
                                         REQUIRE(error);
2!
3599
                                         REQUIRE(error->is_service_error());
2!
3600
                                         REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress);
2!
3601
                                         REQUIRE(error->reason() ==
2!
3602
                                                 "This service is currently undergoing maintenance");
2✔
3603
                                         REQUIRE(error->link_to_server_logs == "https://link.to/server_logs");
2!
3604
                                         REQUIRE(*error->additional_status_code == 500);
2!
3605
                                     });
2✔
3606
    }
2✔
3607

3608
    SECTION("websocket redirects update existing session") {
6✔
3609
        SyncServer server({});
4✔
3610

3611
        transport->request_hook = [&](const Request& req) -> std::optional<Response> {
16✔
3612
            if (req.url.find("/location") != std::string::npos) {
16✔
3613
                return Response{
4✔
3614
                    200,
4✔
3615
                    0,
4✔
3616
                    {},
4✔
3617
                    nlohmann::json({
4✔
3618
                                       {"hostname", "http://some.fake.url"},
4✔
3619
                                       {"ws_hostname", "ws://ws.some.fake.url"},
4✔
3620
                                       {"sync_route", "ws://some.fake.url/realm-sync"},
4✔
3621
                                   })
4✔
3622
                        .dump(),
4✔
3623
                };
4✔
3624
            }
4✔
3625
            return std::nullopt;
12✔
3626
        };
16✔
3627

3628
        // The location info is fake, so we need to override it with the actual
3629
        // server endpoint
3630
        socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
6✔
3631
            ep.address = "127.0.0.1";
6✔
3632
            ep.port = server.port();
6✔
3633
        };
6✔
3634

3635
        SyncTestFile realm_config(oas, "test");
4✔
3636

3637
        std::mutex logout_mutex;
4✔
3638
        std::condition_variable logout_cv;
4✔
3639
        bool logged_out = false;
4✔
3640
        realm_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
4✔
3641
            if (error.status == ErrorCodes::AuthError) {
2✔
3642
                {
2✔
3643
                    std::unique_lock lk(logout_mutex);
2✔
3644
                    logged_out = true;
2✔
3645
                }
2✔
3646
                logout_cv.notify_one();
2✔
3647
                return;
2✔
3648
            }
2✔
3649
            util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
3650
                         error.status);
×
3651
            abort();
×
3652
        };
2✔
3653

3654
        auto r = Realm::get_shared_realm(realm_config);
4✔
3655
        REQUIRE(!wait_for_download(*r));
4!
3656
        auto sync_session = r->sync_session();
4✔
3657
        sync_session->pause();
4✔
3658
        SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*oas.sync_manager());
4✔
3659

3660
        int connect_count = 0;
4✔
3661
        socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
6✔
3662
            // Report a 308 response the first time we try to reconnect the websocket,
3663
            // which should result in App performing a location update.
3664
            // The actual Location header isn't used when we get a redirect on
3665
            // the websocket, so we don't need to supply it here
3666
            if (connect_count++ > 0)
6✔
3667
                return std::nullopt;
2✔
3668
            return sync::HTTPStatus::PermanentRedirect;
4✔
3669
        };
6✔
3670

3671
        SECTION("valid websocket redirect") {
4✔
3672
            socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
4✔
3673
                logger->trace("resolve attempt %1: %2", connect_count, ep.address);
4✔
3674
                // First call happens after the call to the above hook which will
3675
                // force a 308 response. Second call happens after the redirect
3676
                // has been handled.
3677
                REQUIRE(connect_count <= 2);
4!
3678
                if (connect_count == 2) {
4✔
3679
                    REQUIRE(ep.address == "ws.invalid");
×
3680
                }
×
3681

3682
                // Overriding the handshake result happens after dns resolution,
3683
                // so we need to set it to a valid endpoint for even the first call
3684
                ep.address = "127.0.0.1";
4✔
3685
                ep.port = server.port();
4✔
3686
            };
4✔
3687

3688
            int request_count = 0;
2✔
3689
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
4✔
3690
                logger->trace("request.url (%1): %2", request_count, request.url);
4✔
3691
                if (request.url.find("/location") != std::string::npos) {
4✔
3692
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3693
                    return Response{200,
2✔
3694
                                    0,
2✔
3695
                                    {},
2✔
3696
                                    nlohmann::json({
2✔
3697
                                                       {"hostname", "http://http.invalid"},
2✔
3698
                                                       {"ws_hostname", "ws://ws.invalid"},
2✔
3699
                                                       {"sync_route", "ws://ws.invalid/realm-sync"},
2✔
3700
                                                   })
2✔
3701
                                        .dump()};
2✔
3702
                }
2✔
3703

3704
                // Rest of the requests get handled normally
3705
                return std::nullopt;
2✔
3706
            };
4✔
3707

3708
            sync_session->resume();
2✔
3709
            REQUIRE(!wait_for_download(*r));
2!
3710
            REQUIRE(realm_config.sync_config->user->is_logged_in());
2!
3711

3712
            // Verify session is using the updated server url from the redirect
3713
            auto server_url = sync_session->full_realm_url();
2✔
3714
            REQUIRE_THAT(server_url, ContainsSubstring("ws.invalid"));
2✔
3715
        }
2✔
3716

3717
        SECTION("websocket redirect into auth error logs out user") {
4✔
3718
            int request_count = 0;
2✔
3719
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
2✔
3720
                logger->trace("request.url (%1): %2", request_count, request.url);
2✔
3721
                ++request_count;
2✔
3722

3723
                if (request_count == 1) {
2✔
3724
                    // First request should be a location request against the original URL
3725
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3726
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3727
                    return Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
2✔
3728
                                    0,
2✔
3729
                                    {{"Location", "http://asdf.invalid"}},
2✔
3730
                                    ""};
2✔
3731
                }
2✔
3732

3733
                // Second request should be a location request against the new URL
3734
                if (request_count == 2) {
×
3735
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
×
3736
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
×
3737
                    return Response{200,
×
3738
                                    0,
×
3739
                                    {},
×
3740
                                    nlohmann::json({
×
3741
                                                       {"hostname", "http://http.invalid"},
×
3742
                                                       {"ws_hostname", "ws://ws.invalid"},
×
3743
                                                   })
×
3744
                                        .dump()};
×
3745
                }
×
3746

3747
                // Third request should be for an acccess token, which we reject
3748
                REQUIRE(request_count == 3);
×
3749
                REQUIRE_THAT(request.url, ContainsSubstring("auth/session"));
×
3750
                return Response{static_cast<int>(sync::HTTPStatus::Unauthorized), 0, {}, ""};
×
3751
            };
×
3752

3753
            sync_session->resume();
2✔
3754
            REQUIRE(wait_for_download(*r));
2!
3755
            std::unique_lock lk(logout_mutex);
2✔
3756
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
3757
                return logged_out;
4✔
3758
            });
4✔
3759
            REQUIRE(result);
2!
3760
            REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in());
2!
3761
        }
2✔
3762
    }
4✔
3763
}
6✔
3764

3765
TEST_CASE("app: base_url", "[sync][app][base_url]") {
24✔
3766
    struct BaseUrlTransport : UnitTestTransport {
24✔
3767
        std::string expected_url;
24✔
3768
        std::string location_url;
24✔
3769
        std::string location_wsurl;
24✔
3770
        bool location_requested = false;
24✔
3771
        bool location_returns_error = false;
24✔
3772

3773
        void reset(std::string expect_url, std::optional<std::string> url = std::nullopt,
24✔
3774
                   std::optional<std::string> wsurl = std::nullopt)
24✔
3775
        {
58✔
3776
            expected_url = expect_url;
58✔
3777
            REALM_ASSERT(!expected_url.empty());
58✔
3778
            location_url = url.value_or(expect_url);
58✔
3779
            REALM_ASSERT(!location_url.empty());
58✔
3780
            location_wsurl = wsurl.value_or(App::create_ws_host_url(location_url));
58✔
3781
            location_requested = false;
58✔
3782
            location_returns_error = false;
58✔
3783
        }
58✔
3784

3785
        void send_request_to_server(const Request& request,
24✔
3786
                                    util::UniqueFunction<void(const Response&)>&& completion) override
24✔
3787
        {
148✔
3788
            if (request.url.find("/location") != std::string::npos) {
148✔
3789
                CHECK(request.method == HttpMethod::get);
58!
3790
                CHECK_THAT(request.url, ContainsSubstring(expected_url));
58✔
3791
                location_requested = true;
58✔
3792
                if (location_returns_error) {
58✔
3793
                    completion(app::Response{static_cast<int>(sync::HTTPStatus::NotFound), 0, {}, "404 not found"});
20✔
3794
                    return;
20✔
3795
                }
20✔
3796
                completion(
38✔
3797
                    app::Response{static_cast<int>(sync::HTTPStatus::Ok),
38✔
3798
                                  0,
38✔
3799
                                  {},
38✔
3800
                                  util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
38✔
3801
                                               "\"%1\",\"ws_hostname\":\"%2\"}",
38✔
3802
                                               location_url, location_wsurl)});
38✔
3803
                return;
38✔
3804
            }
58✔
3805
            if (location_requested) {
90✔
3806
                CHECK_THAT(request.url, ContainsSubstring(location_url));
90✔
3807
            }
90✔
3808
            else {
×
3809
                CHECK_THAT(request.url, ContainsSubstring(expected_url));
×
3810
            }
×
3811
            UnitTestTransport::send_request_to_server(request, std::move(completion));
90✔
3812
        }
90✔
3813
    };
24✔
3814

3815
    auto logger = util::Logger::get_default_logger();
24✔
3816
    std::string default_base_url = std::string(App::default_base_url());
24✔
3817
    std::string default_base_wsurl = App::create_ws_host_url(App::default_base_url());
24✔
3818
    std::string test_base_url = "https://base.someurl.fake";
24✔
3819
    std::string test_base_wsurl = "wss://base.someurl.fake";
24✔
3820
    std::string test_location_url = "https://loc.someurl.fake";
24✔
3821
    std::string test_location_wsurl = "wss://loc.someurl.fake";
24✔
3822
    std::string test_location_wsurl2 = "wss://ws.loc.someurl.fake";
24✔
3823

3824
    auto location_transport = std::make_shared<BaseUrlTransport>();
24✔
3825
    auto get_config_with_base_url = [&](std::optional<std::string> base_url = std::nullopt) {
26✔
3826
        OfflineAppSession::Config config(location_transport);
26✔
3827
        config.base_url = base_url;
26✔
3828
        return config;
26✔
3829
    };
26✔
3830

3831
    SECTION("Test App::create_ws_host_url") {
24✔
3832
        auto result = App::create_ws_host_url("blah");
2✔
3833
        CHECK(result == "blah");
2!
3834
        result = App::create_ws_host_url("http://localhost:9090");
2✔
3835
        CHECK(result == "ws://localhost:9090");
2!
3836
        result = App::create_ws_host_url("https://localhost:9090");
2✔
3837
        CHECK(result == "wss://localhost:9090");
2!
3838
        result = App::create_ws_host_url("https://localhost:9090/some/extra/stuff");
2✔
3839
        CHECK(result == "wss://localhost:9090/some/extra/stuff");
2!
3840
        result = App::create_ws_host_url("http://172.0.0.1:9090");
2✔
3841
        CHECK(result == "ws://172.0.0.1:9090");
2!
3842
        result = App::create_ws_host_url("https://172.0.0.1:9090");
2✔
3843
        CHECK(result == "wss://172.0.0.1:9090");
2!
3844
        // Old default base url
3845
        result = App::create_ws_host_url("http://realm.mongodb.com");
2✔
3846
        CHECK(result == "ws://ws.realm.mongodb.com");
2!
3847
        result = App::create_ws_host_url("https://realm.mongodb.com");
2✔
3848
        CHECK(result == "wss://ws.realm.mongodb.com");
2!
3849
        result = App::create_ws_host_url("https://realm.mongodb.com/some/extra/stuff");
2✔
3850
        CHECK(result == "wss://ws.realm.mongodb.com/some/extra/stuff");
2!
3851
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3852
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3853
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3854
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3855
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2✔
3856
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2!
3857
        // New default base url
3858
        result = App::create_ws_host_url("http://services.cloud.mongodb.com");
2✔
3859
        CHECK(result == "ws://ws.services.cloud.mongodb.com");
2!
3860
        result = App::create_ws_host_url("https://services.cloud.mongodb.com");
2✔
3861
        CHECK(result == "wss://ws.services.cloud.mongodb.com");
2!
3862
        result = App::create_ws_host_url("https://services.cloud.mongodb.com/some/extra/stuff");
2✔
3863
        CHECK(result == "wss://ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3864
        result = App::create_ws_host_url("http://us-east-1.aws.services.cloud.mongodb.com");
2✔
3865
        CHECK(result == "ws://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3866
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com");
2✔
3867
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3868
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com/some/extra/stuff");
2✔
3869
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3870
    }
2✔
3871

3872
    SECTION("Test app config baseurl") {
24✔
3873
        {
2✔
3874
            // First time through, base_url is empty; https://services.cloud.mongodb.com is expected
3875
            location_transport->reset(std::string(App::default_base_url()));
2✔
3876
            auto config = get_config_with_base_url();
2✔
3877
            OfflineAppSession oas(config);
2✔
3878
            auto app = oas.app();
2✔
3879

3880
            // Location is not requested until first app services request
3881
            CHECK(!location_transport->location_requested);
2!
3882
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3883
            CHECK(app->get_host_url() == App::default_base_url());
2!
3884
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3885

3886
            oas.make_user();
2✔
3887
            CHECK(location_transport->location_requested);
2!
3888
            CHECK(app->get_base_url() == App::default_base_url());
2!
3889
            CHECK(app->get_host_url() == App::default_base_url());
2!
3890
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3891
        }
2✔
3892
        {
2✔
3893
            // Base_url is set to test_base_url and test_location_url is expected after
3894
            // location request
3895
            location_transport->reset(test_base_url, test_location_url);
2✔
3896
            auto config = get_config_with_base_url(test_base_url);
2✔
3897
            OfflineAppSession oas(config);
2✔
3898
            auto app = oas.app();
2✔
3899

3900
            // Location is not requested until first app services request
3901
            CHECK(!location_transport->location_requested);
2!
3902
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3903
            CHECK(app->get_host_url() == test_base_url);
2!
3904
            CHECK(app->get_ws_host_url() == test_base_wsurl);
2!
3905

3906
            oas.make_user();
2✔
3907
            CHECK(location_transport->location_requested);
2!
3908
            CHECK(app->get_base_url() == test_base_url);
2!
3909
            CHECK(app->get_host_url() == test_location_url);
2!
3910
            CHECK(app->get_ws_host_url() == test_location_wsurl);
2!
3911
        }
2✔
3912
        {
2✔
3913
            // Third time through, base_url is not set, expect https://services.cloud.mongodb.com,
3914
            // since metadata is no longer used
3915
            location_transport->reset(default_base_url);
2✔
3916
            auto config = get_config_with_base_url();
2✔
3917
            OfflineAppSession oas(config);
2✔
3918
            auto app = oas.app();
2✔
3919

3920
            // Location is not requested until first app services request
3921
            CHECK(!location_transport->location_requested);
2!
3922
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3923
            CHECK(app->get_host_url() == default_base_url);
2!
3924
            CHECK(app->get_ws_host_url() == default_base_wsurl);
2!
3925

3926
            oas.make_user();
2✔
3927
            CHECK(location_transport->location_requested);
2!
3928
            CHECK(app->get_base_url() == default_base_url);
2!
3929
            CHECK(app->get_host_url() == default_base_url);
2!
3930
            CHECK(app->get_ws_host_url() == default_base_wsurl);
2!
3931
        }
2✔
3932
    }
2✔
3933

3934
    SECTION("Test update_baseurl after first request") {
24✔
3935
        bool error_occurred = GENERATE(true, false);
4✔
3936

3937
        location_transport->reset(test_base_url, test_location_url);
4✔
3938
        auto config = get_config_with_base_url(test_base_url);
4✔
3939
        OfflineAppSession oas(config);
4✔
3940
        auto app = oas.app();
4✔
3941

3942
        // Location is not requested until first app services request
3943
        CHECK(!location_transport->location_requested);
4!
3944

3945
        // Perform an operation prior to updating the base URL
3946
        oas.make_user();
4✔
3947
        CHECK(location_transport->location_requested);
4!
3948
        CHECK(app->get_base_url() == test_base_url);
4!
3949
        CHECK(app->get_host_url() == test_location_url);
4!
3950
        CHECK(app->get_ws_host_url() == test_location_wsurl);
4!
3951

3952
        location_transport->reset(default_base_url);
4✔
3953
        location_transport->location_returns_error = error_occurred;
4✔
3954

3955
        // Revert the base URL to the default URL value using the empty string
3956
        app->update_base_url("", [error_occurred](util::Optional<app::AppError> error) {
4✔
3957
            CHECK(error.has_value() == error_occurred);
4!
3958
        });
4✔
3959
        CHECK(location_transport->location_requested);
4!
3960
        if (error_occurred) {
4✔
3961
            // Not updated due to the error
3962
            CHECK(app->get_base_url() == test_base_url);
2!
3963
            CHECK(app->get_host_url() == test_location_url);
2!
3964
            CHECK(app->get_ws_host_url() == test_location_wsurl);
2!
3965
        }
2✔
3966
        else {
2✔
3967
            // updated successfully
3968
            CHECK(app->get_base_url() == default_base_url);
2!
3969
            CHECK(app->get_host_url() == default_base_url);
2!
3970
            CHECK(app->get_ws_host_url() == default_base_wsurl);
2!
3971
            oas.make_user(); // try another operation
2✔
3972
        }
2✔
3973
    }
4✔
3974

3975
    SECTION("Test update_baseurl before first request") {
24✔
3976
        bool error_occurred = GENERATE(true, false);
4✔
3977

3978
        location_transport->reset(default_base_url, test_location_url, test_location_wsurl2);
4✔
3979
        location_transport->location_returns_error = error_occurred;
4✔
3980
        auto config = get_config_with_base_url(test_base_url);
4✔
3981
        OfflineAppSession oas(config);
4✔
3982
        auto app = oas.app();
4✔
3983

3984
        // Check updating the base URL before an initial app_services request.
3985
        CHECK(!location_transport->location_requested);
4!
3986

3987
        // Revert the base URL to the default URL value using the empty string
3988
        app->update_base_url("", [error_occurred](util::Optional<app::AppError> error) {
4✔
3989
            CHECK(error.has_value() == error_occurred);
4!
3990
        });
4✔
3991
        CHECK(location_transport->location_requested);
4!
3992
        if (error_occurred) {
4✔
3993
            // Not updated due to the error
3994
            CHECK(app->get_base_url() == test_base_url);
2!
3995
            CHECK(app->get_host_url() == test_base_url);
2!
3996
            CHECK(app->get_ws_host_url() == test_base_wsurl);
2!
3997
        }
2✔
3998
        else {
2✔
3999
            // updated successfully
4000
            CHECK(app->get_base_url() == default_base_url);
2!
4001
            CHECK(app->get_host_url() == test_location_url);
2!
4002
            CHECK(app->get_ws_host_url() == test_location_wsurl2);
2!
4003
            oas.make_user(); // try another operation
2✔
4004
        }
2✔
4005
    }
4✔
4006

4007
    // Verify new sync session updates location when created with cached user
4008
    SECTION("Verify new sync session updates location") {
24✔
4009
        bool use_ssl = GENERATE(true, false);
12✔
4010
        std::string base_host = "base.url.fake";
12✔
4011
        std::string location_host = "alternate.url.fake";
12✔
4012
        std::string new_location_host = "new.url.fake";
12✔
4013
        unsigned location_port = use_ssl ? 443 : 80;
12✔
4014
        std::string sync_base_url = util::format("http://%1", base_host);
12✔
4015
        std::string sync_location_url = util::format("http%1://%2", use_ssl ? "s" : "", location_host);
12✔
4016
        std::string sync_location_wsurl = util::format("ws%1://%2", use_ssl ? "s" : "", location_host);
12✔
4017
        std::string new_location_url = util::format("http%1://%2", use_ssl ? "s" : "", new_location_host);
12✔
4018
        std::string new_location_wsurl = util::format("ws%1://%2", use_ssl ? "s" : "", new_location_host);
12✔
4019

4020
        auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
12✔
4021
        socket_provider->websocket_connect_func = []() -> std::optional<SocketProviderError> {
12✔
4022
            return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, "404 not found");
8✔
4023
        };
8✔
4024

4025
        auto config = get_config_with_base_url(sync_base_url);
12✔
4026
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
12✔
4027
        config.socket_provider = socket_provider;
12✔
4028
        config.storage_path = util::make_temp_dir();
12✔
4029
        config.delete_storage = false; // persist the current user
12✔
4030

4031
        // Log in to get a cached user
4032
        {
12✔
4033
            location_transport->reset(sync_base_url, sync_location_url, sync_location_wsurl);
12✔
4034
            OfflineAppSession oas(config);
12✔
4035
            auto app = oas.app();
12✔
4036

4037
            {
12✔
4038
                CHECK_FALSE(location_transport->location_requested);
12!
4039
                auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
4040
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
12✔
4041
                CHECK_FALSE(verified);
12!
4042
            }
12✔
4043

4044
            oas.make_user();
12✔
4045
            CHECK(location_transport->location_requested);
12!
4046
            CHECK(app->get_base_url() == sync_base_url);
12!
4047
            CHECK(app->get_host_url() == sync_location_url);
12!
4048
            CHECK(app->get_ws_host_url() == sync_location_wsurl);
12!
4049
            auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
4050
            CHECK_THAT(sync_route, ContainsSubstring(sync_location_wsurl));
12✔
4051
            CHECK(verified);
12!
4052
        }
12✔
4053

4054
        // the next instance can clean up the files
4055
        config.delete_storage = true;
12✔
4056
        // Recreate the app using the cached user and start a sync session, which will is set to fail on connect
4057
        SECTION("Sync Session fails on connect after updating location") {
12✔
4058
            enum class TestState { start, first_attempt, second_attempt, complete };
4✔
4059
            TestingStateMachine<TestState> state(TestState::start);
4✔
4060
            location_transport->reset(sync_base_url, new_location_url, new_location_wsurl);
4✔
4061

4062
            // Reuse the config so the app uses the cached user
4063
            OfflineAppSession oas(config);
4✔
4064
            auto app = oas.app();
4✔
4065
            REQUIRE(app->current_user());
4!
4066

4067
            // Verify the initial sync route, since the location hasn't been queried
4068
            // and the location is not "verified", the sync route host is based off
4069
            // the value provided in the AppConfig::base_url value
4070
            {
4✔
4071
                auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
4072
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
4✔
4073
                CHECK_FALSE(verified);
4!
4074
            }
4✔
4075

4076
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
8✔
4077
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
8✔
4078
                    if (cur_state == TestState::start) {
8✔
4079
                        // First time through is using the original base URL
4080
                        CHECK(ep.address == base_host);
4!
4081
                        CHECK(ep.port == 80);
4!
4082
                        CHECK(ep.is_ssl == false);
4!
4083
                        return TestState::first_attempt;
4✔
4084
                    }
4✔
4085
                    else if (cur_state == TestState::first_attempt) {
4✔
4086
                        // Second time through is using the values from location endpoint
4087
                        CHECK(ep.address == new_location_host);
4!
4088
                        CHECK(ep.port == location_port);
4!
4089
                        CHECK(ep.is_ssl == use_ssl);
4!
4090
                        return TestState::second_attempt;
4✔
4091
                    }
4✔
4092
                    return std::nullopt;
×
4093
                });
8✔
4094
            };
8✔
4095

4096
            RealmConfig r_config;
4✔
4097
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
4✔
4098
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
4✔
4099
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
4✔
4100
                // Websocket is forcing a 404 failure so it won't actually start
4101
                logger->debug("Received expected error: %1", error.status);
4✔
4102
                CHECK(!error.status.is_ok());
4!
4103
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
4!
4104
                CHECK(!error.is_fatal);
4!
4105
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
4✔
4106
                    CHECK(cur_state == TestState::second_attempt);
4!
4107
                    return TestState::complete;
4✔
4108
                });
4✔
4109
            };
4✔
4110
            auto realm = Realm::get_shared_realm(r_config);
4✔
4111
            state.wait_for(TestState::complete);
4✔
4112

4113
            CHECK(location_transport->location_requested);
4!
4114
            CHECK(app->get_base_url() == sync_base_url);
4!
4115
            CHECK(app->get_host_url() == new_location_url);
4!
4116
            CHECK(app->get_ws_host_url() == new_location_wsurl);
4!
4117
            auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
4118
            CHECK_THAT(sync_route, ContainsSubstring(new_location_wsurl));
4✔
4119
            CHECK(verified);
4!
4120
        }
4✔
4121
        SECTION("Sync Session retries after initial location failure") {
12✔
4122
            enum class TestState { start, location_failed, session_started };
8✔
4123
            TestingStateMachine<TestState> state(TestState::start);
8✔
4124
            const int retry_count = GENERATE(1, 3);
8✔
4125

4126
            location_transport->reset(sync_base_url, new_location_url, new_location_wsurl);
8✔
4127
            location_transport->location_returns_error = true;
8✔
4128

4129
            // Reuse the config so the app uses the cached user
4130
            OfflineAppSession oas(config);
8✔
4131
            auto app = oas.app();
8✔
4132
            REQUIRE(app->current_user());
8!
4133
            // Verify the initial sync route, since the location hasn't been queried
4134
            // and the location is not "verified", the sync route host is based off
4135
            // the value provided in the AppConfig::base_url value
4136
            {
8✔
4137
                auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
4138
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
8✔
4139
                CHECK_FALSE(verified);
8!
4140
            }
8✔
4141

4142
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
24✔
4143
                CHECK(ep.address == base_host);
24!
4144
                CHECK(ep.port == 80);
24!
4145
                CHECK(ep.is_ssl == false);
24!
4146
            };
24✔
4147

4148
            socket_provider->websocket_connect_func = [&, request_count =
8✔
4149
                                                              0]() mutable -> std::optional<SocketProviderError> {
32✔
4150
                if (request_count == 0) {
32✔
4151
                    // First connection attempt is to the unverified initial URL
4152
                    // since we have a valid access token but have never successfully
4153
                    // connected. This failing will trigger a location update.
4154
                    CHECK_FALSE(location_transport->location_requested);
8!
4155
                }
8✔
4156
                else {
24✔
4157
                    // All attempts after the first should have requested location
4158
                    CHECK(location_transport->location_requested);
24!
4159
                    location_transport->location_requested = false;
24✔
4160
                }
24✔
4161

4162
                // Until we allow a location request to succeed we should keep
4163
                // getting the original unverified route
4164
                if (location_transport->location_returns_error) {
32✔
4165
                    CHECK(app->get_base_url() == sync_base_url);
24!
4166
                    CHECK(app->get_host_url() == sync_base_url);
24!
4167
                    CHECK(app->get_ws_host_url() == app::App::create_ws_host_url(sync_base_url));
24!
4168
                    {
24✔
4169
                        auto [sync_route, verified] = app->sync_manager()->sync_route();
24✔
4170
                        CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
24✔
4171
                        CHECK_FALSE(verified);
24!
4172
                    }
24✔
4173
                }
24✔
4174

4175
                // After the chosen number of attempts let the location request succeed
4176
                if (request_count++ >= retry_count) {
32✔
4177
                    location_transport->reset(sync_base_url, new_location_url, new_location_wsurl);
16✔
4178
                    socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
16✔
4179
                        CHECK(ep.address == new_location_host);
8!
4180
                        CHECK(ep.port == location_port);
8!
4181
                        CHECK(ep.is_ssl == use_ssl);
8!
4182
                        state.transition_to(TestState::location_failed);
8✔
4183
                    };
8✔
4184
                }
16✔
4185

4186
                return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
32✔
4187
                                           "404 not found");
32✔
4188
            };
32✔
4189

4190
            RealmConfig r_config;
8✔
4191
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
8✔
4192
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
8✔
4193
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
8✔
4194
                // An error will only be reported if the websocket fails after updating the location and access token
4195
                logger->debug("Received expected error: %1", error.status);
8✔
4196
                CHECK(!error.status.is_ok());
8!
4197
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
8!
4198
                CHECK(!error.is_fatal);
8!
4199
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
8✔
4200
                    if (cur_state == TestState::location_failed) {
8✔
4201
                        // This time, the session was being started, and the location was successful
4202
                        // Websocket is forcing a 404 failure so it won't actually start
4203
                        return TestState::session_started;
8✔
4204
                    }
8✔
4205
                    return std::nullopt;
×
4206
                });
8✔
4207
            };
8✔
4208
            auto realm = Realm::get_shared_realm(r_config);
8✔
4209
            state.wait_for(TestState::session_started);
8✔
4210

4211
            CHECK(app->get_base_url() == sync_base_url);
8!
4212
            CHECK(app->get_host_url() == new_location_url);
8!
4213
            CHECK(app->get_ws_host_url() == new_location_wsurl);
8!
4214
            auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
4215
            CHECK_THAT(sync_route, ContainsSubstring(new_location_wsurl));
8✔
4216
            CHECK(verified);
8!
4217
        }
8✔
4218
    }
12✔
4219
}
24✔
4220

4221
TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") {
2✔
4222
    TestAppSession session;
2✔
4223
    auto app = session.app();
2✔
4224
    auto user = app->current_user();
2✔
4225

4226
    SECTION("custom user data happy path") {
2✔
4227
        bool processed = false;
2✔
4228
        app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})},
2✔
4229
                           [&](auto response, auto error) {
2✔
4230
                               CHECK(error == none);
2!
4231
                               CHECK(response);
2!
4232
                               CHECK(*response == true);
2!
4233
                               processed = true;
2✔
4234
                           });
2✔
4235
        CHECK(processed);
2!
4236
        processed = false;
2✔
4237
        app->refresh_custom_data(user, [&](auto) {
2✔
4238
            processed = true;
2✔
4239
        });
2✔
4240
        CHECK(processed);
2!
4241
        auto data = *user->custom_data();
2✔
4242
        CHECK(data["favorite_color"] == "green");
2!
4243
    }
2✔
4244
}
2✔
4245

4246
TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") {
2✔
4247
    TestAppSession session;
2✔
4248
    auto app = session.app();
2✔
4249
    auto jwt = create_jwt(session.app()->app_id());
2✔
4250

4251
    SECTION("jwt happy path") {
2✔
4252
        bool processed = false;
2✔
4253
        bool logged_in_once = false;
2✔
4254

4255
        auto token = app->subscribe([&logged_in_once, &app](auto&) {
2✔
4256
            REQUIRE(!logged_in_once);
2!
4257
            auto user = app->current_user();
2✔
4258
            auto metadata = user->user_profile();
2✔
4259

4260
            // Ensure that the JWT metadata fields are available when the callback is fired on login.
4261
            CHECK(metadata["name"] == "Foo Bar");
2!
4262
            logged_in_once = true;
2✔
4263
        });
2✔
4264

4265
        std::shared_ptr<User> user = log_in(app, AppCredentials::custom(jwt));
2✔
4266

4267
        app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})},
2✔
4268
                           [&](auto response, auto error) {
2✔
4269
                               CHECK(error == none);
2!
4270
                               CHECK(response);
2!
4271
                               CHECK(*response == true);
2!
4272
                               processed = true;
2✔
4273
                           });
2✔
4274
        CHECK(processed);
2!
4275
        processed = false;
2✔
4276
        app->refresh_custom_data(user, [&](auto) {
2✔
4277
            processed = true;
2✔
4278
        });
2✔
4279
        CHECK(processed);
2!
4280
        auto metadata = user->user_profile();
2✔
4281
        auto custom_data = *user->custom_data();
2✔
4282
        CHECK(custom_data["name"] == "Not Foo Bar");
2!
4283
        CHECK(metadata["name"] == "Foo Bar");
2!
4284

4285
        REQUIRE(logged_in_once);
2!
4286

4287
        app->unsubscribe(token);
2✔
4288
    }
2✔
4289
}
2✔
4290

4291
namespace cf = realm::collection_fixtures;
4292
TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects,
4293
                   cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects,
4294
                   cf::DictionaryOfMixedLinks)
4295
{
12✔
4296
    const std::string valid_pk_name = "_id";
12✔
4297
    const auto partition = random_string(100);
12✔
4298
    TestType test_type("collection", "dest");
12✔
4299
    Schema schema = {{"source",
12✔
4300
                      {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4301
                       {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4302
                       test_type.property()}},
12✔
4303
                     {"dest",
12✔
4304
                      {
12✔
4305
                          {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4306
                          {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4307
                      }}};
12✔
4308
    auto server_app_config = minimal_app_config("collections_of_links", schema);
12✔
4309
    TestAppSession test_session(create_app(server_app_config));
12✔
4310

4311
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
36✔
4312
        timed_sleeping_wait_for([&]() -> bool {
1,226✔
4313
            r->refresh();
1,226✔
4314
            TableRef dest = r->read_group().get_table(table_name);
1,226✔
4315
            size_t cur_count = dest->size();
1,226✔
4316
            return cur_count == count;
1,226✔
4317
        });
1,226✔
4318
    };
36✔
4319
    auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) {
24✔
4320
        timed_sleeping_wait_for([&]() -> bool {
796✔
4321
            r->refresh();
796✔
4322
            return test_type.size_of_collection(obj) == count;
796✔
4323
        });
796✔
4324
    };
24✔
4325

4326
    CppContext c;
12✔
4327
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
12✔
4328
        r->begin_transaction();
12✔
4329
        auto object = Object::create(
12✔
4330
            c, r, "source",
12✔
4331
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
12✔
4332
            CreatePolicy::ForceCreate);
12✔
4333

4334
        for (auto link : links) {
36✔
4335
            auto& obj = object.get_obj();
36✔
4336
            test_type.add_link(obj, link);
36✔
4337
        }
36✔
4338
        r->commit_transaction();
12✔
4339
        return object;
12✔
4340
    };
12✔
4341

4342
    auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink {
36✔
4343
        r->begin_transaction();
36✔
4344
        auto obj = Object::create(
36✔
4345
            c, r, "dest",
36✔
4346
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
36✔
4347
            CreatePolicy::ForceCreate);
36✔
4348
        r->commit_transaction();
36✔
4349
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
36✔
4350
    };
36✔
4351

4352
    auto require_links_to_match_ids = [&](std::vector<Obj> links, std::vector<int64_t> expected) {
48✔
4353
        std::vector<int64_t> actual;
48✔
4354
        for (auto obj : links) {
108✔
4355
            actual.push_back(obj.get<Int>(valid_pk_name));
108✔
4356
        }
108✔
4357
        std::sort(actual.begin(), actual.end());
48✔
4358
        std::sort(expected.begin(), expected.end());
48✔
4359
        REQUIRE(actual == expected);
48!
4360
    };
48✔
4361

4362
    SECTION("integration testing") {
12✔
4363
        auto app = test_session.app();
12✔
4364
        SyncTestFile config1(app->current_user(), partition, schema); // uses the current user created above
12✔
4365
        config1.automatic_change_notifications = false;
12✔
4366
        auto r1 = realm::Realm::get_shared_realm(config1);
12✔
4367
        Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source"));
12✔
4368

4369
        create_user_and_log_in(app);                                  // changes the current user
12✔
4370
        SyncTestFile config2(app->current_user(), partition, schema); // uses the user created above
12✔
4371
        config2.automatic_change_notifications = false;
12✔
4372
        auto r2 = realm::Realm::get_shared_realm(config2);
12✔
4373
        Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source"));
12✔
4374

4375
        constexpr int64_t source_pk = 0;
12✔
4376
        constexpr int64_t dest_pk_1 = 1;
12✔
4377
        constexpr int64_t dest_pk_2 = 2;
12✔
4378
        constexpr int64_t dest_pk_3 = 3;
12✔
4379
        Object object;
12✔
4380

4381
        { // add a container collection with three valid links
12✔
4382
            REQUIRE(r1_source_objs.size() == 0);
12!
4383
            ObjLink dest1 = create_one_dest_object(r1, dest_pk_1);
12✔
4384
            ObjLink dest2 = create_one_dest_object(r1, dest_pk_2);
12✔
4385
            ObjLink dest3 = create_one_dest_object(r1, dest_pk_3);
12✔
4386
            object = create_one_source_object(r1, source_pk, {dest1, dest2, dest3});
12✔
4387
            REQUIRE(r1_source_objs.size() == 1);
12!
4388
            REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4389
            REQUIRE(r1_source_objs.get(0).get<String>("realm_id") == partition);
12!
4390
            require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4391
        }
12✔
4392

4393
        size_t expected_coll_size = 3;
12✔
4394
        std::vector<int64_t> remaining_dest_object_ids;
12✔
4395
        { // erase one of the destination objects
12✔
4396
            wait_for_num_objects_to_equal(r2, "class_source", 1);
12✔
4397
            wait_for_num_objects_to_equal(r2, "class_dest", 3);
12✔
4398
            REQUIRE(r2_source_objs.size() == 1);
12!
4399
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4400
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3);
12!
4401
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4402
            require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4403
            r2->begin_transaction();
12✔
4404
            linked_objects[0].remove();
12✔
4405
            r2->commit_transaction();
12✔
4406
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name),
12✔
4407
                                         linked_objects[2].template get<Int>(valid_pk_name)};
12✔
4408
            expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3;
12✔
4409
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4410
        }
12✔
4411

4412
        { // remove a link from the collection
12✔
4413
            wait_for_num_objects_to_equal(r1, "class_dest", 2);
12✔
4414
            REQUIRE(r1_source_objs.size() == 1);
12!
4415
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4416
            auto linked_objects = test_type.get_links(r1_source_objs.get(0));
12✔
4417
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4418
            r1->begin_transaction();
12✔
4419
            auto obj = r1_source_objs.get(0);
12✔
4420
            test_type.remove_link(obj,
12✔
4421
                                  ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()});
12✔
4422
            r1->commit_transaction();
12✔
4423
            --expected_coll_size;
12✔
4424
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name)};
12✔
4425
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4426
        }
12✔
4427
        bool coll_cleared = false;
12✔
4428
        advance_and_notify(*r1);
12✔
4429
        auto collection = test_type.get_collection(r1, r1_source_objs.get(0));
12✔
4430
        auto token = collection.add_notification_callback([&coll_cleared](CollectionChangeSet c) {
24✔
4431
            coll_cleared = c.collection_was_cleared;
24✔
4432
        });
24✔
4433

4434
        { // clear the collection
12✔
4435
            REQUIRE(r2_source_objs.size() == 1);
12!
4436
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4437
            wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size);
12✔
4438
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4439
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4440
            r2->begin_transaction();
12✔
4441
            test_type.clear_collection(r2_source_objs.get(0));
12✔
4442
            r2->commit_transaction();
12✔
4443
            expected_coll_size = 0;
12✔
4444
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4445
        }
12✔
4446

4447
        { // expect an empty collection
12✔
4448
            REQUIRE(!coll_cleared);
12!
4449
            REQUIRE(r1_source_objs.size() == 1);
12!
4450
            wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size);
12✔
4451
            advance_and_notify(*r1);
12✔
4452
            REQUIRE(coll_cleared);
12!
4453
        }
12✔
4454
    }
12✔
4455
}
12✔
4456

4457
TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID,
4458
                   cf::UUID, cf::BoxedOptional<cf::Int>, cf::UnboxedOptional<cf::String>, cf::BoxedOptional<cf::OID>,
4459
                   cf::BoxedOptional<cf::UUID>)
4460
{
16✔
4461
    const std::string valid_pk_name = "_id";
16✔
4462
    const std::string partition_key_col_name = "partition_key_prop";
16✔
4463
    const std::string table_name = "class_partition_test_type";
16✔
4464
    auto partition_property = Property(partition_key_col_name, TestType::property_type);
16✔
4465
    Schema schema = {{Group::table_name_to_class_name(table_name),
16✔
4466
                      {
16✔
4467
                          {valid_pk_name, PropertyType::Int, true},
16✔
4468
                          partition_property,
16✔
4469
                      }}};
16✔
4470
    auto server_app_config = minimal_app_config("partition_types_app_name", schema);
16✔
4471
    server_app_config.partition_key = partition_property;
16✔
4472
    TestAppSession test_session(create_app(server_app_config));
16✔
4473
    auto app = test_session.app();
16✔
4474

4475
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
48✔
4476
        timed_sleeping_wait_for([&]() -> bool {
3,633✔
4477
            r->refresh();
3,633✔
4478
            TableRef dest = r->read_group().get_table(table_name);
3,633✔
4479
            size_t cur_count = dest->size();
3,633✔
4480
            return cur_count == count;
3,633✔
4481
        });
3,633✔
4482
    };
48✔
4483
    using T = typename TestType::Type;
16✔
4484
    CppContext c;
16✔
4485
    auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) {
48✔
4486
        r->begin_transaction();
48✔
4487
        auto object = Object::create(
48✔
4488
            c, r, Group::table_name_to_class_name(table_name),
48✔
4489
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}),
48✔
4490
            CreatePolicy::ForceCreate);
48✔
4491
        r->commit_transaction();
48✔
4492
    };
48✔
4493

4494
    auto get_bson = [](T val) -> bson::Bson {
96✔
4495
        if constexpr (std::is_same_v<T, StringData>) {
96✔
4496
            return val.is_null() ? bson::Bson(util::none) : bson::Bson(val);
48✔
4497
        }
14✔
4498
        else if constexpr (TestType::is_optional) {
54✔
4499
            return val ? bson::Bson(*val) : bson::Bson(util::none);
40✔
4500
        }
20✔
4501
        else {
28✔
4502
            return bson::Bson(val);
28✔
4503
        }
28✔
4504
    };
96✔
4505

4506
    SECTION("can round trip an object") {
16✔
4507
        auto values = TestType::values();
16✔
4508
        auto user1 = app->current_user();
16✔
4509
        create_user_and_log_in(app);
16✔
4510
        auto user2 = app->current_user();
16✔
4511
        REQUIRE(user1);
16!
4512
        REQUIRE(user2);
16!
4513
        REQUIRE(user1 != user2);
16!
4514
        for (T partition_value : values) {
48✔
4515
            SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above
48✔
4516
            auto r1 = realm::Realm::get_shared_realm(config1);
48✔
4517
            Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name));
48✔
4518

4519
            SyncTestFile config2(user2, get_bson(partition_value), schema); // uses the user created above
48✔
4520
            auto r2 = realm::Realm::get_shared_realm(config2);
48✔
4521
            Results r2_source_objs = realm::Results(r2, r2->read_group().get_table(table_name));
48✔
4522

4523
            const int64_t pk_value = random_int();
48✔
4524
            {
48✔
4525
                REQUIRE(r1_source_objs.size() == 0);
48!
4526
                create_object(r1, pk_value, TestType::to_any(partition_value));
48✔
4527
                REQUIRE(r1_source_objs.size() == 1);
48!
4528
                REQUIRE(r1_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4529
                REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4530
            }
48✔
4531
            {
48✔
4532
                wait_for_num_objects_to_equal(r2, table_name, 1);
48✔
4533
                REQUIRE(r2_source_objs.size() == 1);
48!
4534
                REQUIRE(r2_source_objs.size() == 1);
48!
4535
                REQUIRE(r2_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4536
                REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4537
            }
48✔
4538
        }
48✔
4539
    }
16✔
4540
}
16✔
4541

4542
TEST_CASE("app: full-text compatible with sync", "[sync][app][baas]") {
4✔
4543
    const std::string valid_pk_name = "_id";
4✔
4544

4545
    Schema schema{
4✔
4546
        {"TopLevel",
4✔
4547
         {
4✔
4548
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
4549
             {"full_text", Property::IsFulltextIndexed{true}},
4✔
4550
         }},
4✔
4551
    };
4✔
4552

4553
    auto server_app_config = minimal_app_config("full_text", schema);
4✔
4554
    auto app_session = create_app(server_app_config);
4✔
4555
    const auto partition = random_string(100);
4✔
4556
    TestAppSession test_session(app_session);
4✔
4557
    SyncTestFile config(test_session.app()->current_user(), partition, schema);
4✔
4558
    SharedRealm realm;
4✔
4559
    SECTION("sync open") {
4✔
4560
        INFO("realm opened without async open");
2✔
4561
        realm = Realm::get_shared_realm(config);
2✔
4562
    }
2✔
4563
    SECTION("async open") {
4✔
4564
        INFO("realm opened with async open");
2✔
4565
        auto async_open_task = Realm::get_synchronized_realm(config);
2✔
4566

4567
        auto realm_future = async_open_task->start();
2✔
4568
        realm = Realm::get_shared_realm(std::move(realm_future.get()));
2✔
4569
    }
2✔
4570

4571
    CppContext c(realm);
4✔
4572
    auto obj_id_1 = ObjectId::gen();
4✔
4573
    auto obj_id_2 = ObjectId::gen();
4✔
4574
    realm->begin_transaction();
4✔
4575
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}}));
4✔
4576
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}}));
4✔
4577
    realm->commit_transaction();
4✔
4578

4579
    auto table = realm->read_group().get_table("class_TopLevel");
4✔
4580
    REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext);
4!
4581
    Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world"));
4✔
4582
    REQUIRE(world_results.size() == 1);
4!
4583
    REQUIRE(world_results.get<Obj>(0).get_primary_key() == Mixed{obj_id_1});
4!
4584
}
4✔
4585

4586
#endif // REALM_ENABLE_AUTH_TESTS
4587

4588
TEST_CASE("app: custom error handling", "[sync][app][custom errors]") {
2✔
4589
    class CustomErrorTransport : public GenericNetworkTransport {
2✔
4590
    public:
2✔
4591
        CustomErrorTransport(int code, const std::string& message)
2✔
4592
            : m_code(code)
2✔
4593
            , m_message(message)
2✔
4594
        {
2✔
4595
        }
2✔
4596

4597
        void send_request_to_server(const Request&, util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4598
        {
2✔
4599
            completion(Response{0, m_code, HttpHeaders(), m_message});
2✔
4600
        }
2✔
4601

4602
    private:
2✔
4603
        int m_code;
2✔
4604
        std::string m_message;
2✔
4605
    };
2✔
4606

4607
    SECTION("custom code and message is sent back") {
2✔
4608
        OfflineAppSession offline_session({std::make_shared<CustomErrorTransport>(1001, "Boom!")});
2✔
4609
        auto error = failed_log_in(offline_session.app());
2✔
4610
        CHECK(error.is_custom_error());
2!
4611
        CHECK(*error.additional_status_code == 1001);
2!
4612
        CHECK(error.reason() == "Boom!");
2!
4613
    }
2✔
4614
}
2✔
4615

4616
// MARK: - Unit Tests
4617

4618
static const std::string bad_access_token = "lolwut";
4619
static const std::string dummy_device_id = "123400000000000000000000";
4620

4621
TEST_CASE("subscribable unit tests", "[sync][app]") {
8✔
4622
    struct Foo : public Subscribable<Foo> {
8✔
4623
        void event()
8✔
4624
        {
18✔
4625
            emit_change_to_subscribers(*this);
18✔
4626
        }
18✔
4627
    };
8✔
4628

4629
    auto foo = Foo();
8✔
4630

4631
    SECTION("subscriber receives events") {
8✔
4632
        auto event_count = 0;
2✔
4633
        auto token = foo.subscribe([&event_count](auto&) {
6✔
4634
            event_count++;
6✔
4635
        });
6✔
4636

4637
        foo.event();
2✔
4638
        foo.event();
2✔
4639
        foo.event();
2✔
4640

4641
        CHECK(event_count == 3);
2!
4642
    }
2✔
4643

4644
    SECTION("subscriber can unsubscribe") {
8✔
4645
        auto event_count = 0;
2✔
4646
        auto token = foo.subscribe([&event_count](auto&) {
2✔
4647
            event_count++;
2✔
4648
        });
2✔
4649

4650
        foo.event();
2✔
4651
        CHECK(event_count == 1);
2!
4652

4653
        foo.unsubscribe(token);
2✔
4654
        foo.event();
2✔
4655
        CHECK(event_count == 1);
2!
4656
    }
2✔
4657

4658
    SECTION("subscriber is unsubscribed on dtor") {
8✔
4659
        auto event_count = 0;
2✔
4660
        {
2✔
4661
            auto token = foo.subscribe([&event_count](auto&) {
2✔
4662
                event_count++;
2✔
4663
            });
2✔
4664

4665
            foo.event();
2✔
4666
            CHECK(event_count == 1);
2!
4667
        }
2✔
4668
        foo.event();
2✔
4669
        CHECK(event_count == 1);
2!
4670
    }
2✔
4671

4672
    SECTION("multiple subscribers receive events") {
8✔
4673
        auto event_count = 0;
2✔
4674
        {
2✔
4675
            auto token1 = foo.subscribe([&event_count](auto&) {
2✔
4676
                event_count++;
2✔
4677
            });
2✔
4678
            auto token2 = foo.subscribe([&event_count](auto&) {
2✔
4679
                event_count++;
2✔
4680
            });
2✔
4681

4682
            foo.event();
2✔
4683
            CHECK(event_count == 2);
2!
4684
        }
2✔
4685
        foo.event();
2✔
4686
        CHECK(event_count == 2);
2!
4687
    }
2✔
4688
}
8✔
4689

4690
TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") {
6✔
4691
    auto transport = std::make_shared<UnitTestTransport>();
6✔
4692
    OfflineAppSession::Config config{transport};
6✔
4693
    transport->set_profile(profile_0);
6✔
4694

4695
    SECTION("login_anonymous good") {
6✔
4696
        config.storage_path = util::make_temp_dir();
2✔
4697
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
2✔
4698
        {
2✔
4699
            config.delete_storage = false;
2✔
4700
            OfflineAppSession oas(config);
2✔
4701
            auto app = oas.app();
2✔
4702
            auto user = log_in(app);
2✔
4703

4704
            REQUIRE(user->identities().size() == 1);
2!
4705
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4706
            UserProfile user_profile = user->user_profile();
2✔
4707

4708
            CHECK(user_profile.name() == profile_0_name);
2!
4709
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4710
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4711
            CHECK(user_profile.email() == profile_0_email);
2!
4712
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4713
            CHECK(user_profile.gender() == profile_0_gender);
2!
4714
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4715
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4716
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4717
        }
2✔
4718
        // assert everything is stored properly between runs
4719
        {
2✔
4720
            config.delete_storage = true; // clean up after this session
2✔
4721
            OfflineAppSession oas(config);
2✔
4722
            auto app = oas.app();
2✔
4723
            REQUIRE(app->all_users().size() == 1);
2!
4724
            auto user = app->all_users()[0];
2✔
4725
            REQUIRE(user->identities().size() == 1);
2!
4726
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4727
            UserProfile user_profile = user->user_profile();
2✔
4728

4729
            CHECK(user_profile.name() == profile_0_name);
2!
4730
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4731
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4732
            CHECK(user_profile.email() == profile_0_email);
2!
4733
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4734
            CHECK(user_profile.gender() == profile_0_gender);
2!
4735
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4736
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4737
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4738
        }
2✔
4739
    }
2✔
4740

4741
    SECTION("login_anonymous bad") {
6✔
4742
        struct transport : UnitTestTransport {
2✔
4743
            void send_request_to_server(const Request& request,
2✔
4744
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4745
            {
4✔
4746
                if (request.url.find("/login") != std::string::npos) {
4✔
4747
                    completion({200, 0, {}, user_json(bad_access_token).dump()});
2✔
4748
                }
2✔
4749
                else {
2✔
4750
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
2✔
4751
                }
2✔
4752
            }
4✔
4753
        };
2✔
4754

4755
        config.transport = instance_of<transport>;
2✔
4756
        OfflineAppSession oas(config);
2✔
4757
        auto error = failed_log_in(oas.app());
2✔
4758
        CHECK(error.reason() == std::string("Could not log in user: received malformed JWT"));
2!
4759
        CHECK(error.code_string() == "BadToken");
2!
4760
        CHECK(error.is_json_error());
2!
4761
        CHECK(error.code() == ErrorCodes::BadToken);
2!
4762
    }
2✔
4763

4764
    SECTION("login_anonynous multiple users") {
6✔
4765
        OfflineAppSession oas(config);
2✔
4766
        auto app = oas.app();
2✔
4767

4768
        auto user1 = log_in(app);
2✔
4769
        auto user2 = log_in(app, AppCredentials::anonymous(false));
2✔
4770
        CHECK(user1 != user2);
2!
4771
    }
2✔
4772
}
6✔
4773

4774
TEST_CASE("app: UserAPIKeyProviderClient unit_tests", "[sync][app][user][api key]") {
6✔
4775
    OfflineAppSession oas({std::make_shared<UnitTestTransport>()});
6✔
4776
    auto client = oas.app()->provider_client<App::UserAPIKeyProviderClient>();
6✔
4777

4778
    auto logged_in_user = oas.make_user();
6✔
4779
    bool processed = false;
6✔
4780
    ObjectId obj_id(UnitTestTransport::api_key_id.c_str());
6✔
4781

4782
    SECTION("create api key") {
6✔
4783
        client.create_api_key(UnitTestTransport::api_key_name, logged_in_user,
2✔
4784
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4785
                                  REQUIRE_FALSE(error);
2!
4786
                                  CHECK(user_api_key.disabled == false);
2!
4787
                                  CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4788
                                  CHECK(user_api_key.key == UnitTestTransport::api_key);
2!
4789
                                  CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4790
                              });
2✔
4791
    }
2✔
4792

4793
    SECTION("fetch api key") {
6✔
4794
        client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4795
            REQUIRE_FALSE(error);
2!
4796
            CHECK(user_api_key.disabled == false);
2!
4797
            CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4798
            CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4799
        });
2✔
4800
    }
2✔
4801

4802
    SECTION("fetch api keys") {
6✔
4803
        client.fetch_api_keys(logged_in_user,
2✔
4804
                              [&](std::vector<App::UserAPIKey> user_api_keys, Optional<AppError> error) {
2✔
4805
                                  REQUIRE_FALSE(error);
2!
4806
                                  CHECK(user_api_keys.size() == 2);
2!
4807
                                  for (auto user_api_key : user_api_keys) {
4✔
4808
                                      CHECK(user_api_key.disabled == false);
4!
4809
                                      CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
4!
4810
                                      CHECK(user_api_key.name == UnitTestTransport::api_key_name);
4!
4811
                                  }
4✔
4812
                                  processed = true;
2✔
4813
                              });
2✔
4814
        CHECK(processed);
2!
4815
    }
2✔
4816
}
6✔
4817

4818
TEST_CASE("app: user_semantics", "[sync][app][user]") {
12✔
4819
    OfflineAppSession oas;
12✔
4820
    auto app = oas.app();
12✔
4821

4822
    const auto login_user_email_pass = [=] {
12✔
4823
        return log_in(app, AppCredentials::username_password("bob", "thompson"));
6✔
4824
    };
6✔
4825
    const auto login_user_anonymous = [=] {
18✔
4826
        return log_in(app, AppCredentials::anonymous());
18✔
4827
    };
18✔
4828

4829
    CHECK(!app->current_user());
12!
4830

4831
    int event_processed = 0;
12✔
4832
    auto token = app->subscribe([&](auto&) {
28✔
4833
        event_processed++;
28✔
4834
        // Read the current user to verify that doing so does not deadlock
4835
        app->current_user();
28✔
4836
    });
28✔
4837

4838
    SECTION("current user is populated") {
12✔
4839
        const auto user1 = login_user_anonymous();
2✔
4840
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4841
        CHECK(event_processed == 1);
2!
4842
    }
2✔
4843

4844
    SECTION("current user is updated on login") {
12✔
4845
        const auto user1 = login_user_anonymous();
2✔
4846
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4847
        const auto user2 = login_user_email_pass();
2✔
4848
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4849
        CHECK(user1->user_id() != user2->user_id());
2!
4850
        CHECK(event_processed == 2);
2!
4851
    }
2✔
4852

4853
    SECTION("current user is updated to last used user on logout") {
12✔
4854
        const auto user1 = login_user_anonymous();
2✔
4855
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4856
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4857

4858
        const auto user2 = login_user_email_pass();
2✔
4859
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4860
        CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn);
2!
4861
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4862
        CHECK(user1 != user2);
2!
4863

4864
        // should reuse existing session
4865
        const auto user3 = login_user_anonymous();
2✔
4866
        CHECK(user3 == user1);
2!
4867

4868
        auto user_events_processed = 0;
2✔
4869
        auto _ = user3->subscribe([&user_events_processed](auto&) {
2✔
4870
            user_events_processed++;
2✔
4871
        });
2✔
4872

4873
        app->log_out([](auto) {});
2✔
4874
        CHECK(user_events_processed == 1);
2!
4875
        REQUIRE(app->current_user());
2!
4876
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4877

4878
        CHECK(app->all_users().size() == 1);
2!
4879
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4880

4881
        CHECK(event_processed == 4);
2!
4882
    }
2✔
4883

4884
    SECTION("anon users are removed on logout") {
12✔
4885
        const auto user1 = login_user_anonymous();
2✔
4886
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4887
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4888

4889
        const auto user2 = login_user_anonymous();
2✔
4890
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4891
        CHECK(app->all_users().size() == 1);
2!
4892
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4893
        CHECK(user1->user_id() == user2->user_id());
2!
4894

4895
        app->log_out([](auto) {});
2✔
4896
        CHECK(app->all_users().size() == 0);
2!
4897

4898
        CHECK(event_processed == 3);
2!
4899
    }
2✔
4900

4901
    SECTION("logout user") {
12✔
4902
        auto user1 = login_user_email_pass();
2✔
4903
        auto user2 = login_user_anonymous();
2✔
4904

4905
        // Anonymous users are special
4906
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4907
            REQUIRE_FALSE(error);
2!
4908
        });
2✔
4909
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4910

4911
        // Other users can be LoggedOut
4912
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4913
            REQUIRE_FALSE(error);
2!
4914
        });
2✔
4915
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4916

4917
        // Logging out already logged out users does nothing
4918
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4919
            REQUIRE_FALSE(error);
2!
4920
        });
2✔
4921
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4922

4923
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4924
            REQUIRE_FALSE(error);
2!
4925
        });
2✔
4926
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4927

4928
        CHECK(event_processed == 4);
2!
4929
    }
2✔
4930

4931
    SECTION("unsubscribed observers no longer process events") {
12✔
4932
        app->unsubscribe(token);
2✔
4933

4934
        const auto user1 = login_user_anonymous();
2✔
4935
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4936
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4937

4938
        const auto user2 = login_user_anonymous();
2✔
4939
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4940
        CHECK(app->all_users().size() == 1);
2!
4941
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4942
        CHECK(user1->user_id() == user2->user_id());
2!
4943

4944
        app->log_out([](auto) {});
2✔
4945
        CHECK(app->all_users().size() == 0);
2!
4946

4947
        CHECK(event_processed == 0);
2!
4948
    }
2✔
4949
}
12✔
4950

4951
namespace {
4952
struct ErrorCheckingTransport : public GenericNetworkTransport {
4953
    ErrorCheckingTransport(Response* r)
4954
        : m_response(r)
5✔
4955
    {
10✔
4956
    }
10✔
4957
    void send_request_to_server(const Request& request,
4958
                                util::UniqueFunction<void(const Response&)>&& completion) override
4959
    {
20✔
4960
        // Make sure to return a valid location response
4961
        if (request.url.find("/location") != std::string::npos) {
20✔
4962
            completion(Response{200,
10✔
4963
                                0,
10✔
4964
                                {{"content-type", "application/json"}},
10✔
4965
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
10✔
4966
                                "\"http://some.fake.url\",\"ws_hostname\":\"ws://some.fake.url\"}"});
10✔
4967
            return;
10✔
4968
        }
10✔
4969

4970
        completion(Response(*m_response));
10✔
4971
    }
10✔
4972

4973
private:
4974
    Response* m_response;
4975
};
4976
} // namespace
4977

4978
TEST_CASE("app: response error handling", "[sync][app]") {
10✔
4979
    std::string response_body = nlohmann::json({{"access_token", good_access_token},
10✔
4980
                                                {"refresh_token", good_access_token},
10✔
4981
                                                {"user_id", "Brown Bear"},
10✔
4982
                                                {"device_id", "Panda Bear"}})
10✔
4983
                                    .dump();
10✔
4984

4985
    Response response{200, 0, {{"Content-Type", "text/plain"}}, response_body};
10✔
4986

4987
    OfflineAppSession oas({std::make_shared<ErrorCheckingTransport>(&response)});
10✔
4988
    auto app = oas.app();
10✔
4989

4990
    SECTION("http 404") {
10✔
4991
        response.http_status_code = 404;
2✔
4992
        auto error = failed_log_in(app);
2✔
4993
        CHECK(!error.is_json_error());
2!
4994
        CHECK(!error.is_custom_error());
2!
4995
        CHECK(!error.is_service_error());
2!
4996
        CHECK(error.is_http_error());
2!
4997
        CHECK(*error.additional_status_code == 404);
2!
4998
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4999
    }
2✔
5000
    SECTION("http 500") {
10✔
5001
        response.http_status_code = 500;
2✔
5002
        auto error = failed_log_in(app);
2✔
5003
        CHECK(!error.is_json_error());
2!
5004
        CHECK(!error.is_custom_error());
2!
5005
        CHECK(!error.is_service_error());
2!
5006
        CHECK(error.is_http_error());
2!
5007
        CHECK(*error.additional_status_code == 500);
2!
5008
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
5009
        CHECK(error.link_to_server_logs.empty());
2!
5010
    }
2✔
5011

5012
    SECTION("custom error code") {
10✔
5013
        response.custom_status_code = 42;
2✔
5014
        response.body = "Custom error message";
2✔
5015
        auto error = failed_log_in(app);
2✔
5016
        CHECK(!error.is_http_error());
2!
5017
        CHECK(!error.is_json_error());
2!
5018
        CHECK(!error.is_service_error());
2!
5019
        CHECK(error.is_custom_error());
2!
5020
        CHECK(*error.additional_status_code == 42);
2!
5021
        CHECK(error.reason() == std::string("Custom error message"));
2!
5022
        CHECK(error.link_to_server_logs.empty());
2!
5023
    }
2✔
5024

5025
    SECTION("session error code") {
10✔
5026
        response.headers = HttpHeaders{{"Content-Type", "application/json"}};
2✔
5027
        response.http_status_code = 400;
2✔
5028
        response.body = nlohmann::json({{"error_code", "MongoDBError"},
2✔
5029
                                        {"error", "a fake MongoDB error message!"},
2✔
5030
                                        {"access_token", good_access_token},
2✔
5031
                                        {"refresh_token", good_access_token},
2✔
5032
                                        {"user_id", "Brown Bear"},
2✔
5033
                                        {"device_id", "Panda Bear"},
2✔
5034
                                        {"link", "http://...whatever the server passes us"}})
2✔
5035
                            .dump();
2✔
5036
        auto error = failed_log_in(app);
2✔
5037
        CHECK(!error.is_http_error());
2!
5038
        CHECK(!error.is_json_error());
2!
5039
        CHECK(!error.is_custom_error());
2!
5040
        CHECK(error.is_service_error());
2!
5041
        CHECK(error.code() == ErrorCodes::MongoDBError);
2!
5042
        CHECK(error.reason() == std::string("a fake MongoDB error message!"));
2!
5043
        CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us"));
2!
5044
    }
2✔
5045

5046
    SECTION("json error code") {
10✔
5047
        response.body = "this: is not{} a valid json body!";
2✔
5048
        auto error = failed_log_in(app);
2✔
5049
        CHECK(!error.is_http_error());
2!
5050
        CHECK(error.is_json_error());
2!
5051
        CHECK(!error.is_custom_error());
2!
5052
        CHECK(!error.is_service_error());
2!
5053
        CHECK(error.code() == ErrorCodes::MalformedJson);
2!
5054
        CHECK(error.reason() ==
2!
5055
              std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error "
2✔
5056
                          "while parsing value - invalid literal; last read: 'th'"));
2✔
5057
        CHECK(error.code_string() == "MalformedJson");
2!
5058
    }
2✔
5059
}
10✔
5060

5061
TEST_CASE("app: switch user", "[sync][app][user]") {
4✔
5062
    OfflineAppSession oas;
4✔
5063
    auto app = oas.app();
4✔
5064

5065
    bool processed = false;
4✔
5066

5067
    SECTION("switch user expect success") {
4✔
5068
        CHECK(app->all_users().size() == 0);
2!
5069

5070
        // Log in user 1
5071
        auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password"));
2✔
5072
        CHECK(app->current_user() == user_a);
2!
5073

5074
        // Log in user 2
5075
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
5076
        CHECK(app->current_user() == user_b);
2!
5077

5078
        CHECK(app->all_users().size() == 2);
2!
5079

5080
        app->switch_user(user_a);
2✔
5081
        CHECK(app->current_user() == user_a);
2!
5082

5083
        app->switch_user(user_b);
2✔
5084

5085
        CHECK(app->current_user() == user_b);
2!
5086
        processed = true;
2✔
5087
        CHECK(processed);
2!
5088
    }
2✔
5089

5090
    SECTION("cannot switch to a logged out user") {
4✔
5091
        CHECK(app->all_users().size() == 0);
2!
5092

5093
        // Log in user 1
5094
        auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password"));
2✔
5095
        CHECK(app->current_user() == user_a);
2!
5096

5097
        app->log_out([&](Optional<AppError> error) {
2✔
5098
            REQUIRE_FALSE(error);
2!
5099
        });
2✔
5100

5101
        CHECK(app->current_user() == nullptr);
2!
5102
        CHECK(user_a->state() == SyncUser::State::LoggedOut);
2!
5103

5104
        // Log in user 2
5105
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
5106
        CHECK(app->current_user() == user_b);
2!
5107
        CHECK(app->all_users().size() == 2);
2!
5108

5109
        REQUIRE_THROWS_AS(app->switch_user(user_a), AppError);
2✔
5110
        CHECK(app->current_user() == user_b);
2!
5111
    }
2✔
5112
}
4✔
5113

5114
TEST_CASE("app: remove user", "[sync][app][user]") {
4✔
5115
    OfflineAppSession oas;
4✔
5116
    auto app = oas.app();
4✔
5117

5118
    SECTION("remove anonymous user") {
4✔
5119
        CHECK(app->all_users().size() == 0);
2!
5120

5121
        // Log in user 1
5122
        auto user_a = log_in(app);
2✔
5123
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
5124

5125
        app->log_out(user_a, [&](Optional<AppError> error) {
2✔
5126
            REQUIRE_FALSE(error);
2!
5127
            // a logged out anon user will be marked as Removed, not LoggedOut
5128
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
5129
        });
2✔
5130
        CHECK(app->all_users().empty());
2!
5131

5132
        app->remove_user(user_a, [&](Optional<AppError> error) {
2✔
5133
            CHECK(error->reason() == "User has already been removed");
2!
5134
            CHECK(app->all_users().size() == 0);
2!
5135
        });
2✔
5136

5137
        // Log in user 2
5138
        auto user_b = log_in(app);
2✔
5139
        CHECK(app->current_user() == user_b);
2!
5140
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
5141
        CHECK(app->all_users().size() == 1);
2!
5142

5143
        app->remove_user(user_b, [&](Optional<AppError> error) {
2✔
5144
            REQUIRE_FALSE(error);
2!
5145
            CHECK(app->all_users().size() == 0);
2!
5146
        });
2✔
5147

5148
        CHECK(app->current_user() == nullptr);
2!
5149

5150
        // check both handles are no longer valid
5151
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
5152
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
5153
    }
2✔
5154

5155
    SECTION("remove user with credentials") {
4✔
5156
        CHECK(app->all_users().size() == 0);
2!
5157
        CHECK(app->current_user() == nullptr);
2!
5158

5159
        auto user = log_in(app, AppCredentials::username_password("email", "pass"));
2✔
5160

5161
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
5162

5163
        app->log_out(user, [&](Optional<AppError> error) {
2✔
5164
            REQUIRE_FALSE(error);
2!
5165
        });
2✔
5166

5167
        CHECK(user->state() == SyncUser::State::LoggedOut);
2!
5168

5169
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
5170
            REQUIRE_FALSE(error);
2!
5171
        });
2✔
5172
        CHECK(app->all_users().size() == 0);
2!
5173

5174
        Optional<AppError> error;
2✔
5175
        app->remove_user(user, [&](Optional<AppError> err) {
2✔
5176
            error = err;
2✔
5177
        });
2✔
5178
        CHECK(error->code() > 0);
2!
5179
        CHECK(app->all_users().size() == 0);
2!
5180
        CHECK(user->state() == SyncUser::State::Removed);
2!
5181
    }
2✔
5182
}
4✔
5183

5184
TEST_CASE("app: link_user", "[sync][app][user]") {
4✔
5185
    OfflineAppSession oas;
4✔
5186
    auto app = oas.app();
4✔
5187

5188
    auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10));
4✔
5189
    auto password = random_string(10);
4✔
5190

5191
    auto custom_credentials = AppCredentials::facebook("a_token");
4✔
5192
    auto email_pass_credentials = AppCredentials::username_password(email, password);
4✔
5193

5194
    auto sync_user = log_in(app, email_pass_credentials);
4✔
5195
    REQUIRE(sync_user->identities().size() == 2);
4!
5196
    CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword);
4!
5197

5198
    SECTION("successful link") {
4✔
5199
        bool processed = false;
2✔
5200
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
5201
            REQUIRE_FALSE(error);
2!
5202
            REQUIRE(user);
2!
5203
            CHECK(user->user_id() == sync_user->user_id());
2!
5204
            processed = true;
2✔
5205
        });
2✔
5206
        CHECK(processed);
2!
5207
    }
2✔
5208

5209
    SECTION("link_user should fail when logged out") {
4✔
5210
        app->log_out([&](Optional<AppError> error) {
2✔
5211
            REQUIRE_FALSE(error);
2!
5212
        });
2✔
5213

5214
        bool processed = false;
2✔
5215
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
5216
            CHECK(error->reason() == "The specified user is not logged in.");
2!
5217
            CHECK(!user);
2!
5218
            processed = true;
2✔
5219
        });
2✔
5220
        CHECK(processed);
2!
5221
    }
2✔
5222
}
4✔
5223

5224
TEST_CASE("app: auth providers", "[sync][app][user]") {
20✔
5225
    SECTION("auth providers facebook") {
20✔
5226
        auto credentials = AppCredentials::facebook("a_token");
2✔
5227
        CHECK(credentials.provider() == AuthProvider::FACEBOOK);
2!
5228
        CHECK(credentials.provider_as_string() == IdentityProviderFacebook);
2!
5229
        CHECK(credentials.serialize_as_bson() ==
2!
5230
              bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}});
2✔
5231
    }
2✔
5232

5233
    SECTION("auth providers anonymous") {
20✔
5234
        auto credentials = AppCredentials::anonymous();
2✔
5235
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS);
2!
5236
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5237
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5238
    }
2✔
5239

5240
    SECTION("auth providers anonymous no reuse") {
20✔
5241
        auto credentials = AppCredentials::anonymous(false);
2✔
5242
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE);
2!
5243
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5244
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5245
    }
2✔
5246

5247
    SECTION("auth providers google authCode") {
20✔
5248
        auto credentials = AppCredentials::google(AuthCode("a_token"));
2✔
5249
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5250
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5251
        CHECK(credentials.serialize_as_bson() ==
2!
5252
              bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}});
2✔
5253
    }
2✔
5254

5255
    SECTION("auth providers google idToken") {
20✔
5256
        auto credentials = AppCredentials::google(IdToken("a_token"));
2✔
5257
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5258
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5259
        CHECK(credentials.serialize_as_bson() ==
2!
5260
              bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}});
2✔
5261
    }
2✔
5262

5263
    SECTION("auth providers apple") {
20✔
5264
        auto credentials = AppCredentials::apple("a_token");
2✔
5265
        CHECK(credentials.provider() == AuthProvider::APPLE);
2!
5266
        CHECK(credentials.provider_as_string() == IdentityProviderApple);
2!
5267
        CHECK(credentials.serialize_as_bson() ==
2!
5268
              bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}});
2✔
5269
    }
2✔
5270

5271
    SECTION("auth providers custom") {
20✔
5272
        auto credentials = AppCredentials::custom("a_token");
2✔
5273
        CHECK(credentials.provider() == AuthProvider::CUSTOM);
2!
5274
        CHECK(credentials.provider_as_string() == IdentityProviderCustom);
2!
5275
        CHECK(credentials.serialize_as_bson() ==
2!
5276
              bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}});
2✔
5277
    }
2✔
5278

5279
    SECTION("auth providers username password") {
20✔
5280
        auto credentials = AppCredentials::username_password("user", "pass");
2✔
5281
        CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD);
2!
5282
        CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword);
2!
5283
        CHECK(credentials.serialize_as_bson() ==
2!
5284
              bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}});
2✔
5285
    }
2✔
5286

5287
    SECTION("auth providers function") {
20✔
5288
        bson::BsonDocument function_params{{"name", "mongo"}};
2✔
5289
        auto credentials = AppCredentials::function(function_params);
2✔
5290
        CHECK(credentials.provider() == AuthProvider::FUNCTION);
2!
5291
        CHECK(credentials.provider_as_string() == IdentityProviderFunction);
2!
5292
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}});
2!
5293
    }
2✔
5294

5295
    SECTION("auth providers api key") {
20✔
5296
        auto credentials = AppCredentials::api_key("a key");
2✔
5297
        CHECK(credentials.provider() == AuthProvider::API_KEY);
2!
5298
        CHECK(credentials.provider_as_string() == IdentityProviderAPIKey);
2!
5299
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}});
2!
5300
        CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY);
2!
5301
    }
2✔
5302
}
20✔
5303

5304
TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") {
6✔
5305
    SECTION("refresh custom data happy path") {
6✔
5306
        static bool session_route_hit = false;
2✔
5307

5308
        struct transport : UnitTestTransport {
2✔
5309
            void send_request_to_server(const Request& request,
2✔
5310
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5311
            {
10✔
5312
                if (request.url.find("/session") != std::string::npos) {
10✔
5313
                    session_route_hit = true;
2✔
5314
                    nlohmann::json json{{"access_token", good_access_token}};
2✔
5315
                    completion({200, 0, {}, json.dump()});
2✔
5316
                }
2✔
5317
                else {
8✔
5318
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5319
                }
8✔
5320
            }
10✔
5321
        };
2✔
5322
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5323
        auto app = oas.app();
2✔
5324
        oas.make_user();
2✔
5325

5326
        bool processed = false;
2✔
5327
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5328
            REQUIRE_FALSE(error);
2!
5329
            CHECK(session_route_hit);
2!
5330
            processed = true;
2✔
5331
        });
2✔
5332
        CHECK(processed);
2!
5333
    }
2✔
5334

5335
    SECTION("refresh custom data sad path") {
6✔
5336
        static bool session_route_hit = false;
2✔
5337

5338
        struct transport : UnitTestTransport {
2✔
5339
            void send_request_to_server(const Request& request,
2✔
5340
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5341
            {
10✔
5342
                if (request.url.find("/session") != std::string::npos) {
10✔
5343
                    session_route_hit = true;
2✔
5344
                    nlohmann::json json{{"access_token", bad_access_token}};
2✔
5345
                    completion({200, 0, {}, json.dump()});
2✔
5346
                }
2✔
5347
                else {
8✔
5348
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5349
                }
8✔
5350
            }
10✔
5351
        };
2✔
5352

5353
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5354
        auto app = oas.app();
2✔
5355
        oas.make_user();
2✔
5356

5357
        bool processed = false;
2✔
5358
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5359
            CHECK(error->reason() == "malformed JWT");
2!
5360
            CHECK(error->code() == ErrorCodes::BadToken);
2!
5361
            CHECK(session_route_hit);
2!
5362
            processed = true;
2✔
5363
        });
2✔
5364
        CHECK(processed);
2!
5365
    }
2✔
5366

5367
    SECTION("refresh token ensure flow is correct") {
6✔
5368
        /*
5369
         Expected flow:
5370
         Location - first http request since app was just created
5371
         Login - this gets access and refresh tokens
5372
         Get profile - throw back a 401 error
5373
         Location - return location response
5374
         Refresh token - get a new token for the user
5375
         Get profile - get the profile with the new token
5376
         */
5377
        struct transport : GenericNetworkTransport {
2✔
5378
            enum class TestState { unknown, location, login, profile_1, refresh, profile_2 };
2✔
5379
            TestingStateMachine<TestState> state{TestState::unknown};
2✔
5380
            void send_request_to_server(const Request& request,
2✔
5381
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5382
            {
12✔
5383
                if (request.url.find("/login") != std::string::npos) {
12✔
5384
                    CHECK(state.get() == TestState::location);
2!
5385
                    state.transition_to(TestState::login);
2✔
5386
                    completion({200, 0, {}, user_json(good_access_token).dump()});
2✔
5387
                }
2✔
5388
                else if (request.url.find("/profile") != std::string::npos) {
10✔
5389
                    auto item = AppUtils::find_header("Authorization", request.headers);
4✔
5390
                    CHECK(item);
4!
5391
                    auto access_token = item->second;
4✔
5392
                    // simulated bad token request
5393
                    if (access_token.find(good_access_token2) != std::string::npos) {
4✔
5394
                        CHECK(state.get() == TestState::refresh);
2!
5395
                        state.transition_to(TestState::profile_2);
2✔
5396
                        completion({200, 0, {}, user_profile_json().dump()});
2✔
5397
                    }
2✔
5398
                    else if (access_token.find(good_access_token) != std::string::npos) {
2✔
5399
                        CHECK(state.get() == TestState::login);
2!
5400
                        state.transition_to(TestState::profile_1);
2✔
5401
                        completion({401, 0, {}});
2✔
5402
                    }
2✔
5403
                }
4✔
5404
                else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) {
6✔
5405
                    CHECK(state.get() == TestState::location);
2!
5406
                    state.transition_to(TestState::refresh);
2✔
5407
                    nlohmann::json json{{"access_token", good_access_token2}};
2✔
5408
                    completion({200, 0, {}, json.dump()});
2✔
5409
                }
2✔
5410
                else if (request.url.find("/location") != std::string::npos) {
4✔
5411
                    CHECK((state.get() == TestState::unknown || state.get() == TestState::profile_1));
4!
5412
                    state.transition_to(TestState::location);
4✔
5413
                    CHECK(request.method == HttpMethod::get);
4!
5414
                    completion({200,
4✔
5415
                                0,
4✔
5416
                                {},
4✔
5417
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
4✔
5418
                                "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"});
4✔
5419
                }
4✔
5420
                else {
×
5421
                    FAIL("Unexpected request in test code" + request.url);
×
5422
                }
×
5423
            }
12✔
5424
        };
2✔
5425

5426
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5427
        auto app = oas.app();
2✔
5428
        REQUIRE(log_in(app));
2!
5429
    }
2✔
5430
}
6✔
5431

5432
TEST_CASE("app: app released during async operation", "[app][user]") {
10✔
5433
    struct Transport : public UnitTestTransport {
10✔
5434
        std::string endpoint_to_hook;
10✔
5435
        std::optional<Request> stored_request;
10✔
5436
        util::UniqueFunction<void(const Response&)> stored_completion;
10✔
5437

5438
        void send_request_to_server(const Request& request,
10✔
5439
                                    util::UniqueFunction<void(const Response&)>&& completion) override
10✔
5440
        {
38✔
5441
            // Store the completion handler for the chosen endpoint so that we can
5442
            // invoke it after releasing the test's references to the App to
5443
            // verify that it doesn't crash
5444
            if (request.url.find(endpoint_to_hook) != std::string::npos) {
38✔
5445
                REQUIRE_FALSE(stored_request);
10!
5446
                REQUIRE_FALSE(stored_completion);
10!
5447
                stored_request = request;
10✔
5448
                stored_completion = std::move(completion);
10✔
5449
                return;
10✔
5450
            }
10✔
5451

5452
            UnitTestTransport::send_request_to_server(request, std::move(completion));
28✔
5453
        }
28✔
5454

5455
        bool has_stored() const
10✔
5456
        {
20✔
5457
            return !!stored_completion;
20✔
5458
        }
20✔
5459

5460
        void send_stored()
10✔
5461
        {
10✔
5462
            REQUIRE(stored_request);
10!
5463
            REQUIRE(stored_completion);
10!
5464
            UnitTestTransport::send_request_to_server(*stored_request, std::move(stored_completion));
10✔
5465
            stored_request.reset();
10✔
5466
            stored_completion = nullptr;
10✔
5467
        }
10✔
5468
    };
10✔
5469
    auto transport = std::make_shared<Transport>();
10✔
5470
    test_util::TestDirGuard base_path(util::make_temp_dir(), false);
10✔
5471
    AppConfig app_config;
10✔
5472
    set_app_config_defaults(app_config, transport);
10✔
5473
    app_config.base_file_path = base_path;
10✔
5474

5475
    SECTION("login") {
10✔
5476
        transport->endpoint_to_hook = GENERATE("/location", "/login", "/profile");
6✔
5477
        bool called = false;
6✔
5478
        {
6✔
5479
            auto app = App::get_app(App::CacheMode::Disabled, app_config);
6✔
5480
            app->log_in_with_credentials(AppCredentials::anonymous(),
6✔
5481
                                         [&](std::shared_ptr<SyncUser> user, util::Optional<AppError> error) mutable {
6✔
5482
                                             REQUIRE_FALSE(error);
6!
5483
                                             REQUIRE(user);
6!
5484
                                             REQUIRE(user->is_logged_in());
6!
5485
                                             called = true;
6✔
5486
                                         });
6✔
5487
            REQUIRE(transport->has_stored());
6!
5488
        }
6✔
5489
        REQUIRE_FALSE(called);
6!
5490
        transport->send_stored();
6✔
5491
        REQUIRE(called);
6!
5492
    }
6✔
5493

5494
    SECTION("access token refresh") {
10✔
5495
        transport->endpoint_to_hook = "/auth/session";
4✔
5496
        SECTION("directly via user") {
4✔
5497
            bool completion_called = false;
2✔
5498
            {
2✔
5499
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5500
                create_user_and_log_in(app);
2✔
5501
                app->current_user()->refresh_custom_data([&](std::optional<app::AppError> error) {
2✔
5502
                    REQUIRE_FALSE(error);
2!
5503
                    completion_called = true;
2✔
5504
                });
2✔
5505
                REQUIRE(transport->has_stored());
2!
5506
            }
2✔
5507

5508
            REQUIRE_FALSE(completion_called);
2!
5509
            transport->send_stored();
2✔
5510
            REQUIRE(completion_called);
2!
5511
        }
2✔
5512

5513
        SECTION("via sync session") {
4✔
5514
            {
2✔
5515
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5516
                create_user_and_log_in(app);
2✔
5517
                auto user = app->current_user();
2✔
5518
                SyncTestFile config(user, bson::Bson("test"));
2✔
5519
                // give the user an expired access token so that the first use will try to refresh it
5520
                user->update_data_for_testing([](auto& data) {
2✔
5521
                    data.access_token = RealmJWT(encode_fake_jwt("token", 123, 456));
2✔
5522
                });
2✔
5523
                REQUIRE_FALSE(transport->stored_completion);
2!
5524
                auto realm = Realm::get_shared_realm(config);
2✔
5525
                REQUIRE(transport->has_stored());
2!
5526
            }
2✔
5527
            transport->send_stored();
2✔
5528
        }
2✔
5529
    }
4✔
5530

5531
    REQUIRE_FALSE(transport->has_stored());
10!
5532
}
10✔
5533

5534
TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") {
8✔
5535
    constexpr uint64_t timeout_ms = 60000; // this is the default
8✔
5536
    OfflineAppSession oas({std::make_shared<UnitTestTransport>(timeout_ms)});
8✔
5537
    auto app = oas.app();
8✔
5538

5539
    auto user = log_in(app);
8✔
5540

5541
    using Headers = decltype(Request().headers);
8✔
5542

5543
    const auto url_prefix = "https://some.fake.url/api/client/v2.0/app/app_id/functions/call?baas_request="sv;
8✔
5544
    const auto get_request_args = [&](const Request& req) {
8✔
5545
        REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix);
8!
5546
        auto args = req.url.substr(url_prefix.size());
8✔
5547
        if (auto amp = args.find('&'); amp != std::string::npos) {
8✔
5548
            args.resize(amp);
2✔
5549
        }
2✔
5550

5551
        auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args));
8✔
5552
        REQUIRE(!!vec);
8!
5553
        auto parsed = bson::parse({vec->data(), vec->size()});
8✔
5554
        REQUIRE(parsed.type() == bson::Bson::Type::Document);
8!
5555
        auto out = parsed.operator const bson::BsonDocument&();
8✔
5556
        CHECK(out.size() == 3);
8!
5557
        return out;
8✔
5558
    };
8✔
5559

5560
    const auto make_request = [&](std::shared_ptr<User> user, auto&&... args) {
8✔
5561
        auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"});
8✔
5562
        CHECK(req.method == HttpMethod::get);
8!
5563
        CHECK(req.body == "");
8!
5564
        CHECK(req.headers == Headers{{"Accept", "text/event-stream"}});
8!
5565
        CHECK(req.timeout_ms == timeout_ms);
8!
5566

5567
        auto req_args = get_request_args(req);
8✔
5568
        CHECK(req_args["name"] == "func");
8!
5569
        CHECK(req_args["service"] == "svc");
8!
5570
        CHECK(req_args["arguments"] == bson::BsonArray{args...});
8!
5571

5572
        return req;
8✔
5573
    };
8✔
5574

5575
    SECTION("no args") {
8✔
5576
        auto req = make_request(nullptr);
2✔
5577
        CHECK(req.url.find('&') == std::string::npos);
2!
5578
    }
2✔
5579
    SECTION("args") {
8✔
5580
        auto req = make_request(nullptr, "arg1", "arg2");
2✔
5581
        CHECK(req.url.find('&') == std::string::npos);
2!
5582
    }
2✔
5583
    SECTION("percent encoding") {
8✔
5584
        // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded.
5585
        auto req = make_request(nullptr, ">>>>>?????");
2✔
5586

5587
        CHECK(req.url.find('&') == std::string::npos);
2!
5588
        CHECK_THAT(req.url, ContainsSubstring("%2B"));     // + (from >)
2✔
5589
        CHECK_THAT(req.url, ContainsSubstring("%2F"));     // / (from ?)
2✔
5590
        CHECK_THAT(req.url, ContainsSubstring("%3D"));     // = (tail padding)
2✔
5591
        CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding)
2!
5592
    }
2✔
5593
    SECTION("with user") {
8✔
5594
        auto req = make_request(user, "arg1", "arg2");
2✔
5595

5596
        auto amp = req.url.find('&');
2✔
5597
        REQUIRE(amp != std::string::npos);
2!
5598
        auto tail = req.url.substr(amp);
2✔
5599
        REQUIRE(tail == ("&baas_at=" + user->access_token()));
2!
5600
    }
2✔
5601
}
8✔
5602

5603
TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") {
4✔
5604
    SECTION("with empty map") {
4✔
5605
        auto profile = UserProfile(bson::BsonDocument());
2✔
5606
        CHECK(profile.name() == util::none);
2!
5607
        CHECK(profile.email() == util::none);
2!
5608
        CHECK(profile.picture_url() == util::none);
2!
5609
        CHECK(profile.first_name() == util::none);
2!
5610
        CHECK(profile.last_name() == util::none);
2!
5611
        CHECK(profile.gender() == util::none);
2!
5612
        CHECK(profile.birthday() == util::none);
2!
5613
        CHECK(profile.min_age() == util::none);
2!
5614
        CHECK(profile.max_age() == util::none);
2!
5615
    }
2✔
5616
    SECTION("with full map") {
4✔
5617
        auto profile = UserProfile(bson::BsonDocument({
2✔
5618
            {"first_name", "Jan"},
2✔
5619
            {"last_name", "Jaanson"},
2✔
5620
            {"name", "Jan Jaanson"},
2✔
5621
            {"email", "jan.jaanson@jaanson.com"},
2✔
5622
            {"gender", "none"},
2✔
5623
            {"birthday", "January 1, 1970"},
2✔
5624
            {"min_age", "0"},
2✔
5625
            {"max_age", "100"},
2✔
5626
            {"picture_url", "some"},
2✔
5627
        }));
2✔
5628
        CHECK(profile.name() == "Jan Jaanson");
2!
5629
        CHECK(profile.email() == "jan.jaanson@jaanson.com");
2!
5630
        CHECK(profile.picture_url() == "some");
2!
5631
        CHECK(profile.first_name() == "Jan");
2!
5632
        CHECK(profile.last_name() == "Jaanson");
2!
5633
        CHECK(profile.gender() == "none");
2!
5634
        CHECK(profile.birthday() == "January 1, 1970");
2!
5635
        CHECK(profile.min_age() == "0");
2!
5636
        CHECK(profile.max_age() == "100");
2!
5637
    }
2✔
5638
}
4✔
5639

5640
TEST_CASE("app: shared instances", "[sync][app]") {
2✔
5641
    test_util::TestDirGuard test_dir(util::make_temp_dir(), false);
2✔
5642

5643
    AppConfig base_config;
2✔
5644
    set_app_config_defaults(base_config, instance_of<UnitTestTransport>);
2✔
5645
    base_config.base_file_path = test_dir;
2✔
5646

5647
    auto config1 = base_config;
2✔
5648
    config1.app_id = "app1";
2✔
5649

5650
    auto config2 = base_config;
2✔
5651
    config2.app_id = "app1";
2✔
5652
    config2.base_url = std::string(App::default_base_url());
2✔
5653

5654
    auto config3 = base_config;
2✔
5655
    config3.app_id = "app2";
2✔
5656

5657
    auto config4 = base_config;
2✔
5658
    config4.app_id = "app2";
2✔
5659
    config4.base_url = "http://localhost:9090";
2✔
5660

5661
    // should all point to same underlying app
5662
    auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5663
    auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5664
    auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url);
2✔
5665
    auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2);
2✔
5666
    auto app1_5 = App::get_cached_app(config1.app_id);
2✔
5667

5668
    CHECK(app1_1 == app1_2);
2!
5669
    CHECK(app1_1 == app1_3);
2!
5670
    CHECK(app1_1 == app1_4);
2!
5671
    CHECK(app1_1 == app1_5);
2!
5672

5673
    // config3 and config4 should point to different apps
5674
    auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3);
2✔
5675
    auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url);
2✔
5676
    auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4);
2✔
5677
    auto app2_4 = App::get_cached_app(config3.app_id);
2✔
5678
    auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url");
2✔
5679

5680
    CHECK(app2_1 == app2_2);
2!
5681
    CHECK(app2_1 != app2_3);
2!
5682
    CHECK(app2_4 != nullptr);
2!
5683
    CHECK(app2_5 == nullptr);
2!
5684

5685
    CHECK(app1_1 != app2_1);
2!
5686
    CHECK(app1_1 != app2_3);
2!
5687
    CHECK(app1_1 != app2_4);
2!
5688
}
2✔
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