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

realm / realm-core / github_pull_request_275914

25 Sep 2023 03:10PM UTC coverage: 92.915% (+1.7%) from 91.215%
github_pull_request_275914

Pull #6073

Evergreen

jedelbo
Merge tag 'v13.21.0' into next-major

"Feature/Bugfix release"
Pull Request #6073: Merge next-major

96928 of 177706 branches covered (0.0%)

8324 of 8714 new or added lines in 122 files covered. (95.52%)

181 existing lines in 28 files now uncovered.

247505 of 266379 relevant lines covered (92.91%)

7164945.17 hits per line

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

98.96
/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/sync_test_utils.hpp"
22
#include "util/unit_test_transport.hpp"
23

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

44
#include <catch2/catch_all.hpp>
45
#include <external/json/json.hpp>
46
#include <external/mpark/variant.hpp>
47

48
#include <condition_variable>
49
#include <future>
50
#include <iostream>
51
#include <list>
52
#include <mutex>
53

54
using namespace realm;
55
using namespace realm::app;
56
using util::any_cast;
57
using util::Optional;
58

59
using namespace std::string_view_literals;
60
using namespace std::literals::string_literals;
61

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

78
AppError failed_log_in(std::shared_ptr<App> app, AppCredentials credentials = AppCredentials::anonymous())
79
{
16✔
80
    Optional<AppError> err;
16✔
81
    app->log_in_with_credentials(credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
16✔
82
        REQUIRE(error);
16!
83
        REQUIRE_FALSE(user);
16!
84
        err = error;
16✔
85
    });
16✔
86
    REQUIRE(err);
16!
87
    return *err;
16✔
88
}
16✔
89

90
} // namespace
91

92
namespace realm {
93
class TestHelper {
94
public:
95
    static DBRef get_db(Realm& realm)
96
    {
2✔
97
        return Realm::Internal::get_db(realm);
2✔
98
    }
2✔
99
};
100
} // namespace realm
101

102
#if REALM_ENABLE_AUTH_TESTS
103

104
#include <realm/util/sha_crypto.hpp>
105

106
static std::string create_jwt(const std::string& appId)
107
{
2✔
108
    nlohmann::json header = {{"alg", "HS256"}, {"typ", "JWT"}};
2✔
109
    nlohmann::json payload = {{"aud", appId}, {"sub", "someUserId"}, {"exp", 1961896476}};
2✔
110

1✔
111
    payload["user_data"]["name"] = "Foo Bar";
2✔
112
    payload["user_data"]["occupation"] = "firefighter";
2✔
113

1✔
114
    payload["my_metadata"]["name"] = "Bar Foo";
2✔
115
    payload["my_metadata"]["occupation"] = "stock analyst";
2✔
116

1✔
117
    std::string headerStr = header.dump();
2✔
118
    std::string payloadStr = payload.dump();
2✔
119

1✔
120
    std::string encoded_header;
2✔
121
    encoded_header.resize(util::base64_encoded_size(headerStr.length()));
2✔
122
    util::base64_encode(headerStr.data(), headerStr.length(), encoded_header.data(), encoded_header.size());
2✔
123

1✔
124
    std::string encoded_payload;
2✔
125
    encoded_payload.resize(util::base64_encoded_size(payloadStr.length()));
2✔
126
    util::base64_encode(payloadStr.data(), payloadStr.length(), encoded_payload.data(), encoded_payload.size());
2✔
127

1✔
128
    // Remove padding characters.
1✔
129
    while (encoded_header.back() == '=')
2✔
130
        encoded_header.pop_back();
×
131
    while (encoded_payload.back() == '=')
4✔
132
        encoded_payload.pop_back();
2✔
133

1✔
134
    std::string jwtPayload = encoded_header + "." + encoded_payload;
2✔
135

1✔
136
    std::array<unsigned char, 32> hmac;
2✔
137
    unsigned char key[] = "My_very_confidential_secretttttt";
2✔
138
    util::hmac_sha256(util::unsafe_span_cast<unsigned char>(jwtPayload), hmac, util::Span<uint8_t, 32>(key, 32));
2✔
139

1✔
140
    std::string signature;
2✔
141
    signature.resize(util::base64_encoded_size(hmac.size()));
2✔
142
    util::base64_encode(reinterpret_cast<char*>(hmac.data()), hmac.size(), signature.data(), signature.size());
2✔
143
    while (signature.back() == '=')
4✔
144
        signature.pop_back();
2✔
145
    std::replace(signature.begin(), signature.end(), '+', '-');
2✔
146
    std::replace(signature.begin(), signature.end(), '/', '_');
2✔
147

1✔
148
    return jwtPayload + "." + signature;
2✔
149
}
2✔
150

151
// MARK: - Verify AppError with all error codes
152
TEST_CASE("app: verify app error codes", "[sync][app][local]") {
2✔
153
    auto error_codes = ErrorCodes::get_error_list();
2✔
154
    std::vector<std::pair<int, std::string>> http_status_codes = {
2✔
155
        {0, ""},
2✔
156
        {100, "http error code considered fatal: some http error. Informational: 100"},
2✔
157
        {200, ""},
2✔
158
        {300, "http error code considered fatal: some http error. Redirection: 300"},
2✔
159
        {400, "http error code considered fatal: some http error. Client Error: 400"},
2✔
160
        {500, "http error code considered fatal: some http error. Server Error: 500"},
2✔
161
        {600, "http error code considered fatal: some http error. Unknown HTTP Error: 600"}};
2✔
162

1✔
163
    auto make_http_error = [](std::optional<std::string_view> error_code, int http_status = 500,
2✔
164
                              std::optional<std::string_view> error = "some error",
2✔
165
                              std::optional<std::string_view> link = "http://dummy-link/") -> app::Response {
324✔
166
        nlohmann::json body;
324✔
167
        if (error_code) {
324✔
168
            body["error_code"] = *error_code;
320✔
169
        }
320✔
170
        if (error) {
324✔
171
            body["error"] = *error;
322✔
172
        }
322✔
173
        if (link) {
324✔
174
            body["link"] = *link;
324✔
175
        }
324✔
176

162✔
177
        return {
324✔
178
            http_status,
324✔
179
            0,
324✔
180
            {{"Content-Type", "application/json"}},
324✔
181
            body.empty() ? "{}" : body.dump(),
324✔
182
        };
324✔
183
    };
324✔
184

1✔
185
    // Success response
1✔
186
    app::Response response = {200, 0, {}, ""};
2✔
187
    auto app_error = AppUtils::check_for_errors(response);
2✔
188
    REQUIRE(!app_error);
2!
189

1✔
190
    // Empty error code
1✔
191
    response = make_http_error("");
2✔
192
    app_error = AppUtils::check_for_errors(response);
2✔
193
    REQUIRE(app_error);
2!
194
    REQUIRE(app_error->code() == ErrorCodes::AppUnknownError);
2!
195
    REQUIRE(app_error->code_string() == "AppUnknownError");
2!
196
    REQUIRE(app_error->server_error.empty());
2!
197
    REQUIRE(app_error->reason() == "some error");
2!
198
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
199
    REQUIRE(*app_error->additional_status_code == 500);
2!
200

1✔
201
    // Missing error code
1✔
202
    response = make_http_error(std::nullopt);
2✔
203
    app_error = AppUtils::check_for_errors(response);
2✔
204
    REQUIRE(app_error);
2!
205
    REQUIRE(app_error->code() == ErrorCodes::AppUnknownError);
2!
206
    REQUIRE(app_error->code_string() == "AppUnknownError");
2!
207
    REQUIRE(app_error->server_error.empty());
2!
208
    REQUIRE(app_error->reason() == "some error");
2!
209
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
210
    REQUIRE(*app_error->additional_status_code == 500);
2!
211

1✔
212
    // Missing error code and error message with success http status
1✔
213
    response = make_http_error(std::nullopt, 200, std::nullopt);
2✔
214
    app_error = AppUtils::check_for_errors(response);
2✔
215
    REQUIRE(!app_error);
2!
216

1✔
217
    for (auto [name, error] : error_codes) {
320✔
218
        // All error codes should not cause an exception
160✔
219
        if (error != ErrorCodes::HTTPError && error != ErrorCodes::OK) {
320✔
220
            response = make_http_error(name);
316✔
221
            app_error = AppUtils::check_for_errors(response);
316✔
222
            REQUIRE(app_error);
316!
223
            if (ErrorCodes::error_categories(error).test(ErrorCategory::app_error)) {
316✔
224
                REQUIRE(app_error->code() == error);
130!
225
                REQUIRE(app_error->code_string() == name);
130!
226
            }
130✔
227
            else {
186✔
228
                REQUIRE(app_error->code() == ErrorCodes::AppServerError);
186!
229
                REQUIRE(app_error->code_string() == "AppServerError");
186!
230
            }
186✔
231
            REQUIRE(app_error->server_error == name);
316!
232
            REQUIRE(app_error->reason() == "some error");
316!
233
            REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
316!
234
            REQUIRE(app_error->additional_status_code);
316!
235
            REQUIRE(*app_error->additional_status_code == 500);
316!
236
        }
316✔
237
    }
320✔
238

1✔
239
    response = make_http_error("AppErrorMissing", 404);
2✔
240
    app_error = AppUtils::check_for_errors(response);
2✔
241
    REQUIRE(app_error);
2!
242
    REQUIRE(app_error->code() == ErrorCodes::AppServerError);
2!
243
    REQUIRE(app_error->code_string() == "AppServerError");
2!
244
    REQUIRE(app_error->server_error == "AppErrorMissing");
2!
245
    REQUIRE(app_error->reason() == "some error");
2!
246
    REQUIRE(app_error->link_to_server_logs == "http://dummy-link/");
2!
247
    REQUIRE(app_error->additional_status_code);
2!
248
    REQUIRE(*app_error->additional_status_code == 404);
2!
249

1✔
250
    // HTTPError with different status values
1✔
251
    for (auto [status, message] : http_status_codes) {
14✔
252
        response = {
14✔
253
            status,
14✔
254
            0,
14✔
255
            {},
14✔
256
            "some http error",
14✔
257
        };
14✔
258
        app_error = AppUtils::check_for_errors(response);
14✔
259
        if (message.empty()) {
14✔
260
            REQUIRE(!app_error);
4!
261
            continue;
4✔
262
        }
10✔
263
        REQUIRE(app_error);
10!
264
        REQUIRE(app_error->code() == ErrorCodes::HTTPError);
10!
265
        REQUIRE(app_error->code_string() == "HTTPError");
10!
266
        REQUIRE(app_error->server_error.empty());
10!
267
        REQUIRE(app_error->reason() == message);
10!
268
        REQUIRE(app_error->link_to_server_logs.empty());
10!
269
        REQUIRE(app_error->additional_status_code);
10!
270
        REQUIRE(*app_error->additional_status_code == status);
10!
271
    }
10✔
272

1✔
273
    // Missing error code and error message with fatal http status
1✔
274
    response = {
2✔
275
        501,
2✔
276
        0,
2✔
277
        {},
2✔
278
        "",
2✔
279
    };
2✔
280
    app_error = AppUtils::check_for_errors(response);
2✔
281
    REQUIRE(app_error);
2!
282
    REQUIRE(app_error->code() == ErrorCodes::HTTPError);
2!
283
    REQUIRE(app_error->code_string() == "HTTPError");
2!
284
    REQUIRE(app_error->server_error.empty());
2!
285
    REQUIRE(app_error->reason() == "http error code considered fatal. Server Error: 501");
2!
286
    REQUIRE(app_error->link_to_server_logs.empty());
2!
287
    REQUIRE(app_error->additional_status_code);
2!
288
    REQUIRE(*app_error->additional_status_code == 501);
2!
289

1✔
290
    // Valid client error code, with body, but no json
1✔
291
    app::Response client_response = {
2✔
292
        501,
2✔
293
        0,
2✔
294
        {},
2✔
295
        "Some error occurred",
2✔
296
        ErrorCodes::BadBsonParse, // client_error_code
2✔
297
    };
2✔
298
    app_error = AppUtils::check_for_errors(client_response);
2✔
299
    REQUIRE(app_error);
2!
300
    REQUIRE(app_error->code() == ErrorCodes::BadBsonParse);
2!
301
    REQUIRE(app_error->code_string() == "BadBsonParse");
2!
302
    REQUIRE(app_error->server_error.empty());
2!
303
    REQUIRE(app_error->reason() == "Some error occurred");
2!
304
    REQUIRE(app_error->link_to_server_logs.empty());
2!
305
    REQUIRE(app_error->additional_status_code);
2!
306
    REQUIRE(*app_error->additional_status_code == 501);
2!
307

1✔
308
    // Same response with client error code, but no body
1✔
309
    client_response.body = "";
2✔
310
    app_error = AppUtils::check_for_errors(client_response);
2✔
311
    REQUIRE(app_error);
2!
312
    REQUIRE(app_error->reason() == "client error code value considered fatal");
2!
313

1✔
314
    // Valid custom status code, with body, but no json
1✔
315
    app::Response custom_response = {501,
2✔
316
                                     4999, // custom_status_code
2✔
317
                                     {},
2✔
318
                                     "Some custom error occurred"};
2✔
319
    app_error = AppUtils::check_for_errors(custom_response);
2✔
320
    REQUIRE(app_error);
2!
321
    REQUIRE(app_error->code() == ErrorCodes::CustomError);
2!
322
    REQUIRE(app_error->code_string() == "CustomError");
2!
323
    REQUIRE(app_error->server_error.empty());
2!
324
    REQUIRE(app_error->reason() == "Some custom error occurred");
2!
325
    REQUIRE(app_error->link_to_server_logs.empty());
2!
326
    REQUIRE(app_error->additional_status_code);
2!
327
    REQUIRE(*app_error->additional_status_code == 4999);
2!
328

1✔
329
    // Same response with custom status code, but no body
1✔
330
    custom_response.body = "";
2✔
331
    app_error = AppUtils::check_for_errors(custom_response);
2✔
332
    REQUIRE(app_error);
2!
333
    REQUIRE(app_error->reason() == "non-zero custom status code considered fatal");
2!
334
}
2✔
335

336
// MARK: - Login with Credentials Tests
337

338
TEST_CASE("app: login_with_credentials integration", "[sync][app][user][baas]") {
2✔
339
    SECTION("login") {
2✔
340
        TestAppSession session;
2✔
341
        auto app = session.app();
2✔
342
        app->log_out([](auto) {});
2✔
343

1✔
344
        int subscribe_processed = 0;
2✔
345
        auto token = app->subscribe([&subscribe_processed](auto& app) {
4✔
346
            if (!subscribe_processed) {
4✔
347
                REQUIRE(app.current_user());
2!
348
            }
2✔
349
            else {
2✔
350
                REQUIRE_FALSE(app.current_user());
2!
351
            }
2✔
352
            subscribe_processed++;
4✔
353
        });
4✔
354

1✔
355
        auto user = log_in(app);
2✔
356
        CHECK(!user->device_id().empty());
2!
357
        CHECK(user->has_device_id());
2!
358

1✔
359
        bool processed = false;
2✔
360
        app->log_out([&](auto error) {
2✔
361
            REQUIRE_FALSE(error);
2!
362
            processed = true;
2✔
363
        });
2✔
364

1✔
365
        CHECK(processed);
2!
366
        CHECK(subscribe_processed == 2);
2!
367

1✔
368
        app->unsubscribe(token);
2✔
369
    }
2✔
370
}
2✔
371

372
// MARK: - UsernamePasswordProviderClient Tests
373

374
TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][baas]") {
26✔
375
    const std::string base_url = get_base_url();
26✔
376
    AutoVerifiedEmailCredentials creds;
26✔
377
    auto email = creds.email;
26✔
378
    auto password = creds.password;
26✔
379

13✔
380
    TestAppSession session;
26✔
381
    auto app = session.app();
26✔
382
    auto client = app->provider_client<App::UsernamePasswordProviderClient>();
26✔
383

13✔
384
    bool processed = false;
26✔
385

13✔
386
    client.register_email(email, password, [&](Optional<AppError> error) {
26✔
387
        CAPTURE(email);
26✔
388
        CAPTURE(password);
26✔
389
        REQUIRE_FALSE(error); // first registration success
26!
390
    });
26✔
391

13✔
392
    SECTION("double registration should fail") {
26✔
393
        client.register_email(email, password, [&](Optional<AppError> error) {
2✔
394
            // Error returned states the account has already been created
1✔
395
            REQUIRE(error);
2!
396
            CHECK(error->reason() == "name already in use");
2!
397
            CHECK(error->code() == ErrorCodes::AccountNameInUse);
2!
398
            CHECK(!error->link_to_server_logs.empty());
2!
399
            CHECK(error->link_to_server_logs.find(base_url) != std::string::npos);
2!
400
            processed = true;
2✔
401
        });
2✔
402
        CHECK(processed);
2!
403
    }
2✔
404

13✔
405
    SECTION("double registration should fail") {
26✔
406
        // the server registration function will reject emails that do not contain "realm_tests_do_autoverify"
1✔
407
        std::string email_to_reject = util::format("%1@%2.com", random_string(10), random_string(10));
2✔
408
        client.register_email(email_to_reject, password, [&](Optional<AppError> error) {
2✔
409
            REQUIRE(error);
2!
410
            CHECK(error->reason() == util::format("failed to confirm user \"%1\"", email_to_reject));
2!
411
            CHECK(error->code() == ErrorCodes::BadRequest);
2!
412
            processed = true;
2✔
413
        });
2✔
414
        CHECK(processed);
2!
415
    }
2✔
416

13✔
417
    SECTION("can login with registered account") {
26✔
418
        auto user = log_in(app, creds);
2✔
419
        CHECK(user->user_profile().email() == email);
2!
420
    }
2✔
421

13✔
422
    SECTION("cannot login with wrong password") {
26✔
423
        app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"),
2✔
424
                                     [&](std::shared_ptr<realm::SyncUser> user, Optional<AppError> error) {
2✔
425
                                         CHECK(!user);
2!
426
                                         REQUIRE(error);
2!
427
                                         REQUIRE(error->code() == ErrorCodes::InvalidPassword);
2!
428
                                         processed = true;
2✔
429
                                     });
2✔
430
        CHECK(processed);
2!
431
    }
2✔
432

13✔
433
    SECTION("confirm user") {
26✔
434
        client.confirm_user("a_token", "a_token_id", [&](Optional<AppError> error) {
2✔
435
            REQUIRE(error);
2!
436
            CHECK(error->reason() == "invalid token data");
2!
437
            processed = true;
2✔
438
        });
2✔
439
        CHECK(processed);
2!
440
    }
2✔
441

13✔
442
    SECTION("resend confirmation email") {
26✔
443
        client.resend_confirmation_email(email, [&](Optional<AppError> error) {
2✔
444
            REQUIRE(error);
2!
445
            CHECK(error->reason() == "already confirmed");
2!
446
            processed = true;
2✔
447
        });
2✔
448
        CHECK(processed);
2!
449
    }
2✔
450

13✔
451
    SECTION("reset password invalid tokens") {
26✔
452
        client.reset_password(password, "token_sample", "token_id_sample", [&](Optional<AppError> error) {
2✔
453
            REQUIRE(error);
2!
454
            CHECK(error->reason() == "invalid token data");
2!
455
            CHECK(!error->link_to_server_logs.empty());
2!
456
            CHECK(error->link_to_server_logs.find(base_url) != std::string::npos);
2!
457
            processed = true;
2✔
458
        });
2✔
459
        CHECK(processed);
2!
460
    }
2✔
461

13✔
462
    SECTION("reset password function success") {
26✔
463
        // the imported test app will accept password reset if the password contains "realm_tests_do_reset" via a
1✔
464
        // function
1✔
465
        std::string accepted_new_password = util::format("realm_tests_do_reset%1", random_string(10));
2✔
466
        client.call_reset_password_function(email, accepted_new_password, {}, [&](Optional<AppError> error) {
2✔
467
            REQUIRE_FALSE(error);
2!
468
            processed = true;
2✔
469
        });
2✔
470
        CHECK(processed);
2!
471
    }
2✔
472

13✔
473
    SECTION("reset password function failure") {
26✔
474
        std::string rejected_password = util::format("%1", random_string(10));
2✔
475
        client.call_reset_password_function(email, rejected_password, {"foo", "bar"}, [&](Optional<AppError> error) {
2✔
476
            REQUIRE(error);
2!
477
            CHECK(error->reason() == util::format("failed to reset password for user \"%1\"", email));
2!
478
            CHECK(error->is_service_error());
2!
479
            processed = true;
2✔
480
        });
2✔
481
        CHECK(processed);
2!
482
    }
2✔
483

13✔
484
    SECTION("reset password function for invalid user fails") {
26✔
485
        client.call_reset_password_function(util::format("%1@%2.com", random_string(5), random_string(5)), password,
2✔
486
                                            {"foo", "bar"}, [&](Optional<AppError> error) {
2✔
487
                                                REQUIRE(error);
2!
488
                                                CHECK(error->reason() == "user not found");
2!
489
                                                CHECK(error->is_service_error());
2!
490
                                                CHECK(error->code() == ErrorCodes::UserNotFound);
2!
491
                                                processed = true;
2✔
492
                                            });
2✔
493
        CHECK(processed);
2!
494
    }
2✔
495

13✔
496
    SECTION("retry custom confirmation") {
26✔
497
        client.retry_custom_confirmation(email, [&](Optional<AppError> error) {
2✔
498
            REQUIRE(error);
2!
499
            CHECK(error->reason() == "already confirmed");
2!
500
            processed = true;
2✔
501
        });
2✔
502
        CHECK(processed);
2!
503
    }
2✔
504

13✔
505
    SECTION("retry custom confirmation for invalid user fails") {
26✔
506
        client.retry_custom_confirmation(util::format("%1@%2.com", random_string(5), random_string(5)),
2✔
507
                                         [&](Optional<AppError> error) {
2✔
508
                                             REQUIRE(error);
2!
509
                                             CHECK(error->reason() == "user not found");
2!
510
                                             CHECK(error->is_service_error());
2!
511
                                             CHECK(error->code() == ErrorCodes::UserNotFound);
2!
512
                                             processed = true;
2✔
513
                                         });
2✔
514
        CHECK(processed);
2!
515
    }
2✔
516

13✔
517
    SECTION("log in, remove, log in") {
26✔
518
        app->remove_user(app->current_user(), [](auto) {});
2✔
519
        CHECK(app->sync_manager()->all_users().size() == 0);
2!
520
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
521

1✔
522
        auto user = log_in(app, AppCredentials::username_password(email, password));
2✔
523
        CHECK(user->user_profile().email() == email);
2!
524
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
525

1✔
526
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
527
            REQUIRE_FALSE(error);
2!
528
        });
2✔
529
        CHECK(user->state() == SyncUser::State::Removed);
2!
530

1✔
531
        log_in(app, AppCredentials::username_password(email, password));
2✔
532
        CHECK(user->state() == SyncUser::State::Removed);
2!
533
        CHECK(app->current_user() != user);
2!
534
        user = app->current_user();
2✔
535
        CHECK(user->user_profile().email() == email);
2!
536
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
537

1✔
538
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
539
            REQUIRE(!error);
2!
540
            CHECK(app->sync_manager()->all_users().size() == 0);
2!
541
            processed = true;
2✔
542
        });
2✔
543

1✔
544
        CHECK(user->state() == SyncUser::State::Removed);
2!
545
        CHECK(processed);
2!
546
        CHECK(app->all_users().size() == 0);
2!
547
    }
2✔
548
}
26✔
549

550
// MARK: - UserAPIKeyProviderClient Tests
551

552
TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baas]") {
6✔
553
    TestAppSession session;
6✔
554
    auto app = session.app();
6✔
555
    auto client = app->provider_client<App::UserAPIKeyProviderClient>();
6✔
556

3✔
557
    bool processed = false;
6✔
558
    App::UserAPIKey api_key;
6✔
559

3✔
560
    SECTION("api-key") {
6✔
561
        std::shared_ptr<SyncUser> logged_in_user = app->current_user();
2✔
562
        auto api_key_name = util::format("%1", random_string(15));
2✔
563
        client.create_api_key(api_key_name, logged_in_user,
2✔
564
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
565
                                  REQUIRE_FALSE(error);
2!
566
                                  CHECK(user_api_key.name == api_key_name);
2!
567
                                  api_key = user_api_key;
2✔
568
                              });
2✔
569

1✔
570
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
571
            REQUIRE_FALSE(error);
2!
572
            CHECK(user_api_key.name == api_key_name);
2!
573
            CHECK(user_api_key.id == api_key.id);
2!
574
        });
2✔
575

1✔
576
        client.fetch_api_keys(logged_in_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
577
            CHECK(api_keys.size() == 1);
2!
578
            for (auto key : api_keys) {
2✔
579
                CHECK(key.id.to_string() == api_key.id.to_string());
2!
580
                CHECK(api_key.name == api_key_name);
2!
581
                CHECK(key.id == api_key.id);
2!
582
            }
2✔
583
            REQUIRE_FALSE(error);
2!
584
        });
2✔
585

1✔
586
        client.enable_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
587
            REQUIRE_FALSE(error);
2!
588
        });
2✔
589

1✔
590
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
591
            REQUIRE_FALSE(error);
2!
592
            CHECK(user_api_key.disabled == false);
2!
593
            CHECK(user_api_key.name == api_key_name);
2!
594
            CHECK(user_api_key.id == api_key.id);
2!
595
        });
2✔
596

1✔
597
        client.disable_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
598
            REQUIRE_FALSE(error);
2!
599
        });
2✔
600

1✔
601
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
602
            REQUIRE_FALSE(error);
2!
603
            CHECK(user_api_key.disabled == true);
2!
604
            CHECK(user_api_key.name == api_key_name);
2!
605
        });
2✔
606

1✔
607
        client.delete_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
608
            REQUIRE_FALSE(error);
2!
609
        });
2✔
610

1✔
611
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
612
            CHECK(user_api_key.name == "");
2!
613
            CHECK(error);
2!
614
            processed = true;
2✔
615
        });
2✔
616

1✔
617
        CHECK(processed);
2!
618
    }
2✔
619

3✔
620
    SECTION("api-key without a user") {
6✔
621
        std::shared_ptr<SyncUser> no_user = nullptr;
2✔
622
        auto api_key_name = util::format("%1", random_string(15));
2✔
623
        client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
624
            REQUIRE(error);
2!
625
            CHECK(error->is_service_error());
2!
626
            CHECK(error->reason() == "must authenticate first");
2!
627
            CHECK(user_api_key.name == "");
2!
628
        });
2✔
629

1✔
630
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
631
            REQUIRE(error);
2!
632
            CHECK(error->is_service_error());
2!
633
            CHECK(error->reason() == "must authenticate first");
2!
634
            CHECK(user_api_key.name == "");
2!
635
        });
2✔
636

1✔
637
        client.fetch_api_keys(no_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
638
            REQUIRE(error);
2!
639
            CHECK(error->is_service_error());
2!
640
            CHECK(error->reason() == "must authenticate first");
2!
641
            CHECK(api_keys.size() == 0);
2!
642
        });
2✔
643

1✔
644
        client.enable_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
645
            REQUIRE(error);
2!
646
            CHECK(error->is_service_error());
2!
647
            CHECK(error->reason() == "must authenticate first");
2!
648
        });
2✔
649

1✔
650
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
651
            REQUIRE(error);
2!
652
            CHECK(error->is_service_error());
2!
653
            CHECK(error->reason() == "must authenticate first");
2!
654
            CHECK(user_api_key.name == "");
2!
655
        });
2✔
656

1✔
657
        client.disable_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
658
            REQUIRE(error);
2!
659
            CHECK(error->is_service_error());
2!
660
            CHECK(error->reason() == "must authenticate first");
2!
661
        });
2✔
662

1✔
663
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
664
            REQUIRE(error);
2!
665
            CHECK(error->is_service_error());
2!
666
            CHECK(error->reason() == "must authenticate first");
2!
667
            CHECK(user_api_key.name == "");
2!
668
        });
2✔
669

1✔
670
        client.delete_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
671
            REQUIRE(error);
2!
672
            CHECK(error->is_service_error());
2!
673
            CHECK(error->reason() == "must authenticate first");
2!
674
        });
2✔
675

1✔
676
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
677
            CHECK(user_api_key.name == "");
2!
678
            REQUIRE(error);
2!
679
            CHECK(error->is_service_error());
2!
680
            CHECK(error->reason() == "must authenticate first");
2!
681
            processed = true;
2✔
682
        });
2✔
683
        CHECK(processed);
2!
684
    }
2✔
685

3✔
686
    SECTION("api-key against the wrong user") {
6✔
687
        std::shared_ptr<SyncUser> first_user = app->current_user();
2✔
688
        create_user_and_log_in(app);
2✔
689
        std::shared_ptr<SyncUser> second_user = app->current_user();
2✔
690
        REQUIRE(first_user != second_user);
2!
691
        auto api_key_name = util::format("%1", random_string(15));
2✔
692
        App::UserAPIKey api_key;
2✔
693
        App::UserAPIKeyProviderClient provider = app->provider_client<App::UserAPIKeyProviderClient>();
2✔
694

1✔
695
        provider.create_api_key(api_key_name, first_user,
2✔
696
                                [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
697
                                    REQUIRE_FALSE(error);
2!
698
                                    CHECK(user_api_key.name == api_key_name);
2!
699
                                    api_key = user_api_key;
2✔
700
                                });
2✔
701

1✔
702
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
703
            REQUIRE_FALSE(error);
2!
704
            CHECK(user_api_key.name == api_key_name);
2!
705
            CHECK(user_api_key.id.to_string() == user_api_key.id.to_string());
2!
706
        });
2✔
707

1✔
708
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
709
            REQUIRE(error);
2!
710
            CHECK(error->reason() == "API key not found");
2!
711
            CHECK(error->is_service_error());
2!
712
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
713
            CHECK(user_api_key.name == "");
2!
714
        });
2✔
715

1✔
716
        provider.fetch_api_keys(first_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
717
            CHECK(api_keys.size() == 1);
2!
718
            for (auto api_key : api_keys) {
2✔
719
                CHECK(api_key.name == api_key_name);
2!
720
            }
2✔
721
            REQUIRE_FALSE(error);
2!
722
        });
2✔
723

1✔
724
        provider.fetch_api_keys(second_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
725
            CHECK(api_keys.size() == 0);
2!
726
            REQUIRE_FALSE(error);
2!
727
        });
2✔
728

1✔
729
        provider.enable_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
730
            REQUIRE_FALSE(error);
2!
731
        });
2✔
732

1✔
733
        provider.enable_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
734
            REQUIRE(error);
2!
735
            CHECK(error->reason() == "API key not found");
2!
736
            CHECK(error->is_service_error());
2!
737
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
738
        });
2✔
739

1✔
740
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
741
            REQUIRE_FALSE(error);
2!
742
            CHECK(user_api_key.disabled == false);
2!
743
            CHECK(user_api_key.name == api_key_name);
2!
744
        });
2✔
745

1✔
746
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
747
            REQUIRE(error);
2!
748
            CHECK(user_api_key.name == "");
2!
749
            CHECK(error->reason() == "API key not found");
2!
750
            CHECK(error->is_service_error());
2!
751
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
752
        });
2✔
753

1✔
754
        provider.disable_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
755
            REQUIRE_FALSE(error);
2!
756
        });
2✔
757

1✔
758
        provider.disable_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
759
            REQUIRE(error);
2!
760
            CHECK(error->reason() == "API key not found");
2!
761
            CHECK(error->is_service_error());
2!
762
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
763
        });
2✔
764

1✔
765
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
766
            REQUIRE_FALSE(error);
2!
767
            CHECK(user_api_key.disabled == true);
2!
768
            CHECK(user_api_key.name == api_key_name);
2!
769
        });
2✔
770

1✔
771
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
772
            REQUIRE(error);
2!
773
            CHECK(user_api_key.name == "");
2!
774
            CHECK(error->reason() == "API key not found");
2!
775
            CHECK(error->is_service_error());
2!
776
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
777
        });
2✔
778

1✔
779
        provider.delete_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
780
            REQUIRE(error);
2!
781
            CHECK(error->reason() == "API key not found");
2!
782
            CHECK(error->is_service_error());
2!
783
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
784
        });
2✔
785

1✔
786
        provider.delete_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
787
            REQUIRE_FALSE(error);
2!
788
        });
2✔
789

1✔
790
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
791
            CHECK(user_api_key.name == "");
2!
792
            REQUIRE(error);
2!
793
            CHECK(error->reason() == "API key not found");
2!
794
            CHECK(error->is_service_error());
2!
795
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
796
            processed = true;
2✔
797
        });
2✔
798

1✔
799
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
800
            CHECK(user_api_key.name == "");
2!
801
            REQUIRE(error);
2!
802
            CHECK(error->reason() == "API key not found");
2!
803
            CHECK(error->is_service_error());
2!
804
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
805
            processed = true;
2✔
806
        });
2✔
807

1✔
808
        CHECK(processed);
2!
809
    }
2✔
810
}
6✔
811

812
// MARK: - Auth Providers Function Tests
813

814
TEST_CASE("app: auth providers function integration", "[sync][app][user][baas]") {
2✔
815
    TestAppSession session;
2✔
816
    auto app = session.app();
2✔
817

1✔
818
    SECTION("auth providers function integration") {
2✔
819
        bson::BsonDocument function_params{{"realmCustomAuthFuncUserId", "123456"}};
2✔
820
        auto credentials = AppCredentials::function(function_params);
2✔
821
        auto user = log_in(app, credentials);
2✔
822
        REQUIRE(user->identities()[0].provider_type == IdentityProviderFunction);
2!
823
    }
2✔
824
}
2✔
825

826
// MARK: - Link User Tests
827

828
TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") {
8✔
829
    TestAppSession session;
8✔
830
    auto app = session.app();
8✔
831
    auto user = log_in(app);
8✔
832

4✔
833
    AutoVerifiedEmailCredentials creds;
8✔
834
    app->provider_client<App::UsernamePasswordProviderClient>().register_email(creds.email, creds.password,
8✔
835
                                                                               [&](Optional<AppError> error) {
8✔
836
                                                                                   REQUIRE_FALSE(error);
8!
837
                                                                               });
8✔
838

4✔
839
    SECTION("anonymous users are reused before they are linked to an identity") {
8✔
840
        REQUIRE(user == log_in(app));
2!
841
    }
2✔
842

4✔
843
    SECTION("linking a user adds that identity to the user") {
8✔
844
        REQUIRE(user->identities().size() == 1);
2!
845
        CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous);
2!
846

1✔
847
        app->link_user(user, creds, [&](std::shared_ptr<SyncUser> user2, Optional<AppError> error) {
2✔
848
            REQUIRE_FALSE(error);
2!
849
            REQUIRE(user == user2);
2!
850
            REQUIRE(user->identities().size() == 2);
2!
851
            CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous);
2!
852
            CHECK(user->identities()[1].provider_type == IdentityProviderUsernamePassword);
2!
853
        });
2✔
854
    }
2✔
855

4✔
856
    SECTION("linking an identity makes the user no longer returned by anonymous logins") {
8✔
857
        app->link_user(user, creds, [&](std::shared_ptr<SyncUser>, Optional<AppError> error) {
2✔
858
            REQUIRE_FALSE(error);
2!
859
        });
2✔
860
        auto user2 = log_in(app);
2✔
861
        REQUIRE(user != user2);
2!
862
    }
2✔
863

4✔
864
    SECTION("existing users are reused when logging in via linked identities") {
8✔
865
        app->link_user(user, creds, [](std::shared_ptr<SyncUser>, Optional<AppError> error) {
2✔
866
            REQUIRE_FALSE(error);
2!
867
        });
2✔
868
        app->log_out([](auto error) {
2✔
869
            REQUIRE_FALSE(error);
2!
870
        });
2✔
871
        REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
872
        // Should give us the same user instance despite logging in with a
1✔
873
        // different identity
1✔
874
        REQUIRE(user == log_in(app, creds));
2!
875
        REQUIRE(user->state() == SyncUser::State::LoggedIn);
2!
876
    }
2✔
877
}
8✔
878

879
// MARK: - Delete User Tests
880

881
TEST_CASE("app: delete anonymous user integration", "[sync][app][user][baas]") {
2✔
882
    TestAppSession session;
2✔
883
    auto app = session.app();
2✔
884

1✔
885
    SECTION("delete user expect success") {
2✔
886
        CHECK(app->sync_manager()->all_users().size() == 1);
2!
887

1✔
888
        // Log in user 1
1✔
889
        auto user_a = app->current_user();
2✔
890
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
891
        app->delete_user(user_a, [&](Optional<app::AppError> error) {
2✔
892
            REQUIRE_FALSE(error);
2!
893
            // a logged out anon user will be marked as Removed, not LoggedOut
1✔
894
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
895
        });
2✔
896
        CHECK(app->sync_manager()->all_users().empty());
2!
897
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
898

1✔
899
        app->delete_user(user_a, [&](Optional<app::AppError> error) {
2✔
900
            CHECK(error->reason() == "User must be logged in to be deleted.");
2!
901
            CHECK(app->sync_manager()->all_users().size() == 0);
2!
902
        });
2✔
903

1✔
904
        // Log in user 2
1✔
905
        auto user_b = log_in(app);
2✔
906
        CHECK(app->sync_manager()->get_current_user() == user_b);
2!
907
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
908
        CHECK(app->sync_manager()->all_users().size() == 1);
2!
909

1✔
910
        app->delete_user(user_b, [&](Optional<app::AppError> error) {
2✔
911
            REQUIRE_FALSE(error);
2!
912
            CHECK(app->sync_manager()->all_users().size() == 0);
2!
913
        });
2✔
914

1✔
915
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
916

1✔
917
        // check both handles are no longer valid
1✔
918
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
919
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
920
    }
2✔
921
}
2✔
922

923
TEST_CASE("app: delete user with credentials integration", "[sync][app][user][baas]") {
2✔
924
    TestAppSession session;
2✔
925
    auto app = session.app();
2✔
926
    app->remove_user(app->current_user(), [](auto) {});
2✔
927

1✔
928
    SECTION("log in and delete") {
2✔
929
        CHECK(app->sync_manager()->all_users().size() == 0);
2!
930
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
931

1✔
932
        auto credentials = create_user_and_log_in(app);
2✔
933
        auto user = app->current_user();
2✔
934

1✔
935
        CHECK(app->sync_manager()->get_current_user() == user);
2!
936
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
937
        app->delete_user(user, [&](Optional<app::AppError> error) {
2✔
938
            REQUIRE_FALSE(error);
2!
939
            CHECK(app->sync_manager()->all_users().size() == 0);
2!
940
        });
2✔
941
        CHECK(user->state() == SyncUser::State::Removed);
2!
942
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
943

1✔
944
        app->log_in_with_credentials(credentials, [](std::shared_ptr<SyncUser> user, util::Optional<AppError> error) {
2✔
945
            CHECK(!user);
2!
946
            REQUIRE(error);
2!
947
            REQUIRE(error->code() == ErrorCodes::InvalidPassword);
2!
948
        });
2✔
949
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
950

1✔
951
        CHECK(app->sync_manager()->all_users().size() == 0);
2!
952
        app->delete_user(user, [](Optional<app::AppError> err) {
2✔
953
            CHECK(err->code() > 0);
2!
954
        });
2✔
955

1✔
956
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
957
        CHECK(app->sync_manager()->all_users().size() == 0);
2!
958
        CHECK(user->state() == SyncUser::State::Removed);
2!
959
    }
2✔
960
}
2✔
961

962
// MARK: - Call Function Tests
963

964
TEST_CASE("app: call function", "[sync][app][function][baas]") {
2✔
965
    TestAppSession session;
2✔
966
    auto app = session.app();
2✔
967

1✔
968
    bson::BsonArray toSum(5);
2✔
969
    std::iota(toSum.begin(), toSum.end(), static_cast<int64_t>(1));
2✔
970
    const auto checkFn = [](Optional<int64_t>&& sum, Optional<AppError>&& error) {
4✔
971
        REQUIRE(!error);
4!
972
        CHECK(*sum == 15);
4!
973
    };
4✔
974
    app->call_function<int64_t>("sumFunc", toSum, checkFn);
2✔
975
    app->call_function<int64_t>(app->sync_manager()->get_current_user(), "sumFunc", toSum, checkFn);
2✔
976
}
2✔
977

978
// MARK: - Remote Mongo Client Tests
979

980
TEST_CASE("app: remote mongo client", "[sync][app][mongo][baas]") {
16✔
981
    TestAppSession session;
16✔
982
    auto app = session.app();
16✔
983

8✔
984
    auto remote_client = app->current_user()->mongo_client("BackingDB");
16✔
985
    auto db = remote_client.db(get_runtime_app_session("").config.mongo_dbname);
16✔
986
    auto dog_collection = db["Dog"];
16✔
987
    auto cat_collection = db["Cat"];
16✔
988
    auto person_collection = db["Person"];
16✔
989

8✔
990
    bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}};
16✔
991

8✔
992
    bson::BsonDocument dog_document2{{"name", "bob"}, {"breed", "french bulldog"}};
16✔
993

8✔
994
    auto dog3_object_id = ObjectId::gen();
16✔
995
    bson::BsonDocument dog_document3{
16✔
996
        {"_id", dog3_object_id},
16✔
997
        {"name", "petunia"},
16✔
998
        {"breed", "french bulldog"},
16✔
999
    };
16✔
1000

8✔
1001
    auto cat_id_string = random_string(10);
16✔
1002
    bson::BsonDocument cat_document{
16✔
1003
        {"_id", cat_id_string},
16✔
1004
        {"name", "luna"},
16✔
1005
        {"breed", "scottish fold"},
16✔
1006
    };
16✔
1007

8✔
1008
    bson::BsonDocument person_document{
16✔
1009
        {"firstName", "John"},
16✔
1010
        {"lastName", "Johnson"},
16✔
1011
        {"age", 30},
16✔
1012
    };
16✔
1013

8✔
1014
    bson::BsonDocument person_document2{
16✔
1015
        {"firstName", "Bob"},
16✔
1016
        {"lastName", "Johnson"},
16✔
1017
        {"age", 30},
16✔
1018
    };
16✔
1019

8✔
1020
    bson::BsonDocument bad_document{{"bad", "value"}};
16✔
1021

8✔
1022
    dog_collection.delete_many(dog_document, [&](uint64_t, Optional<AppError> error) {
16✔
1023
        REQUIRE_FALSE(error);
16!
1024
    });
16✔
1025

8✔
1026
    dog_collection.delete_many(dog_document2, [&](uint64_t, Optional<AppError> error) {
16✔
1027
        REQUIRE_FALSE(error);
16!
1028
    });
16✔
1029

8✔
1030
    dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
16✔
1031
        REQUIRE_FALSE(error);
16!
1032
    });
16✔
1033

8✔
1034
    dog_collection.delete_many(person_document, [&](uint64_t, Optional<AppError> error) {
16✔
1035
        REQUIRE_FALSE(error);
16!
1036
    });
16✔
1037

8✔
1038
    dog_collection.delete_many(person_document2, [&](uint64_t, Optional<AppError> error) {
16✔
1039
        REQUIRE_FALSE(error);
16!
1040
    });
16✔
1041

8✔
1042
    SECTION("insert") {
16✔
1043
        bool processed = false;
2✔
1044
        ObjectId dog_object_id;
2✔
1045
        ObjectId dog2_object_id;
2✔
1046

1✔
1047
        dog_collection.insert_one_bson(bad_document, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1048
            CHECK(error);
2!
1049
            CHECK(!bson);
2!
1050
        });
2✔
1051

1✔
1052
        dog_collection.insert_one_bson(dog_document3, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1053
            REQUIRE_FALSE(error);
2!
1054
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1055
            CHECK(static_cast<ObjectId>(bson["insertedId"]) == dog3_object_id);
2!
1056
        });
2✔
1057

1✔
1058
        cat_collection.insert_one_bson(cat_document, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1059
            REQUIRE_FALSE(error);
2!
1060
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1061
            CHECK(static_cast<std::string>(bson["insertedId"]) == cat_id_string);
2!
1062
        });
2✔
1063

1✔
1064
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1065
            REQUIRE_FALSE(error);
2!
1066
        });
2✔
1067

1✔
1068
        cat_collection.delete_one(cat_document, [&](uint64_t, Optional<AppError> error) {
2✔
1069
            REQUIRE_FALSE(error);
2!
1070
        });
2✔
1071

1✔
1072
        dog_collection.insert_one(bad_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1073
            CHECK(error);
2!
1074
            CHECK(!object_id);
2!
1075
        });
2✔
1076

1✔
1077
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1078
            REQUIRE_FALSE(error);
2!
1079
            CHECK((*object_id).to_string() != "");
2!
1080
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1081
        });
2✔
1082

1✔
1083
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1084
            REQUIRE_FALSE(error);
2!
1085
            CHECK((*object_id).to_string() != "");
2!
1086
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1087
        });
2✔
1088

1✔
1089
        dog_collection.insert_one(dog_document3, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1090
            REQUIRE_FALSE(error);
2!
1091
            CHECK(object_id->type() == bson::Bson::Type::ObjectId);
2!
1092
            CHECK(static_cast<ObjectId>(*object_id) == dog3_object_id);
2!
1093
        });
2✔
1094

1✔
1095
        cat_collection.insert_one(cat_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1096
            REQUIRE_FALSE(error);
2!
1097
            CHECK(object_id->type() == bson::Bson::Type::String);
2!
1098
            CHECK(static_cast<std::string>(*object_id) == cat_id_string);
2!
1099
        });
2✔
1100

1✔
1101
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id, dog3_object_id});
2✔
1102
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1103
            REQUIRE_FALSE(error);
2!
1104
            CHECK((*object_id).to_string() != "");
2!
1105
        });
2✔
1106

1✔
1107
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1108
            REQUIRE_FALSE(error);
2!
1109
        });
2✔
1110

1✔
1111
        cat_collection.delete_one(cat_document, [&](uint64_t, Optional<AppError> error) {
2✔
1112
            REQUIRE_FALSE(error);
2!
1113
        });
2✔
1114

1✔
1115
        bson::BsonArray documents{
2✔
1116
            dog_document,
2✔
1117
            dog_document2,
2✔
1118
            dog_document3,
2✔
1119
        };
2✔
1120

1✔
1121
        dog_collection.insert_many_bson(documents, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1122
            REQUIRE_FALSE(error);
2!
1123
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1124
            auto insertedIds = static_cast<bson::BsonArray>(bson["insertedIds"]);
2✔
1125
        });
2✔
1126

1✔
1127
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1128
            REQUIRE_FALSE(error);
2!
1129
        });
2✔
1130

1✔
1131
        dog_collection.insert_many(documents, [&](std::vector<bson::Bson> inserted_docs, Optional<AppError> error) {
2✔
1132
            REQUIRE_FALSE(error);
2!
1133
            CHECK(inserted_docs.size() == 3);
2!
1134
            CHECK(inserted_docs[0].type() == bson::Bson::Type::ObjectId);
2!
1135
            CHECK(inserted_docs[1].type() == bson::Bson::Type::ObjectId);
2!
1136
            CHECK(inserted_docs[2].type() == bson::Bson::Type::ObjectId);
2!
1137
            CHECK(static_cast<ObjectId>(inserted_docs[2]) == dog3_object_id);
2!
1138
            processed = true;
2✔
1139
        });
2✔
1140

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

8✔
1144
    SECTION("find") {
16✔
1145
        bool processed = false;
2✔
1146

1✔
1147
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1148
            REQUIRE_FALSE(error);
2!
1149
            CHECK((*document_array).size() == 0);
2!
1150
        });
2✔
1151

1✔
1152
        dog_collection.find_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1153
            REQUIRE_FALSE(error);
2!
1154
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 0);
2!
1155
        });
2✔
1156

1✔
1157
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1158
            REQUIRE_FALSE(error);
2!
1159
            CHECK(!document);
2!
1160
        });
2✔
1161

1✔
1162
        dog_collection.find_one_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1163
            REQUIRE_FALSE(error);
2!
1164
            CHECK((!bson || bson::holds_alternative<util::None>(*bson)));
2!
1165
        });
2✔
1166

1✔
1167
        ObjectId dog_object_id;
2✔
1168
        ObjectId dog2_object_id;
2✔
1169

1✔
1170
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1171
            REQUIRE_FALSE(error);
2!
1172
            CHECK((*object_id).to_string() != "");
2!
1173
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1174
        });
2✔
1175

1✔
1176
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1177
            REQUIRE_FALSE(error);
2!
1178
            CHECK((*object_id).to_string() != "");
2!
1179
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1180
        });
2✔
1181

1✔
1182
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id});
2✔
1183
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1184
            REQUIRE_FALSE(error);
2!
1185
            CHECK((*object_id).to_string() != "");
2!
1186
        });
2✔
1187

1✔
1188
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1189
            REQUIRE_FALSE(error);
2!
1190
            CHECK((*documents).size() == 1);
2!
1191
        });
2✔
1192

1✔
1193
        dog_collection.find_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1194
            REQUIRE_FALSE(error);
2!
1195
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 1);
2!
1196
        });
2✔
1197

1✔
1198
        person_collection.find(person_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1199
            REQUIRE_FALSE(error);
2!
1200
            CHECK((*documents).size() == 1);
2!
1201
        });
2✔
1202

1✔
1203
        MongoCollection::FindOptions options{
2✔
1204
            2,                                                         // document limit
2✔
1205
            Optional<bson::BsonDocument>({{"name", 1}, {"breed", 1}}), // project
2✔
1206
            Optional<bson::BsonDocument>({{"breed", 1}})               // sort
2✔
1207
        };
2✔
1208

1✔
1209
        dog_collection.find(dog_document, options,
2✔
1210
                            [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1211
                                REQUIRE_FALSE(error);
2!
1212
                                CHECK((*document_array).size() == 1);
2!
1213
                            });
2✔
1214

1✔
1215
        dog_collection.find({{"name", "fido"}}, options,
2✔
1216
                            [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1217
                                REQUIRE_FALSE(error);
2!
1218
                                CHECK((*document_array).size() == 1);
2!
1219
                                auto king_charles = static_cast<bson::BsonDocument>((*document_array)[0]);
2✔
1220
                                CHECK(king_charles["breed"] == "king charles");
2!
1221
                            });
2✔
1222

1✔
1223
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1224
            REQUIRE_FALSE(error);
2!
1225
            auto name = (*document)["name"];
2✔
1226
            CHECK(name == "fido");
2!
1227
        });
2✔
1228

1✔
1229
        dog_collection.find_one(dog_document, options,
2✔
1230
                                [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1231
                                    REQUIRE_FALSE(error);
2!
1232
                                    auto name = (*document)["name"];
2✔
1233
                                    CHECK(name == "fido");
2!
1234
                                });
2✔
1235

1✔
1236
        dog_collection.find_one_bson(dog_document, options, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1237
            REQUIRE_FALSE(error);
2!
1238
            auto name = (static_cast<bson::BsonDocument>(*bson))["name"];
2✔
1239
            CHECK(name == "fido");
2!
1240
        });
2✔
1241

1✔
1242
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1243
            REQUIRE_FALSE(error);
2!
1244
            CHECK((*documents).size() == 1);
2!
1245
        });
2✔
1246

1✔
1247
        dog_collection.find_one_and_delete(dog_document,
2✔
1248
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1249
                                               REQUIRE_FALSE(error);
2!
1250
                                               REQUIRE(document);
2!
1251
                                           });
2✔
1252

1✔
1253
        dog_collection.find_one_and_delete({{}},
2✔
1254
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1255
                                               REQUIRE_FALSE(error);
2!
1256
                                               REQUIRE(document);
2!
1257
                                           });
2✔
1258

1✔
1259
        dog_collection.find_one_and_delete({{"invalid", "key"}},
2✔
1260
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1261
                                               REQUIRE_FALSE(error);
2!
1262
                                               CHECK(!document);
2!
1263
                                           });
2✔
1264

1✔
1265
        dog_collection.find_one_and_delete_bson({{"invalid", "key"}}, {},
2✔
1266
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1267
                                                    REQUIRE_FALSE(error);
2!
1268
                                                    CHECK((!bson || bson::holds_alternative<util::None>(*bson)));
2!
1269
                                                });
2✔
1270

1✔
1271
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1272
            REQUIRE_FALSE(error);
2!
1273
            CHECK((*documents).size() == 0);
2!
1274
            processed = true;
2✔
1275
        });
2✔
1276

1✔
1277
        CHECK(processed);
2!
1278
    }
2✔
1279

8✔
1280
    SECTION("count and aggregate") {
16✔
1281
        bool processed = false;
2✔
1282

1✔
1283
        ObjectId dog_object_id;
2✔
1284
        ObjectId dog2_object_id;
2✔
1285

1✔
1286
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1287
            REQUIRE_FALSE(error);
2!
1288
            CHECK((*object_id).to_string() != "");
2!
1289
        });
2✔
1290

1✔
1291
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1292
            REQUIRE_FALSE(error);
2!
1293
            CHECK((*object_id).to_string() != "");
2!
1294
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1295
        });
2✔
1296

1✔
1297
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1298
            REQUIRE_FALSE(error);
2!
1299
            CHECK((*object_id).to_string() != "");
2!
1300
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1301
        });
2✔
1302

1✔
1303
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id});
2✔
1304
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1305
            REQUIRE_FALSE(error);
2!
1306
            CHECK((*object_id).to_string() != "");
2!
1307
        });
2✔
1308

1✔
1309
        bson::BsonDocument match{{"$match", bson::BsonDocument({{"name", "fido"}})}};
2✔
1310

1✔
1311
        bson::BsonDocument group{{"$group", bson::BsonDocument({{"_id", "$name"}})}};
2✔
1312

1✔
1313
        bson::BsonArray pipeline{match, group};
2✔
1314

1✔
1315
        dog_collection.aggregate(pipeline, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1316
            REQUIRE_FALSE(error);
2!
1317
            CHECK((*documents).size() == 1);
2!
1318
        });
2✔
1319

1✔
1320
        dog_collection.aggregate_bson(pipeline, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1321
            REQUIRE_FALSE(error);
2!
1322
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 1);
2!
1323
        });
2✔
1324

1✔
1325
        dog_collection.count({{"breed", "king charles"}}, [&](uint64_t count, Optional<AppError> error) {
2✔
1326
            REQUIRE_FALSE(error);
2!
1327
            CHECK(count == 2);
2!
1328
        });
2✔
1329

1✔
1330
        dog_collection.count_bson({{"breed", "king charles"}}, 0,
2✔
1331
                                  [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1332
                                      REQUIRE_FALSE(error);
2!
1333
                                      CHECK(static_cast<int64_t>(*bson) == 2);
2!
1334
                                  });
2✔
1335

1✔
1336
        dog_collection.count({{"breed", "french bulldog"}}, [&](uint64_t count, Optional<AppError> error) {
2✔
1337
            REQUIRE_FALSE(error);
2!
1338
            CHECK(count == 1);
2!
1339
        });
2✔
1340

1✔
1341
        dog_collection.count({{"breed", "king charles"}}, 1, [&](uint64_t count, Optional<AppError> error) {
2✔
1342
            REQUIRE_FALSE(error);
2!
1343
            CHECK(count == 1);
2!
1344
        });
2✔
1345

1✔
1346
        person_collection.count(
2✔
1347
            {{"firstName", "John"}, {"lastName", "Johnson"}, {"age", bson::BsonDocument({{"$gt", 25}})}}, 1,
2✔
1348
            [&](uint64_t count, Optional<AppError> error) {
2✔
1349
                REQUIRE_FALSE(error);
2!
1350
                CHECK(count == 1);
2!
1351
                processed = true;
2✔
1352
            });
2✔
1353

1✔
1354
        CHECK(processed);
2!
1355
    }
2✔
1356

8✔
1357
    SECTION("find and update") {
16✔
1358
        bool processed = false;
2✔
1359

1✔
1360
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1361
            Optional<bson::BsonDocument>({{"name", 1}, {"breed", 1}}), // project
2✔
1362
            Optional<bson::BsonDocument>({{"name", 1}}),               // sort,
2✔
1363
            true,                                                      // upsert
2✔
1364
            true                                                       // return new doc
2✔
1365
        };
2✔
1366

1✔
1367
        dog_collection.find_one_and_update(dog_document, dog_document2,
2✔
1368
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1369
                                               REQUIRE_FALSE(error);
2!
1370
                                               CHECK(!document);
2!
1371
                                           });
2✔
1372

1✔
1373
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1374
            REQUIRE_FALSE(error);
2!
1375
            CHECK((*object_id).to_string() != "");
2!
1376
        });
2✔
1377

1✔
1378
        dog_collection.find_one_and_update(dog_document, dog_document2, find_and_modify_options,
2✔
1379
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1380
                                               REQUIRE_FALSE(error);
2!
1381
                                               auto breed = static_cast<std::string>((*document)["breed"]);
2✔
1382
                                               CHECK(breed == "french bulldog");
2!
1383
                                           });
2✔
1384

1✔
1385
        dog_collection.find_one_and_update(dog_document2, dog_document, find_and_modify_options,
2✔
1386
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1387
                                               REQUIRE_FALSE(error);
2!
1388
                                               auto breed = static_cast<std::string>((*document)["breed"]);
2✔
1389
                                               CHECK(breed == "king charles");
2!
1390
                                           });
2✔
1391

1✔
1392
        dog_collection.find_one_and_update_bson(dog_document, dog_document2, find_and_modify_options,
2✔
1393
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1394
                                                    REQUIRE_FALSE(error);
2!
1395
                                                    auto breed = static_cast<std::string>(
2✔
1396
                                                        static_cast<bson::BsonDocument>(*bson)["breed"]);
2✔
1397
                                                    CHECK(breed == "french bulldog");
2!
1398
                                                });
2✔
1399

1✔
1400
        dog_collection.find_one_and_update_bson(dog_document2, dog_document, find_and_modify_options,
2✔
1401
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1402
                                                    REQUIRE_FALSE(error);
2!
1403
                                                    auto breed = static_cast<std::string>(
2✔
1404
                                                        static_cast<bson::BsonDocument>(*bson)["breed"]);
2✔
1405
                                                    CHECK(breed == "king charles");
2!
1406
                                                });
2✔
1407

1✔
1408
        dog_collection.find_one_and_update({{"name", "invalid name"}}, {{"name", "some name"}},
2✔
1409
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1410
                                               REQUIRE_FALSE(error);
2!
1411
                                               CHECK(!document);
2!
1412
                                               processed = true;
2✔
1413
                                           });
2✔
1414
        CHECK(processed);
2!
1415
        processed = false;
2✔
1416

1✔
1417
        dog_collection.find_one_and_update({{"name", "invalid name"}}, {{}}, find_and_modify_options,
2✔
1418
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1419
                                               REQUIRE(error);
2!
1420
                                               CHECK(error->reason() == "insert not permitted");
2!
1421
                                               CHECK(!document);
2!
1422
                                               processed = true;
2✔
1423
                                           });
2✔
1424
        CHECK(processed);
2!
1425
    }
2✔
1426

8✔
1427
    SECTION("update") {
16✔
1428
        bool processed = false;
2✔
1429
        ObjectId dog_object_id;
2✔
1430

1✔
1431
        dog_collection.update_one(dog_document, dog_document2, true,
2✔
1432
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1433
                                      REQUIRE_FALSE(error);
2!
1434
                                      CHECK((*result.upserted_id).to_string() != "");
2!
1435
                                  });
2✔
1436

1✔
1437
        dog_collection.update_one(dog_document2, dog_document,
2✔
1438
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1439
                                      REQUIRE_FALSE(error);
2!
1440
                                      CHECK(!result.upserted_id);
2!
1441
                                  });
2✔
1442

1✔
1443
        cat_collection.update_one({}, cat_document, true,
2✔
1444
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1445
                                      REQUIRE_FALSE(error);
2!
1446
                                      CHECK(result.upserted_id->type() == bson::Bson::Type::String);
2!
1447
                                      CHECK(result.upserted_id == cat_id_string);
2!
1448
                                  });
2✔
1449

1✔
1450
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1451
            REQUIRE_FALSE(error);
2!
1452
        });
2✔
1453

1✔
1454
        cat_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1455
            REQUIRE_FALSE(error);
2!
1456
        });
2✔
1457

1✔
1458
        dog_collection.update_one_bson(dog_document, dog_document2, true,
2✔
1459
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1460
                                           REQUIRE_FALSE(error);
2!
1461
                                           auto upserted_id = static_cast<bson::BsonDocument>(*bson)["upsertedId"];
2✔
1462

1✔
1463
                                           REQUIRE(upserted_id.type() == bson::Bson::Type::ObjectId);
2!
1464
                                       });
2✔
1465

1✔
1466
        dog_collection.update_one_bson(dog_document2, dog_document, true,
2✔
1467
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1468
                                           REQUIRE_FALSE(error);
2!
1469
                                           auto document = static_cast<bson::BsonDocument>(*bson);
2✔
1470
                                           auto foundUpsertedId = document.find("upsertedId") != document.end();
2✔
1471
                                           REQUIRE(!foundUpsertedId);
2!
1472
                                       });
2✔
1473

1✔
1474
        cat_collection.update_one_bson({}, cat_document, true,
2✔
1475
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1476
                                           REQUIRE_FALSE(error);
2!
1477
                                           auto upserted_id = static_cast<bson::BsonDocument>(*bson)["upsertedId"];
2✔
1478
                                           REQUIRE(upserted_id.type() == bson::Bson::Type::String);
2!
1479
                                           REQUIRE(upserted_id == cat_id_string);
2!
1480
                                       });
2✔
1481

1✔
1482
        person_document["dogs"] = bson::BsonArray();
2✔
1483
        bson::BsonDocument person_document_copy = bson::BsonDocument(person_document);
2✔
1484
        person_document_copy["dogs"] = bson::BsonArray({dog_object_id});
2✔
1485
        person_collection.update_one(person_document, person_document, true,
2✔
1486
                                     [&](MongoCollection::UpdateResult, Optional<AppError> error) {
2✔
1487
                                         REQUIRE_FALSE(error);
2!
1488
                                         processed = true;
2✔
1489
                                     });
2✔
1490

1✔
1491
        CHECK(processed);
2!
1492
    }
2✔
1493

8✔
1494
    SECTION("update many") {
16✔
1495
        bool processed = false;
2✔
1496

1✔
1497
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1498
            REQUIRE_FALSE(error);
2!
1499
            CHECK((*object_id).to_string() != "");
2!
1500
        });
2✔
1501

1✔
1502
        dog_collection.update_many(dog_document2, dog_document, true,
2✔
1503
                                   [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1504
                                       REQUIRE_FALSE(error);
2!
1505
                                       CHECK((*result.upserted_id).to_string() != "");
2!
1506
                                   });
2✔
1507

1✔
1508
        dog_collection.update_many(dog_document2, dog_document,
2✔
1509
                                   [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1510
                                       REQUIRE_FALSE(error);
2!
1511
                                       CHECK(!result.upserted_id);
2!
1512
                                       processed = true;
2✔
1513
                                   });
2✔
1514

1✔
1515
        CHECK(processed);
2!
1516
    }
2✔
1517

8✔
1518
    SECTION("find and replace") {
16✔
1519
        bool processed = false;
2✔
1520
        ObjectId dog_object_id;
2✔
1521
        ObjectId person_object_id;
2✔
1522

1✔
1523
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1524
            Optional<bson::BsonDocument>({{"name", "fido"}}), // project
2✔
1525
            Optional<bson::BsonDocument>({{"name", 1}}),      // sort,
2✔
1526
            true,                                             // upsert
2✔
1527
            true                                              // return new doc
2✔
1528
        };
2✔
1529

1✔
1530
        dog_collection.find_one_and_replace(dog_document, dog_document2,
2✔
1531
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1532
                                                REQUIRE_FALSE(error);
2!
1533
                                                CHECK(!document);
2!
1534
                                            });
2✔
1535

1✔
1536
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1537
            REQUIRE_FALSE(error);
2!
1538
            CHECK((*object_id).to_string() != "");
2!
1539
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1540
        });
2✔
1541

1✔
1542
        dog_collection.find_one_and_replace(dog_document, dog_document2,
2✔
1543
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1544
                                                REQUIRE_FALSE(error);
2!
1545
                                                auto name = static_cast<std::string>((*document)["name"]);
2✔
1546
                                                CHECK(name == "fido");
2!
1547
                                            });
2✔
1548

1✔
1549
        dog_collection.find_one_and_replace(dog_document2, dog_document, find_and_modify_options,
2✔
1550
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1551
                                                REQUIRE_FALSE(error);
2!
1552
                                                auto name = static_cast<std::string>((*document)["name"]);
2✔
1553
                                                CHECK(static_cast<std::string>(name) == "fido");
2!
1554
                                            });
2✔
1555

1✔
1556
        person_document["dogs"] = bson::BsonArray({dog_object_id});
2✔
1557
        person_document2["dogs"] = bson::BsonArray({dog_object_id});
2✔
1558
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1559
            REQUIRE_FALSE(error);
2!
1560
            CHECK((*object_id).to_string() != "");
2!
1561
            person_object_id = static_cast<ObjectId>(*object_id);
2✔
1562
        });
2✔
1563

1✔
1564
        MongoCollection::FindOneAndModifyOptions person_find_and_modify_options{
2✔
1565
            Optional<bson::BsonDocument>({{"firstName", 1}}), // project
2✔
1566
            Optional<bson::BsonDocument>({{"firstName", 1}}), // sort,
2✔
1567
            false,                                            // upsert
2✔
1568
            true                                              // return new doc
2✔
1569
        };
2✔
1570

1✔
1571
        person_collection.find_one_and_replace(person_document, person_document2,
2✔
1572
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1573
                                                   REQUIRE_FALSE(error);
2!
1574
                                                   auto name = static_cast<std::string>((*document)["firstName"]);
2✔
1575
                                                   // Should return the old document
1✔
1576
                                                   CHECK(name == "John");
2!
1577
                                                   processed = true;
2✔
1578
                                               });
2✔
1579

1✔
1580
        person_collection.find_one_and_replace(person_document2, person_document, person_find_and_modify_options,
2✔
1581
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1582
                                                   REQUIRE_FALSE(error);
2!
1583
                                                   auto name = static_cast<std::string>((*document)["firstName"]);
2✔
1584
                                                   // Should return new document, Bob -> John
1✔
1585
                                                   CHECK(name == "John");
2!
1586
                                               });
2✔
1587

1✔
1588
        person_collection.find_one_and_replace({{"invalid", "item"}}, {{}},
2✔
1589
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1590
                                                   // If a document is not found then null will be returned for the
1✔
1591
                                                   // document and no error will be returned
1✔
1592
                                                   REQUIRE_FALSE(error);
2!
1593
                                                   CHECK(!document);
2!
1594
                                               });
2✔
1595

1✔
1596
        person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, person_find_and_modify_options,
2✔
1597
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1598
                                                   REQUIRE_FALSE(error);
2!
1599
                                                   CHECK(!document);
2!
1600
                                                   processed = true;
2✔
1601
                                               });
2✔
1602

1✔
1603
        CHECK(processed);
2!
1604
    }
2✔
1605

8✔
1606
    SECTION("delete") {
16✔
1607

1✔
1608
        bool processed = false;
2✔
1609

1✔
1610
        bson::BsonArray documents;
2✔
1611
        documents.assign(3, dog_document);
2✔
1612

1✔
1613
        dog_collection.insert_many(documents, [&](std::vector<bson::Bson> inserted_docs, Optional<AppError> error) {
2✔
1614
            REQUIRE_FALSE(error);
2!
1615
            CHECK(inserted_docs.size() == 3);
2!
1616
        });
2✔
1617

1✔
1618
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1619
            Optional<bson::BsonDocument>({{"name", "fido"}}), // project
2✔
1620
            Optional<bson::BsonDocument>({{"name", 1}}),      // sort,
2✔
1621
            true,                                             // upsert
2✔
1622
            true                                              // return new doc
2✔
1623
        };
2✔
1624

1✔
1625
        dog_collection.delete_one(dog_document, [&](uint64_t deleted_count, Optional<AppError> error) {
2✔
1626
            REQUIRE_FALSE(error);
2!
1627
            CHECK(deleted_count >= 1);
2!
1628
        });
2✔
1629

1✔
1630
        dog_collection.delete_many(dog_document, [&](uint64_t deleted_count, Optional<AppError> error) {
2✔
1631
            REQUIRE_FALSE(error);
2!
1632
            CHECK(deleted_count >= 1);
2!
1633
            processed = true;
2✔
1634
        });
2✔
1635

1✔
1636
        person_collection.delete_many_bson(person_document, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1637
            REQUIRE_FALSE(error);
2!
1638
            CHECK(static_cast<int32_t>(static_cast<bson::BsonDocument>(*bson)["deletedCount"]) >= 1);
2!
1639
            processed = true;
2✔
1640
        });
2✔
1641

1✔
1642
        CHECK(processed);
2!
1643
    }
2✔
1644
}
16✔
1645

1646
// MARK: - Push Notifications Tests
1647

1648
TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") {
8✔
1649
    TestAppSession session;
8✔
1650
    auto app = session.app();
8✔
1651
    std::shared_ptr<SyncUser> sync_user = app->current_user();
8✔
1652

4✔
1653
    SECTION("register") {
8✔
1654
        bool processed;
2✔
1655

1✔
1656
        app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
1657
            REQUIRE_FALSE(error);
2!
1658
            processed = true;
2✔
1659
        });
2✔
1660

1✔
1661
        CHECK(processed);
2!
1662
    }
2✔
1663
    /*
4✔
1664
        // FIXME: It seems this test fails when the two register_device calls are invoked too quickly,
4✔
1665
        // The error returned will be 'Device not found' on the second register_device call.
4✔
1666
        SECTION("register twice") {
4✔
1667
            // registering the same device twice should not result in an error
4✔
1668
            bool processed;
4✔
1669

4✔
1670
            app->push_notification_client("gcm").register_device("hello",
4✔
1671
                                                                 sync_user,
4✔
1672
                                                                 [&](Optional<AppError> error) {
4✔
1673
                REQUIRE_FALSE(error);
4✔
1674
            });
4✔
1675

4✔
1676
            app->push_notification_client("gcm").register_device("hello",
4✔
1677
                                                                 sync_user,
4✔
1678
                                                                 [&](Optional<AppError> error) {
4✔
1679
                REQUIRE_FALSE(error);
4✔
1680
                processed = true;
4✔
1681
            });
4✔
1682

4✔
1683
            CHECK(processed);
4✔
1684
        }
4✔
1685
    */
4✔
1686
    SECTION("deregister") {
8✔
1687
        bool processed;
2✔
1688

1✔
1689
        app->push_notification_client("gcm").deregister_device(sync_user, [&](Optional<AppError> error) {
2✔
1690
            REQUIRE_FALSE(error);
2!
1691
            processed = true;
2✔
1692
        });
2✔
1693
        CHECK(processed);
2!
1694
    }
2✔
1695

4✔
1696
    SECTION("register with unavailable service") {
8✔
1697
        bool processed;
2✔
1698

1✔
1699
        app->push_notification_client("gcm_blah").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
1700
            REQUIRE(error);
2!
1701
            CHECK(error->reason() == "service not found: 'gcm_blah'");
2!
1702
            processed = true;
2✔
1703
        });
2✔
1704
        CHECK(processed);
2!
1705
    }
2✔
1706

4✔
1707
    SECTION("register with logged out user") {
8✔
1708
        bool processed;
2✔
1709

1✔
1710
        app->log_out([=](Optional<AppError> error) {
2✔
1711
            REQUIRE_FALSE(error);
2!
1712
        });
2✔
1713

1✔
1714
        app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
1715
            REQUIRE(error);
2!
1716
            processed = true;
2✔
1717
        });
2✔
1718

1✔
1719
        app->push_notification_client("gcm").register_device("hello", nullptr, [&](Optional<AppError> error) {
2✔
1720
            REQUIRE(error);
2!
1721
            processed = true;
2✔
1722
        });
2✔
1723

1✔
1724
        CHECK(processed);
2!
1725
    }
2✔
1726
}
8✔
1727

1728
// MARK: - Token refresh
1729

1730
TEST_CASE("app: token refresh", "[sync][app][token][baas]") {
2✔
1731
    TestAppSession session;
2✔
1732
    auto app = session.app();
2✔
1733
    std::shared_ptr<SyncUser> sync_user = app->current_user();
2✔
1734
    sync_user->update_access_token(ENCODE_FAKE_JWT("fake_access_token"));
2✔
1735

1✔
1736
    auto remote_client = app->current_user()->mongo_client("BackingDB");
2✔
1737
    auto db = remote_client.db(get_runtime_app_session("").config.mongo_dbname);
2✔
1738
    auto dog_collection = db["Dog"];
2✔
1739
    bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}};
2✔
1740

1✔
1741
    SECTION("access token should refresh") {
2✔
1742
        /*
1✔
1743
         Expected sequence of events:
1✔
1744
         - `find_one` tries to hit the server with a bad access token
1✔
1745
         - Server returns an error because of the bad token, error should be something like:
1✔
1746
            {\"error\":\"json: cannot unmarshal array into Go value of type map[string]interface
1✔
1747
         {}\",\"link\":\"http://localhost:9090/groups/5f84167e776aa0f9dc27081a/apps/5f841686776aa0f9dc270876/logs?co_id=5f844c8c776aa0f9dc273db6\"}
1✔
1748
            http_status_code = 401
1✔
1749
            custom_status_code = 0
1✔
1750
         - App::handle_auth_failure is then called and an attempt to refresh the access token will be peformed.
1✔
1751
         - If the token refresh was successful, the original request will retry and we should expect no error in the
1✔
1752
         callback of `find_one`
1✔
1753
         */
1✔
1754
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument>, Optional<AppError> error) {
2✔
1755
            REQUIRE_FALSE(error);
2!
1756
        });
2✔
1757
    }
2✔
1758
}
2✔
1759

1760
// MARK: - Sync Tests
1761

1762
TEST_CASE("app: mixed lists with object links", "[sync][pbs][app][links][baas]") {
2✔
1763
    std::string base_url = get_base_url();
2✔
1764
    const std::string valid_pk_name = "_id";
2✔
1765
    REQUIRE(!base_url.empty());
2!
1766

1✔
1767
    Schema schema{
2✔
1768
        {"TopLevel",
2✔
1769
         {
2✔
1770
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1771
             {"mixed_array", PropertyType::Mixed | PropertyType::Array | PropertyType::Nullable},
2✔
1772
         }},
2✔
1773
        {"Target",
2✔
1774
         {
2✔
1775
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1776
             {"value", PropertyType::Int},
2✔
1777
         }},
2✔
1778
    };
2✔
1779

1✔
1780
    auto server_app_config = minimal_app_config(base_url, "set_new_embedded_object", schema);
2✔
1781
    auto app_session = create_app(server_app_config);
2✔
1782
    auto partition = random_string(100);
2✔
1783

1✔
1784
    auto obj_id = ObjectId::gen();
2✔
1785
    auto target_id = ObjectId::gen();
2✔
1786
    auto mixed_list_values = AnyVector{
2✔
1787
        Mixed{int64_t(1234)},
2✔
1788
        Mixed{},
2✔
1789
        Mixed{target_id},
2✔
1790
    };
2✔
1791
    {
2✔
1792
        TestAppSession test_session(app_session, nullptr, DeleteApp{false});
2✔
1793
        SyncTestFile config(test_session.app(), partition, schema);
2✔
1794
        auto realm = Realm::get_shared_realm(config);
2✔
1795

1✔
1796
        CppContext c(realm);
2✔
1797
        realm->begin_transaction();
2✔
1798
        auto target_obj = Object::create(
2✔
1799
            c, realm, "Target", std::any(AnyDict{{valid_pk_name, target_id}, {"value", static_cast<int64_t>(1234)}}));
2✔
1800
        mixed_list_values.push_back(Mixed(target_obj.get_obj().get_link()));
2✔
1801

1✔
1802
        Object::create(c, realm, "TopLevel",
2✔
1803
                       std::any(AnyDict{
2✔
1804
                           {valid_pk_name, obj_id},
2✔
1805
                           {"mixed_array", mixed_list_values},
2✔
1806
                       }),
2✔
1807
                       CreatePolicy::ForceCreate);
2✔
1808
        realm->commit_transaction();
2✔
1809
        CHECK(!wait_for_upload(*realm));
2!
1810
    }
2✔
1811

1✔
1812
    {
2✔
1813
        TestAppSession test_session(app_session);
2✔
1814
        SyncTestFile config(test_session.app(), partition, schema);
2✔
1815
        auto realm = Realm::get_shared_realm(config);
2✔
1816

1✔
1817
        CHECK(!wait_for_download(*realm));
2!
1818
        CppContext c(realm);
2✔
1819
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{obj_id});
2✔
1820
        auto list = util::any_cast<List&&>(obj.get_property_value<std::any>(c, "mixed_array"));
2✔
1821
        for (size_t idx = 0; idx < list.size(); ++idx) {
10✔
1822
            Mixed mixed = list.get_any(idx);
8✔
1823
            if (idx == 3) {
8✔
1824
                CHECK(mixed.is_type(type_TypedLink));
2!
1825
                auto link = mixed.get<ObjLink>();
2✔
1826
                auto link_table = realm->read_group().get_table(link.get_table_key());
2✔
1827
                CHECK(link_table->get_name() == "class_Target");
2!
1828
                auto link_obj = link_table->get_object(link.get_obj_key());
2✔
1829
                CHECK(link_obj.get_primary_key() == target_id);
2!
1830
            }
2✔
1831
            else {
6✔
1832
                CHECK(mixed == util::any_cast<Mixed>(mixed_list_values[idx]));
6!
1833
            }
6✔
1834
        }
8✔
1835
    }
2✔
1836
}
2✔
1837

1838
TEST_CASE("app: roundtrip values", "[sync][pbs][app][baas]") {
2✔
1839
    std::string base_url = get_base_url();
2✔
1840
    const std::string valid_pk_name = "_id";
2✔
1841
    REQUIRE(!base_url.empty());
2!
1842

1✔
1843
    Schema schema{
2✔
1844
        {"TopLevel",
2✔
1845
         {
2✔
1846
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1847
             {"decimal", PropertyType::Decimal | PropertyType::Nullable},
2✔
1848
         }},
2✔
1849
    };
2✔
1850

1✔
1851
    auto server_app_config = minimal_app_config(base_url, "roundtrip_values", schema);
2✔
1852
    auto app_session = create_app(server_app_config);
2✔
1853
    auto partition = random_string(100);
2✔
1854

1✔
1855
    Decimal128 large_significand = Decimal128(70) / Decimal128(1.09);
2✔
1856
    auto obj_id = ObjectId::gen();
2✔
1857
    {
2✔
1858
        TestAppSession test_session(app_session, nullptr, DeleteApp{false});
2✔
1859
        SyncTestFile config(test_session.app(), partition, schema);
2✔
1860
        auto realm = Realm::get_shared_realm(config);
2✔
1861

1✔
1862
        CppContext c(realm);
2✔
1863
        realm->begin_transaction();
2✔
1864
        Object::create(c, realm, "TopLevel",
2✔
1865
                       util::Any(AnyDict{
2✔
1866
                           {valid_pk_name, obj_id},
2✔
1867
                           {"decimal", large_significand},
2✔
1868
                       }),
2✔
1869
                       CreatePolicy::ForceCreate);
2✔
1870
        realm->commit_transaction();
2✔
1871
        CHECK(!wait_for_upload(*realm, std::chrono::seconds(600)));
2!
1872
    }
2✔
1873

1✔
1874
    {
2✔
1875
        TestAppSession test_session(app_session);
2✔
1876
        SyncTestFile config(test_session.app(), partition, schema);
2✔
1877
        auto realm = Realm::get_shared_realm(config);
2✔
1878

1✔
1879
        CHECK(!wait_for_download(*realm));
2!
1880
        CppContext c(realm);
2✔
1881
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", util::Any{obj_id});
2✔
1882
        auto val = obj.get_column_value<Decimal128>("decimal");
2✔
1883
        CHECK(val == large_significand);
2!
1884
    }
2✔
1885
}
2✔
1886

1887
TEST_CASE("app: upgrade from local to synced realm", "[sync][pbs][app][upgrade][baas]") {
4✔
1888
    std::string base_url = get_base_url();
4✔
1889
    const std::string valid_pk_name = "_id";
4✔
1890
    REQUIRE(!base_url.empty());
4!
1891

2✔
1892
    Schema schema{
4✔
1893
        {"origin",
4✔
1894
         {{valid_pk_name, PropertyType::Int, Property::IsPrimary{true}},
4✔
1895
          {"link", PropertyType::Object | PropertyType::Nullable, "target"},
4✔
1896
          {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"}}},
4✔
1897
        {"target",
4✔
1898
         {{valid_pk_name, PropertyType::String, Property::IsPrimary{true}},
4✔
1899
          {"value", PropertyType::Int},
4✔
1900
          {"name", PropertyType::String}}},
4✔
1901
        {"other_origin",
4✔
1902
         {{valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
1903
          {"array", PropertyType::Array | PropertyType::Object, "other_target"}}},
4✔
1904
        {"other_target",
4✔
1905
         {{valid_pk_name, PropertyType::UUID, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
4✔
1906
        {"embedded", ObjectSchema::ObjectType::Embedded, {{"name", PropertyType::String | PropertyType::Nullable}}},
4✔
1907
    };
4✔
1908

2✔
1909
    /*             Create local realm             */
2✔
1910
    TestFile local_config;
4✔
1911
    local_config.schema = schema;
4✔
1912
    auto local_realm = Realm::get_shared_realm(local_config);
4✔
1913
    {
4✔
1914
        auto origin = local_realm->read_group().get_table("class_origin");
4✔
1915
        auto target = local_realm->read_group().get_table("class_target");
4✔
1916
        auto other_origin = local_realm->read_group().get_table("class_other_origin");
4✔
1917
        auto other_target = local_realm->read_group().get_table("class_other_target");
4✔
1918

2✔
1919
        local_realm->begin_transaction();
4✔
1920
        auto o = target->create_object_with_primary_key("Foo").set("name", "Egon");
4✔
1921
        // 'embedded_link' property is null.
2✔
1922
        origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
1923
        // 'embedded_link' property is not null.
2✔
1924
        auto obj = origin->create_object_with_primary_key(42);
4✔
1925
        auto col_key = origin->get_column_key("embedded_link");
4✔
1926
        obj.create_and_set_linked_object(col_key);
4✔
1927
        other_target->create_object_with_primary_key(UUID("3b241101-e2bb-4255-8caf-4136c566a961"));
4✔
1928
        other_origin->create_object_with_primary_key(ObjectId::gen());
4✔
1929
        local_realm->commit_transaction();
4✔
1930
    }
4✔
1931

2✔
1932
    /* Create a synced realm and upload some data */
2✔
1933
    auto server_app_config = minimal_app_config(base_url, "upgrade_from_local", schema);
4✔
1934
    TestAppSession test_session(create_app(server_app_config));
4✔
1935
    auto partition = random_string(100);
4✔
1936
    auto user1 = test_session.app()->current_user();
4✔
1937
    SyncTestFile config1(user1, partition, schema);
4✔
1938

2✔
1939
    auto r1 = Realm::get_shared_realm(config1);
4✔
1940

2✔
1941
    auto origin = r1->read_group().get_table("class_origin");
4✔
1942
    auto target = r1->read_group().get_table("class_target");
4✔
1943
    auto other_origin = r1->read_group().get_table("class_other_origin");
4✔
1944
    auto other_target = r1->read_group().get_table("class_other_target");
4✔
1945

2✔
1946
    r1->begin_transaction();
4✔
1947
    auto o = target->create_object_with_primary_key("Baa").set("name", "Børge");
4✔
1948
    origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
1949
    other_target->create_object_with_primary_key(UUID("01234567-89ab-cdef-edcb-a98765432101"));
4✔
1950
    other_origin->create_object_with_primary_key(ObjectId::gen());
4✔
1951
    r1->commit_transaction();
4✔
1952
    CHECK(!wait_for_upload(*r1));
4!
1953

2✔
1954
    /* Copy local realm data over in a synced one*/
2✔
1955
    create_user_and_log_in(test_session.app());
4✔
1956
    auto user2 = test_session.app()->current_user();
4✔
1957
    REQUIRE(user1 != user2);
4!
1958

2✔
1959
    SyncTestFile config2(user1, partition, schema);
4✔
1960

2✔
1961
    SharedRealm r2;
4✔
1962
    SECTION("Copy before connecting to server") {
4✔
1963
        local_realm->convert(config2);
2✔
1964
        r2 = Realm::get_shared_realm(config2);
2✔
1965
    }
2✔
1966

2✔
1967
    SECTION("Open synced realm first") {
4✔
1968
        r2 = Realm::get_shared_realm(config2);
2✔
1969
        CHECK(!wait_for_download(*r2));
2!
1970
        local_realm->convert(config2);
2✔
1971
        CHECK(!wait_for_upload(*r2));
2!
1972
    }
2✔
1973

2✔
1974
    CHECK(!wait_for_download(*r2));
4!
1975
    advance_and_notify(*r2);
4✔
1976
    Group& g = r2->read_group();
4✔
1977
    // g.to_json(std::cout);
2✔
1978
    REQUIRE(g.get_table("class_origin")->size() == 2);
4!
1979
    REQUIRE(g.get_table("class_target")->size() == 2);
4!
1980
    REQUIRE(g.get_table("class_other_origin")->size() == 2);
4!
1981
    REQUIRE(g.get_table("class_other_target")->size() == 2);
4!
1982

2✔
1983
    CHECK(!wait_for_upload(*r2));
4!
1984
    CHECK(!wait_for_download(*r1));
4!
1985
    advance_and_notify(*r1);
4✔
1986
    // r1->read_group().to_json(std::cout);
2✔
1987
}
4✔
1988

1989
TEST_CASE("app: set new embedded object", "[sync][pbs][app][baas]") {
2✔
1990
    std::string base_url = get_base_url();
2✔
1991
    const std::string valid_pk_name = "_id";
2✔
1992
    REQUIRE(!base_url.empty());
2!
1993

1✔
1994
    Schema schema{
2✔
1995
        {"TopLevel",
2✔
1996
         {
2✔
1997
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
1998
             {"array_of_objs", PropertyType::Object | PropertyType::Array, "TopLevel_array_of_objs"},
2✔
1999
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"},
2✔
2000
             {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable,
2✔
2001
              "TopLevel_embedded_dict"},
2✔
2002
         }},
2✔
2003
        {"TopLevel_array_of_objs",
2✔
2004
         ObjectSchema::ObjectType::Embedded,
2✔
2005
         {
2✔
2006
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2007
         }},
2✔
2008
        {"TopLevel_embedded_obj",
2✔
2009
         ObjectSchema::ObjectType::Embedded,
2✔
2010
         {
2✔
2011
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2012
         }},
2✔
2013
        {"TopLevel_embedded_dict",
2✔
2014
         ObjectSchema::ObjectType::Embedded,
2✔
2015
         {
2✔
2016
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2017
         }},
2✔
2018
    };
2✔
2019

1✔
2020
    auto server_app_config = minimal_app_config(base_url, "set_new_embedded_object", schema);
2✔
2021
    TestAppSession test_session(create_app(server_app_config));
2✔
2022
    auto partition = random_string(100);
2✔
2023

1✔
2024
    auto array_of_objs_id = ObjectId::gen();
2✔
2025
    auto embedded_obj_id = ObjectId::gen();
2✔
2026
    auto dict_obj_id = ObjectId::gen();
2✔
2027

1✔
2028
    {
2✔
2029
        SyncTestFile config(test_session.app(), partition, schema);
2✔
2030
        auto realm = Realm::get_shared_realm(config);
2✔
2031

1✔
2032
        CppContext c(realm);
2✔
2033
        realm->begin_transaction();
2✔
2034
        auto array_of_objs =
2✔
2035
            Object::create(c, realm, "TopLevel",
2✔
2036
                           std::any(AnyDict{
2✔
2037
                               {valid_pk_name, array_of_objs_id},
2✔
2038
                               {"array_of_objs", AnyVector{AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}},
2✔
2039
                           }),
2✔
2040
                           CreatePolicy::ForceCreate);
2✔
2041

1✔
2042
        auto embedded_obj =
2✔
2043
            Object::create(c, realm, "TopLevel",
2✔
2044
                           std::any(AnyDict{
2✔
2045
                               {valid_pk_name, embedded_obj_id},
2✔
2046
                               {"embedded_obj", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}},
2✔
2047
                           }),
2✔
2048
                           CreatePolicy::ForceCreate);
2✔
2049

1✔
2050
        auto dict_obj = Object::create(
2✔
2051
            c, realm, "TopLevel",
2✔
2052
            std::any(AnyDict{
2✔
2053
                {valid_pk_name, dict_obj_id},
2✔
2054
                {"embedded_dict", AnyDict{{"foo", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}},
2✔
2055
            }),
2✔
2056
            CreatePolicy::ForceCreate);
2✔
2057

1✔
2058
        realm->commit_transaction();
2✔
2059
        {
2✔
2060
            realm->begin_transaction();
2✔
2061
            embedded_obj.set_property_value(c, "embedded_obj",
2✔
2062
                                            std::any(AnyDict{{
2✔
2063
                                                "array",
2✔
2064
                                                AnyVector{INT64_C(3), INT64_C(4)},
2✔
2065
                                            }}),
2✔
2066
                                            CreatePolicy::UpdateAll);
2✔
2067
            realm->commit_transaction();
2✔
2068
        }
2✔
2069

1✔
2070
        {
2✔
2071
            realm->begin_transaction();
2✔
2072
            List array(array_of_objs, array_of_objs.get_object_schema().property_for_name("array_of_objs"));
2✔
2073
            CppContext c2(realm, &array.get_object_schema());
2✔
2074
            array.set(c2, 0, std::any{AnyDict{{"array", AnyVector{INT64_C(5), INT64_C(6)}}}});
2✔
2075
            realm->commit_transaction();
2✔
2076
        }
2✔
2077

1✔
2078
        {
2✔
2079
            realm->begin_transaction();
2✔
2080
            object_store::Dictionary dict(dict_obj, dict_obj.get_object_schema().property_for_name("embedded_dict"));
2✔
2081
            CppContext c2(realm, &dict.get_object_schema());
2✔
2082
            dict.insert(c2, "foo", std::any{AnyDict{{"array", AnyVector{INT64_C(7), INT64_C(8)}}}});
2✔
2083
            realm->commit_transaction();
2✔
2084
        }
2✔
2085
        CHECK(!wait_for_upload(*realm));
2!
2086
    }
2✔
2087

1✔
2088
    {
2✔
2089
        SyncTestFile config(test_session.app(), partition, schema);
2✔
2090
        auto realm = Realm::get_shared_realm(config);
2✔
2091

1✔
2092
        CHECK(!wait_for_download(*realm));
2!
2093
        CppContext c(realm);
2✔
2094
        {
2✔
2095
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{embedded_obj_id});
2✔
2096
            auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
2097
            auto array_list = util::any_cast<List&&>(embedded_obj.get_property_value<std::any>(c, "array"));
2✔
2098
            CHECK(array_list.size() == 2);
2!
2099
            CHECK(array_list.get<int64_t>(0) == int64_t(3));
2!
2100
            CHECK(array_list.get<int64_t>(1) == int64_t(4));
2!
2101
        }
2✔
2102

1✔
2103
        {
2✔
2104
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{array_of_objs_id});
2✔
2105
            auto embedded_list = util::any_cast<List&&>(obj.get_property_value<std::any>(c, "array_of_objs"));
2✔
2106
            CppContext c2(realm, &embedded_list.get_object_schema());
2✔
2107
            auto embedded_array_obj = util::any_cast<Object&&>(embedded_list.get(c2, 0));
2✔
2108
            auto array_list = util::any_cast<List&&>(embedded_array_obj.get_property_value<std::any>(c2, "array"));
2✔
2109
            CHECK(array_list.size() == 2);
2!
2110
            CHECK(array_list.get<int64_t>(0) == int64_t(5));
2!
2111
            CHECK(array_list.get<int64_t>(1) == int64_t(6));
2!
2112
        }
2✔
2113

1✔
2114
        {
2✔
2115
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{dict_obj_id});
2✔
2116
            object_store::Dictionary dict(obj, obj.get_object_schema().property_for_name("embedded_dict"));
2✔
2117
            CppContext c2(realm, &dict.get_object_schema());
2✔
2118
            auto embedded_obj = util::any_cast<Object&&>(dict.get(c2, "foo"));
2✔
2119
            auto array_list = util::any_cast<List&&>(embedded_obj.get_property_value<std::any>(c2, "array"));
2✔
2120
            CHECK(array_list.size() == 2);
2!
2121
            CHECK(array_list.get<int64_t>(0) == int64_t(7));
2!
2122
            CHECK(array_list.get<int64_t>(1) == int64_t(8));
2!
2123
        }
2✔
2124
    }
2✔
2125
}
2✔
2126

2127
TEST_CASE("app: make distributable client file", "[sync][pbs][app][baas]") {
2✔
2128
    TestAppSession session;
2✔
2129
    auto app = session.app();
2✔
2130

1✔
2131
    auto schema = default_app_config("").schema;
2✔
2132
    SyncTestFile original_config(app, bson::Bson("foo"), schema);
2✔
2133
    create_user_and_log_in(app);
2✔
2134
    SyncTestFile target_config(app, bson::Bson("foo"), schema);
2✔
2135

1✔
2136
    // Create realm file without client file id
1✔
2137
    {
2✔
2138
        auto realm = Realm::get_shared_realm(original_config);
2✔
2139

1✔
2140
        // Write some data
1✔
2141
        realm->begin_transaction();
2✔
2142
        CppContext c;
2✔
2143
        Object::create(c, realm, "Person",
2✔
2144
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2145
                                               {"age", INT64_C(64)},
2✔
2146
                                               {"firstName", std::string("Paul")},
2✔
2147
                                               {"lastName", std::string("McCartney")}}));
2✔
2148
        realm->commit_transaction();
2✔
2149
        wait_for_upload(*realm);
2✔
2150
        wait_for_download(*realm);
2✔
2151

1✔
2152
        realm->convert(target_config);
2✔
2153

1✔
2154
        // Write some additional data
1✔
2155
        realm->begin_transaction();
2✔
2156
        Object::create(c, realm, "Dog",
2✔
2157
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2158
                                               {"breed", std::string("stabyhoun")},
2✔
2159
                                               {"name", std::string("albert")},
2✔
2160
                                               {"realm_id", std::string("foo")}}));
2✔
2161
        realm->commit_transaction();
2✔
2162
        wait_for_upload(*realm);
2✔
2163
    }
2✔
2164
    // Starting a new session based on the copy
1✔
2165
    {
2✔
2166
        auto realm = Realm::get_shared_realm(target_config);
2✔
2167
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2168
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 0);
2!
2169

1✔
2170
        // Should be able to download the object created in the source Realm
1✔
2171
        // after writing the copy
1✔
2172
        wait_for_download(*realm);
2✔
2173
        realm->refresh();
2✔
2174
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2175
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1);
2!
2176

1✔
2177
        // Check that we can continue committing to this realm
1✔
2178
        realm->begin_transaction();
2✔
2179
        CppContext c;
2✔
2180
        Object::create(c, realm, "Dog",
2✔
2181
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2182
                                               {"breed", std::string("bulldog")},
2✔
2183
                                               {"name", std::string("fido")},
2✔
2184
                                               {"realm_id", std::string("foo")}}));
2✔
2185
        realm->commit_transaction();
2✔
2186
        wait_for_upload(*realm);
2✔
2187
    }
2✔
2188
    // Original Realm should be able to read the object which was written to the copy
1✔
2189
    {
2✔
2190
        auto realm = Realm::get_shared_realm(original_config);
2✔
2191
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2192
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1);
2!
2193

1✔
2194
        wait_for_download(*realm);
2✔
2195
        realm->refresh();
2✔
2196
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2197
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 2);
2!
2198
    }
2✔
2199
}
2✔
2200

2201
TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
54✔
2202
    auto logger = std::make_shared<util::StderrLogger>(realm::util::Logger::Level::TEST_LOGGING_LEVEL);
54✔
2203

27✔
2204
    const auto schema = default_app_config("").schema;
54✔
2205

27✔
2206
    auto get_dogs = [](SharedRealm r) -> Results {
52✔
2207
        wait_for_upload(*r, std::chrono::seconds(10));
50✔
2208
        wait_for_download(*r, std::chrono::seconds(10));
50✔
2209
        return Results(r, r->read_group().get_table("class_Dog"));
50✔
2210
    };
50✔
2211

27✔
2212
    auto create_one_dog = [](SharedRealm r) {
36✔
2213
        r->begin_transaction();
18✔
2214
        CppContext c;
18✔
2215
        Object::create(c, r, "Dog",
18✔
2216
                       std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
18✔
2217
                                        {"breed", std::string("bulldog")},
18✔
2218
                                        {"name", std::string("fido")}}),
18✔
2219
                       CreatePolicy::ForceCreate);
18✔
2220
        r->commit_transaction();
18✔
2221
    };
18✔
2222

27✔
2223
    TestAppSession session;
54✔
2224
    auto app = session.app();
54✔
2225
    const auto partition = random_string(100);
54✔
2226

27✔
2227
    // MARK: Add Objects -
27✔
2228
    SECTION("Add Objects") {
54✔
2229
        {
2✔
2230
            SyncTestFile config(app, partition, schema);
2✔
2231
            auto r = Realm::get_shared_realm(config);
2✔
2232

1✔
2233
            REQUIRE(get_dogs(r).size() == 0);
2!
2234
            create_one_dog(r);
2✔
2235
            REQUIRE(get_dogs(r).size() == 1);
2!
2236
        }
2✔
2237

1✔
2238
        {
2✔
2239
            create_user_and_log_in(app);
2✔
2240
            SyncTestFile config(app, partition, schema);
2✔
2241
            auto r = Realm::get_shared_realm(config);
2✔
2242
            Results dogs = get_dogs(r);
2✔
2243
            REQUIRE(dogs.size() == 1);
2!
2244
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2245
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2246
        }
2✔
2247
    }
2✔
2248

27✔
2249
    SECTION("MemOnly durability") {
54✔
2250
        {
2✔
2251
            SyncTestFile config(app, partition, schema);
2✔
2252
            config.in_memory = true;
2✔
2253
            config.encryption_key = std::vector<char>();
2✔
2254

1✔
2255
            REQUIRE(config.options().durability == DBOptions::Durability::MemOnly);
2!
2256
            auto r = Realm::get_shared_realm(config);
2✔
2257

1✔
2258
            REQUIRE(get_dogs(r).size() == 0);
2!
2259
            create_one_dog(r);
2✔
2260
            REQUIRE(get_dogs(r).size() == 1);
2!
2261
        }
2✔
2262

1✔
2263
        {
2✔
2264
            create_user_and_log_in(app);
2✔
2265
            SyncTestFile config(app, partition, schema);
2✔
2266
            config.in_memory = true;
2✔
2267
            config.encryption_key = std::vector<char>();
2✔
2268
            auto r = Realm::get_shared_realm(config);
2✔
2269
            Results dogs = get_dogs(r);
2✔
2270
            REQUIRE(dogs.size() == 1);
2!
2271
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2272
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2273
        }
2✔
2274
    }
2✔
2275

27✔
2276
    // MARK: Expired Session Refresh -
27✔
2277
    SECTION("Invalid Access Token is Refreshed") {
54✔
2278
        {
2✔
2279
            SyncTestFile config(app, partition, schema);
2✔
2280
            auto r = Realm::get_shared_realm(config);
2✔
2281
            REQUIRE(get_dogs(r).size() == 0);
2!
2282
            create_one_dog(r);
2✔
2283
            REQUIRE(get_dogs(r).size() == 1);
2!
2284
        }
2✔
2285

1✔
2286
        {
2✔
2287
            create_user_and_log_in(app);
2✔
2288
            auto user = app->current_user();
2✔
2289
            // set a bad access token. this will trigger a refresh when the sync session opens
1✔
2290
            user->update_access_token(encode_fake_jwt("fake_access_token"));
2✔
2291

1✔
2292
            SyncTestFile config(app, partition, schema);
2✔
2293
            auto r = Realm::get_shared_realm(config);
2✔
2294
            Results dogs = get_dogs(r);
2✔
2295
            REQUIRE(dogs.size() == 1);
2!
2296
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2297
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2298
        }
2✔
2299
    }
2✔
2300

27✔
2301
    class HookedTransport : public SynchronousTestTransport {
54✔
2302
    public:
54✔
2303
        void send_request_to_server(const Request& request,
54✔
2304
                                    util::UniqueFunction<void(const Response&)>&& completion) override
54✔
2305
        {
208✔
2306
            if (request_hook) {
208✔
2307
                request_hook(request);
166✔
2308
            }
166✔
2309
            if (simulated_response) {
208✔
2310
                return completion(*simulated_response);
110✔
2311
            }
110✔
2312
            SynchronousTestTransport::send_request_to_server(request, [&](const Response& response) mutable {
98✔
2313
                if (response_hook) {
98✔
2314
                    response_hook(request, response);
14✔
2315
                }
14✔
2316
                completion(response);
98✔
2317
            });
98✔
2318
        }
98✔
2319
        // Optional handler for the request and response before it is returned to completion
27✔
2320
        std::function<void(const Request&, const Response&)> response_hook;
54✔
2321
        // Optional handler for the request before it is sent to the server
27✔
2322
        std::function<void(const Request&)> request_hook;
54✔
2323
        // Optional Response object to return immediately instead of communicating with the server
27✔
2324
        std::optional<Response> simulated_response;
54✔
2325
    };
54✔
2326

27✔
2327
    struct HookedSocketProvider : public sync::websocket::DefaultSocketProvider {
54✔
2328
        HookedSocketProvider(const std::shared_ptr<util::Logger>& logger, const std::string user_agent,
54✔
2329
                             AutoStart auto_start = AutoStart{true})
54✔
2330
            : DefaultSocketProvider(logger, user_agent, nullptr, auto_start)
54✔
2331
        {
30✔
2332
        }
6✔
2333

27✔
2334
        std::unique_ptr<sync::WebSocketInterface> connect(std::unique_ptr<sync::WebSocketObserver> observer,
54✔
2335
                                                          sync::WebSocketEndpoint&& endpoint) override
54✔
2336
        {
34✔
2337
            int status_code = 101;
14✔
2338
            std::string body;
14✔
2339
            bool use_simulated_response = websocket_connect_func && websocket_connect_func(status_code, body);
14✔
2340

7✔
2341
            auto websocket = DefaultSocketProvider::connect(std::move(observer), std::move(endpoint));
14✔
2342
            if (use_simulated_response) {
14✔
2343
                auto default_websocket = static_cast<sync::websocket::DefaultWebSocket*>(websocket.get());
6✔
2344
                if (default_websocket)
6✔
2345
                    default_websocket->force_handshake_response_for_testing(status_code, body);
6✔
2346
            }
6✔
2347
            return websocket;
14✔
2348
        }
14✔
2349

27✔
2350
        std::function<bool(int&, std::string&)> websocket_connect_func;
54✔
2351
    };
54✔
2352

27✔
2353
    {
54✔
2354
        std::unique_ptr<realm::AppSession> app_session;
54✔
2355
        std::string base_file_path = util::make_temp_dir() + random_string(10);
54✔
2356
        auto redir_transport = std::make_shared<HookedTransport>();
54✔
2357
        AutoVerifiedEmailCredentials creds;
54✔
2358

27✔
2359
        auto app_config = get_config(redir_transport, session.app_session());
54✔
2360
        set_app_config_defaults(app_config, redir_transport);
54✔
2361

27✔
2362
        util::try_make_dir(base_file_path);
54✔
2363
        SyncClientConfig sc_config;
54✔
2364
        sc_config.base_file_path = base_file_path;
54✔
2365
        sc_config.metadata_mode = realm::SyncManager::MetadataMode::NoEncryption;
54✔
2366

27✔
2367
        // initialize app and sync client
27✔
2368
        auto redir_app = app::App::get_uncached_app(app_config, sc_config);
54✔
2369

27✔
2370
        SECTION("Test invalid redirect response") {
54✔
2371
            int request_count = 0;
2✔
2372
            redir_transport->request_hook = [&](const Request& request) {
4✔
2373
                if (request_count == 0) {
4✔
2374
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2375
                    redir_transport->simulated_response = {
2✔
2376
                        301, 0, {{"Content-Type", "application/json"}}, "Some body data"};
2✔
2377
                    request_count++;
2✔
2378
                }
2✔
2379
                else if (request_count == 1) {
2✔
2380
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2381
                    redir_transport->simulated_response = {
2✔
2382
                        301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"};
2✔
2383
                    request_count++;
2✔
2384
                }
2✔
2385
            };
4✔
2386

1✔
2387
            // This will fail due to no Location header
1✔
2388
            redir_app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
2389
                creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
2390
                    REQUIRE(error);
2!
2391
                    REQUIRE(error->is_client_error());
2!
2392
                    REQUIRE(error->code() == ErrorCodes::ClientRedirectError);
2!
2393
                    REQUIRE(error->reason() == "Redirect response missing location header");
2!
2394
                });
2✔
2395

1✔
2396
            // This will fail due to empty Location header
1✔
2397
            redir_app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
2398
                creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
2399
                    REQUIRE(error);
2!
2400
                    REQUIRE(error->is_client_error());
2!
2401
                    REQUIRE(error->code() == ErrorCodes::ClientRedirectError);
2!
2402
                    REQUIRE(error->reason() == "Redirect response missing location header");
2!
2403
                });
2✔
2404
        }
2✔
2405

27✔
2406
        SECTION("Test redirect response") {
54✔
2407
            int request_count = 0;
2✔
2408
            // redirect URL is localhost or 127.0.0.1 depending on what the initial value is
1✔
2409
            std::string original_host = "localhost:9090";
2✔
2410
            std::string redirect_scheme = "http://";
2✔
2411
            std::string redirect_host = "127.0.0.1:9090";
2✔
2412
            std::string redirect_url = "http://127.0.0.1:9090";
2✔
2413
            redir_transport->request_hook = [&](const Request& request) {
12✔
2414
                logger->trace("Received request[%1]: %2", request_count, request.url);
12✔
2415
                if (request_count == 0) {
12✔
2416
                    // First request should be to location
1✔
2417
                    REQUIRE(request.url.find("/location") != std::string::npos);
2!
2418
                    if (request.url.find("https://") != std::string::npos) {
2✔
2419
                        redirect_scheme = "https://";
×
2420
                    }
×
2421
                    // using local baas
1✔
2422
                    if (request.url.find("127.0.0.1:9090") != std::string::npos) {
2✔
2423
                        redirect_host = "localhost:9090";
×
2424
                        original_host = "127.0.0.1:9090";
×
2425
                    }
×
2426
                    // using baas docker - can't test redirect
1✔
2427
                    else if (request.url.find("mongodb-realm:9090") != std::string::npos) {
2✔
2428
                        redirect_host = "mongodb-realm:9090";
×
2429
                        original_host = "mongodb-realm:9090";
×
2430
                    }
×
2431

1✔
2432
                    redirect_url = redirect_scheme + redirect_host;
2✔
2433
                    logger->trace("redirect_url (%1): %2", request_count, redirect_url);
2✔
2434
                    request_count++;
2✔
2435
                }
2✔
2436
                else if (request_count == 1) {
10✔
2437
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2438
                    REQUIRE(!request.redirect_count);
2!
2439
                    redir_transport->simulated_response = {
2✔
2440
                        301,
2✔
2441
                        0,
2✔
2442
                        {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}},
2✔
2443
                        "Some body data"};
2✔
2444
                    request_count++;
2✔
2445
                }
2✔
2446
                else if (request_count == 2) {
8✔
2447
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2448
                    REQUIRE(request.url.find("somehost:9090") != std::string::npos);
2!
2449
                    redir_transport->simulated_response = {
2✔
2450
                        308, 0, {{"Location", redirect_url}, {"Content-Type", "application/json"}}, "Some body data"};
2✔
2451
                    request_count++;
2✔
2452
                }
2✔
2453
                else if (request_count == 3) {
6✔
2454
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2455
                    REQUIRE(request.url.find(redirect_url) != std::string::npos);
2!
2456
                    redir_transport->simulated_response = {
2✔
2457
                        301,
2✔
2458
                        0,
2✔
2459
                        {{"Location", redirect_scheme + original_host}, {"Content-Type", "application/json"}},
2✔
2460
                        "Some body data"};
2✔
2461
                    request_count++;
2✔
2462
                }
2✔
2463
                else if (request_count == 4) {
4✔
2464
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2465
                    REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos);
2!
2466
                    // Let the init_app_metadata request go through
1✔
2467
                    redir_transport->simulated_response.reset();
2✔
2468
                    request_count++;
2✔
2469
                }
2✔
2470
                else if (request_count == 5) {
2✔
2471
                    // This is the original request after the init app metadata
1✔
2472
                    logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2473
                    auto sync_manager = redir_app->sync_manager();
2✔
2474
                    REQUIRE(sync_manager);
2!
2475
                    auto app_metadata = sync_manager->app_metadata();
2✔
2476
                    REQUIRE(app_metadata);
2!
2477
                    logger->trace("Deployment model: %1", app_metadata->deployment_model);
2✔
2478
                    logger->trace("Location: %1", app_metadata->location);
2✔
2479
                    logger->trace("Hostname: %1", app_metadata->hostname);
2✔
2480
                    logger->trace("WS Hostname: %1", app_metadata->ws_hostname);
2✔
2481
                    REQUIRE(app_metadata->hostname.find(original_host) != std::string::npos);
2!
2482
                    REQUIRE(request.url.find(redirect_scheme + original_host) != std::string::npos);
2!
2483
                    redir_transport->simulated_response.reset();
2✔
2484
                    // Validate the retry count tracked in the original message
1✔
2485
                    REQUIRE(request.redirect_count == 3);
2!
2486
                    request_count++;
2✔
2487
                }
2✔
2488
            };
12✔
2489

1✔
2490
            // This will be successful after a couple of retries due to the redirect response
1✔
2491
            redir_app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
2492
                creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
2493
                    REQUIRE(!error);
2!
2494
                });
2✔
2495
        }
2✔
2496
        SECTION("Test too many redirects") {
54✔
2497
            int request_count = 0;
2✔
2498
            redir_transport->request_hook = [&](const Request& request) {
42✔
2499
                logger->trace("request.url (%1): %2", request_count, request.url);
42✔
2500
                REQUIRE(request_count <= 21);
42!
2501
                redir_transport->simulated_response = {
42✔
2502
                    request_count % 2 == 1 ? 308 : 301,
32✔
2503
                    0,
42✔
2504
                    {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}},
42✔
2505
                    "Some body data"};
42✔
2506
                request_count++;
42✔
2507
            };
42✔
2508

1✔
2509
            redir_app->log_in_with_credentials(
2✔
2510
                realm::app::AppCredentials::username_password(creds.email, creds.password),
2✔
2511
                [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
2512
                    REQUIRE(!user);
2!
2513
                    REQUIRE(error);
2!
2514
                    REQUIRE(error->is_client_error());
2!
2515
                    REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects);
2!
2516
                    REQUIRE(error->reason() == "number of redirections exceeded 20");
2!
2517
                });
2✔
2518
        }
2✔
2519
        SECTION("Test server in maintenance") {
54✔
2520
            redir_transport->request_hook = [&](const Request&) {
2✔
2521
                nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"},
2✔
2522
                                                    {"error", "This service is currently undergoing maintenance"},
2✔
2523
                                                    {"link", "https://link.to/server_logs"}};
2✔
2524
                redir_transport->simulated_response = {
2✔
2525
                    500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()};
2✔
2526
            };
2✔
2527

1✔
2528
            redir_app->log_in_with_credentials(
2✔
2529
                realm::app::AppCredentials::username_password(creds.email, creds.password),
2✔
2530
                [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
2531
                    REQUIRE(!user);
2!
2532
                    REQUIRE(error);
2!
2533
                    REQUIRE(error->is_service_error());
2!
2534
                    REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress);
2!
2535
                    REQUIRE(error->reason() == "This service is currently undergoing maintenance");
2!
2536
                    REQUIRE(error->link_to_server_logs == "https://link.to/server_logs");
2!
2537
                    REQUIRE(*error->additional_status_code == 500);
2!
2538
                });
2✔
2539
        }
2✔
2540
    }
54✔
2541
    SECTION("Test app redirect with no metadata") {
54✔
2542
        std::unique_ptr<realm::AppSession> app_session;
2✔
2543
        std::string base_file_path = util::make_temp_dir() + random_string(10);
2✔
2544
        auto redir_transport = std::make_shared<HookedTransport>();
2✔
2545
        AutoVerifiedEmailCredentials creds, creds2;
2✔
2546

1✔
2547
        auto app_config = get_config(redir_transport, session.app_session());
2✔
2548
        set_app_config_defaults(app_config, redir_transport);
2✔
2549

1✔
2550
        util::try_make_dir(base_file_path);
2✔
2551
        SyncClientConfig sc_config;
2✔
2552
        sc_config.base_file_path = base_file_path;
2✔
2553
        sc_config.metadata_mode = realm::SyncManager::MetadataMode::NoMetadata;
2✔
2554

1✔
2555
        // initialize app and sync client
1✔
2556
        auto redir_app = app::App::get_uncached_app(app_config, sc_config);
2✔
2557

1✔
2558
        int request_count = 0;
2✔
2559
        // redirect URL is localhost or 127.0.0.1 depending on what the initial value is
1✔
2560
        std::string original_host = "localhost:9090";
2✔
2561
        std::string original_scheme = "http://";
2✔
2562
        std::string websocket_url = "ws://some-websocket:9090";
2✔
2563
        std::string original_url;
2✔
2564
        redir_transport->request_hook = [&](const Request& request) {
8✔
2565
            logger->trace("request.url (%1): %2", request_count, request.url);
8✔
2566
            if (request_count == 0) {
8✔
2567
                // First request should be to location
1✔
2568
                REQUIRE(request.url.find("/location") != std::string::npos);
2!
2569
                if (request.url.find("https://") != std::string::npos) {
2✔
2570
                    original_scheme = "https://";
×
2571
                }
×
2572
                // using local baas
1✔
2573
                if (request.url.find("127.0.0.1:9090") != std::string::npos) {
2✔
2574
                    original_host = "127.0.0.1:9090";
×
2575
                }
×
2576
                // using baas docker
1✔
2577
                else if (request.url.find("mongodb-realm:9090") != std::string::npos) {
2✔
2578
                    original_host = "mongodb-realm:9090";
×
2579
                }
×
2580
                original_url = original_scheme + original_host;
2✔
2581
                logger->trace("original_url (%1): %2", request_count, original_url);
2✔
2582
            }
2✔
2583
            else if (request_count == 1) {
6✔
2584
                REQUIRE(!request.redirect_count);
2!
2585
                redir_transport->simulated_response = {
2✔
2586
                    308,
2✔
2587
                    0,
2✔
2588
                    {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}},
2✔
2589
                    "Some body data"};
2✔
2590
            }
2✔
2591
            else if (request_count == 2) {
4✔
2592
                REQUIRE(request.url.find("http://somehost:9090") != std::string::npos);
2!
2593
                REQUIRE(request.url.find("location") != std::string::npos);
2!
2594
                // app hostname will be updated via the metadata info
1✔
2595
                redir_transport->simulated_response = {
2✔
2596
                    static_cast<int>(sync::HTTPStatus::Ok),
2✔
2597
                    0,
2✔
2598
                    {{"Content-Type", "application/json"}},
2✔
2599
                    util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%1\",\"ws_"
2✔
2600
                                 "hostname\":\"%2\"}",
2✔
2601
                                 original_url, websocket_url)};
2✔
2602
            }
2✔
2603
            else {
2✔
2604
                REQUIRE(request.url.find(original_url) != std::string::npos);
2!
2605
                redir_transport->simulated_response.reset();
2✔
2606
            }
2✔
2607
            request_count++;
8✔
2608
        };
8✔
2609

1✔
2610
        // This will be successful after a couple of retries due to the redirect response
1✔
2611
        redir_app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
2612
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
2613
                REQUIRE(!error);
2!
2614
            });
2✔
2615
        REQUIRE(!redir_app->sync_manager()->app_metadata()); // no stored app metadata
2!
2616
        REQUIRE(redir_app->sync_manager()->sync_route().find(websocket_url) != std::string::npos);
2!
2617

1✔
2618
        // Register another email address and verify location data isn't requested again
1✔
2619
        request_count = 0;
2✔
2620
        redir_transport->request_hook = [&](const Request& request) {
2✔
2621
            logger->trace("request.url (%1): %2", request_count, request.url);
2✔
2622
            redir_transport->simulated_response.reset();
2✔
2623
            REQUIRE(request.url.find("location") == std::string::npos);
2!
2624
            request_count++;
2✔
2625
        };
2✔
2626

1✔
2627
        redir_app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
2628
            creds2.email, creds2.password, [&](util::Optional<app::AppError> error) {
2✔
2629
                REQUIRE(!error);
2!
2630
            });
2✔
2631
    }
2✔
2632

27✔
2633
    SECTION("Test websocket redirect with existing session") {
54✔
2634
        std::string original_host = "localhost:9090";
6✔
2635
        std::string redirect_scheme = "http://";
6✔
2636
        std::string websocket_scheme = "ws://";
6✔
2637
        std::string redirect_host = "127.0.0.1:9090";
6✔
2638
        std::string redirect_url = "http://127.0.0.1:9090";
6✔
2639

3✔
2640
        auto redir_transport = std::make_shared<HookedTransport>();
6✔
2641
        auto redir_provider = std::make_shared<HookedSocketProvider>(logger, "");
6✔
2642
        std::mutex logout_mutex;
6✔
2643
        std::condition_variable logout_cv;
6✔
2644
        bool logged_out = false;
6✔
2645

3✔
2646
        // Use the transport to grab the current url so it can be converted
3✔
2647
        redir_transport->request_hook = [&](const Request& request) {
24✔
2648
            if (request.url.find("https://") != std::string::npos) {
24✔
2649
                redirect_scheme = "https://";
×
2650
                websocket_scheme = "wss://";
×
2651
            }
×
2652
            // using local baas
12✔
2653
            if (request.url.find("127.0.0.1:9090") != std::string::npos) {
24✔
2654
                redirect_host = "localhost:9090";
×
2655
                original_host = "127.0.0.1:9090";
×
2656
            }
×
2657
            // using baas docker - can't test redirect
12✔
2658
            else if (request.url.find("mongodb-realm:9090") != std::string::npos) {
24✔
2659
                redirect_host = "mongodb-realm:9090";
×
2660
                original_host = "mongodb-realm:9090";
×
2661
            }
×
2662

12✔
2663
            redirect_url = redirect_scheme + redirect_host;
24✔
2664
            logger->trace("redirect_url: %1", redirect_url);
24✔
2665
        };
24✔
2666

3✔
2667
        auto base_url = get_base_url();
6✔
2668
        auto server_app_config = minimal_app_config(base_url, "websocket_redirect", schema);
6✔
2669
        TestAppSession test_session(create_app(server_app_config), redir_transport, DeleteApp{true},
6✔
2670
                                    realm::ReconnectMode::normal, redir_provider);
6✔
2671
        auto partition = random_string(100);
6✔
2672
        auto user1 = test_session.app()->current_user();
6✔
2673
        SyncTestFile r_config(user1, partition, schema);
6✔
2674
        // Override the default
3✔
2675
        r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
5✔
2676
            if (error.status == ErrorCodes::AuthError) {
4✔
2677
                util::format(std::cerr, "Websocket redirect test: User logged out\n");
4✔
2678
                std::unique_lock lk(logout_mutex);
4✔
2679
                logged_out = true;
4✔
2680
                logout_cv.notify_one();
4✔
2681
                return;
4✔
2682
            }
4✔
2683
            util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
2684
                         error.status);
×
2685
            abort();
×
2686
        };
×
2687

3✔
2688
        auto r = Realm::get_shared_realm(r_config);
6✔
2689

3✔
2690
        REQUIRE(!wait_for_download(*r));
6!
2691

3✔
2692
        SECTION("Valid websocket redirect") {
6✔
2693
            auto sync_manager = test_session.app()->sync_manager();
2✔
2694
            auto sync_session = sync_manager->get_existing_session(r->config().path);
2✔
2695
            sync_session->pause();
2✔
2696

1✔
2697
            int connect_count = 0;
2✔
2698
            redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) {
4✔
2699
                if (connect_count++ > 0)
4✔
2700
                    return false;
2✔
2701

1✔
2702
                status_code = static_cast<int>(sync::HTTPStatus::PermanentRedirect);
2✔
2703
                body = "";
2✔
2704
                return true;
2✔
2705
            };
2✔
2706
            int request_count = 0;
2✔
2707
            redir_transport->request_hook = [&](const Request& request) {
6✔
2708
                logger->trace("request.url (%1): %2", request_count, request.url);
6✔
2709
                if (request_count++ == 0) {
6✔
2710
                    // First request should be a location request against the original URL
1✔
2711
                    REQUIRE(request.url.find(original_host) != std::string::npos);
2!
2712
                    REQUIRE(request.url.find("/location") != std::string::npos);
2!
2713
                    REQUIRE(request.redirect_count == 0);
2!
2714
                    redir_transport->simulated_response = {
2✔
2715
                        static_cast<int>(sync::HTTPStatus::PermanentRedirect),
2✔
2716
                        0,
2✔
2717
                        {{"Location", redirect_url}, {"Content-Type", "application/json"}},
2✔
2718
                        "Some body data"};
2✔
2719
                }
2✔
2720
                else if (request.url.find("/location") != std::string::npos) {
4✔
2721
                    redir_transport->simulated_response = {
2✔
2722
                        static_cast<int>(sync::HTTPStatus::Ok),
2✔
2723
                        0,
2✔
2724
                        {{"Content-Type", "application/json"}},
2✔
2725
                        util::format(
2✔
2726
                            "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_"
2✔
2727
                            "hostname\":\"%3%1\"}",
2✔
2728
                            redirect_host, redirect_scheme, websocket_scheme)};
2✔
2729
                }
2✔
2730
                else {
2✔
2731
                    redir_transport->simulated_response.reset();
2✔
2732
                }
2✔
2733
            };
6✔
2734

1✔
2735
            SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager);
2✔
2736
            sync_session->resume();
2✔
2737
            REQUIRE(!wait_for_download(*r));
2!
2738
            REQUIRE(user1->is_logged_in());
2!
2739

1✔
2740
            // Verify session is using the updated server url from the redirect
1✔
2741
            auto server_url = sync_session->full_realm_url();
2✔
2742
            logger->trace("FULL_REALM_URL: %1", server_url);
2✔
2743
            REQUIRE((server_url && server_url->find(redirect_host) != std::string::npos));
2!
2744
        }
2✔
2745
        SECTION("Websocket redirect logs out user") {
6✔
2746
            auto sync_manager = test_session.app()->sync_manager();
2✔
2747
            auto sync_session = sync_manager->get_existing_session(r->config().path);
2✔
2748
            sync_session->pause();
2✔
2749

1✔
2750
            int connect_count = 0;
2✔
2751
            redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) {
2✔
2752
                if (connect_count++ > 0)
2✔
2753
                    return false;
×
2754

1✔
2755
                status_code = static_cast<int>(sync::HTTPStatus::MovedPermanently);
2✔
2756
                body = "";
2✔
2757
                return true;
2✔
2758
            };
2✔
2759
            int request_count = 0;
2✔
2760
            redir_transport->request_hook = [&](const Request& request) {
6✔
2761
                logger->trace("request.url (%1): %2", request_count, request.url);
6✔
2762
                if (request_count++ == 0) {
6✔
2763
                    // First request should be a location request against the original URL
1✔
2764
                    REQUIRE(request.url.find(original_host) != std::string::npos);
2!
2765
                    REQUIRE(request.url.find("/location") != std::string::npos);
2!
2766
                    REQUIRE(request.redirect_count == 0);
2!
2767
                    redir_transport->simulated_response = {
2✔
2768
                        static_cast<int>(sync::HTTPStatus::MovedPermanently),
2✔
2769
                        0,
2✔
2770
                        {{"Location", redirect_url}, {"Content-Type", "application/json"}},
2✔
2771
                        "Some body data"};
2✔
2772
                }
2✔
2773
                else if (request.url.find("/location") != std::string::npos) {
4✔
2774
                    redir_transport->simulated_response = {
2✔
2775
                        static_cast<int>(sync::HTTPStatus::Ok),
2✔
2776
                        0,
2✔
2777
                        {{"Content-Type", "application/json"}},
2✔
2778
                        util::format(
2✔
2779
                            "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"%2%1\",\"ws_"
2✔
2780
                            "hostname\":\"%3%1\"}",
2✔
2781
                            redirect_host, redirect_scheme, websocket_scheme)};
2✔
2782
                }
2✔
2783
                else if (request.url.find("auth/session") != std::string::npos) {
2✔
2784
                    redir_transport->simulated_response = {static_cast<int>(sync::HTTPStatus::Unauthorized),
2✔
2785
                                                           0,
2✔
2786
                                                           {{"Content-Type", "application/json"}},
2✔
2787
                                                           ""};
2✔
2788
                }
2✔
2789
                else {
×
2790
                    redir_transport->simulated_response.reset();
×
2791
                }
×
2792
            };
6✔
2793

1✔
2794
            SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager);
2✔
2795
            sync_session->resume();
2✔
2796
            REQUIRE(wait_for_download(*r));
2!
2797
            std::unique_lock lk(logout_mutex);
2✔
2798
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
2799
                return logged_out;
4✔
2800
            });
4✔
2801
            REQUIRE(result);
2!
2802
            REQUIRE(!user1->is_logged_in());
2!
2803
        }
2✔
2804
        SECTION("Too many websocket redirects logs out user") {
6✔
2805
            auto sync_manager = test_session.app()->sync_manager();
2✔
2806
            auto sync_session = sync_manager->get_existing_session(r->config().path);
2✔
2807
            sync_session->pause();
2✔
2808

1✔
2809
            int connect_count = 0;
2✔
2810
            redir_provider->websocket_connect_func = [&connect_count](int& status_code, std::string& body) {
2✔
2811
                if (connect_count++ > 0)
2✔
2812
                    return false;
×
2813

1✔
2814
                status_code = static_cast<int>(sync::HTTPStatus::MovedPermanently);
2✔
2815
                body = "";
2✔
2816
                return true;
2✔
2817
            };
2✔
2818
            int request_count = 0;
2✔
2819
            const int max_http_redirects = 20; // from app.cpp in object-store
2✔
2820
            redir_transport->request_hook = [&](const Request& request) {
42✔
2821
                logger->trace("request.url (%1): %2", request_count, request.url);
42✔
2822
                if (request_count++ == 0) {
42✔
2823
                    // First request should be a location request against the original URL
1✔
2824
                    REQUIRE(request.url.find(original_host) != std::string::npos);
2!
2825
                    REQUIRE(request.url.find("/location") != std::string::npos);
2!
2826
                    REQUIRE(request.redirect_count == 0);
2!
2827
                }
2✔
2828
                if (request.url.find("/location") != std::string::npos) {
42✔
2829
                    // Keep returning the redirected response
21✔
2830
                    REQUIRE(request.redirect_count < max_http_redirects);
42!
2831
                    redir_transport->simulated_response = {
42✔
2832
                        static_cast<int>(sync::HTTPStatus::MovedPermanently),
42✔
2833
                        0,
42✔
2834
                        {{"Location", redirect_url}, {"Content-Type", "application/json"}},
42✔
2835
                        "Some body data"};
42✔
2836
                }
42✔
2837
                else {
×
2838
                    // should not get any other types of requests during the test - the log out is local
2839
                    REQUIRE(false);
×
2840
                }
×
2841
            };
42✔
2842

1✔
2843
            SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*sync_manager);
2✔
2844
            sync_session->resume();
2✔
2845
            REQUIRE(wait_for_download(*r));
2!
2846
            std::unique_lock lk(logout_mutex);
2✔
2847
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
2848
                return logged_out;
4✔
2849
            });
4✔
2850
            REQUIRE(result);
2!
2851
            REQUIRE(!user1->is_logged_in());
2!
2852
        }
2✔
2853
    }
6✔
2854

27✔
2855
    SECTION("Fast clock on client") {
54✔
2856
        {
2✔
2857
            SyncTestFile config(app, partition, schema);
2✔
2858
            auto r = Realm::get_shared_realm(config);
2✔
2859

1✔
2860
            REQUIRE(get_dogs(r).size() == 0);
2!
2861
            create_one_dog(r);
2✔
2862
            REQUIRE(get_dogs(r).size() == 1);
2!
2863
        }
2✔
2864

1✔
2865
        auto transport = std::make_shared<HookedTransport>();
2✔
2866
        TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false});
2✔
2867
        auto app = hooked_session.app();
2✔
2868
        std::shared_ptr<SyncUser> user = app->current_user();
2✔
2869
        REQUIRE(user);
2!
2870
        REQUIRE(!user->access_token_refresh_required());
2!
2871
        // Make the SyncUser behave as if the client clock is 31 minutes fast, so the token looks expired locally
1✔
2872
        // (access tokens have an lifetime of 30 minutes today).
1✔
2873
        user->set_seconds_to_adjust_time_for_testing(31 * 60);
2✔
2874
        REQUIRE(user->access_token_refresh_required());
2!
2875

1✔
2876
        // This assumes that we make an http request for the new token while
1✔
2877
        // already in the WaitingForAccessToken state.
1✔
2878
        bool seen_waiting_for_access_token = false;
2✔
2879
        transport->request_hook = [&](const Request&) {
2✔
2880
            auto user = app->current_user();
2✔
2881
            REQUIRE(user);
2!
2882
            for (auto& session : user->all_sessions()) {
2✔
2883
                // Prior to the fix for #4941, this callback would be called from an infinite loop, always in the
1✔
2884
                // WaitingForAccessToken state.
1✔
2885
                if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2886
                    REQUIRE(!seen_waiting_for_access_token);
2!
2887
                    seen_waiting_for_access_token = true;
2✔
2888
                }
2✔
2889
            }
2✔
2890
            return true;
2✔
2891
        };
2✔
2892
        SyncTestFile config(app, partition, schema);
2✔
2893
        auto r = Realm::get_shared_realm(config);
2✔
2894
        REQUIRE(seen_waiting_for_access_token);
2!
2895
        Results dogs = get_dogs(r);
2✔
2896
        REQUIRE(dogs.size() == 1);
2!
2897
        REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2898
        REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2899
    }
2✔
2900

27✔
2901
    SECTION("Expired Tokens") {
54✔
2902
        sync::AccessToken token;
8✔
2903
        {
8✔
2904
            std::shared_ptr<SyncUser> user = app->current_user();
8✔
2905
            SyncTestFile config(app, partition, schema);
8✔
2906
            auto r = Realm::get_shared_realm(config);
8✔
2907

4✔
2908
            REQUIRE(get_dogs(r).size() == 0);
8!
2909
            create_one_dog(r);
8✔
2910

4✔
2911
            REQUIRE(get_dogs(r).size() == 1);
8!
2912
            sync::AccessToken::ParseError error_state = realm::sync::AccessToken::ParseError::none;
8✔
2913
            sync::AccessToken::parse(user->access_token(), token, error_state, nullptr);
8✔
2914
            REQUIRE(error_state == sync::AccessToken::ParseError::none);
8!
2915
            REQUIRE(token.timestamp);
8!
2916
            REQUIRE(token.expires);
8!
2917
            REQUIRE(token.timestamp < token.expires);
8!
2918
            std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
8✔
2919
            using namespace std::chrono_literals;
8✔
2920
            token.expires = std::chrono::system_clock::to_time_t(now - 30s);
8✔
2921
            REQUIRE(token.expired(now));
8!
2922
        }
8✔
2923

4✔
2924
        auto transport = std::make_shared<HookedTransport>();
8✔
2925
        TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false});
8✔
2926
        auto app = hooked_session.app();
8✔
2927
        std::shared_ptr<SyncUser> user = app->current_user();
8✔
2928
        REQUIRE(user);
8!
2929
        REQUIRE(!user->access_token_refresh_required());
8!
2930
        // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client.
4✔
2931
        user->update_access_token(encode_fake_jwt("fake_access_token", token.expires, token.timestamp));
8✔
2932
        REQUIRE(user->access_token_refresh_required());
8!
2933

4✔
2934
        SECTION("Expired Access Token is Refreshed") {
8✔
2935
            // This assumes that we make an http request for the new token while
1✔
2936
            // already in the WaitingForAccessToken state.
1✔
2937
            bool seen_waiting_for_access_token = false;
2✔
2938
            transport->request_hook = [&](const Request&) {
2✔
2939
                auto user = app->current_user();
2✔
2940
                REQUIRE(user);
2!
2941
                for (auto& session : user->all_sessions()) {
2✔
2942
                    if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2943
                        REQUIRE(!seen_waiting_for_access_token);
2!
2944
                        seen_waiting_for_access_token = true;
2✔
2945
                    }
2✔
2946
                }
2✔
2947
            };
2✔
2948
            SyncTestFile config(app, partition, schema);
2✔
2949
            auto r = Realm::get_shared_realm(config);
2✔
2950
            REQUIRE(seen_waiting_for_access_token);
2!
2951
            Results dogs = get_dogs(r);
2✔
2952
            REQUIRE(dogs.size() == 1);
2!
2953
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2954
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2955
        }
2✔
2956

4✔
2957
        SECTION("User is logged out if the refresh request is denied") {
8✔
2958
            REQUIRE(user->is_logged_in());
2!
2959
            transport->response_hook = [&](const Request& request, const Response& response) {
2✔
2960
                auto user = app->current_user();
2✔
2961
                REQUIRE(user);
2!
2962
                // simulate the server denying the refresh
1✔
2963
                if (request.url.find("/session") != std::string::npos) {
2✔
2964
                    auto& response_ref = const_cast<Response&>(response);
2✔
2965
                    response_ref.http_status_code = 401;
2✔
2966
                    response_ref.body = "fake: refresh token could not be refreshed";
2✔
2967
                }
2✔
2968
            };
2✔
2969
            SyncTestFile config(app, partition, schema);
2✔
2970
            std::atomic<bool> sync_error_handler_called{false};
2✔
2971
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2972
                sync_error_handler_called.store(true);
2✔
2973
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
2!
2974
                REQUIRE_THAT(std::string{error.status.reason()},
2✔
2975
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
2✔
2976
            };
2✔
2977
            auto r = Realm::get_shared_realm(config);
2✔
2978
            timed_wait_for([&] {
3✔
2979
                return sync_error_handler_called.load();
3✔
2980
            });
3✔
2981
            // the failed refresh logs out the user
1✔
2982
            REQUIRE(!user->is_logged_in());
2!
2983
        }
2✔
2984

4✔
2985
        SECTION("User is left logged out if logged out while the refresh is in progress") {
8✔
2986
            REQUIRE(user->is_logged_in());
2!
2987
            transport->request_hook = [&](const Request&) {
2✔
2988
                user->log_out();
2✔
2989
            };
2✔
2990
            SyncTestFile config(app, partition, schema);
2✔
2991
            auto r = Realm::get_shared_realm(config);
2✔
2992
            REQUIRE_FALSE(user->is_logged_in());
2!
2993
            REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
2994
        }
2✔
2995

4✔
2996
        SECTION("Requests that receive an error are retried on a backoff") {
8✔
2997
            using namespace std::chrono;
2✔
2998
            std::vector<time_point<steady_clock>> response_times;
2✔
2999
            std::atomic<bool> did_receive_valid_token{false};
2✔
3000
            constexpr size_t num_error_responses = 6;
2✔
3001

1✔
3002
            transport->response_hook = [&](const Request& request, const Response& response) {
12✔
3003
                // simulate the server experiencing an internal server error
6✔
3004
                if (request.url.find("/session") != std::string::npos) {
12✔
3005
                    if (response_times.size() >= num_error_responses) {
12✔
3006
                        did_receive_valid_token.store(true);
2✔
3007
                        return;
2✔
3008
                    }
2✔
3009
                    auto& response_ref = const_cast<Response&>(response);
10✔
3010
                    response_ref.http_status_code = 500;
10✔
3011
                }
10✔
3012
            };
12✔
3013
            transport->request_hook = [&](const Request& request) {
12✔
3014
                if (!did_receive_valid_token.load() && request.url.find("/session") != std::string::npos) {
12✔
3015
                    response_times.push_back(steady_clock::now());
12✔
3016
                }
12✔
3017
            };
12✔
3018
            SyncTestFile config(app, partition, schema);
2✔
3019
            auto r = Realm::get_shared_realm(config);
2✔
3020
            create_one_dog(r);
2✔
3021
            timed_wait_for(
2✔
3022
                [&] {
18,707,063✔
3023
                    return did_receive_valid_token.load();
18,707,063✔
3024
                },
18,707,063✔
3025
                30s);
2✔
3026
            REQUIRE(user->is_logged_in());
2!
3027
            REQUIRE(response_times.size() >= num_error_responses);
2!
3028
            std::vector<uint64_t> delay_times;
2✔
3029
            for (size_t i = 1; i < response_times.size(); ++i) {
12✔
3030
                delay_times.push_back(duration_cast<milliseconds>(response_times[i] - response_times[i - 1]).count());
10✔
3031
            }
10✔
3032

1✔
3033
            // sync delays start at 1000ms minus a random number of up to 25%.
1✔
3034
            // the subsequent delay is double the previous one minus a random 25% again.
1✔
3035
            // this calculation happens in Connection::initiate_reconnect_wait()
1✔
3036
            bool increasing_delay = true;
2✔
3037
            for (size_t i = 1; i < delay_times.size(); ++i) {
10✔
3038
                if (delay_times[i - 1] >= delay_times[i]) {
8✔
3039
                    increasing_delay = false;
×
3040
                }
×
3041
            }
8✔
3042
            // fail if the first delay isn't longer than half a second
1✔
3043
            if (delay_times.size() <= 1 || delay_times[1] < 500) {
2✔
3044
                increasing_delay = false;
×
3045
            }
×
3046
            if (!increasing_delay) {
2✔
3047
                std::cerr << "delay times are not increasing: ";
×
3048
                for (auto& delay : delay_times) {
×
3049
                    std::cerr << delay << ", ";
×
3050
                }
×
3051
                std::cerr << std::endl;
×
3052
            }
×
3053
            REQUIRE(increasing_delay);
2!
3054
        }
2✔
3055
    }
8✔
3056

27✔
3057
    SECTION("Invalid refresh token") {
54✔
3058
        auto& app_session = session.app_session();
8✔
3059
        std::mutex mtx;
8✔
3060
        auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr<SyncUser> user,
8✔
3061
                                                                   Realm::Config config) {
7✔
3062
            REQUIRE(user);
6!
3063
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
6!
3064

3✔
3065
            // requesting a new access token fails because the refresh token used for this request is revoked
3✔
3066
            user->refresh_custom_data([&](Optional<AppError> error) {
6✔
3067
                REQUIRE(error);
6!
3068
                REQUIRE(error->additional_status_code == 401);
6!
3069
                REQUIRE(error->code() == ErrorCodes::InvalidSession);
6!
3070
            });
6✔
3071

3✔
3072
            // Set a bad access token. This will force a request for a new access token when the sync session opens
3✔
3073
            // this is only necessary because the server doesn't actually revoke previously issued access tokens
3✔
3074
            // instead allowing their session to time out as normal. So this simulates the access token expiring.
3✔
3075
            // see:
3✔
3076
            // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386
3✔
3077
            user->update_access_token(encode_fake_jwt("fake_access_token"));
6✔
3078
            REQUIRE(!app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
6!
3079

3✔
3080
            auto [sync_error_promise, sync_error] = util::make_promise_future<SyncError>();
6✔
3081
            config.sync_config->error_handler =
6✔
3082
                [promise = util::CopyablePromiseHolder(std::move(sync_error_promise))](std::shared_ptr<SyncSession>,
6✔
3083
                                                                                       SyncError error) mutable {
6✔
3084
                    promise.get_promise().emplace_value(std::move(error));
6✔
3085
                };
6✔
3086

3✔
3087
            auto transport = static_cast<SynchronousTestTransport*>(session.transport());
6✔
3088
            transport->block(); // don't let the token refresh happen until we're ready for it
6✔
3089
            auto r = Realm::get_shared_realm(config);
6✔
3090
            auto session = user->session_for_on_disk_path(config.path);
6✔
3091
            REQUIRE(user->is_logged_in());
6!
3092
            REQUIRE(!sync_error.is_ready());
6!
3093
            {
6✔
3094
                std::atomic<bool> called{false};
6✔
3095
                session->wait_for_upload_completion([&](Status stat) {
6✔
3096
                    std::lock_guard<std::mutex> lock(mtx);
6✔
3097
                    called.store(true);
6✔
3098
                    REQUIRE(stat.code() == ErrorCodes::InvalidSession);
6!
3099
                });
6✔
3100
                transport->unblock();
6✔
3101
                timed_wait_for([&] {
15,625✔
3102
                    return called.load();
15,625✔
3103
                });
15,625✔
3104
                std::lock_guard<std::mutex> lock(mtx);
6✔
3105
                REQUIRE(called);
6!
3106
            }
6✔
3107

3✔
3108
            auto sync_error_res = wait_for_future(std::move(sync_error)).get();
6✔
3109
            REQUIRE(sync_error_res.status == ErrorCodes::AuthError);
6!
3110
            REQUIRE_THAT(std::string{sync_error_res.status.reason()},
6✔
3111
                         Catch::Matchers::StartsWith("Unable to refresh the user access token"));
6✔
3112

3✔
3113
            // the failed refresh logs out the user
3✔
3114
            std::lock_guard<std::mutex> lock(mtx);
6✔
3115
            REQUIRE(!user->is_logged_in());
6!
3116
        };
6✔
3117

4✔
3118
        SECTION("Disabled user results in a sync error") {
8✔
3119
            auto creds = create_user_and_log_in(app);
2✔
3120
            SyncTestFile config(app, partition, schema);
2✔
3121
            auto user = app->current_user();
2✔
3122
            REQUIRE(user);
2!
3123
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
3124
            app_session.admin_api.disable_user_sessions(app->current_user()->identity(), app_session.server_app_id);
2✔
3125

1✔
3126
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
3127

1✔
3128
            // logging in again doesn't fix things while the account is disabled
1✔
3129
            auto error = failed_log_in(app, creds);
2✔
3130
            REQUIRE(error.code() == ErrorCodes::UserDisabled);
2!
3131

1✔
3132
            // admin enables user sessions again which should allow the session to continue
1✔
3133
            app_session.admin_api.enable_user_sessions(user->identity(), app_session.server_app_id);
2✔
3134

1✔
3135
            // logging in now works properly
1✔
3136
            log_in(app, creds);
2✔
3137

1✔
3138
            // still referencing the same user
1✔
3139
            REQUIRE(user == app->current_user());
2!
3140
            REQUIRE(user->is_logged_in());
2!
3141

1✔
3142
            {
2✔
3143
                // check that there are no errors initiating a session now by making sure upload/download succeeds
1✔
3144
                auto r = Realm::get_shared_realm(config);
2✔
3145
                Results dogs = get_dogs(r);
2✔
3146
            }
2✔
3147
        }
2✔
3148

4✔
3149
        SECTION("Revoked refresh token results in a sync error") {
8✔
3150
            auto creds = create_user_and_log_in(app);
2✔
3151
            SyncTestFile config(app, partition, schema);
2✔
3152
            auto user = app->current_user();
2✔
3153
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
3154
            app_session.admin_api.revoke_user_sessions(user->identity(), app_session.server_app_id);
2✔
3155
            // revoking a user session only affects the refresh token, so the access token should still continue to
1✔
3156
            // work.
1✔
3157
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
3158

1✔
3159
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
3160

1✔
3161
            // logging in again succeeds and generates a new and valid refresh token
1✔
3162
            log_in(app, creds);
2✔
3163

1✔
3164
            // still referencing the same user and now the user is logged in
1✔
3165
            REQUIRE(user == app->current_user());
2!
3166
            REQUIRE(user->is_logged_in());
2!
3167

1✔
3168
            // new requests for an access token succeed again
1✔
3169
            user->refresh_custom_data([&](Optional<AppError> error) {
2✔
3170
                REQUIRE_FALSE(error);
2!
3171
            });
2✔
3172

1✔
3173
            {
2✔
3174
                // check that there are no errors initiating a new sync session by making sure upload/download
1✔
3175
                // succeeds
1✔
3176
                auto r = Realm::get_shared_realm(config);
2✔
3177
                Results dogs = get_dogs(r);
2✔
3178
            }
2✔
3179
        }
2✔
3180

4✔
3181
        SECTION("Revoked refresh token on an anonymous user results in a sync error") {
8✔
3182
            app->current_user()->log_out();
2✔
3183
            auto anon_user = log_in(app);
2✔
3184
            REQUIRE(app->current_user() == anon_user);
2!
3185
            SyncTestFile config(app, partition, schema);
2✔
3186
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
3187
            app_session.admin_api.revoke_user_sessions(anon_user->identity(), app_session.server_app_id);
2✔
3188
            // revoking a user session only affects the refresh token, so the access token should still continue to
1✔
3189
            // work.
1✔
3190
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
3191

1✔
3192
            verify_error_on_sync_with_invalid_refresh_token(anon_user, config);
2✔
3193

1✔
3194
            // the user has been logged out, and current user is reset
1✔
3195
            REQUIRE(!app->current_user());
2!
3196
            REQUIRE(!anon_user->is_logged_in());
2!
3197
            REQUIRE(anon_user->state() == SyncUser::State::Removed);
2!
3198

1✔
3199
            // new requests for an access token do not work for anon users
1✔
3200
            anon_user->refresh_custom_data([&](Optional<AppError> error) {
2✔
3201
                REQUIRE(error);
2!
3202
                REQUIRE(error->reason() ==
2!
3203
                        util::format("Cannot initiate a refresh on user '%1' because the user has been removed",
2✔
3204
                                     anon_user->identity()));
2✔
3205
            });
2✔
3206

1✔
3207
            REQUIRE_EXCEPTION(
2✔
3208
                Realm::get_shared_realm(config), ClientUserNotFound,
2✔
3209
                util::format("Cannot start a sync session for user '%1' because this user has been removed.",
2✔
3210
                             anon_user->identity()));
2✔
3211
        }
2✔
3212

4✔
3213
        SECTION("Opening a Realm with a removed email user results produces an exception") {
8✔
3214
            auto creds = create_user_and_log_in(app);
2✔
3215
            auto email_user = app->current_user();
2✔
3216
            const std::string user_ident = email_user->identity();
2✔
3217
            REQUIRE(email_user);
2!
3218
            SyncTestFile config(app, partition, schema);
2✔
3219
            REQUIRE(email_user->is_logged_in());
2!
3220
            {
2✔
3221
                // sync works on a valid user
1✔
3222
                auto r = Realm::get_shared_realm(config);
2✔
3223
                Results dogs = get_dogs(r);
2✔
3224
            }
2✔
3225
            app->sync_manager()->remove_user(user_ident);
2✔
3226
            REQUIRE_FALSE(email_user->is_logged_in());
2!
3227
            REQUIRE(email_user->state() == SyncUser::State::Removed);
2!
3228

1✔
3229
            // should not be able to open a synced Realm with an invalid user
1✔
3230
            REQUIRE_EXCEPTION(
2✔
3231
                Realm::get_shared_realm(config), ClientUserNotFound,
2✔
3232
                util::format("Cannot start a sync session for user '%1' because this user has been removed.",
2✔
3233
                             user_ident));
2✔
3234

1✔
3235
            std::shared_ptr<SyncUser> new_user_instance = log_in(app, creds);
2✔
3236
            // the previous instance is still invalid
1✔
3237
            REQUIRE_FALSE(email_user->is_logged_in());
2!
3238
            REQUIRE(email_user->state() == SyncUser::State::Removed);
2!
3239
            // but the new instance will work and has the same server issued ident
1✔
3240
            REQUIRE(new_user_instance);
2!
3241
            REQUIRE(new_user_instance->is_logged_in());
2!
3242
            REQUIRE(new_user_instance->identity() == user_ident);
2!
3243
            {
2✔
3244
                // sync works again if the same user is logged back in
1✔
3245
                config.sync_config->user = new_user_instance;
2✔
3246
                auto r = Realm::get_shared_realm(config);
2✔
3247
                Results dogs = get_dogs(r);
2✔
3248
            }
2✔
3249
        }
2✔
3250
    }
8✔
3251

27✔
3252
    SECTION("large write transactions which would be too large if batched") {
54✔
3253
        SyncTestFile config(app, partition, schema);
2✔
3254

1✔
3255
        std::mutex mutex;
2✔
3256
        bool done = false;
2✔
3257
        auto r = Realm::get_shared_realm(config);
2✔
3258
        r->sync_session()->pause();
2✔
3259

1✔
3260
        // Create 26 MB worth of dogs in 26 transactions, which should work but
1✔
3261
        // will result in an error from the server if the changesets are batched
1✔
3262
        // for upload.
1✔
3263
        CppContext c;
2✔
3264
        for (auto i = 'a'; i < 'z'; ++i) {
52✔
3265
            r->begin_transaction();
50✔
3266
            Object::create(c, r, "Dog",
50✔
3267
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
50✔
3268
                                            {"breed", std::string("bulldog")},
50✔
3269
                                            {"name", random_string(1024 * 1024)}}),
50✔
3270
                           CreatePolicy::ForceCreate);
50✔
3271
            r->commit_transaction();
50✔
3272
        }
50✔
3273
        r->sync_session()->wait_for_upload_completion([&](Status status) {
2✔
3274
            std::lock_guard lk(mutex);
2✔
3275
            REQUIRE(status.is_ok());
2!
3276
            done = true;
2✔
3277
        });
2✔
3278
        r->sync_session()->resume();
2✔
3279

1✔
3280
        // If we haven't gotten an error in more than 5 minutes, then something has gone wrong
1✔
3281
        // and we should fail the test.
1✔
3282
        timed_wait_for(
2✔
3283
            [&] {
2,639,677✔
3284
                std::lock_guard lk(mutex);
2,639,677✔
3285
                return done;
2,639,677✔
3286
            },
2,639,677✔
3287
            std::chrono::minutes(5));
2✔
3288
    }
2✔
3289

27✔
3290
    SECTION("too large sync message error handling") {
54✔
3291
        SyncTestFile config(app, partition, schema);
2✔
3292

1✔
3293
        auto pf = util::make_promise_future<SyncError>();
2✔
3294
        config.sync_config->error_handler =
2✔
3295
            [sp = util::CopyablePromiseHolder(std::move(pf.promise))](auto, SyncError error) mutable {
2✔
3296
                sp.get_promise().emplace_value(std::move(error));
2✔
3297
            };
2✔
3298
        auto r = Realm::get_shared_realm(config);
2✔
3299

1✔
3300
        // Create 26 MB worth of dogs in a single transaction - this should all get put into one changeset
1✔
3301
        // and get uploaded at once, which for now is an error on the server.
1✔
3302
        r->begin_transaction();
2✔
3303
        CppContext c;
2✔
3304
        for (auto i = 'a'; i < 'z'; ++i) {
52✔
3305
            Object::create(c, r, "Dog",
50✔
3306
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
50✔
3307
                                            {"breed", std::string("bulldog")},
50✔
3308
                                            {"name", random_string(1024 * 1024)}}),
50✔
3309
                           CreatePolicy::ForceCreate);
50✔
3310
        }
50✔
3311
        r->commit_transaction();
2✔
3312

1✔
3313
        auto error = wait_for_future(std::move(pf.future), std::chrono::minutes(5)).get();
2✔
3314
        REQUIRE(error.status == ErrorCodes::LimitExceeded);
2!
3315
        REQUIRE(error.status.reason() ==
2!
3316
                "Sync websocket closed because the server received a message that was too large: "
2✔
3317
                "read limited at 16777217 bytes");
2✔
3318
        REQUIRE(error.is_client_reset_requested());
2!
3319
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
2!
3320
    }
2✔
3321

27✔
3322
    SECTION("freezing realm does not resume session") {
54✔
3323
        SyncTestFile config(app, partition, schema);
2✔
3324
        auto realm = Realm::get_shared_realm(config);
2✔
3325
        wait_for_download(*realm);
2✔
3326

1✔
3327
        auto state = realm->sync_session()->state();
2✔
3328
        REQUIRE(state == SyncSession::State::Active);
2!
3329

1✔
3330
        realm->sync_session()->pause();
2✔
3331
        state = realm->sync_session()->state();
2✔
3332
        REQUIRE(state == SyncSession::State::Paused);
2!
3333

1✔
3334
        realm->read_group();
2✔
3335

1✔
3336
        {
2✔
3337
            auto frozen = realm->freeze();
2✔
3338
            REQUIRE(realm->sync_session() == realm->sync_session());
2!
3339
            REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused);
2!
3340
        }
2✔
3341

1✔
3342
        {
2✔
3343
            auto frozen = Realm::get_frozen_realm(config, realm->read_transaction_version());
2✔
3344
            REQUIRE(realm->sync_session() == realm->sync_session());
2!
3345
            REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused);
2!
3346
        }
2✔
3347
    }
2✔
3348

27✔
3349
    SECTION("pausing a session does not hold the DB open") {
54✔
3350
        SyncTestFile config(app, partition, schema);
2✔
3351
        DBRef dbref;
2✔
3352
        std::shared_ptr<SyncSession> sync_sess_ext_ref;
2✔
3353
        {
2✔
3354
            auto realm = Realm::get_shared_realm(config);
2✔
3355
            wait_for_download(*realm);
2✔
3356

1✔
3357
            auto state = realm->sync_session()->state();
2✔
3358
            REQUIRE(state == SyncSession::State::Active);
2!
3359

1✔
3360
            sync_sess_ext_ref = realm->sync_session()->external_reference();
2✔
3361
            dbref = TestHelper::get_db(*realm);
2✔
3362
            // One ref each for the
1✔
3363
            // - RealmCoordinator
1✔
3364
            // - SyncSession
1✔
3365
            // - SessionWrapper
1✔
3366
            // - local dbref
1✔
3367
            REQUIRE(dbref.use_count() >= 4);
2!
3368

1✔
3369
            realm->sync_session()->pause();
2✔
3370
            state = realm->sync_session()->state();
2✔
3371
            REQUIRE(state == SyncSession::State::Paused);
2!
3372
        }
2✔
3373

1✔
3374
        // Closing the realm should leave one ref for the SyncSession and one for the local dbref.
1✔
3375
        REQUIRE_THAT(
2✔
3376
            [&] {
2✔
3377
                return dbref.use_count() < 4;
2✔
3378
            },
2✔
3379
            ReturnsTrueWithinTimeLimit{});
2✔
3380

1✔
3381
        // Releasing the external reference should leave one ref (the local dbref) only.
1✔
3382
        sync_sess_ext_ref.reset();
2✔
3383
        REQUIRE_THAT(
2✔
3384
            [&] {
2✔
3385
                return dbref.use_count() == 1;
2✔
3386
            },
2✔
3387
            ReturnsTrueWithinTimeLimit{});
2✔
3388
    }
2✔
3389

27✔
3390
    SECTION("validation") {
54✔
3391
        SyncTestFile config(app, partition, schema);
6✔
3392

3✔
3393
        SECTION("invalid partition error handling") {
6✔
3394
            config.sync_config->partition_value = "not a bson serialized string";
2✔
3395
            std::atomic<bool> error_did_occur = false;
2✔
3396
            config.sync_config->error_handler = [&error_did_occur](std::shared_ptr<SyncSession>, SyncError error) {
2✔
3397
                CHECK(error.status.reason().find(
2!
3398
                          "Illegal Realm path (BIND): serialized partition 'not a bson serialized "
2✔
3399
                          "string' is invalid") != std::string::npos);
2✔
3400
                error_did_occur.store(true);
2✔
3401
            };
2✔
3402
            auto r = Realm::get_shared_realm(config);
2✔
3403
            auto session = app->current_user()->session_for_on_disk_path(r->config().path);
2✔
3404
            timed_wait_for([&] {
8,612✔
3405
                return error_did_occur.load();
8,612✔
3406
            });
8,612✔
3407
            REQUIRE(error_did_occur.load());
2!
3408
        }
2✔
3409

3✔
3410
        SECTION("invalid pk schema error handling") {
6✔
3411
            const std::string invalid_pk_name = "my_primary_key";
2✔
3412
            auto it = config.schema->find("Dog");
2✔
3413
            REQUIRE(it != config.schema->end());
2!
3414
            REQUIRE(it->primary_key_property());
2!
3415
            REQUIRE(it->primary_key_property()->name == "_id");
2!
3416
            it->primary_key_property()->name = invalid_pk_name;
2✔
3417
            it->primary_key = invalid_pk_name;
2✔
3418
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3419
                                      "The primary key property on a synchronized Realm must be named '_id' but "
2✔
3420
                                      "found 'my_primary_key' for type 'Dog'");
2✔
3421
        }
2✔
3422

3✔
3423
        SECTION("missing pk schema error handling") {
6✔
3424
            auto it = config.schema->find("Dog");
2✔
3425
            REQUIRE(it != config.schema->end());
2!
3426
            REQUIRE(it->primary_key_property());
2!
3427
            it->primary_key_property()->is_primary = false;
2✔
3428
            it->primary_key = "";
2✔
3429
            REQUIRE(!it->primary_key_property());
2!
3430
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3431
                                      "There must be a primary key property named '_id' on a synchronized "
2✔
3432
                                      "Realm but none was found for type 'Dog'");
2✔
3433
        }
2✔
3434
    }
6✔
3435
}
54✔
3436

3437
TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") {
2✔
3438
    TestAppSession session;
2✔
3439
    auto app = session.app();
2✔
3440
    auto user = app->current_user();
2✔
3441

1✔
3442
    SECTION("custom user data happy path") {
2✔
3443
        bool processed = false;
2✔
3444
        app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})},
2✔
3445
                           [&](auto response, auto error) {
2✔
3446
                               CHECK(error == none);
2!
3447
                               CHECK(response);
2!
3448
                               CHECK(*response == true);
2!
3449
                               processed = true;
2✔
3450
                           });
2✔
3451
        CHECK(processed);
2!
3452
        processed = false;
2✔
3453
        app->refresh_custom_data(user, [&](auto) {
2✔
3454
            processed = true;
2✔
3455
        });
2✔
3456
        CHECK(processed);
2!
3457
        auto data = *user->custom_data();
2✔
3458
        CHECK(data["favorite_color"] == "green");
2!
3459
    }
2✔
3460
}
2✔
3461

3462
TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") {
2✔
3463
    TestAppSession session;
2✔
3464
    auto app = session.app();
2✔
3465
    auto jwt = create_jwt(session.app()->config().app_id);
2✔
3466

1✔
3467
    SECTION("jwt happy path") {
2✔
3468
        bool processed = false;
2✔
3469

1✔
3470
        std::shared_ptr<SyncUser> user = log_in(app, AppCredentials::custom(jwt));
2✔
3471

1✔
3472
        app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})},
2✔
3473
                           [&](auto response, auto error) {
2✔
3474
                               CHECK(error == none);
2!
3475
                               CHECK(response);
2!
3476
                               CHECK(*response == true);
2!
3477
                               processed = true;
2✔
3478
                           });
2✔
3479
        CHECK(processed);
2!
3480
        processed = false;
2✔
3481
        app->refresh_custom_data(user, [&](auto) {
2✔
3482
            processed = true;
2✔
3483
        });
2✔
3484
        CHECK(processed);
2!
3485
        auto metadata = user->user_profile();
2✔
3486
        auto custom_data = *user->custom_data();
2✔
3487
        CHECK(custom_data["name"] == "Not Foo Bar");
2!
3488
        CHECK(metadata["name"] == "Foo Bar");
2!
3489
    }
2✔
3490
}
2✔
3491

3492
namespace cf = realm::collection_fixtures;
3493
TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects,
3494
                   cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects,
3495
                   cf::DictionaryOfMixedLinks)
3496
{
12✔
3497
    std::string base_url = get_base_url();
12✔
3498
    const std::string valid_pk_name = "_id";
12✔
3499
    REQUIRE(!base_url.empty());
12!
3500
    const auto partition = random_string(100);
12✔
3501
    TestType test_type("collection", "dest");
12✔
3502
    Schema schema = {{"source",
12✔
3503
                      {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
3504
                       {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
3505
                       test_type.property()}},
12✔
3506
                     {"dest",
12✔
3507
                      {
12✔
3508
                          {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
3509
                          {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
3510
                      }}};
12✔
3511
    auto server_app_config = minimal_app_config(base_url, "collections_of_links", schema);
12✔
3512
    TestAppSession test_session(create_app(server_app_config));
12✔
3513

6✔
3514
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
36✔
3515
        timed_sleeping_wait_for([&]() -> bool {
1,003✔
3516
            r->refresh();
1,003✔
3517
            TableRef dest = r->read_group().get_table(table_name);
1,003✔
3518
            size_t cur_count = dest->size();
1,003✔
3519
            return cur_count == count;
1,003✔
3520
        });
1,003✔
3521
    };
36✔
3522
    auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) {
24✔
3523
        timed_sleeping_wait_for([&]() -> bool {
387✔
3524
            r->refresh();
387✔
3525
            return test_type.size_of_collection(obj) == count;
387✔
3526
        });
387✔
3527
    };
24✔
3528

6✔
3529
    CppContext c;
12✔
3530
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
12✔
3531
        r->begin_transaction();
12✔
3532
        auto object = Object::create(
12✔
3533
            c, r, "source",
12✔
3534
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
12✔
3535
            CreatePolicy::ForceCreate);
12✔
3536

6✔
3537
        for (auto link : links) {
36✔
3538
            auto& obj = object.get_obj();
36✔
3539
            test_type.add_link(obj, link);
36✔
3540
        }
36✔
3541
        r->commit_transaction();
12✔
3542
    };
12✔
3543

12✔
3544
    auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink {
18✔
3545
        r->begin_transaction();
36✔
3546
        auto obj = Object::create(
36✔
3547
            c, r, "dest",
36✔
3548
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
36✔
3549
            CreatePolicy::ForceCreate);
36✔
3550
        r->commit_transaction();
36✔
3551
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
36✔
3552
    };
36✔
3553

24✔
3554
    auto require_links_to_match_ids = [&](std::vector<Obj> links, std::vector<int64_t> expected) {
24✔
3555
        std::vector<int64_t> actual;
48✔
3556
        for (auto obj : links) {
78✔
3557
            actual.push_back(obj.get<Int>(valid_pk_name));
108✔
3558
        }
108✔
3559
        std::sort(actual.begin(), actual.end());
78✔
3560
        std::sort(expected.begin(), expected.end());
48✔
3561
        REQUIRE(actual == expected);
48✔
3562
    };
48!
3563

30✔
3564
    SECTION("integration testing") {
6✔
3565
        auto app = test_session.app();
12✔
3566
        SyncTestFile config1(app, partition, schema); // uses the current user created above
12✔
3567
        auto r1 = realm::Realm::get_shared_realm(config1);
12✔
3568
        Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source"));
12✔
3569

12✔
3570
        create_user_and_log_in(app);
12✔
3571
        SyncTestFile config2(app, partition, schema); // uses the user created above
6✔
3572
        auto r2 = realm::Realm::get_shared_realm(config2);
12✔
3573
        Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source"));
12✔
3574

12✔
3575
        constexpr int64_t source_pk = 0;
12✔
3576
        constexpr int64_t dest_pk_1 = 1;
12✔
3577
        constexpr int64_t dest_pk_2 = 2;
6✔
3578
        constexpr int64_t dest_pk_3 = 3;
12✔
3579
        { // add a container collection with three valid links
12✔
3580
            REQUIRE(r1_source_objs.size() == 0);
12✔
3581
            ObjLink dest1 = create_one_dest_object(r1, dest_pk_1);
12✔
3582
            ObjLink dest2 = create_one_dest_object(r1, dest_pk_2);
12✔
3583
            ObjLink dest3 = create_one_dest_object(r1, dest_pk_3);
6✔
3584
            create_one_source_object(r1, source_pk, {dest1, dest2, dest3});
12✔
3585
            REQUIRE(r1_source_objs.size() == 1);
12!
3586
            REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12✔
3587
            REQUIRE(r1_source_objs.get(0).get<String>("realm_id") == partition);
12✔
3588
            require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
3589
        }
12✔
3590

12!
3591
        size_t expected_coll_size = 3;
12!
3592
        std::vector<int64_t> remaining_dest_object_ids;
12!
3593
        { // erase one of the destination objects
12✔
3594
            wait_for_num_objects_to_equal(r2, "class_source", 1);
12✔
3595
            wait_for_num_objects_to_equal(r2, "class_dest", 3);
6✔
3596
            REQUIRE(r2_source_objs.size() == 1);
12✔
3597
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12✔
3598
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3);
12✔
3599
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
3600
            require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
3601
            r2->begin_transaction();
12!
3602
            linked_objects[0].remove();
12!
3603
            r2->commit_transaction();
12!
3604
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name),
12✔
3605
                                         linked_objects[2].template get<Int>(valid_pk_name)};
12✔
3606
            expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3;
12✔
3607
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12✔
3608
        }
12✔
3609

12✔
3610
        { // remove a link from the collection
12✔
3611
            wait_for_num_objects_to_equal(r1, "class_dest", 2);
10✔
3612
            REQUIRE(r1_source_objs.size() == 1);
12!
3613
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12✔
3614
            auto linked_objects = test_type.get_links(r1_source_objs.get(0));
6✔
3615
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
3616
            r1->begin_transaction();
12✔
3617
            auto obj = r1_source_objs.get(0);
12!
3618
            test_type.remove_link(obj,
12!
3619
                                  ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()});
12✔
3620
            r1->commit_transaction();
12✔
3621
            --expected_coll_size;
12✔
3622
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name)};
12✔
3623
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12✔
3624
        }
12✔
3625

12✔
3626
        { // clear the collection
12✔
3627
            REQUIRE(r2_source_objs.size() == 1);
12✔
3628
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
3629
            wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size);
12✔
3630
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
3631
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
3632
            r2->begin_transaction();
12✔
3633
            test_type.clear_collection(r2_source_objs.get(0));
18✔
3634
            r2->commit_transaction();
18✔
3635
            expected_coll_size = 0;
18✔
3636
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
6✔
3637
        }
12✔
3638

12!
3639
        { // expect an empty collection
12!
3640
            REQUIRE(r1_source_objs.size() == 1);
12✔
3641
            wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size);
12✔
3642
        }
12✔
3643
    }
12✔
3644
}
12✔
3645

6✔
3646
TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID,
6✔
3647
                   cf::UUID, cf::BoxedOptional<cf::Int>, cf::UnboxedOptional<cf::String>, cf::BoxedOptional<cf::OID>,
6!
3648
                   cf::BoxedOptional<cf::UUID>)
6✔
3649
{
8✔
3650
    std::string base_url = get_base_url();
14✔
3651
    const std::string valid_pk_name = "_id";
14!
3652
    const std::string partition_key_col_name = "partition_key_prop";
14!
3653
    const std::string table_name = "class_partition_test_type";
14✔
3654
    REQUIRE(!base_url.empty());
14✔
3655
    auto partition_property = Property(partition_key_col_name, TestType::property_type);
14!
3656
    Schema schema = {{Group::table_name_to_class_name(table_name),
14✔
3657
                      {
14✔
3658
                          {valid_pk_name, PropertyType::Int, true},
14✔
3659
                          partition_property,
8✔
3660
                      }}};
8✔
3661
    auto server_app_config = minimal_app_config(base_url, "partition_types_app_name", schema);
8✔
3662
    server_app_config.partition_key = partition_property;
8✔
3663
    TestAppSession test_session(create_app(server_app_config));
16✔
3664
    auto app = test_session.app();
16✔
3665

16✔
3666
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
32✔
3667
        timed_sleeping_wait_for([&]() -> bool {
1,808✔
3668
            r->refresh();
1,808!
3669
            TableRef dest = r->read_group().get_table(table_name);
1,808✔
3670
            size_t cur_count = dest->size();
1,808✔
3671
            return cur_count == count;
1,808✔
3672
        });
1,808✔
3673
    };
32✔
3674
    using T = typename TestType::Type;
16✔
3675
    CppContext c;
16✔
3676
    auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) {
32✔
3677
        r->begin_transaction();
32✔
3678
        auto object = Object::create(
32✔
3679
            c, r, Group::table_name_to_class_name(table_name),
24✔
3680
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}),
48✔
3681
            CreatePolicy::ForceCreate);
1,792✔
3682
        r->commit_transaction();
1,792✔
3683
    };
1,792✔
3684

1,776✔
3685
    auto get_bson = [](T val) -> bson::Bson {
1,816✔
3686
        if constexpr (std::is_same_v<T, StringData>) {
1,816✔
3687
            return val.is_null() ? bson::Bson(util::none) : bson::Bson(val);
58✔
3688
        }
22✔
3689
        else if constexpr (TestType::is_optional) {
42✔
3690
            return val ? bson::Bson(*val) : bson::Bson(util::none);
38✔
3691
        }
44✔
3692
        else {
38✔
3693
            return bson::Bson(val);
38✔
3694
        }
38✔
3695
    };
72✔
3696

32✔
3697
    SECTION("can round trip an object") {
32✔
3698
        auto values = TestType::values();
8✔
3699
        auto user1 = app->current_user();
56✔
3700
        create_user_and_log_in(app);
56✔
3701
        auto user2 = app->current_user();
20✔
3702
        REQUIRE(user1);
22✔
3703
        REQUIRE(user2);
42✔
3704
        REQUIRE(user1 != user2);
22✔
3705
        for (T partition_value : values) {
44✔
3706
            SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above
38✔
3707
            auto r1 = realm::Realm::get_shared_realm(config1);
38✔
3708
            Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name));
38✔
3709

72✔
3710
            SyncTestFile config2(user2, get_bson(partition_value), schema); // uses the user created above
24✔
3711
            auto r2 = realm::Realm::get_shared_realm(config2);
32✔
3712
            Results r2_source_objs = realm::Results(r2, r2->read_group().get_table(table_name));
32✔
3713

32✔
3714
            const int64_t pk_value = random_int();
32✔
3715
            {
32✔
3716
                REQUIRE(r1_source_objs.size() == 0);
32!
3717
                create_object(r1, pk_value, TestType::to_any(partition_value));
32!
3718
                REQUIRE(r1_source_objs.size() == 1);
32!
3719
                REQUIRE(r1_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48✔
3720
                REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48✔
3721
            }
48✔
3722
            {
48✔
3723
                wait_for_num_objects_to_equal(r2, table_name, 1);
24✔
3724
                REQUIRE(r2_source_objs.size() == 1);
48✔
3725
                REQUIRE(r2_source_objs.size() == 1);
48✔
3726
                REQUIRE(r2_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48✔
3727
                REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
24✔
3728
            }
48✔
3729
        }
48✔
3730
    }
32!
3731
}
32✔
3732

24!
3733
TEST_CASE("app: full-text compatible with sync", "[sync][app][baas]") {
26!
3734
    std::string base_url = get_base_url();
26!
3735
    const std::string valid_pk_name = "_id";
26✔
3736
    REQUIRE(!base_url.empty());
26✔
3737

26✔
3738
    Schema schema{
26!
3739
        {"TopLevel",
26!
3740
         {
26!
3741
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
26!
3742
             {"full_text", Property::IsFulltextIndexed{true}},
26✔
3743
         }},
26✔
3744
    };
10✔
3745

10✔
3746
    auto server_app_config = minimal_app_config(base_url, "full_text", schema);
2✔
3747
    auto app_session = create_app(server_app_config);
4✔
3748
    const auto partition = random_string(100);
4✔
3749
    TestAppSession test_session(app_session, nullptr);
4✔
3750
    SyncTestFile config(test_session.app(), partition, schema);
4!
3751
    SharedRealm realm;
2✔
3752
    SECTION("sync open") {
4✔
3753
        INFO("realm opened without async open");
3✔
3754
        realm = Realm::get_shared_realm(config);
3✔
3755
    }
3✔
3756
    SECTION("async open") {
4✔
3757
        INFO("realm opened with async open");
3✔
3758
        auto async_open_task = Realm::get_synchronized_realm(config);
3✔
3759

1✔
3760
        auto [realm_promise, realm_future] = util::make_promise_future<ThreadSafeReference>();
3✔
3761
        async_open_task->start(
3✔
3762
            [promise = std::move(realm_promise)](ThreadSafeReference ref, std::exception_ptr ouch) mutable {
3✔
3763
                if (ouch) {
3✔
3764
                    try {
2✔
3765
                        std::rethrow_exception(ouch);
2✔
3766
                    }
2✔
3767
                    catch (...) {
1✔
3768
                        promise.set_error(exception_to_status());
1✔
3769
                    }
1✔
3770
                }
2✔
3771
                else {
2✔
3772
                    promise.emplace_value(std::move(ref));
2✔
3773
                }
1✔
3774
            });
2✔
3775

2✔
3776
        realm = Realm::get_shared_realm(std::move(realm_future.get()));
2✔
3777
    }
2✔
3778

2✔
3779
    CppContext c(realm);
2✔
3780
    auto obj_id_1 = ObjectId::gen();
2✔
3781
    auto obj_id_2 = ObjectId::gen();
2✔
3782
    realm->begin_transaction();
2✔
3783
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}}));
2✔
3784
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}}));
2✔
3785
    realm->commit_transaction();
3✔
3786

3✔
3787
    auto table = realm->read_group().get_table("class_TopLevel");
3✔
3788
    REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext);
3✔
3789
    Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world"));
2✔
3790
    REQUIRE(world_results.size() == 1);
3✔
3791
    REQUIRE(world_results.get<Obj>(0).get_primary_key() == Mixed{obj_id_1});
3✔
3792
}
2✔
3793

2✔
3794
#endif // REALM_ENABLE_AUTH_TESTS
2✔
3795

2✔
3796
TEST_CASE("app: custom error handling", "[sync][app][custom errors]") {
3✔
3797
    class CustomErrorTransport : public GenericNetworkTransport {
3✔
3798
    public:
3✔
3799
        CustomErrorTransport(int code, const std::string& message)
3✔
3800
            : m_code(code)
1✔
3801
            , m_message(message)
3✔
3802
        {
3!
3803
        }
3✔
3804

3!
3805
        void send_request_to_server(const Request&, util::UniqueFunction<void(const Response&)>&& completion) override
3!
3806
        {
3✔
3807
            completion(Response{0, m_code, HttpHeaders(), m_message});
1✔
3808
        }
1✔
3809

1✔
3810
    private:
2✔
3811
        int m_code;
2✔
3812
        std::string m_message;
2✔
3813
    };
2✔
3814

2✔
3815
    SECTION("custom code and message is sent back") {
2✔
3816
        TestSyncManager::Config config;
2✔
3817
        config.transport = std::make_shared<CustomErrorTransport>(1001, "Boom!");
2✔
3818
        TestSyncManager tsm(config);
1✔
3819
        auto error = failed_log_in(tsm.app());
2✔
3820
        CHECK(error.is_custom_error());
2✔
3821
        CHECK(*error.additional_status_code == 1001);
2✔
3822
        CHECK(error.reason() == "Boom!");
2✔
3823
    }
1✔
3824
}
2✔
3825

1✔
3826
static const std::string profile_0_name = "Ursus americanus Ursus boeckhi";
1✔
3827
static const std::string profile_0_first_name = "Ursus americanus";
1✔
3828
static const std::string profile_0_last_name = "Ursus boeckhi";
3829
static const std::string profile_0_email = "Ursus ursinus";
1✔
3830
static const std::string profile_0_picture_url = "Ursus malayanus";
1✔
3831
static const std::string profile_0_gender = "Ursus thibetanus";
1✔
3832
static const std::string profile_0_birthday = "Ursus americanus";
1✔
3833
static const std::string profile_0_min_age = "Ursus maritimus";
1✔
3834
static const std::string profile_0_max_age = "Ursus arctos";
1!
3835

1!
3836
static const nlohmann::json profile_0 = {
1!
3837
    {"name", profile_0_name},         {"first_name", profile_0_first_name},   {"last_name", profile_0_last_name},
1✔
3838
    {"email", profile_0_email},       {"picture_url", profile_0_picture_url}, {"gender", profile_0_gender},
1✔
3839
    {"birthday", profile_0_birthday}, {"min_age", profile_0_min_age},         {"max_age", profile_0_max_age}};
3840

3841
static nlohmann::json user_json(std::string access_token, std::string user_id = random_string(15))
3842
{
4✔
3843
    return {{"access_token", access_token},
4✔
3844
            {"refresh_token", access_token},
4✔
3845
            {"user_id", user_id},
4✔
3846
            {"device_id", "Panda Bear"}};
4✔
3847
}
4✔
3848

3849
static nlohmann::json user_profile_json(std::string user_id = random_string(15),
3850
                                        std::string identity_0_id = "Ursus arctos isabellinus",
3851
                                        std::string identity_1_id = "Ursus arctos horribilis",
3852
                                        std::string provider_type = "anon-user")
3853
{
3✔
3854
    return {{"user_id", user_id},
3✔
3855
            {"identities",
3✔
3856
             {{{"id", identity_0_id}, {"provider_type", provider_type}},
7✔
3857
              {{"id", identity_1_id}, {"provider_type", "lol_wut"}}}},
7✔
3858
            {"data", profile_0}};
7✔
3859
}
7✔
3860

4✔
3861
// MARK: - Unit Tests
4✔
3862

3863
static TestSyncManager::Config get_config()
3864
{
22✔
3865
    return get_config(instance_of<UnitTestTransport>);
22✔
3866
}
22✔
3867

3✔
3868
static const std::string good_access_token =
3✔
3869
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
3✔
3870
    "eyJleHAiOjE1ODE1MDc3OTYsImlhdCI6MTU4MTUwNTk5NiwiaXNzIjoiNWU0M2RkY2M2MzZlZTEwNmVhYTEyYmRjIiwic3RpdGNoX2RldklkIjoi"
3✔
3871
    "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU0M2Rk"
3✔
3872
    "Y2M2MzZlZTEwNmVhYTEyYmRhIiwidHlwIjoiYWNjZXNzIn0.0q3y9KpFxEnbmRwahvjWU1v9y1T1s3r2eozu93vMc3s";
3✔
3873

3✔
3874
static const std::string good_access_token2 =
3875
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
3876
    "eyJleHAiOjE1ODkzMDE3MjAsImlhdCI6MTU4NDExODcyMCwiaXNzIjoiNWU2YmJiYzBhNmI3ZGZkM2UyNTA0OGI3Iiwic3RpdGNoX2RldklkIjoi"
3877
    "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwic3RpdGNoX2RvbWFpbklkIjoiNWUxNDk5MTNjOTBiNGFmMGViZTkzNTI3Iiwic3ViIjoiNWU2YmJi"
3878
    "YzBhNmI3ZGZkM2UyNTA0OGIzIiwidHlwIjoiYWNjZXNzIn0.eSX4QMjIOLbdOYOPzQrD_racwLUk1HGFgxtx2a34k80";
22✔
3879

22✔
3880
static const std::string bad_access_token = "lolwut";
22✔
3881
static const std::string dummy_device_id = "123400000000000000000000";
3882

3883
TEST_CASE("subscribable unit tests", "[sync][app]") {
4✔
3884
    struct Foo : public Subscribable<Foo> {
4✔
3885
        void event()
4✔
3886
        {
9✔
3887
            emit_change_to_subscribers(*this);
9✔
3888
        }
9✔
3889
    };
4✔
3890

4✔
3891
    auto foo = Foo();
4✔
3892

4✔
3893
    SECTION("subscriber receives events") {
4✔
3894
        auto event_count = 0;
1✔
3895
        auto token = foo.subscribe([&event_count](auto&) {
3✔
3896
            event_count++;
3✔
3897
        });
7✔
3898

5✔
3899
        foo.event();
5✔
3900
        foo.event();
10✔
3901
        foo.event();
10✔
3902

10✔
3903
        CHECK(event_count == 3);
5✔
3904
    }
1✔
3905

8✔
3906
    SECTION("subscriber can unsubscribe") {
4✔
3907
        auto event_count = 0;
5✔
3908
        auto token = foo.subscribe([&event_count](auto&) {
2✔
3909
            event_count++;
4✔
3910
        });
4✔
3911

4✔
3912
        foo.event();
1✔
3913
        CHECK(event_count == 1);
2✔
3914

2✔
3915
        foo.unsubscribe(token);
2✔
3916
        foo.event();
1✔
3917
        CHECK(event_count == 1);
2!
3918
    }
2✔
3919

4✔
3920
    SECTION("subscriber is unsubscribed on dtor") {
8✔
3921
        auto event_count = 0;
2✔
3922
        {
2✔
3923
            auto token = foo.subscribe([&event_count](auto&) {
2✔
3924
                event_count++;
2✔
3925
            });
1✔
3926

2✔
3927
            foo.event();
2!
3928
            CHECK(event_count == 1);
1✔
3929
        }
2✔
3930
        foo.event();
2✔
3931
        CHECK(event_count == 1);
2!
3932
    }
2✔
3933

4✔
3934
    SECTION("multiple subscribers receive events") {
8✔
3935
        auto event_count = 0;
2✔
3936
        {
2✔
3937
            auto token1 = foo.subscribe([&event_count](auto&) {
2✔
3938
                event_count++;
2✔
3939
            });
2✔
3940
            auto token2 = foo.subscribe([&event_count](auto&) {
1✔
3941
                event_count++;
2✔
3942
            });
2!
3943

2✔
3944
            foo.event();
2✔
3945
            CHECK(event_count == 2);
2!
3946
        }
2✔
3947
        foo.event();
1✔
3948
        CHECK(event_count == 2);
5✔
3949
    }
2✔
3950
}
5✔
3951

1✔
3952
TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") {
4✔
3953
    auto config = get_config();
4✔
3954
    static_cast<UnitTestTransport*>(config.transport.get())->set_profile(profile_0);
4✔
3955

4✔
3956
    SECTION("login_anonymous good") {
4✔
3957
        UnitTestTransport::access_token = good_access_token;
1✔
3958
        config.base_path = util::make_temp_dir();
2✔
3959
        config.should_teardown_test_directory = false;
2!
3960
        {
2✔
3961
            TestSyncManager tsm(config);
2✔
3962
            auto app = tsm.app();
2!
3963

2✔
3964
            auto user = log_in(app);
5✔
3965

1✔
3966
            REQUIRE(user->identities().size() == 1);
4✔
3967
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
4✔
3968
            SyncUserProfile user_profile = user->user_profile();
4✔
3969

1✔
3970
            CHECK(user_profile.name() == profile_0_name);
4✔
3971
            CHECK(user_profile.first_name() == profile_0_first_name);
2✔
3972
            CHECK(user_profile.last_name() == profile_0_last_name);
2✔
3973
            CHECK(user_profile.email() == profile_0_email);
2✔
3974
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2✔
3975
            CHECK(user_profile.gender() == profile_0_gender);
2✔
3976
            CHECK(user_profile.birthday() == profile_0_birthday);
2✔
3977
            CHECK(user_profile.min_age() == profile_0_min_age);
1✔
3978
            CHECK(user_profile.max_age() == profile_0_max_age);
2✔
3979
        }
1✔
3980
        App::clear_cached_apps();
2!
3981
        // assert everything is stored properly between runs
2!
3982
        {
2✔
3983
            TestSyncManager tsm(config);
1✔
3984
            auto app = tsm.app();
2!
3985
            REQUIRE(app->all_users().size() == 1);
2!
3986
            auto user = app->all_users()[0];
2!
3987
            REQUIRE(user->identities().size() == 1);
2!
3988
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
3989
            SyncUserProfile user_profile = user->user_profile();
2!
3990

2!
3991
            CHECK(user_profile.name() == profile_0_name);
2!
3992
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
3993
            CHECK(user_profile.last_name() == profile_0_last_name);
2✔
3994
            CHECK(user_profile.email() == profile_0_email);
2✔
3995
            CHECK(user_profile.picture_url() == profile_0_picture_url);
1✔
3996
            CHECK(user_profile.gender() == profile_0_gender);
2✔
3997
            CHECK(user_profile.birthday() == profile_0_birthday);
2✔
3998
            CHECK(user_profile.min_age() == profile_0_min_age);
2✔
3999
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4000
        }
2✔
4001
    }
2!
4002

4!
4003
    SECTION("login_anonymous bad") {
4✔
4004
        struct transport : UnitTestTransport {
1✔
4005
            void send_request_to_server(const Request& request,
2!
4006
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2!
4007
            {
3!
4008
                if (request.url.find("/login") != std::string::npos) {
3!
4009
                    completion({200, 0, {}, user_json(bad_access_token).dump()});
2!
4010
                }
2!
4011
                else {
2!
4012
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
2!
4013
                }
2!
4014
            }
3✔
4015
        };
2✔
4016

1✔
4017
        config.transport = instance_of<transport>;
4✔
4018
        TestSyncManager tsm(config);
2✔
4019
        auto error = failed_log_in(tsm.app());
2✔
4020
        CHECK(error.reason() == std::string("jwt missing parts"));
2✔
4021
        CHECK(error.code_string() == "BadToken");
3✔
4022
        CHECK(error.is_json_error());
3✔
4023
        CHECK(error.code() == ErrorCodes::BadToken);
2✔
4024
    }
2✔
4025

4✔
4026
    SECTION("login_anonynous multiple users") {
4✔
4027
        UnitTestTransport::access_token = good_access_token;
2✔
4028
        config.base_path = util::make_temp_dir();
3✔
4029
        config.should_teardown_test_directory = false;
2✔
4030
        TestSyncManager tsm(config);
1✔
4031
        auto app = tsm.app();
2✔
4032

2✔
4033
        auto user1 = log_in(app);
2✔
4034
        auto user2 = log_in(app, AppCredentials::anonymous(false));
2!
4035
        CHECK(user1 != user2);
2!
4036
    }
2!
4037
}
4!
4038

1✔
4039
TEST_CASE("app: UserAPIKeyProviderClient unit_tests", "[sync][app][user][api key]") {
3✔
4040
    TestSyncManager sync_manager(get_config(), {});
6✔
4041
    auto app = sync_manager.app();
4✔
4042
    auto client = app->provider_client<App::UserAPIKeyProviderClient>();
4✔
4043

4✔
4044
    std::shared_ptr<SyncUser> logged_in_user =
4✔
4045
        app->sync_manager()->get_user("userid", good_access_token, good_access_token, dummy_device_id);
4✔
4046
    bool processed = false;
3✔
4047
    ObjectId obj_id(UnitTestTransport::api_key_id.c_str());
4✔
4048

4✔
4049
    SECTION("create api key") {
4!
4050
        client.create_api_key(UnitTestTransport::api_key_name, logged_in_user,
2✔
4051
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
4✔
4052
                                  REQUIRE_FALSE(error);
1✔
4053
                                  CHECK(user_api_key.disabled == false);
4✔
4054
                                  CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
4✔
4055
                                  CHECK(user_api_key.key == UnitTestTransport::api_key);
4✔
4056
                                  CHECK(user_api_key.name == UnitTestTransport::api_key_name);
4✔
4057
                              });
1✔
4058
    }
4✔
4059

6✔
4060
    SECTION("fetch api key") {
6✔
4061
        client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
4✔
4062
            REQUIRE_FALSE(error);
1✔
4063
            CHECK(user_api_key.disabled == false);
4✔
4064
            CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2✔
4065
            CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2✔
4066
        });
2!
4067
    }
2!
4068

4!
4069
    SECTION("fetch api keys") {
4!
4070
        client.fetch_api_keys(logged_in_user,
2!
4071
                              [&](std::vector<App::UserAPIKey> user_api_keys, Optional<AppError> error) {
2✔
4072
                                  REQUIRE_FALSE(error);
2✔
4073
                                  CHECK(user_api_keys.size() == 2);
1✔
4074
                                  for (auto user_api_key : user_api_keys) {
5✔
4075
                                      CHECK(user_api_key.disabled == false);
3✔
4076
                                      CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
3!
4077
                                      CHECK(user_api_key.name == UnitTestTransport::api_key_name);
3!
4078
                                  }
3!
4079
                                  processed = true;
2!
4080
                              });
2✔
4081
        CHECK(processed);
2✔
4082
    }
1✔
4083
}
6✔
4084

1✔
4085

1✔
4086
TEST_CASE("app: user_semantics", "[sync][app][user]") {
7!
4087
    TestSyncManager tsm(get_config(), {});
7!
4088
    auto app = tsm.app();
8✔
4089

8!
4090
    const auto login_user_email_pass = [=] {
8!
4091
        return log_in(app, AppCredentials::username_password("bob", "thompson"));
5!
4092
    };
5✔
4093
    const auto login_user_anonymous = [=] {
10✔
4094
        return log_in(app, AppCredentials::anonymous());
10✔
4095
    };
10!
4096

7✔
4097
    CHECK(!app->current_user());
9✔
4098

6✔
4099
    int event_processed = 0;
6✔
4100
    auto token = app->subscribe([&event_processed](auto&) {
20✔
4101
        event_processed++;
20✔
4102
    });
20✔
4103

6✔
4104
    SECTION("current user is populated") {
9✔
4105
        const auto user1 = login_user_anonymous();
4✔
4106
        CHECK(app->current_user()->identity() == user1->identity());
4✔
4107
        CHECK(event_processed == 1);
10✔
4108
    }
10✔
4109

15✔
4110
    SECTION("current user is updated on login") {
6✔
4111
        const auto user1 = login_user_anonymous();
7!
4112
        CHECK(app->current_user()->identity() == user1->identity());
1✔
4113
        const auto user2 = login_user_email_pass();
7✔
4114
        CHECK(app->current_user()->identity() == user2->identity());
15✔
4115
        CHECK(user1->identity() != user2->identity());
15✔
4116
        CHECK(event_processed == 2);
15✔
4117
    }
1✔
4118

12✔
4119
    SECTION("current user is updated to last used user on logout") {
7✔
4120
        const auto user1 = login_user_anonymous();
2!
4121
        CHECK(app->current_user()->identity() == user1->identity());
2!
4122
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2✔
4123

1✔
4124
        const auto user2 = login_user_email_pass();
7✔
4125
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2✔
4126
        CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn);
2!
4127
        CHECK(app->current_user()->identity() == user2->identity());
2✔
4128
        CHECK(user1 != user2);
2!
4129

2!
4130
        // should reuse existing session
2!
4131
        const auto user3 = login_user_anonymous();
2✔
4132
        CHECK(user3 == user1);
1✔
4133

7✔
4134
        auto user_events_processed = 0;
2✔
4135
        auto _ = user3->subscribe([&user_events_processed](auto&) {
2!
4136
            user_events_processed++;
2!
4137
        });
1✔
4138

2✔
4139
        app->log_out([](auto) {});
2!
4140
        CHECK(user_events_processed == 1);
2!
4141

2!
4142
        CHECK(app->current_user()->identity() == user2->identity());
2!
4143

1✔
4144
        CHECK(app->all_users().size() == 1);
1✔
4145
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2✔
4146

2!
4147
        CHECK(event_processed == 4);
1✔
4148
    }
2✔
4149

7✔
4150
    SECTION("anon users are removed on logout") {
7✔
4151
        const auto user1 = login_user_anonymous();
2✔
4152
        CHECK(app->current_user()->identity() == user1->identity());
1✔
4153
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2✔
4154

2!
4155
        const auto user2 = login_user_anonymous();
1✔
4156
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4157
        CHECK(app->all_users().size() == 1);
1✔
4158
        CHECK(app->current_user()->identity() == user2->identity());
2!
4159
        CHECK(user1->identity() == user2->identity());
2!
4160

1✔
4161
        app->log_out([](auto) {});
2!
4162
        CHECK(app->all_users().size() == 0);
2✔
4163

1✔
4164
        CHECK(event_processed == 3);
7✔
4165
    }
2✔
4166

7!
4167
    SECTION("logout user") {
7!
4168
        auto user1 = login_user_email_pass();
1✔
4169
        auto user2 = login_user_anonymous();
2✔
4170

2!
4171
        // Anonymous users are special
2!
4172
        app->log_out(user2, [](Optional<AppError> error) {
2!
4173
            REQUIRE_FALSE(error);
2!
4174
        });
1✔
4175
        CHECK(user2->state() == SyncUser::State::Removed);
2✔
4176

2!
4177
        // Other users can be LoggedOut
1✔
4178
        app->log_out(user1, [](Optional<AppError> error) {
2!
4179
            REQUIRE_FALSE(error);
2✔
4180
        });
1✔
4181
        CHECK(user1->state() == SyncUser::State::LoggedOut);
7✔
4182

2✔
4183
        // Logging out already logged out users, does nothing
2✔
4184
        app->log_out(user1, [](Optional<AppError> error) {
1✔
4185
            REQUIRE_FALSE(error);
1✔
4186
        });
2✔
4187
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4188

2✔
4189
        app->log_out(user2, [](Optional<AppError> error) {
2!
4190
            REQUIRE_FALSE(error);
1✔
4191
        });
1✔
4192
        CHECK(user2->state() == SyncUser::State::Removed);
2✔
4193

2!
4194
        CHECK(event_processed == 4);
2✔
4195
    }
2!
4196

6✔
4197
    SECTION("unsubscribed observers no longer process events") {
6✔
4198
        app->unsubscribe(token);
2✔
4199

2!
4200
        const auto user1 = login_user_anonymous();
2✔
4201
        CHECK(app->current_user()->identity() == user1->identity());
2!
4202
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
1✔
4203

2✔
4204
        const auto user2 = login_user_anonymous();
2!
4205
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2✔
4206
        CHECK(app->all_users().size() == 1);
2!
4207
        CHECK(app->current_user()->identity() == user2->identity());
1✔
4208
        CHECK(user1->identity() == user2->identity());
2!
4209

2✔
4210
        app->log_out([](auto) {});
1✔
4211
        CHECK(app->all_users().size() == 0);
7✔
4212

2✔
4213
        CHECK(event_processed == 0);
1✔
4214
    }
2✔
4215
}
7!
4216

1!
4217
struct ErrorCheckingTransport : public GenericNetworkTransport {
4218
    ErrorCheckingTransport(Response* r)
1✔
4219
        : m_response(r)
1!
4220
    {
6!
4221
    }
6!
4222
    void send_request_to_server(const Request&, util::UniqueFunction<void(const Response&)>&& completion) override
1!
4223
    {
5✔
4224
        completion(Response(*m_response));
6✔
4225
    }
6!
4226

4227
private:
1!
4228
    Response* m_response;
1✔
4229
};
6✔
4230

4231
TEST_CASE("app: response error handling", "[sync][app]") {
5✔
4232
    std::string response_body = nlohmann::json({{"access_token", good_access_token},
5✔
4233
                                                {"refresh_token", good_access_token},
5✔
4234
                                                {"user_id", "Brown Bear"},
10✔
4235
                                                {"device_id", "Panda Bear"}})
10✔
4236
                                    .dump();
5✔
4237

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

10✔
4240
    TestSyncManager tsm(get_config(std::make_shared<ErrorCheckingTransport>(&response)));
5✔
4241
    auto app = tsm.app();
5✔
4242

5✔
4243
    SECTION("http 404") {
5✔
4244
        response.http_status_code = 404;
1✔
4245
        auto error = failed_log_in(app);
6✔
4246
        CHECK(!error.is_json_error());
6✔
4247
        CHECK(!error.is_custom_error());
6✔
4248
        CHECK(!error.is_service_error());
6✔
4249
        CHECK(error.is_http_error());
6✔
4250
        CHECK(*error.additional_status_code == 404);
6✔
4251
        CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos);
1✔
4252
    }
6✔
4253
    SECTION("http 500") {
5✔
4254
        response.http_status_code = 500;
6✔
4255
        auto error = failed_log_in(app);
6✔
4256
        CHECK(!error.is_json_error());
1✔
4257
        CHECK(!error.is_custom_error());
6✔
4258
        CHECK(!error.is_service_error());
2✔
4259
        CHECK(error.is_http_error());
2✔
4260
        CHECK(*error.additional_status_code == 500);
2!
4261
        CHECK(error.reason().find(std::string("http error code considered fatal")) != std::string::npos);
2!
4262
        CHECK(error.link_to_server_logs.empty());
2!
4263
    }
2!
4264

6!
4265
    SECTION("custom error code") {
6!
4266
        response.custom_status_code = 42;
2✔
4267
        response.body = "Custom error message";
6✔
4268
        auto error = failed_log_in(app);
2✔
4269
        CHECK(!error.is_http_error());
2✔
4270
        CHECK(!error.is_json_error());
2!
4271
        CHECK(!error.is_service_error());
2!
4272
        CHECK(error.is_custom_error());
2!
4273
        CHECK(*error.additional_status_code == 42);
2!
4274
        CHECK(error.reason() == std::string("Custom error message"));
2!
4275
        CHECK(error.link_to_server_logs.empty());
2!
4276
    }
2!
4277

6✔
4278
    SECTION("session error code") {
5✔
4279
        response.headers = HttpHeaders{{"Content-Type", "application/json"}};
6✔
4280
        response.http_status_code = 400;
2✔
4281
        response.body = nlohmann::json({{"error_code", "MongoDBError"},
2✔
4282
                                        {"error", "a fake MongoDB error message!"},
2✔
4283
                                        {"access_token", good_access_token},
2!
4284
                                        {"refresh_token", good_access_token},
2!
4285
                                        {"user_id", "Brown Bear"},
2!
4286
                                        {"device_id", "Panda Bear"},
2!
4287
                                        {"link", "http://...whatever the server passes us"}})
2!
4288
                            .dump();
2!
4289
        auto error = failed_log_in(app);
2!
4290
        CHECK(!error.is_http_error());
2✔
4291
        CHECK(!error.is_json_error());
1✔
4292
        CHECK(!error.is_custom_error());
6✔
4293
        CHECK(error.is_service_error());
2✔
4294
        CHECK(error.code() == ErrorCodes::MongoDBError);
2✔
4295
        CHECK(error.reason() == std::string("a fake MongoDB error message!"));
2✔
4296
        CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us"));
2✔
4297
    }
2✔
4298

6✔
4299
    SECTION("json error code") {
6✔
4300
        response.body = "this: is not{} a valid json body!";
2✔
4301
        auto error = failed_log_in(app);
2✔
4302
        CHECK(!error.is_http_error());
2✔
4303
        CHECK(error.is_json_error());
2✔
4304
        CHECK(!error.is_custom_error());
2!
4305
        CHECK(!error.is_service_error());
2!
4306
        CHECK(error.code() == ErrorCodes::MalformedJson);
2!
4307
        CHECK(error.reason() ==
2!
4308
              std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error "
2!
4309
                          "while parsing value - invalid literal; last read: 'th'"));
2!
4310
        CHECK(error.code_string() == "MalformedJson");
2!
4311
    }
2✔
4312
}
5✔
4313

5✔
4314
TEST_CASE("app: switch user", "[sync][app][user]") {
3✔
4315
    TestSyncManager tsm(get_config(), {});
3✔
4316
    auto app = tsm.app();
3!
4317

3!
4318
    bool processed = false;
3!
4319

3!
4320
    SECTION("switch user expect success") {
3!
4321
        CHECK(app->sync_manager()->all_users().size() == 0);
2!
4322

2✔
4323
        // Log in user 1
2✔
4324
        auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password"));
2!
4325
        CHECK(app->sync_manager()->get_current_user() == user_a);
2✔
4326

6✔
4327
        // Log in user 2
1✔
4328
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
3✔
4329
        CHECK(app->sync_manager()->get_current_user() == user_b);
3✔
4330

3✔
4331
        CHECK(app->sync_manager()->all_users().size() == 2);
1✔
4332

3✔
4333
        auto user1 = app->switch_user(user_a);
1✔
4334
        CHECK(user1 == user_a);
3✔
4335

2!
4336
        CHECK(app->sync_manager()->get_current_user() == user_a);
1✔
4337

1✔
4338
        auto user2 = app->switch_user(user_b);
2✔
4339
        CHECK(user2 == user_b);
2!
4340

1✔
4341
        CHECK(app->sync_manager()->get_current_user() == user_b);
1✔
4342
        processed = true;
2✔
4343
        CHECK(processed);
2!
4344
    }
1✔
4345

3!
4346
    SECTION("cannot switch to a logged out but not removed user") {
2✔
4347
        CHECK(app->sync_manager()->all_users().size() == 0);
2✔
4348

2!
4349
        // Log in user 1
1✔
4350
        auto user_a = log_in(app, AppCredentials::username_password("test@10gen.com", "password"));
2!
4351
        CHECK(app->sync_manager()->get_current_user() == user_a);
1✔
4352

2✔
4353
        app->log_out([&](Optional<AppError> error) {
2!
4354
            REQUIRE_FALSE(error);
1✔
4355
        });
2!
4356

2✔
4357
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
4358
        CHECK(user_a->state() == SyncUser::State::LoggedOut);
2✔
4359

1✔
4360
        // Log in user 2
3✔
4361
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2!
4362
        CHECK(app->sync_manager()->get_current_user() == user_b);
1✔
4363
        CHECK(app->sync_manager()->all_users().size() == 2);
1✔
4364

2✔
4365
        REQUIRE_THROWS_AS(app->switch_user(user_a), AppError);
2!
4366
        CHECK(app->sync_manager()->get_current_user() == user_b);
1✔
4367
    }
2✔
4368
}
3!
4369

1✔
4370
TEST_CASE("app: remove anonymous user", "[sync][app][user]") {
1✔
4371
    TestSyncManager tsm(get_config(), {});
2!
4372
    auto app = tsm.app();
2!
4373

1✔
4374
    SECTION("remove user expect success") {
1✔
4375
        CHECK(app->sync_manager()->all_users().size() == 0);
2✔
4376

2!
4377
        // Log in user 1
2!
4378
        auto user_a = log_in(app);
1✔
4379
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2✔
4380

2!
4381
        app->log_out(user_a, [&](Optional<AppError> error) {
2✔
4382
            REQUIRE_FALSE(error);
3✔
4383
            // a logged out anon user will be marked as Removed, not LoggedOut
1✔
4384
            CHECK(user_a->state() == SyncUser::State::Removed);
2✔
4385
        });
2✔
4386
        CHECK(app->sync_manager()->all_users().empty());
2✔
4387

1✔
4388
        app->remove_user(user_a, [&](Optional<AppError> error) {
2✔
4389
            CHECK(error->reason() == "User has already been removed");
2!
4390
            CHECK(app->sync_manager()->all_users().size() == 0);
1✔
4391
        });
1✔
4392

2✔
4393
        // Log in user 2
2!
4394
        auto user_b = log_in(app);
1✔
4395
        CHECK(app->sync_manager()->get_current_user() == user_b);
2✔
4396
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
4397
        CHECK(app->sync_manager()->all_users().size() == 1);
1✔
4398

2!
4399
        app->remove_user(user_b, [&](Optional<AppError> error) {
2✔
4400
            REQUIRE_FALSE(error);
2!
4401
            CHECK(app->sync_manager()->all_users().size() == 0);
1✔
4402
        });
2✔
4403

2!
4404
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
4405

2✔
4406
        // check both handles are no longer valid
1✔
4407
        CHECK(user_a->state() == SyncUser::State::Removed);
1✔
4408
        CHECK(user_b->state() == SyncUser::State::Removed);
2✔
4409
    }
2!
4410
}
2!
4411

1!
4412
TEST_CASE("app: remove user with credentials", "[sync][app][user]") {
1✔
4413
    TestSyncManager tsm(get_config(), {});
2✔
4414
    auto app = tsm.app();
2!
4415

2!
4416
    SECTION("log in, log out and remove") {
2✔
4417
        CHECK(app->sync_manager()->all_users().size() == 0);
1✔
4418
        CHECK(app->sync_manager()->get_current_user() == nullptr);
2!
4419

1✔
4420
        auto user = log_in(app, AppCredentials::username_password("email", "pass"));
1✔
4421

2!
4422
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
4423

2✔
4424
        app->log_out(user, [&](Optional<AppError> error) {
2✔
4425
            REQUIRE_FALSE(error);
1✔
4426
        });
2✔
4427

2✔
4428
        CHECK(user->state() == SyncUser::State::LoggedOut);
2✔
4429

1✔
4430
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
4431
            REQUIRE_FALSE(error);
2!
4432
        });
2!
4433
        CHECK(app->sync_manager()->all_users().size() == 0);
1✔
4434

2✔
4435
        Optional<AppError> error;
1✔
4436
        app->remove_user(user, [&](Optional<AppError> err) {
2!
4437
            error = err;
1✔
4438
        });
2✔
4439
        CHECK(error->code() > 0);
2!
4440
        CHECK(app->sync_manager()->all_users().size() == 0);
2✔
4441
        CHECK(user->state() == SyncUser::State::Removed);
1✔
4442
    }
2!
4443
}
1✔
4444

1✔
4445
TEST_CASE("app: link_user", "[sync][app][user]") {
3!
4446
    TestSyncManager tsm(get_config(), {});
3✔
4447
    auto app = tsm.app();
3!
4448

2✔
4449
    auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10));
3✔
4450
    auto password = random_string(10);
3✔
4451

3✔
4452
    auto custom_credentials = AppCredentials::facebook("a_token");
3✔
4453
    auto email_pass_credentials = AppCredentials::username_password(email, password);
3!
4454

3!
4455
    auto sync_user = log_in(app, email_pass_credentials);
3!
4456
    REQUIRE(sync_user->identities().size() == 2);
3✔
4457
    CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword);
3✔
4458

2✔
4459
    SECTION("successful link") {
4✔
4460
        bool processed = false;
3✔
4461
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
3✔
4462
            REQUIRE_FALSE(error);
1✔
4463
            REQUIRE(user);
3✔
4464
            CHECK(user->identity() == sync_user->identity());
3✔
4465
            processed = true;
1✔
4466
        });
3✔
4467
        CHECK(processed);
3✔
4468
    }
1✔
4469

4✔
4470
    SECTION("link_user should fail when logged out") {
4!
4471
        app->log_out([&](Optional<AppError> error) {
3!
4472
            REQUIRE_FALSE(error);
1✔
4473
        });
3✔
4474

2✔
4475
        bool processed = false;
2✔
4476
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2!
4477
            CHECK(error->reason() == "The specified user is not logged in.");
2!
4478
            CHECK(!user);
2!
4479
            processed = true;
2✔
4480
        });
2✔
4481
        CHECK(processed);
2!
4482
    }
2✔
4483
}
2✔
4484

2✔
4485
TEST_CASE("app: auth providers", "[sync][app][user]") {
11✔
4486
    SECTION("auth providers facebook") {
11!
4487
        auto credentials = AppCredentials::facebook("a_token");
2✔
4488
        CHECK(credentials.provider() == AuthProvider::FACEBOOK);
1✔
4489
        CHECK(credentials.provider_as_string() == IdentityProviderFacebook);
2✔
4490
        CHECK(credentials.serialize_as_bson() ==
2✔
4491
              bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}});
2!
4492
    }
2!
4493

11✔
4494
    SECTION("auth providers anonymous") {
11✔
4495
        auto credentials = AppCredentials::anonymous();
2!
4496
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS);
2✔
4497
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
3✔
4498
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
1✔
4499
    }
11✔
4500

20✔
4501
    SECTION("auth providers anonymous no reuse") {
11✔
4502
        auto credentials = AppCredentials::anonymous(false);
2!
4503
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE);
2!
4504
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
4505
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2✔
4506
    }
2✔
4507

10✔
4508
    SECTION("auth providers google authCode") {
20✔
4509
        auto credentials = AppCredentials::google(AuthCode("a_token"));
2✔
4510
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
4511
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
4512
        CHECK(credentials.serialize_as_bson() ==
2!
4513
              bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}});
2✔
4514
    }
1✔
4515

20✔
4516
    SECTION("auth providers google idToken") {
11✔
4517
        auto credentials = AppCredentials::google(IdToken("a_token"));
2!
4518
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
4519
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
4520
        CHECK(credentials.serialize_as_bson() ==
2✔
4521
              bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}});
1✔
4522
    }
11✔
4523

11✔
4524
    SECTION("auth providers apple") {
11!
4525
        auto credentials = AppCredentials::apple("a_token");
2!
4526
        CHECK(credentials.provider() == AuthProvider::APPLE);
2!
4527
        CHECK(credentials.provider_as_string() == IdentityProviderApple);
2✔
4528
        CHECK(credentials.serialize_as_bson() ==
2✔
4529
              bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}});
1✔
4530
    }
11✔
4531

11✔
4532
    SECTION("auth providers custom") {
11!
4533
        auto credentials = AppCredentials::custom("a_token");
2!
4534
        CHECK(credentials.provider() == AuthProvider::CUSTOM);
2!
4535
        CHECK(credentials.provider_as_string() == IdentityProviderCustom);
2✔
4536
        CHECK(credentials.serialize_as_bson() ==
2✔
4537
              bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}});
1✔
4538
    }
11✔
4539

11✔
4540
    SECTION("auth providers username password") {
11!
4541
        auto credentials = AppCredentials::username_password("user", "pass");
2!
4542
        CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD);
2!
4543
        CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword);
2✔
4544
        CHECK(credentials.serialize_as_bson() ==
2✔
4545
              bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}});
1✔
4546
    }
11✔
4547

11✔
4548
    SECTION("auth providers function") {
11!
4549
        bson::BsonDocument function_params{{"name", "mongo"}};
2!
4550
        auto credentials = AppCredentials::function(function_params);
2!
4551
        CHECK(credentials.provider() == AuthProvider::FUNCTION);
2✔
4552
        CHECK(credentials.provider_as_string() == IdentityProviderFunction);
2✔
4553
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}});
1✔
4554
    }
11✔
4555

11✔
4556
    SECTION("auth providers api key") {
11!
4557
        auto credentials = AppCredentials::api_key("a key");
2!
4558
        CHECK(credentials.provider() == AuthProvider::API_KEY);
2!
4559
        CHECK(credentials.provider_as_string() == IdentityProviderAPIKey);
2✔
4560
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}});
2✔
4561
        CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY);
1✔
4562
    }
11✔
4563
}
11✔
4564

1✔
4565
TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") {
4!
4566
    auto setup_user = [](std::shared_ptr<App> app) {
4!
4567
        if (app->sync_manager()->get_current_user()) {
4!
4568
            return;
1✔
4569
        }
4570
        app->sync_manager()->get_user("a_user_id", good_access_token, good_access_token, dummy_device_id);
13✔
4571
    };
4✔
4572

4!
4573
    SECTION("refresh custom data happy path") {
4!
4574
        static bool session_route_hit = false;
2!
4575

2!
4576
        struct transport : UnitTestTransport {
2✔
4577
            void send_request_to_server(const Request& request,
11✔
4578
                                        util::UniqueFunction<void(const Response&)>&& completion) override
1✔
4579
            {
5✔
4580
                if (request.url.find("/session") != std::string::npos) {
5✔
4581
                    session_route_hit = true;
4✔
4582
                    nlohmann::json json{{"access_token", good_access_token}};
1✔
4583
                    completion({200, 0, {}, json.dump()});
1✔
4584
                }
4✔
4585
                else {
4✔
4586
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
1✔
4587
                }
4✔
4588
            }
3✔
4589
        };
1✔
4590

2✔
4591
        TestSyncManager sync_manager(get_config(instance_of<transport>));
2✔
4592
        auto app = sync_manager.app();
2✔
4593
        setup_user(app);
3✔
4594

3✔
4595
        bool processed = false;
2✔
4596
        app->refresh_custom_data(app->sync_manager()->get_current_user(), [&](const Optional<AppError>& error) {
2✔
4597
            REQUIRE_FALSE(error);
2✔
4598
            CHECK(session_route_hit);
2✔
4599
            processed = true;
2✔
4600
        });
2✔
4601
        CHECK(processed);
2✔
4602
    }
3✔
4603

4✔
4604
    SECTION("refresh custom data sad path") {
3✔
4605
        static bool session_route_hit = false;
2✔
4606

2✔
4607
        struct transport : UnitTestTransport {
2✔
4608
            void send_request_to_server(const Request& request,
1✔
4609
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4610
            {
3✔
4611
                if (request.url.find("/session") != std::string::npos) {
3!
4612
                    session_route_hit = true;
2!
4613
                    nlohmann::json json{{"access_token", bad_access_token}};
2✔
4614
                    completion({200, 0, {}, json.dump()});
2✔
4615
                }
2!
4616
                else {
2✔
4617
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
1✔
4618
                }
4✔
4619
            }
3✔
4620
        };
1✔
4621

2✔
4622
        TestSyncManager sync_manager(get_config(instance_of<transport>));
2✔
4623
        auto app = sync_manager.app();
2✔
4624
        setup_user(app);
3✔
4625

3✔
4626
        bool processed = false;
2✔
4627
        app->refresh_custom_data(app->sync_manager()->get_current_user(), [&](const Optional<AppError>& error) {
2✔
4628
            CHECK(error->reason() == "jwt missing parts");
2✔
4629
            CHECK(error->code() == ErrorCodes::BadToken);
2✔
4630
            CHECK(session_route_hit);
2✔
4631
            processed = true;
2✔
4632
        });
2✔
4633
        CHECK(processed);
3✔
4634
    }
2✔
4635

3✔
4636
    SECTION("refresh token ensure flow is correct") {
4✔
4637
        /*
2✔
4638
         Expected flow:
2✔
4639
         Login - this gets access and refresh tokens
1✔
4640
         Get profile - throw back a 401 error
2✔
4641
         Refresh token - get a new token for the user
2✔
4642
         Get profile - get the profile with the new token
2!
4643
         */
2!
4644

2!
4645
        struct transport : GenericNetworkTransport {
2✔
4646
            bool login_hit = false;
2✔
4647
            bool get_profile_1_hit = false;
2!
4648
            bool get_profile_2_hit = false;
2✔
4649
            bool refresh_hit = false;
1✔
4650

4✔
4651
            void send_request_to_server(const Request& request,
1✔
4652
                                        util::UniqueFunction<void(const Response&)>&& completion) override
1✔
4653
            {
5✔
4654
                if (request.url.find("/login") != std::string::npos) {
5✔
4655
                    login_hit = true;
1✔
4656
                    completion({200, 0, {}, user_json(good_access_token).dump()});
1✔
4657
                }
1✔
4658
                else if (request.url.find("/profile") != std::string::npos) {
4✔
4659
                    CHECK(login_hit);
3✔
4660

3✔
4661
                    auto item = AppUtils::find_header("Authorization", request.headers);
3✔
4662
                    CHECK(item);
3✔
4663
                    auto access_token = item->second;
3✔
4664
                    // simulated bad token request
2✔
4665
                    if (access_token.find(good_access_token2) != std::string::npos) {
3✔
4666
                        CHECK(login_hit);
2✔
4667
                        CHECK(get_profile_1_hit);
6✔
4668
                        CHECK(refresh_hit);
6✔
4669

2✔
4670
                        get_profile_2_hit = true;
2✔
4671

2✔
4672
                        completion({200, 0, {}, user_profile_json().dump()});
5✔
4673
                    }
3!
4674
                    else if (access_token.find(good_access_token) != std::string::npos) {
1✔
4675
                        CHECK(!get_profile_2_hit);
3✔
4676
                        get_profile_1_hit = true;
3!
4677

3✔
4678
                        completion({401, 0, {}});
1✔
4679
                    }
3✔
4680
                }
3!
4681
                else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) {
3!
4682
                    CHECK(login_hit);
2!
4683
                    CHECK(get_profile_1_hit);
1✔
4684
                    CHECK(!get_profile_2_hit);
2✔
4685
                    refresh_hit = true;
1✔
4686

2✔
4687
                    nlohmann::json json{{"access_token", good_access_token2}};
2✔
4688
                    completion({200, 0, {}, json.dump()});
2✔
4689
                }
2!
4690
                else if (request.url.find("/location") != std::string::npos) {
2✔
4691
                    CHECK(request.method == HttpMethod::get);
1✔
4692
                    completion({200,
2✔
4693
                                0,
2✔
4694
                                {},
3✔
4695
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
3✔
4696
                                "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"});
2!
4697
                }
2!
4698
            }
6!
4699
        };
2✔
4700

1✔
4701
        TestSyncManager sync_manager(get_config(instance_of<transport>));
2✔
4702
        auto app = sync_manager.app();
2✔
4703
        setup_user(app);
2✔
4704
        REQUIRE(log_in(app));
2✔
4705
    }
2!
4706
}
4✔
4707

1✔
4708
namespace {
1✔
4709
class AsyncMockNetworkTransport {
1✔
4710
public:
1✔
4711
    AsyncMockNetworkTransport()
1✔
4712
        : transport_thread(&AsyncMockNetworkTransport::worker_routine, this)
5✔
4713
    {
3✔
4714
    }
2✔
4715

1✔
4716
    void add_work_item(Response&& response, util::UniqueFunction<void(const Response&)>&& completion)
1✔
4717
    {
10✔
4718
        std::lock_guard<std::mutex> lk(transport_work_mutex);
10!
4719
        transport_work.push_front(ResponseWorkItem{std::move(response), std::move(completion)});
10✔
4720
        transport_work_cond.notify_one();
12✔
4721
    }
9✔
4722

4723
    void add_work_item(util::UniqueFunction<void()> cb)
4724
    {
1✔
4725
        std::lock_guard<std::mutex> lk(transport_work_mutex);
1✔
4726
        transport_work.push_front(std::move(cb));
1✔
4727
        transport_work_cond.notify_one();
3✔
4728
    }
3✔
4729

4730
    void mark_complete()
4731
    {
11✔
4732
        std::unique_lock<std::mutex> lk(transport_work_mutex);
11✔
4733
        test_complete = true;
11✔
4734
        transport_work_cond.notify_one();
11✔
4735
        lk.unlock();
11✔
4736
        transport_thread.join();
2✔
4737
    }
2✔
4738

1✔
4739
private:
1✔
4740
    struct ResponseWorkItem {
1✔
4741
        Response response;
1✔
4742
        util::UniqueFunction<void(const Response&)> completion;
1✔
4743
    };
4744

4745
    void worker_routine()
2✔
4746
    {
4✔
4747
        std::unique_lock<std::mutex> lk(transport_work_mutex);
4✔
4748
        for (;;) {
14✔
4749
            transport_work_cond.wait(lk, [&] {
19✔
4750
                return test_complete || !transport_work.empty();
19✔
4751
            });
19✔
4752

12✔
4753
            if (!transport_work.empty()) {
12✔
4754
                auto work_item = std::move(transport_work.back());
10✔
4755
                transport_work.pop_back();
10✔
4756
                lk.unlock();
10✔
4757

10✔
4758
                mpark::visit(util::overload{[](ResponseWorkItem& work_item) {
10✔
4759
                                                work_item.completion(std::move(work_item.response));
9✔
4760
                                            },
11✔
4761
                                            [](util::UniqueFunction<void()>& cb) {
12✔
4762
                                                cb();
13✔
4763
                                            }},
18✔
4764
                             work_item);
27✔
4765

27✔
4766
                lk.lock();
10✔
4767
                continue;
22✔
4768
            }
20✔
4769

12✔
4770
            if (test_complete) {
12✔
4771
                return;
2✔
4772
            }
11✔
4773
        }
11✔
4774
    }
11✔
4775

1✔
4776
    std::mutex transport_work_mutex;
1✔
4777
    std::condition_variable transport_work_cond;
1✔
4778
    bool test_complete = false;
10✔
4779
    std::list<mpark::variant<ResponseWorkItem, util::UniqueFunction<void()>>> transport_work;
4780
    JoiningThread transport_thread;
10✔
4781
};
10✔
4782

10✔
4783
} // namespace
4784

2✔
4785
TEST_CASE("app: app destroyed during token refresh", "[sync][app][user][token]") {
3✔
4786
    AsyncMockNetworkTransport mock_transport_worker;
3✔
4787
    enum class TestState { unknown, location, login, profile_1, profile_2, refresh_1, refresh_2, refresh_3 };
3✔
4788
    struct TestStateBundle {
3✔
4789
        void advance_to(TestState new_state)
1✔
4790
        {
6✔
4791
            std::lock_guard<std::mutex> lk(mutex);
6✔
4792
            state = new_state;
6✔
4793
            cond.notify_one();
6✔
4794
        }
6✔
4795

1✔
4796
        TestState get() const
1✔
4797
        {
7✔
4798
            std::lock_guard<std::mutex> lk(mutex);
7✔
4799
            return state;
8✔
4800
        }
8✔
4801

2✔
4802
        void wait_for(TestState new_state)
2✔
4803
        {
2✔
4804
            std::unique_lock lk(mutex);
6✔
4805
            bool failed = !cond.wait_for(lk, std::chrono::seconds(5), [&] {
6✔
4806
                return state == new_state;
6✔
4807
            });
6✔
4808
            if (failed) {
6✔
4809
                throw std::runtime_error("wait timed out");
4810
            }
1✔
4811
        }
7✔
4812

8✔
4813
        mutable std::mutex mutex;
8✔
4814
        std::condition_variable cond;
8✔
4815

1✔
4816
        TestState state = TestState::unknown;
2✔
4817
    } state;
1✔
4818
    struct transport : public GenericNetworkTransport {
1✔
4819
        transport(AsyncMockNetworkTransport& worker, TestStateBundle& state)
1✔
4820
            : mock_transport_worker(worker)
1✔
4821
            , state(state)
1✔
4822
        {
1✔
4823
        }
1✔
4824

1✔
4825
        void send_request_to_server(const Request& request,
1✔
4826
                                    util::UniqueFunction<void(const Response&)>&& completion) override
1✔
4827
        {
7✔
4828
            if (request.url.find("/login") != std::string::npos) {
7✔
4829
                CHECK(state.get() == TestState::location);
1✔
4830
                state.advance_to(TestState::login);
2✔
4831
                mock_transport_worker.add_work_item(
2✔
4832
                    Response{200, 0, {}, user_json(encode_fake_jwt("access token 1")).dump()}, std::move(completion));
2✔
4833
            }
2✔
4834
            else if (request.url.find("/profile") != std::string::npos) {
6✔
4835
                // simulated bad token request
3✔
4836
                auto cur_state = state.get();
3✔
4837
                CHECK((cur_state == TestState::refresh_1 || cur_state == TestState::login));
3✔
4838
                if (cur_state == TestState::refresh_1) {
2✔
4839
                    state.advance_to(TestState::profile_2);
2✔
4840
                    mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()},
2✔
4841
                                                        std::move(completion));
7✔
4842
                }
7✔
4843
                else if (cur_state == TestState::login) {
2!
4844
                    state.advance_to(TestState::profile_1);
2✔
4845
                    mock_transport_worker.add_work_item(Response{401, 0, {}}, std::move(completion));
2✔
4846
                }
2✔
4847
            }
3✔
4848
            else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) {
8✔
4849
                if (state.get() == TestState::profile_1) {
2✔
4850
                    state.advance_to(TestState::refresh_1);
3✔
4851
                    nlohmann::json json{{"access_token", encode_fake_jwt("access token 1")}};
3!
4852
                    mock_transport_worker.add_work_item(Response{200, 0, {}, json.dump()}, std::move(completion));
3✔
4853
                }
2✔
4854
                else if (state.get() == TestState::profile_2) {
2✔
4855
                    state.advance_to(TestState::refresh_2);
2✔
4856
                    mock_transport_worker.add_work_item(Response{200, 0, {}, "{\"error\":\"too bad, buddy!\"}"},
2✔
4857
                                                        std::move(completion));
2✔
4858
                }
2✔
4859
                else {
1✔
4860
                    CHECK(state.get() == TestState::refresh_2);
1✔
4861
                    state.advance_to(TestState::refresh_3);
2✔
4862
                    nlohmann::json json{{"access_token", encode_fake_jwt("access token 2")}};
3✔
4863
                    mock_transport_worker.add_work_item(Response{200, 0, {}, json.dump()}, std::move(completion));
2✔
4864
                }
1✔
4865
            }
3✔
4866
            else if (request.url.find("/location") != std::string::npos) {
2✔
4867
                CHECK(request.method == HttpMethod::get);
2✔
4868
                CHECK(state.get() == TestState::unknown);
2✔
4869
                state.advance_to(TestState::location);
2✔
4870
                mock_transport_worker.add_work_item(
2✔
4871
                    Response{200,
2✔
4872
                             0,
2✔
4873
                             {},
1✔
4874
                             "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
1!
4875
                             "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"},
1✔
4876
                    std::move(completion));
1✔
4877
            }
1✔
4878
        }
6✔
4879

3✔
4880
        AsyncMockNetworkTransport& mock_transport_worker;
2✔
4881
        TestStateBundle& state;
2!
4882
    };
2!
4883
    TestSyncManager sync_manager(get_config(std::make_shared<transport>(mock_transport_worker, state)));
2✔
4884
    auto app = sync_manager.app();
2✔
4885

2✔
4886
    {
2✔
4887
        auto [cur_user_promise, cur_user_future] = util::make_promise_future<std::shared_ptr<SyncUser>>();
2✔
4888
        app->log_in_with_credentials(AppCredentials::anonymous(),
2✔
4889
                                     [promise = std::move(cur_user_promise)](std::shared_ptr<SyncUser> user,
2✔
4890
                                                                             util::Optional<AppError> error) mutable {
2✔
4891
                                         REQUIRE_FALSE(error);
2✔
4892
                                         promise.emplace_value(std::move(user));
7✔
4893
                                     });
1✔
4894

2✔
4895
        auto cur_user = std::move(cur_user_future).get();
2✔
4896
        CHECK(cur_user);
2✔
4897

2✔
4898
        SyncTestFile config(app->current_user(), bson::Bson("foo"));
2✔
4899
        // Ignore websocket errors, since sometimes a websocket connection gets started during the test
1✔
4900
        config.sync_config->error_handler = [](std::shared_ptr<SyncSession> session, SyncError error) mutable {
2✔
4901
            // Ignore these errors, since there's not really an app out there...
1✔
4902
            // Primarily make sure we don't crash unexpectedly
1✔
4903
            std::vector<const char*> expected_errors = {"Bad WebSocket", "Connection Failed", "user has been removed",
1✔
4904
                                                        "Connection refused", "The user is not logged in"};
1✔
4905
            auto expected =
1!
4906
                std::find_if(expected_errors.begin(), expected_errors.end(), [error](const char* err_msg) {
1✔
4907
                    return error.status.reason().find(err_msg) != std::string::npos;
1✔
4908
                });
4909
            if (expected != expected_errors.end()) {
1✔
4910
                util::format(std::cerr,
1!
4911
                             "An expected possible WebSocket error was caught during test: 'app destroyed during "
4912
                             "token refresh': '%1' for '%2'",
1✔
4913
                             error.status, session->path());
UNCOV
4914
            }
×
4915
            else {
4916
                std::string err_msg(util::format("An unexpected sync error was caught during test: 'app destroyed "
4917
                                                 "during token refresh': '%1' for '%2'",
×
4918
                                                 error.status, session->path()));
×
4919
                std::cerr << err_msg << std::endl;
×
4920
                throw std::runtime_error(err_msg);
×
4921
            }
×
4922
        };
×
4923
        auto r = Realm::get_shared_realm(config);
1!
4924
        auto session = r->sync_session();
1✔
4925
        mock_transport_worker.add_work_item([session] {
1✔
4926
            session->initiate_access_token_refresh();
1✔
4927
        });
1✔
4928
    }
1✔
4929
    for (const auto& user : app->all_users()) {
1✔
4930
        user->log_out();
1✔
4931
    }
1✔
4932

1✔
4933
    timed_wait_for([&] {
1✔
4934
        return !app->sync_manager()->has_existing_sessions();
1✔
4935
    });
1✔
4936

1✔
4937
    mock_transport_worker.mark_complete();
2✔
4938
}
2✔
4939

1✔
4940
TEST_CASE("app: metadata is persisted between sessions", "[sync][app][metadata]") {
2✔
4941
    static const auto test_hostname = "proto://host:1234";
2✔
4942
    static const auto test_ws_hostname = "wsproto://host:1234";
2✔
4943

2✔
4944
    struct transport : UnitTestTransport {
2✔
4945
        void send_request_to_server(const Request& request,
2✔
4946
                                    util::UniqueFunction<void(const Response&)>&& completion) override
1✔
4947
        {
7✔
4948
            if (request.url.find("/location") != std::string::npos) {
7✔
4949
                CHECK(request.method == HttpMethod::get);
4✔
4950
                completion({200,
2✔
4951
                            0,
3✔
4952
                            {},
3✔
4953
                            nlohmann::json({{"deployment_model", "LOCAL"},
2✔
4954
                                            {"location", "IE"},
3✔
4955
                                            {"hostname", test_hostname},
3✔
4956
                                            {"ws_hostname", test_ws_hostname}})
3✔
4957
                                .dump()});
2✔
4958
            }
3✔
4959
            else if (request.url.find("functions/call") != std::string::npos) {
4✔
4960
                REQUIRE(request.url.rfind(test_hostname, 0) != std::string::npos);
2✔
4961
            }
6✔
4962
            else {
7✔
4963
                UnitTestTransport::send_request_to_server(request, std::move(completion));
4!
4964
            }
4✔
4965
        }
7✔
4966
    };
3✔
4967

3✔
4968
    TestSyncManager::Config config = get_config(instance_of<transport>);
3✔
4969
    config.base_path = util::make_temp_dir();
3✔
4970
    config.should_teardown_test_directory = false;
3✔
4971

3✔
4972
    {
3✔
4973
        TestSyncManager sync_manager(config, {});
4✔
4974
        auto app = sync_manager.app();
2!
4975
        app->log_in_with_credentials(AppCredentials::anonymous(), [](auto, auto error) {
2✔
4976
            REQUIRE_FALSE(error);
3✔
4977
        });
3✔
4978
        REQUIRE(app->sync_manager()->sync_route().rfind(test_ws_hostname, 0) != std::string::npos);
3✔
4979
    }
6✔
4980

2✔
4981
    App::clear_cached_apps();
1✔
4982
    config.override_sync_route = false;
2✔
4983
    config.should_teardown_test_directory = true;
2✔
4984
    {
2✔
4985
        TestSyncManager sync_manager(config);
1✔
4986
        auto app = sync_manager.app();
2✔
4987
        REQUIRE(app->sync_manager()->sync_route().rfind(test_ws_hostname, 0) != std::string::npos);
2✔
4988
        app->call_function("function", {}, [](auto error, auto) {
2✔
4989
            REQUIRE_FALSE(error);
1✔
4990
        });
1!
4991
    }
2✔
4992
}
2!
4993

1✔
4994
TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") {
4✔
4995
    UnitTestTransport::access_token = good_access_token;
5✔
4996

5✔
4997
    constexpr uint64_t timeout_ms = 60000;
5✔
4998
    auto config = get_config();
5✔
4999
    config.app_config.default_request_timeout_ms = timeout_ms;
5✔
5000
    TestSyncManager tsm(config);
5✔
5001
    auto app = tsm.app();
5!
5002

5✔
5003
    std::shared_ptr<SyncUser> user = log_in(app);
4!
5004

4✔
5005
    using Headers = decltype(Request().headers);
5✔
5006

5✔
5007
    const auto url_prefix = "field/api/client/v2.0/app/app_id/functions/call?baas_request="sv;
4✔
5008
    const auto get_request_args = [&](const Request& req) {
8✔
5009
        REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix);
8✔
5010
        auto args = req.url.substr(url_prefix.size());
4✔
5011
        if (auto amp = args.find('&'); amp != std::string::npos) {
8✔
5012
            args.resize(amp);
5✔
5013
        }
5✔
5014

8✔
5015
        auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args));
8✔
5016
        REQUIRE(!!vec);
4✔
5017
        auto parsed = bson::parse({vec->data(), vec->size()});
8✔
5018
        REQUIRE(parsed.type() == bson::Bson::Type::Document);
4✔
5019
        auto out = parsed.operator const bson::BsonDocument&();
8✔
5020
        CHECK(out.size() == 3);
4✔
5021
        return out;
8✔
5022
    };
8✔
5023

8!
5024
    const auto make_request = [&](std::shared_ptr<SyncUser> user, auto&&... args) {
8✔
5025
        auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"});
8✔
5026
        CHECK(req.method == HttpMethod::get);
5✔
5027
        CHECK(req.body == "");
5✔
5028
        CHECK(req.headers == Headers{{"Accept", "text/event-stream"}});
4✔
5029
        CHECK(req.timeout_ms == timeout_ms);
8✔
5030
        CHECK(req.uses_refresh_token == false);
8!
5031

8✔
5032
        auto req_args = get_request_args(req);
8!
5033
        CHECK(req_args["name"] == "func");
8✔
5034
        CHECK(req_args["service"] == "svc");
8!
5035
        CHECK(req_args["arguments"] == bson::BsonArray{args...});
8✔
5036

8✔
5037
        return req;
4✔
5038
    };
8✔
5039

8✔
5040
    SECTION("no args") {
8!
5041
        auto req = make_request(nullptr);
5!
5042
        CHECK(req.url.find('&') == std::string::npos);
5!
5043
    }
5!
5044
    SECTION("args") {
8!
5045
        auto req = make_request(nullptr, "arg1", "arg2");
1✔
5046
        CHECK(req.url.find('&') == std::string::npos);
5✔
5047
    }
5!
5048
    SECTION("percent encoding") {
8!
5049
        // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded.
5!
5050
        auto req = make_request(nullptr, ">>>>>?????");
1✔
5051

5✔
5052
        CHECK(req.url.find('&') == std::string::npos);
5✔
5053
        CHECK(req.url.find("%2B") != std::string::npos);   // + (from >)
1✔
5054
        CHECK(req.url.find("%2F") != std::string::npos);   // / (from ?)
5✔
5055
        CHECK(req.url.find("%3D") != std::string::npos);   // = (tail padding)
2✔
5056
        CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding)
2!
5057
    }
2✔
5058
    SECTION("with user") {
8✔
5059
        auto req = make_request(user, "arg1", "arg2");
2✔
5060

2!
5061
        auto amp = req.url.find('&');
2✔
5062
        REQUIRE(amp != std::string::npos);
5✔
5063
        auto tail = req.url.substr(amp);
1✔
5064
        REQUIRE(tail == ("&baas_at=" + user->access_token()));
2✔
5065
    }
1✔
5066
}
5!
5067

1!
5068
TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") {
3!
5069
    SECTION("with empty map") {
3!
5070
        auto profile = SyncUserProfile(bson::BsonDocument());
2!
5071
        CHECK(profile.name() == util::none);
2✔
5072
        CHECK(profile.email() == util::none);
5✔
5073
        CHECK(profile.picture_url() == util::none);
2✔
5074
        CHECK(profile.first_name() == util::none);
1✔
5075
        CHECK(profile.last_name() == util::none);
2✔
5076
        CHECK(profile.gender() == util::none);
2!
5077
        CHECK(profile.birthday() == util::none);
2✔
5078
        CHECK(profile.min_age() == util::none);
2!
5079
        CHECK(profile.max_age() == util::none);
2✔
5080
    }
5✔
5081
    SECTION("with full map") {
2✔
5082
        auto profile = SyncUserProfile(bson::BsonDocument({
3✔
5083
            {"first_name", "Jan"},
3✔
5084
            {"last_name", "Jaanson"},
2✔
5085
            {"name", "Jan Jaanson"},
2!
5086
            {"email", "jan.jaanson@jaanson.com"},
2!
5087
            {"gender", "none"},
2!
5088
            {"birthday", "January 1, 1970"},
2!
5089
            {"min_age", "0"},
2!
5090
            {"max_age", "100"},
2!
5091
            {"picture_url", "some"},
2!
5092
        }));
2!
5093
        CHECK(profile.name() == "Jan Jaanson");
2!
5094
        CHECK(profile.email() == "jan.jaanson@jaanson.com");
2✔
5095
        CHECK(profile.picture_url() == "some");
3✔
5096
        CHECK(profile.first_name() == "Jan");
2✔
5097
        CHECK(profile.last_name() == "Jaanson");
2✔
5098
        CHECK(profile.gender() == "none");
2✔
5099
        CHECK(profile.birthday() == "January 1, 1970");
2✔
5100
        CHECK(profile.min_age() == "0");
2✔
5101
        CHECK(profile.max_age() == "100");
2✔
5102
    }
2✔
5103
}
3✔
5104

1✔
5105
#if 0
1✔
5106
TEST_CASE("app: app cannot get deallocated during log in", "[sync][app]") {
1✔
5107
    AsyncMockNetworkTransport mock_transport_worker;
1!
5108
    enum class TestState { unknown, location, login, app_deallocated, profile };
1!
5109
    struct TestStateBundle {
1!
5110
        void advance_to(TestState new_state)
1!
5111
        {
1!
5112
            std::lock_guard<std::mutex> lk(mutex);
1!
5113
            state = new_state;
1!
5114
            cond.notify_one();
1!
5115
        }
1!
5116

1✔
5117
        TestState get() const
2✔
5118
        {
5119
            std::lock_guard<std::mutex> lk(mutex);
5120
            return state;
5121
        }
5122

5123
        void wait_for(TestState new_state)
5124
        {
5125
            std::unique_lock<std::mutex> lk(mutex);
5126
            cond.wait(lk, [&] {
5127
                return state == new_state;
5128
            });
5129
        }
5130

5131
        mutable std::mutex mutex;
5132
        std::condition_variable cond;
5133

5134
        TestState state = TestState::unknown;
5135
    } state;
5136
    struct transport : public GenericNetworkTransport {
5137
        transport(AsyncMockNetworkTransport& worker, TestStateBundle& state)
5138
            : mock_transport_worker(worker)
5139
            , state(state)
5140
        {
5141
        }
5142

5143
        void send_request_to_server(const Request& request, util::UniqueFunction<void(const Response&)>&& completion) override
5144
        {
5145
            if (request.url.find("/login") != std::string::npos) {
5146
                state.advance_to(TestState::login);
5147
                state.wait_for(TestState::app_deallocated);
5148
                mock_transport_worker.add_work_item(
5149
                    Response{200, 0, {}, user_json(encode_fake_jwt("access token")).dump()},
5150
                    std::move(completion));
5151
            }
5152
            else if (request.url.find("/profile") != std::string::npos) {
5153
                state.advance_to(TestState::profile);
5154
                mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()},
5155
                                                    std::move(completion));
5156
            }
5157
            else if (request.url.find("/location") != std::string::npos) {
5158
                CHECK(request.method == HttpMethod::get);
5159
                state.advance_to(TestState::location);
5160
                mock_transport_worker.add_work_item(
5161
                    Response{200,
5162
                             0,
5163
                             {},
5164
                             "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
5165
                             "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"},
5166
                    std::move(completion));
5167
            }
5168
        }
5169

5170
        AsyncMockNetworkTransport& mock_transport_worker;
5171
        TestStateBundle& state;
5172
    };
5173

5174
    auto [cur_user_promise, cur_user_future] = util::make_promise_future<std::shared_ptr<SyncUser>>();
5175
    auto transporter = std::make_shared<transport>(mock_transport_worker, state);
5176

5177
    {
5178
        TestSyncManager sync_manager(get_config(transporter));
5179
        auto app = sync_manager.app();
5180

5181
        app->log_in_with_credentials(AppCredentials::anonymous(),
5182
                                     [promise = std::move(cur_user_promise)](std::shared_ptr<SyncUser> user,
5183
                                                                             util::Optional<AppError> error) mutable {
5184
                                         REQUIRE_FALSE(error);
5185
                                         promise.emplace_value(std::move(user));
5186
                                     });
5187
    }
5188

5189
    // At this point the test does not hold any reference to `app`.
5190
    state.advance_to(TestState::app_deallocated);
5191
    auto cur_user = std::move(cur_user_future).get();
5192
    CHECK(cur_user);
5193

5194
    mock_transport_worker.mark_complete();
5195
}
5196
#endif
5197

5198
TEST_CASE("app: user logs out while profile is fetched", "[sync][app][user]") {
1✔
5199
    AsyncMockNetworkTransport mock_transport_worker;
1✔
5200
    enum class TestState { unknown, location, login, profile };
1✔
5201
    struct TestStateBundle {
1✔
5202
        void advance_to(TestState new_state)
1✔
5203
        {
3✔
5204
            std::lock_guard<std::mutex> lk(mutex);
3✔
5205
            state = new_state;
3✔
5206
            cond.notify_one();
3✔
5207
        }
3✔
5208

1✔
5209
        TestState get() const
1✔
5210
        {
1✔
5211
            std::lock_guard<std::mutex> lk(mutex);
1✔
5212
            return state;
2✔
5213
        }
2✔
5214

2✔
5215
        void wait_for(TestState new_state)
2✔
5216
        {
2✔
5217
            std::unique_lock<std::mutex> lk(mutex);
3✔
5218
            cond.wait(lk, [&] {
3✔
5219
                return state == new_state;
3✔
5220
            });
3✔
5221
        }
3✔
5222

1✔
5223
        mutable std::mutex mutex;
2✔
5224
        std::condition_variable cond;
2✔
5225

2✔
5226
        TestState state = TestState::unknown;
2✔
5227
    } state;
2✔
5228
    struct transport : public GenericNetworkTransport {
1✔
5229
        transport(AsyncMockNetworkTransport& worker, TestStateBundle& state,
2✔
5230
                  std::shared_ptr<SyncUser>& logged_in_user)
1✔
5231
            : mock_transport_worker(worker)
1✔
5232
            , state(state)
1✔
5233
            , logged_in_user(logged_in_user)
1✔
5234
        {
1✔
5235
        }
1✔
5236

1✔
5237
        void send_request_to_server(const Request& request,
2✔
5238
                                    util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5239
        {
3✔
5240
            if (request.url.find("/login") != std::string::npos) {
4✔
5241
                state.advance_to(TestState::login);
2✔
5242
                mock_transport_worker.add_work_item(
2✔
5243
                    Response{200, 0, {}, user_json(encode_fake_jwt("access token")).dump()}, std::move(completion));
2✔
5244
            }
2✔
5245
            else if (request.url.find("/profile") != std::string::npos) {
3✔
5246
                logged_in_user->log_out();
2✔
5247
                state.advance_to(TestState::profile);
2✔
5248
                mock_transport_worker.add_work_item(Response{200, 0, {}, user_profile_json().dump()},
2✔
5249
                                                    std::move(completion));
2✔
5250
            }
1✔
5251
            else if (request.url.find("/location") != std::string::npos) {
2✔
5252
                CHECK(request.method == HttpMethod::get);
2✔
5253
                state.advance_to(TestState::location);
4✔
5254
                mock_transport_worker.add_work_item(
4✔
5255
                    Response{200,
2✔
5256
                             0,
2✔
5257
                             {},
2✔
5258
                             "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
2✔
5259
                             "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"},
3✔
5260
                    std::move(completion));
2✔
5261
            }
2✔
5262
        }
4✔
5263

2✔
5264
        AsyncMockNetworkTransport& mock_transport_worker;
2✔
5265
        TestStateBundle& state;
2✔
5266
        std::shared_ptr<SyncUser>& logged_in_user;
2!
5267
    };
2✔
5268

2✔
5269
    std::shared_ptr<SyncUser> logged_in_user;
2✔
5270
    auto transporter = std::make_shared<transport>(mock_transport_worker, state, logged_in_user);
2✔
5271

2✔
5272
    TestSyncManager sync_manager(get_config(transporter));
2✔
5273
    auto app = sync_manager.app();
2✔
5274

2✔
5275
    logged_in_user = app->sync_manager()->get_user("userid", good_access_token, good_access_token, dummy_device_id);
2✔
5276
    auto custom_credentials = AppCredentials::facebook("a_token");
4✔
5277
    auto [cur_user_promise, cur_user_future] = util::make_promise_future<std::shared_ptr<SyncUser>>();
1✔
5278

2✔
5279
    app->link_user(logged_in_user, custom_credentials,
2✔
5280
                   [promise = std::move(cur_user_promise)](std::shared_ptr<SyncUser> user,
2✔
5281
                                                           util::Optional<AppError> error) mutable {
2✔
5282
                       REQUIRE_FALSE(error);
1✔
5283
                       promise.emplace_value(std::move(user));
2✔
5284
                   });
2✔
5285

1✔
5286
    auto cur_user = std::move(cur_user_future).get();
2✔
5287
    CHECK(state.get() == TestState::profile);
2✔
5288
    CHECK(cur_user);
1✔
5289
    CHECK(cur_user == logged_in_user);
2✔
5290

2✔
5291
    mock_transport_worker.mark_complete();
2✔
5292
}
1✔
5293

1✔
5294
TEST_CASE("app: shared instances", "[sync][app]") {
2✔
5295
    App::Config base_config;
2✔
5296
    set_app_config_defaults(base_config, instance_of<UnitTestTransport>);
2!
5297

2✔
5298
    SyncClientConfig sync_config;
2✔
5299
    sync_config.metadata_mode = SyncClientConfig::MetadataMode::NoMetadata;
1✔
5300
    sync_config.base_file_path = util::make_temp_dir() + random_string(10);
2✔
5301
    util::try_make_dir(sync_config.base_file_path);
2!
5302

2!
5303
    auto config1 = base_config;
2!
5304
    config1.app_id = "app1";
1✔
5305

2✔
5306
    auto config2 = base_config;
2✔
5307
    config2.app_id = "app1";
1✔
5308
    config2.base_url = "https://realm.mongodb.com"; // equivalent to default_base_url
2✔
5309

2✔
5310
    auto config3 = base_config;
2✔
5311
    config3.app_id = "app2";
1✔
5312

2✔
5313
    auto config4 = base_config;
2✔
5314
    config4.app_id = "app2";
2✔
5315
    config4.base_url = "http://localhost:9090";
2✔
5316

1✔
5317
    // should all point to same underlying app
2✔
5318
    auto app1_1 = App::get_shared_app(config1, sync_config);
2✔
5319
    auto app1_2 = App::get_shared_app(config1, sync_config);
1✔
5320
    auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url);
2✔
5321
    auto app1_4 = App::get_shared_app(config2, sync_config);
2✔
5322
    auto app1_5 = App::get_cached_app(config1.app_id);
2✔
5323

1✔
5324
    CHECK(app1_1 == app1_2);
2✔
5325
    CHECK(app1_1 == app1_3);
2✔
5326
    CHECK(app1_1 == app1_4);
1✔
5327
    CHECK(app1_1 == app1_5);
2✔
5328

2✔
5329
    // config3 and config4 should point to different apps
2✔
5330
    auto app2_1 = App::get_shared_app(config3, sync_config);
1✔
5331
    auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url);
1✔
5332
    auto app2_3 = App::get_shared_app(config4, sync_config);
2✔
5333
    auto app2_4 = App::get_cached_app(config3.app_id);
2✔
5334
    auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url");
2✔
5335

2✔
5336
    CHECK(app2_1 == app2_2);
2✔
5337
    CHECK(app2_1 != app2_3);
1✔
5338
    CHECK(app2_4 != nullptr);
2!
5339
    CHECK(app2_5 == nullptr);
2!
5340

2!
5341
    CHECK(app1_1 != app2_1);
2!
5342
    CHECK(app1_1 != app2_3);
1✔
5343
    CHECK(app1_1 != app2_4);
1✔
5344
}
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