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

realm / realm-core / 2595

30 Aug 2024 04:09PM UTC coverage: 91.096% (-0.008%) from 91.104%
2595

push

Evergreen

web-flow
RCORE-2222 Remove 308 redirect support from App/AppUser (#7996)

* Removed redirect tests
* Removed one more location redirect test case
* Removed 301/308 redirection support from App
* Updates from review
* Updated changelog after release
* Fixed misspelling and updated comment a bit

102774 of 181478 branches covered (56.63%)

40 of 44 new or added lines in 1 file covered. (90.91%)

100 existing lines in 15 files now uncovered.

217156 of 238381 relevant lines covered (91.1%)

5917653.11 hits per line

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

98.5
/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/test_path.hpp"
23
#include "util/unit_test_transport.hpp"
24
#include "util/test_path.hpp"
25

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

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

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

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

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

66

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

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

95
} // namespace
96

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

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

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

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

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

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

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

154
#if REALM_ENABLE_AUTH_TESTS
155

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

670
// MARK: - Login with Credentials Tests
671

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

678
        int subscribe_processed = 0;
2✔
679

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

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

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

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

704
// MARK: - UsernamePasswordProviderClient Tests
705

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

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

716
    bool processed = false;
26✔
717

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

882
// MARK: - UserAPIKeyProviderClient Tests
883

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1144
// MARK: - Auth Providers Function Tests
1145

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

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

1158
// MARK: - Link User Tests
1159

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

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

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

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

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

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

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

1211
// MARK: - Delete User Tests
1212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1294
// MARK: - Call Function Tests
1295

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

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

1310
// MARK: - Remote Mongo Client Tests
1311

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1939
    SECTION("delete") {
16✔
1940

1941
        bool processed = false;
2✔
1942

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

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

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

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

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

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

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

1981
// MARK: - Push Notifications Tests
1982

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2063
// MARK: - Token refresh
2064

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

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

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

2098
// MARK: - Sync Tests
2099

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2903
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2904

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

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

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

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

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

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

2936
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2937

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

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

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

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

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

2969
            verify_error_on_sync_with_invalid_refresh_token(anon_user, config);
2✔
2970

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3120
        realm->read_group();
2✔
3121

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3251
TEST_CASE("app: sync logs contain baas coid", "[sync][app][baas]") {
2✔
3252
    class InMemoryLogger : public util::Logger {
2✔
3253
    public:
2✔
3254
        void do_log(const util::LogCategory& cat, Level level, const std::string& msg) final
2✔
3255
        {
138✔
3256
            auto formatted_line = util::format("%1 %2 %3", cat.get_name(), level, msg);
138✔
3257
            std::lock_guard lk(mtx);
138✔
3258
            log_messages.emplace_back(std::move(formatted_line));
138✔
3259
        }
138✔
3260

3261
        std::vector<std::string> get_log_messages()
2✔
3262
        {
2✔
3263
            std::lock_guard lk(mtx);
2✔
3264
            std::vector<std::string> ret;
2✔
3265
            std::swap(ret, log_messages);
2✔
3266
            return ret;
2✔
3267
        }
2✔
3268

3269
        std::mutex mtx;
2✔
3270
        std::vector<std::string> log_messages;
2✔
3271
    };
2✔
3272

3273
    auto in_mem_logger = std::make_shared<InMemoryLogger>();
2✔
3274
    in_mem_logger->set_level_threshold(InMemoryLogger::Level::all);
2✔
3275
    TestAppSession::Config session_config;
2✔
3276
    session_config.logger = in_mem_logger;
2✔
3277
    TestAppSession app_session(get_runtime_app_session(), session_config, DeleteApp{false});
2✔
3278

3279
    const auto partition = random_string(100);
2✔
3280
    SyncTestFile config(app_session.app()->current_user(), partition, util::none);
2✔
3281
    auto realm = successfully_async_open_realm(config);
2✔
3282
    auto sync_session = realm->sync_session();
2✔
3283
    auto coid = SyncSession::OnlyForTesting::get_appservices_connection_id(*sync_session);
2✔
3284

3285
    auto transition_log_msg =
2✔
3286
        util::format("Connection[1] Connected to app services with request id: \"%1\". Further log entries for this "
2✔
3287
                     "connection will be prefixed with \"Connection[1:%1]\" instead of \"Connection[1]\"",
2✔
3288
                     coid);
2✔
3289
    auto bind_send_msg = util::format("Connection[1:%1] Session[1]: Sending: BIND", coid);
2✔
3290
    auto ping_send_msg = util::format("Connection[1:%1] Will emit a ping in", coid);
2✔
3291

3292
    auto log_messages = in_mem_logger->get_log_messages();
2✔
3293
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(transition_log_msg)));
2✔
3294
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(bind_send_msg)));
2✔
3295
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(ping_send_msg)));
2✔
3296
}
2✔
3297

3298

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

3302
    const auto schema = get_default_schema();
2✔
3303

3304
    SyncServer server({});
2✔
3305
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
2✔
3306
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
2✔
3307
    OfflineAppSession::Config oas_config(transport);
2✔
3308
    oas_config.base_url = util::format("http://localhost:%1/", server.port());
2✔
3309
    oas_config.socket_provider = socket_provider;
2✔
3310
    OfflineAppSession oas(oas_config);
2✔
3311
    AutoVerifiedEmailCredentials creds;
2✔
3312
    auto app = oas.app();
2✔
3313
    const auto partition = random_string(100);
2✔
3314

3315
    transport->request_hook = [&](const Request& req) -> std::optional<Response> {
8✔
3316
        if (req.url.find("/location") == std::string::npos) {
8✔
3317
            return std::nullopt;
6✔
3318
        }
6✔
3319

3320
        REQUIRE(req.url == util::format("http://localhost:%1/api/client/v2.0/app/app_id/location", server.port()));
2!
3321
        return Response{
2✔
3322
            200,
2✔
3323
            0,
2✔
3324
            {},
2✔
3325
            nlohmann::json(nlohmann::json::object({
2✔
3326
                               {"hostname", util::format("http://localhost:%1", server.port())},
2✔
3327
                               {"ws_hostname", util::format("ws://localhost:%1", server.port())},
2✔
3328
                               {"sync_route", util::format("ws://localhost:%1/realm-sync", server.port())},
2✔
3329
                           }))
2✔
3330
                .dump(),
2✔
3331
        };
2✔
3332
    };
2✔
3333

3334
    SyncTestFile realm_config(oas, "test");
2✔
3335

3336
    auto r = Realm::get_shared_realm(realm_config);
2✔
3337
    REQUIRE(!wait_for_download(*r));
2!
3338
}
2✔
3339

3340
TEST_CASE("app: redirect handling", "[sync][pbs][app]") {
6✔
3341
    auto logger = util::Logger::get_default_logger();
6✔
3342

3343
    const auto schema = get_default_schema();
6✔
3344

3345
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
6✔
3346
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
6✔
3347
    OfflineAppSession::Config oas_config(transport);
6✔
3348
    oas_config.base_url = "http://original.invalid:9090";
6✔
3349
    oas_config.socket_provider = socket_provider;
6✔
3350
    OfflineAppSession oas(oas_config);
6✔
3351
    AutoVerifiedEmailCredentials creds;
6✔
3352
    auto app = oas.app();
6✔
3353
    const auto partition = random_string(100);
6✔
3354

3355
    SECTION("server in maintenance reports error") {
6✔
3356
        transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
3357
            nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"},
2✔
3358
                                                {"error", "This service is currently undergoing maintenance"},
2✔
3359
                                                {"link", "https://link.to/server_logs"}};
2✔
3360
            return Response{500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()};
2✔
3361
        };
2✔
3362

3363
        app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password),
2✔
3364
                                     [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
3365
                                         REQUIRE(!user);
2!
3366
                                         REQUIRE(error);
2!
3367
                                         REQUIRE(error->is_service_error());
2!
3368
                                         REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress);
2!
3369
                                         REQUIRE(error->reason() ==
2!
3370
                                                 "This service is currently undergoing maintenance");
2✔
3371
                                         REQUIRE(error->link_to_server_logs == "https://link.to/server_logs");
2!
3372
                                         REQUIRE(*error->additional_status_code == 500);
2!
3373
                                     });
2✔
3374
    }
2✔
3375

3376
    SECTION("websocket redirects update existing session") {
6✔
3377
        SyncServer server({});
4✔
3378

3379
        transport->request_hook = [&](const Request& req) -> std::optional<Response> {
16✔
3380
            if (req.url.find("/location") != std::string::npos) {
16✔
3381
                return Response{
4✔
3382
                    200,
4✔
3383
                    0,
4✔
3384
                    {},
4✔
3385
                    nlohmann::json({
4✔
3386
                                       {"hostname", "http://some.fake.url"},
4✔
3387
                                       {"ws_hostname", "ws://ws.some.fake.url"},
4✔
3388
                                       {"sync_route", "ws://some.fake.url/realm-sync"},
4✔
3389
                                   })
4✔
3390
                        .dump(),
4✔
3391
                };
4✔
3392
            }
4✔
3393
            return std::nullopt;
12✔
3394
        };
16✔
3395

3396
        // The location info is fake, so we need to override it with the actual
3397
        // server endpoint
3398
        socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
6✔
3399
            ep.address = "127.0.0.1";
6✔
3400
            ep.port = server.port();
6✔
3401
        };
6✔
3402

3403
        SyncTestFile realm_config(oas, "test");
4✔
3404

3405
        std::mutex logout_mutex;
4✔
3406
        std::condition_variable logout_cv;
4✔
3407
        bool logged_out = false;
4✔
3408
        realm_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
4✔
3409
            if (error.status == ErrorCodes::AuthError) {
2✔
3410
                {
2✔
3411
                    std::unique_lock lk(logout_mutex);
2✔
3412
                    logged_out = true;
2✔
3413
                }
2✔
3414
                logout_cv.notify_one();
2✔
3415
                return;
2✔
3416
            }
2✔
3417
            util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
3418
                         error.status);
×
3419
            abort();
×
3420
        };
2✔
3421

3422
        auto r = Realm::get_shared_realm(realm_config);
4✔
3423
        REQUIRE(!wait_for_download(*r));
4!
3424
        auto sync_session = r->sync_session();
4✔
3425
        sync_session->pause();
4✔
3426
        SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*oas.sync_manager());
4✔
3427

3428
        int connect_count = 0;
4✔
3429
        socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
6✔
3430
            // Report a 308 response the first time we try to reconnect the websocket,
3431
            // which should result in App performing a location update.
3432
            // The actual Location header isn't used when we get a redirect on
3433
            // the websocket, so we don't need to supply it here
3434
            if (connect_count++ > 0)
6✔
3435
                return std::nullopt;
2✔
3436
            return sync::HTTPStatus::PermanentRedirect;
4✔
3437
        };
6✔
3438

3439
        SECTION("valid websocket redirect") {
4✔
3440
            socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
4✔
3441
                logger->trace("resolve attempt %1: %2", connect_count, ep.address);
4✔
3442
                // First call happens after the call to the above hook which will
3443
                // force a 308 response. Second call happens after the redirect
3444
                // has been handled.
3445
                REQUIRE(connect_count <= 2);
4!
3446
                if (connect_count == 2) {
4✔
3447
                    REQUIRE(ep.address == "ws.invalid");
×
3448
                }
×
3449

3450
                // Overriding the handshake result happens after dns resolution,
3451
                // so we need to set it to a valid endpoint for even the first call
3452
                ep.address = "127.0.0.1";
4✔
3453
                ep.port = server.port();
4✔
3454
            };
4✔
3455

3456
            int request_count = 0;
2✔
3457
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
4✔
3458
                logger->trace("request.url (%1): %2", request_count, request.url);
4✔
3459
                if (request.url.find("/location") != std::string::npos) {
4✔
3460
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3461
                    return Response{200,
2✔
3462
                                    0,
2✔
3463
                                    {},
2✔
3464
                                    nlohmann::json({
2✔
3465
                                                       {"hostname", "http://http.invalid"},
2✔
3466
                                                       {"ws_hostname", "ws://ws.invalid"},
2✔
3467
                                                       {"sync_route", "ws://ws.invalid/realm-sync"},
2✔
3468
                                                   })
2✔
3469
                                        .dump()};
2✔
3470
                }
2✔
3471

3472
                // Rest of the requests get handled normally
3473
                return std::nullopt;
2✔
3474
            };
4✔
3475

3476
            sync_session->resume();
2✔
3477
            REQUIRE(!wait_for_download(*r));
2!
3478
            REQUIRE(realm_config.sync_config->user->is_logged_in());
2!
3479

3480
            // Verify session is using the updated server url from the redirect
3481
            auto server_url = sync_session->full_realm_url();
2✔
3482
            REQUIRE_THAT(server_url, ContainsSubstring("ws.invalid"));
2✔
3483
        }
2✔
3484

3485
        SECTION("websocket redirect into auth error logs out user") {
4✔
3486
            int request_count = 0;
2✔
3487
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
2✔
3488
                logger->trace("request.url (%1): %2", request_count, request.url);
2✔
3489
                ++request_count;
2✔
3490

3491
                if (request_count == 1) {
2✔
3492
                    // First request should be a location request against the original URL
3493
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3494
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3495
                    return Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
2✔
3496
                                    0,
2✔
3497
                                    {{"Location", "http://asdf.invalid"}},
2✔
3498
                                    ""};
2✔
3499
                }
2✔
3500

3501
                // Second request should be a location request against the new URL
UNCOV
3502
                if (request_count == 2) {
×
UNCOV
3503
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
×
UNCOV
3504
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
×
UNCOV
3505
                    return Response{200,
×
UNCOV
3506
                                    0,
×
UNCOV
3507
                                    {},
×
UNCOV
3508
                                    nlohmann::json({
×
UNCOV
3509
                                                       {"hostname", "http://http.invalid"},
×
UNCOV
3510
                                                       {"ws_hostname", "ws://ws.invalid"},
×
UNCOV
3511
                                                   })
×
UNCOV
3512
                                        .dump()};
×
UNCOV
3513
                }
×
3514

3515
                // Third request should be for an acccess token, which we reject
UNCOV
3516
                REQUIRE(request_count == 3);
×
UNCOV
3517
                REQUIRE_THAT(request.url, ContainsSubstring("auth/session"));
×
UNCOV
3518
                return Response{static_cast<int>(sync::HTTPStatus::Unauthorized), 0, {}, ""};
×
UNCOV
3519
            };
×
3520

3521
            sync_session->resume();
2✔
3522
            REQUIRE(wait_for_download(*r));
2!
3523
            std::unique_lock lk(logout_mutex);
2✔
3524
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
3525
                return logged_out;
4✔
3526
            });
4✔
3527
            REQUIRE(result);
2!
3528
            REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in());
2!
3529
        }
2✔
3530
    }
4✔
3531
}
6✔
3532

3533
TEST_CASE("app: base_url", "[sync][app][base_url]") {
24✔
3534
    struct BaseUrlTransport : UnitTestTransport {
24✔
3535
        std::string expected_url;
24✔
3536
        std::string location_url;
24✔
3537
        std::string location_wsurl;
24✔
3538
        bool location_requested = false;
24✔
3539
        bool location_returns_error = false;
24✔
3540

3541
        void reset(std::string expect_url, std::optional<std::string> url = std::nullopt,
24✔
3542
                   std::optional<std::string> wsurl = std::nullopt)
24✔
3543
        {
58✔
3544
            expected_url = expect_url;
58✔
3545
            REALM_ASSERT(!expected_url.empty());
58✔
3546
            location_url = url.value_or(expect_url);
58✔
3547
            REALM_ASSERT(!location_url.empty());
58✔
3548
            location_wsurl = wsurl.value_or(App::create_ws_host_url(location_url));
58✔
3549
            location_requested = false;
58✔
3550
            location_returns_error = false;
58✔
3551
        }
58✔
3552

3553
        void send_request_to_server(const Request& request,
24✔
3554
                                    util::UniqueFunction<void(const Response&)>&& completion) override
24✔
3555
        {
148✔
3556
            if (request.url.find("/location") != std::string::npos) {
148✔
3557
                CHECK(request.method == HttpMethod::get);
58!
3558
                CHECK_THAT(request.url, ContainsSubstring(expected_url));
58✔
3559
                location_requested = true;
58✔
3560
                if (location_returns_error) {
58✔
3561
                    completion(app::Response{static_cast<int>(sync::HTTPStatus::NotFound), 0, {}, "404 not found"});
20✔
3562
                    return;
20✔
3563
                }
20✔
3564
                completion(
38✔
3565
                    app::Response{static_cast<int>(sync::HTTPStatus::Ok),
38✔
3566
                                  0,
38✔
3567
                                  {},
38✔
3568
                                  util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
38✔
3569
                                               "\"%1\",\"ws_hostname\":\"%2\"}",
38✔
3570
                                               location_url, location_wsurl)});
38✔
3571
                return;
38✔
3572
            }
58✔
3573
            if (location_requested) {
90✔
3574
                CHECK_THAT(request.url, ContainsSubstring(location_url));
90✔
3575
            }
90✔
3576
            else {
×
3577
                CHECK_THAT(request.url, ContainsSubstring(expected_url));
×
3578
            }
×
3579
            UnitTestTransport::send_request_to_server(request, std::move(completion));
90✔
3580
        }
90✔
3581
    };
24✔
3582

3583
    auto logger = util::Logger::get_default_logger();
24✔
3584
    std::string default_base_url = std::string(App::default_base_url());
24✔
3585
    std::string default_base_wsurl = App::create_ws_host_url(App::default_base_url());
24✔
3586
    std::string test_base_url = "https://base.someurl.fake";
24✔
3587
    std::string test_base_wsurl = "wss://base.someurl.fake";
24✔
3588
    std::string test_location_url = "https://loc.someurl.fake";
24✔
3589
    std::string test_location_wsurl = "wss://loc.someurl.fake";
24✔
3590
    std::string test_location_wsurl2 = "wss://ws.loc.someurl.fake";
24✔
3591

3592
    auto location_transport = std::make_shared<BaseUrlTransport>();
24✔
3593
    auto get_config_with_base_url = [&](std::optional<std::string> base_url = std::nullopt) {
26✔
3594
        OfflineAppSession::Config config(location_transport);
26✔
3595
        config.base_url = base_url;
26✔
3596
        return config;
26✔
3597
    };
26✔
3598

3599
    SECTION("Test App::create_ws_host_url") {
24✔
3600
        auto result = App::create_ws_host_url("blah");
2✔
3601
        CHECK(result == "blah");
2!
3602
        result = App::create_ws_host_url("http://localhost:9090");
2✔
3603
        CHECK(result == "ws://localhost:9090");
2!
3604
        result = App::create_ws_host_url("https://localhost:9090");
2✔
3605
        CHECK(result == "wss://localhost:9090");
2!
3606
        result = App::create_ws_host_url("https://localhost:9090/some/extra/stuff");
2✔
3607
        CHECK(result == "wss://localhost:9090/some/extra/stuff");
2!
3608
        result = App::create_ws_host_url("http://172.0.0.1:9090");
2✔
3609
        CHECK(result == "ws://172.0.0.1:9090");
2!
3610
        result = App::create_ws_host_url("https://172.0.0.1:9090");
2✔
3611
        CHECK(result == "wss://172.0.0.1:9090");
2!
3612
        // Old default base url
3613
        result = App::create_ws_host_url("http://realm.mongodb.com");
2✔
3614
        CHECK(result == "ws://ws.realm.mongodb.com");
2!
3615
        result = App::create_ws_host_url("https://realm.mongodb.com");
2✔
3616
        CHECK(result == "wss://ws.realm.mongodb.com");
2!
3617
        result = App::create_ws_host_url("https://realm.mongodb.com/some/extra/stuff");
2✔
3618
        CHECK(result == "wss://ws.realm.mongodb.com/some/extra/stuff");
2!
3619
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3620
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3621
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3622
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3623
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2✔
3624
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2!
3625
        // New default base url
3626
        result = App::create_ws_host_url("http://services.cloud.mongodb.com");
2✔
3627
        CHECK(result == "ws://ws.services.cloud.mongodb.com");
2!
3628
        result = App::create_ws_host_url("https://services.cloud.mongodb.com");
2✔
3629
        CHECK(result == "wss://ws.services.cloud.mongodb.com");
2!
3630
        result = App::create_ws_host_url("https://services.cloud.mongodb.com/some/extra/stuff");
2✔
3631
        CHECK(result == "wss://ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3632
        result = App::create_ws_host_url("http://us-east-1.aws.services.cloud.mongodb.com");
2✔
3633
        CHECK(result == "ws://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3634
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com");
2✔
3635
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3636
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com/some/extra/stuff");
2✔
3637
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3638
    }
2✔
3639

3640
    SECTION("Test app config baseurl") {
24✔
3641
        {
2✔
3642
            // First time through, base_url is empty; https://services.cloud.mongodb.com is expected
3643
            location_transport->reset(std::string(App::default_base_url()));
2✔
3644
            auto config = get_config_with_base_url();
2✔
3645
            OfflineAppSession oas(config);
2✔
3646
            auto app = oas.app();
2✔
3647

3648
            // Location is not requested until first app services request
3649
            CHECK(!location_transport->location_requested);
2!
3650
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3651
            CHECK(app->get_host_url() == App::default_base_url());
2!
3652
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3653

3654
            oas.make_user();
2✔
3655
            CHECK(location_transport->location_requested);
2!
3656
            CHECK(app->get_base_url() == App::default_base_url());
2!
3657
            CHECK(app->get_host_url() == App::default_base_url());
2!
3658
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3659
        }
2✔
3660
        {
2✔
3661
            // Base_url is set to test_base_url and test_location_url is expected after
3662
            // location request
3663
            location_transport->reset(test_base_url, test_location_url);
2✔
3664
            auto config = get_config_with_base_url(test_base_url);
2✔
3665
            OfflineAppSession oas(config);
2✔
3666
            auto app = oas.app();
2✔
3667

3668
            // Location is not requested until first app services request
3669
            CHECK(!location_transport->location_requested);
2!
3670
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3671
            CHECK(app->get_host_url() == test_base_url);
2!
3672
            CHECK(app->get_ws_host_url() == test_base_wsurl);
2!
3673

3674
            oas.make_user();
2✔
3675
            CHECK(location_transport->location_requested);
2!
3676
            CHECK(app->get_base_url() == test_base_url);
2!
3677
            CHECK(app->get_host_url() == test_location_url);
2!
3678
            CHECK(app->get_ws_host_url() == test_location_wsurl);
2!
3679
        }
2✔
3680
        {
2✔
3681
            // Third time through, base_url is not set, expect https://services.cloud.mongodb.com,
3682
            // since metadata is no longer used
3683
            location_transport->reset(default_base_url);
2✔
3684
            auto config = get_config_with_base_url();
2✔
3685
            OfflineAppSession oas(config);
2✔
3686
            auto app = oas.app();
2✔
3687

3688
            // Location is not requested until first app services request
3689
            CHECK(!location_transport->location_requested);
2!
3690
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3691
            CHECK(app->get_host_url() == default_base_url);
2!
3692
            CHECK(app->get_ws_host_url() == default_base_wsurl);
2!
3693

3694
            oas.make_user();
2✔
3695
            CHECK(location_transport->location_requested);
2!
3696
            CHECK(app->get_base_url() == default_base_url);
2!
3697
            CHECK(app->get_host_url() == default_base_url);
2!
3698
            CHECK(app->get_ws_host_url() == default_base_wsurl);
2!
3699
        }
2✔
3700
    }
2✔
3701

3702
    SECTION("Test update_baseurl after first request") {
24✔
3703
        bool error_occurred = GENERATE(true, false);
4✔
3704

3705
        location_transport->reset(test_base_url, test_location_url);
4✔
3706
        auto config = get_config_with_base_url(test_base_url);
4✔
3707
        OfflineAppSession oas(config);
4✔
3708
        auto app = oas.app();
4✔
3709

3710
        // Location is not requested until first app services request
3711
        CHECK(!location_transport->location_requested);
4!
3712

3713
        // Perform an operation prior to updating the base URL
3714
        oas.make_user();
4✔
3715
        CHECK(location_transport->location_requested);
4!
3716
        CHECK(app->get_base_url() == test_base_url);
4!
3717
        CHECK(app->get_host_url() == test_location_url);
4!
3718
        CHECK(app->get_ws_host_url() == test_location_wsurl);
4!
3719

3720
        location_transport->reset(default_base_url);
4✔
3721
        location_transport->location_returns_error = error_occurred;
4✔
3722

3723
        // Revert the base URL to the default URL value using the empty string
3724
        app->update_base_url("", [error_occurred](util::Optional<app::AppError> error) {
4✔
3725
            CHECK(error.has_value() == error_occurred);
4!
3726
        });
4✔
3727
        CHECK(location_transport->location_requested);
4!
3728
        if (error_occurred) {
4✔
3729
            // Not updated due to the error
3730
            CHECK(app->get_base_url() == test_base_url);
2!
3731
            CHECK(app->get_host_url() == test_location_url);
2!
3732
            CHECK(app->get_ws_host_url() == test_location_wsurl);
2!
3733
        }
2✔
3734
        else {
2✔
3735
            // updated successfully
3736
            CHECK(app->get_base_url() == default_base_url);
2!
3737
            CHECK(app->get_host_url() == default_base_url);
2!
3738
            CHECK(app->get_ws_host_url() == default_base_wsurl);
2!
3739
            oas.make_user(); // try another operation
2✔
3740
        }
2✔
3741
    }
4✔
3742

3743
    SECTION("Test update_baseurl before first request") {
24✔
3744
        bool error_occurred = GENERATE(true, false);
4✔
3745

3746
        location_transport->reset(default_base_url, test_location_url, test_location_wsurl2);
4✔
3747
        location_transport->location_returns_error = error_occurred;
4✔
3748
        auto config = get_config_with_base_url(test_base_url);
4✔
3749
        OfflineAppSession oas(config);
4✔
3750
        auto app = oas.app();
4✔
3751

3752
        // Check updating the base URL before an initial app_services request.
3753
        CHECK(!location_transport->location_requested);
4!
3754

3755
        // Revert the base URL to the default URL value using the empty string
3756
        app->update_base_url("", [error_occurred](util::Optional<app::AppError> error) {
4✔
3757
            CHECK(error.has_value() == error_occurred);
4!
3758
        });
4✔
3759
        CHECK(location_transport->location_requested);
4!
3760
        if (error_occurred) {
4✔
3761
            // Not updated due to the error
3762
            CHECK(app->get_base_url() == test_base_url);
2!
3763
            CHECK(app->get_host_url() == test_base_url);
2!
3764
            CHECK(app->get_ws_host_url() == test_base_wsurl);
2!
3765
        }
2✔
3766
        else {
2✔
3767
            // updated successfully
3768
            CHECK(app->get_base_url() == default_base_url);
2!
3769
            CHECK(app->get_host_url() == test_location_url);
2!
3770
            CHECK(app->get_ws_host_url() == test_location_wsurl2);
2!
3771
            oas.make_user(); // try another operation
2✔
3772
        }
2✔
3773
    }
4✔
3774

3775
    // Verify new sync session updates location when created with cached user
3776
    SECTION("Verify new sync session updates location") {
24✔
3777
        bool use_ssl = GENERATE(true, false);
12✔
3778
        std::string base_host = "base.url.fake";
12✔
3779
        std::string location_host = "alternate.url.fake";
12✔
3780
        std::string new_location_host = "new.url.fake";
12✔
3781
        unsigned location_port = use_ssl ? 443 : 80;
12✔
3782
        std::string sync_base_url = util::format("http://%1", base_host);
12✔
3783
        std::string sync_location_url = util::format("http%1://%2", use_ssl ? "s" : "", location_host);
12✔
3784
        std::string sync_location_wsurl = util::format("ws%1://%2", use_ssl ? "s" : "", location_host);
12✔
3785
        std::string new_location_url = util::format("http%1://%2", use_ssl ? "s" : "", new_location_host);
12✔
3786
        std::string new_location_wsurl = util::format("ws%1://%2", use_ssl ? "s" : "", new_location_host);
12✔
3787

3788
        auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
12✔
3789
        socket_provider->websocket_connect_func = []() -> std::optional<SocketProviderError> {
12✔
3790
            return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, "404 not found");
8✔
3791
        };
8✔
3792

3793
        auto config = get_config_with_base_url(sync_base_url);
12✔
3794
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
12✔
3795
        config.socket_provider = socket_provider;
12✔
3796
        config.storage_path = util::make_temp_dir();
12✔
3797
        config.delete_storage = false; // persist the current user
12✔
3798

3799
        // Log in to get a cached user
3800
        {
12✔
3801
            location_transport->reset(sync_base_url, sync_location_url, sync_location_wsurl);
12✔
3802
            OfflineAppSession oas(config);
12✔
3803
            auto app = oas.app();
12✔
3804

3805
            {
12✔
3806
                CHECK_FALSE(location_transport->location_requested);
12!
3807
                auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
3808
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
12✔
3809
                CHECK_FALSE(verified);
12!
3810
            }
12✔
3811

3812
            oas.make_user();
12✔
3813
            CHECK(location_transport->location_requested);
12!
3814
            CHECK(app->get_base_url() == sync_base_url);
12!
3815
            CHECK(app->get_host_url() == sync_location_url);
12!
3816
            CHECK(app->get_ws_host_url() == sync_location_wsurl);
12!
3817
            auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
3818
            CHECK_THAT(sync_route, ContainsSubstring(sync_location_wsurl));
12✔
3819
            CHECK(verified);
12!
3820
        }
12✔
3821

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

3830
            // Reuse the config so the app uses the cached user
3831
            OfflineAppSession oas(config);
4✔
3832
            auto app = oas.app();
4✔
3833
            REQUIRE(app->current_user());
4!
3834

3835
            // Verify the initial sync route, since the location hasn't been queried
3836
            // and the location is not "verified", the sync route host is based off
3837
            // the value provided in the AppConfig::base_url value
3838
            {
4✔
3839
                auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
3840
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
4✔
3841
                CHECK_FALSE(verified);
4!
3842
            }
4✔
3843

3844
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
8✔
3845
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
8✔
3846
                    if (cur_state == TestState::start) {
8✔
3847
                        // First time through is using the original base URL
3848
                        CHECK(ep.address == base_host);
4!
3849
                        CHECK(ep.port == 80);
4!
3850
                        CHECK(ep.is_ssl == false);
4!
3851
                        return TestState::first_attempt;
4✔
3852
                    }
4✔
3853
                    else if (cur_state == TestState::first_attempt) {
4✔
3854
                        // Second time through is using the values from location endpoint
3855
                        CHECK(ep.address == new_location_host);
4!
3856
                        CHECK(ep.port == location_port);
4!
3857
                        CHECK(ep.is_ssl == use_ssl);
4!
3858
                        return TestState::second_attempt;
4✔
3859
                    }
4✔
3860
                    return std::nullopt;
×
3861
                });
8✔
3862
            };
8✔
3863

3864
            RealmConfig r_config;
4✔
3865
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
4✔
3866
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
4✔
3867
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
4✔
3868
                // Websocket is forcing a 404 failure so it won't actually start
3869
                logger->debug("Received expected error: %1", error.status);
4✔
3870
                CHECK(!error.status.is_ok());
4!
3871
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
4!
3872
                CHECK(!error.is_fatal);
4!
3873
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
4✔
3874
                    CHECK(cur_state == TestState::second_attempt);
4!
3875
                    return TestState::complete;
4✔
3876
                });
4✔
3877
            };
4✔
3878
            auto realm = Realm::get_shared_realm(r_config);
4✔
3879
            state.wait_for(TestState::complete);
4✔
3880

3881
            CHECK(location_transport->location_requested);
4!
3882
            CHECK(app->get_base_url() == sync_base_url);
4!
3883
            CHECK(app->get_host_url() == new_location_url);
4!
3884
            CHECK(app->get_ws_host_url() == new_location_wsurl);
4!
3885
            auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
3886
            CHECK_THAT(sync_route, ContainsSubstring(new_location_wsurl));
4✔
3887
            CHECK(verified);
4!
3888
        }
4✔
3889
        SECTION("Sync Session retries after initial location failure") {
12✔
3890
            enum class TestState { start, location_failed, session_started };
8✔
3891
            TestingStateMachine<TestState> state(TestState::start);
8✔
3892
            const int retry_count = GENERATE(1, 3);
8✔
3893

3894
            location_transport->reset(sync_base_url, new_location_url, new_location_wsurl);
8✔
3895
            location_transport->location_returns_error = true;
8✔
3896

3897
            // Reuse the config so the app uses the cached user
3898
            OfflineAppSession oas(config);
8✔
3899
            auto app = oas.app();
8✔
3900
            REQUIRE(app->current_user());
8!
3901
            // Verify the initial sync route, since the location hasn't been queried
3902
            // and the location is not "verified", the sync route host is based off
3903
            // the value provided in the AppConfig::base_url value
3904
            {
8✔
3905
                auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
3906
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
8✔
3907
                CHECK_FALSE(verified);
8!
3908
            }
8✔
3909

3910
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
24✔
3911
                CHECK(ep.address == base_host);
24!
3912
                CHECK(ep.port == 80);
24!
3913
                CHECK(ep.is_ssl == false);
24!
3914
            };
24✔
3915

3916
            socket_provider->websocket_connect_func = [&, request_count =
8✔
3917
                                                              0]() mutable -> std::optional<SocketProviderError> {
32✔
3918
                if (request_count == 0) {
32✔
3919
                    // First connection attempt is to the unverified initial URL
3920
                    // since we have a valid access token but have never successfully
3921
                    // connected. This failing will trigger a location update.
3922
                    CHECK_FALSE(location_transport->location_requested);
8!
3923
                }
8✔
3924
                else {
24✔
3925
                    // All attempts after the first should have requested location
3926
                    CHECK(location_transport->location_requested);
24!
3927
                    location_transport->location_requested = false;
24✔
3928
                }
24✔
3929

3930
                // Until we allow a location request to succeed we should keep
3931
                // getting the original unverified route
3932
                if (location_transport->location_returns_error) {
32✔
3933
                    CHECK(app->get_base_url() == sync_base_url);
24!
3934
                    CHECK(app->get_host_url() == sync_base_url);
24!
3935
                    CHECK(app->get_ws_host_url() == app::App::create_ws_host_url(sync_base_url));
24!
3936
                    {
24✔
3937
                        auto [sync_route, verified] = app->sync_manager()->sync_route();
24✔
3938
                        CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(sync_base_url)));
24✔
3939
                        CHECK_FALSE(verified);
24!
3940
                    }
24✔
3941
                }
24✔
3942

3943
                // After the chosen number of attempts let the location request succeed
3944
                if (request_count++ >= retry_count) {
32✔
3945
                    location_transport->reset(sync_base_url, new_location_url, new_location_wsurl);
16✔
3946
                    socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
16✔
3947
                        CHECK(ep.address == new_location_host);
8!
3948
                        CHECK(ep.port == location_port);
8!
3949
                        CHECK(ep.is_ssl == use_ssl);
8!
3950
                        state.transition_to(TestState::location_failed);
8✔
3951
                    };
8✔
3952
                }
16✔
3953

3954
                return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
32✔
3955
                                           "404 not found");
32✔
3956
            };
32✔
3957

3958
            RealmConfig r_config;
8✔
3959
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
8✔
3960
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
8✔
3961
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
8✔
3962
                // An error will only be reported if the websocket fails after updating the location and access token
3963
                logger->debug("Received expected error: %1", error.status);
8✔
3964
                CHECK(!error.status.is_ok());
8!
3965
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
8!
3966
                CHECK(!error.is_fatal);
8!
3967
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
8✔
3968
                    if (cur_state == TestState::location_failed) {
8✔
3969
                        // This time, the session was being started, and the location was successful
3970
                        // Websocket is forcing a 404 failure so it won't actually start
3971
                        return TestState::session_started;
8✔
3972
                    }
8✔
3973
                    return std::nullopt;
×
3974
                });
8✔
3975
            };
8✔
3976
            auto realm = Realm::get_shared_realm(r_config);
8✔
3977
            state.wait_for(TestState::session_started);
8✔
3978

3979
            CHECK(app->get_base_url() == sync_base_url);
8!
3980
            CHECK(app->get_host_url() == new_location_url);
8!
3981
            CHECK(app->get_ws_host_url() == new_location_wsurl);
8!
3982
            auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
3983
            CHECK_THAT(sync_route, ContainsSubstring(new_location_wsurl));
8✔
3984
            CHECK(verified);
8!
3985
        }
8✔
3986
    }
12✔
3987
}
24✔
3988

3989
TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") {
2✔
3990
    TestAppSession session;
2✔
3991
    auto app = session.app();
2✔
3992
    auto user = app->current_user();
2✔
3993

3994
    SECTION("custom user data happy path") {
2✔
3995
        bool processed = false;
2✔
3996
        app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})},
2✔
3997
                           [&](auto response, auto error) {
2✔
3998
                               CHECK(error == none);
2!
3999
                               CHECK(response);
2!
4000
                               CHECK(*response == true);
2!
4001
                               processed = true;
2✔
4002
                           });
2✔
4003
        CHECK(processed);
2!
4004
        processed = false;
2✔
4005
        app->refresh_custom_data(user, [&](auto) {
2✔
4006
            processed = true;
2✔
4007
        });
2✔
4008
        CHECK(processed);
2!
4009
        auto data = *user->custom_data();
2✔
4010
        CHECK(data["favorite_color"] == "green");
2!
4011
    }
2✔
4012
}
2✔
4013

4014
TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") {
2✔
4015
    TestAppSession session;
2✔
4016
    auto app = session.app();
2✔
4017
    auto jwt = create_jwt(session.app()->app_id());
2✔
4018

4019
    SECTION("jwt happy path") {
2✔
4020
        bool processed = false;
2✔
4021
        bool logged_in_once = false;
2✔
4022

4023
        auto token = app->subscribe([&logged_in_once, &app](auto&) {
2✔
4024
            REQUIRE(!logged_in_once);
2!
4025
            auto user = app->current_user();
2✔
4026
            auto metadata = user->user_profile();
2✔
4027

4028
            // Ensure that the JWT metadata fields are available when the callback is fired on login.
4029
            CHECK(metadata["name"] == "Foo Bar");
2!
4030
            logged_in_once = true;
2✔
4031
        });
2✔
4032

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

4035
        app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})},
2✔
4036
                           [&](auto response, auto error) {
2✔
4037
                               CHECK(error == none);
2!
4038
                               CHECK(response);
2!
4039
                               CHECK(*response == true);
2!
4040
                               processed = true;
2✔
4041
                           });
2✔
4042
        CHECK(processed);
2!
4043
        processed = false;
2✔
4044
        app->refresh_custom_data(user, [&](auto) {
2✔
4045
            processed = true;
2✔
4046
        });
2✔
4047
        CHECK(processed);
2!
4048
        auto metadata = user->user_profile();
2✔
4049
        auto custom_data = *user->custom_data();
2✔
4050
        CHECK(custom_data["name"] == "Not Foo Bar");
2!
4051
        CHECK(metadata["name"] == "Foo Bar");
2!
4052

4053
        REQUIRE(logged_in_once);
2!
4054

4055
        app->unsubscribe(token);
2✔
4056
    }
2✔
4057
}
2✔
4058

4059
namespace cf = realm::collection_fixtures;
4060
TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects,
4061
                   cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects,
4062
                   cf::DictionaryOfMixedLinks)
4063
{
12✔
4064
    const std::string valid_pk_name = "_id";
12✔
4065
    const auto partition = random_string(100);
12✔
4066
    TestType test_type("collection", "dest");
12✔
4067
    Schema schema = {{"source",
12✔
4068
                      {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4069
                       {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4070
                       test_type.property()}},
12✔
4071
                     {"dest",
12✔
4072
                      {
12✔
4073
                          {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4074
                          {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4075
                      }}};
12✔
4076
    auto server_app_config = minimal_app_config("collections_of_links", schema);
12✔
4077
    TestAppSession test_session(create_app(server_app_config));
12✔
4078

4079
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
36✔
4080
        timed_sleeping_wait_for([&]() -> bool {
1,675✔
4081
            r->refresh();
1,675✔
4082
            TableRef dest = r->read_group().get_table(table_name);
1,675✔
4083
            size_t cur_count = dest->size();
1,675✔
4084
            return cur_count == count;
1,675✔
4085
        });
1,675✔
4086
    };
36✔
4087
    auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) {
24✔
4088
        timed_sleeping_wait_for([&]() -> bool {
760✔
4089
            r->refresh();
760✔
4090
            return test_type.size_of_collection(obj) == count;
760✔
4091
        });
760✔
4092
    };
24✔
4093

4094
    CppContext c;
12✔
4095
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
12✔
4096
        r->begin_transaction();
12✔
4097
        auto object = Object::create(
12✔
4098
            c, r, "source",
12✔
4099
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
12✔
4100
            CreatePolicy::ForceCreate);
12✔
4101

4102
        for (auto link : links) {
36✔
4103
            auto& obj = object.get_obj();
36✔
4104
            test_type.add_link(obj, link);
36✔
4105
        }
36✔
4106
        r->commit_transaction();
12✔
4107
        return object;
12✔
4108
    };
12✔
4109

4110
    auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink {
36✔
4111
        r->begin_transaction();
36✔
4112
        auto obj = Object::create(
36✔
4113
            c, r, "dest",
36✔
4114
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
36✔
4115
            CreatePolicy::ForceCreate);
36✔
4116
        r->commit_transaction();
36✔
4117
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
36✔
4118
    };
36✔
4119

4120
    auto require_links_to_match_ids = [&](std::vector<Obj> links, std::vector<int64_t> expected) {
48✔
4121
        std::vector<int64_t> actual;
48✔
4122
        for (auto obj : links) {
108✔
4123
            actual.push_back(obj.get<Int>(valid_pk_name));
108✔
4124
        }
108✔
4125
        std::sort(actual.begin(), actual.end());
48✔
4126
        std::sort(expected.begin(), expected.end());
48✔
4127
        REQUIRE(actual == expected);
48!
4128
    };
48✔
4129

4130
    SECTION("integration testing") {
12✔
4131
        auto app = test_session.app();
12✔
4132
        SyncTestFile config1(app->current_user(), partition, schema); // uses the current user created above
12✔
4133
        config1.automatic_change_notifications = false;
12✔
4134
        auto r1 = realm::Realm::get_shared_realm(config1);
12✔
4135
        Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source"));
12✔
4136

4137
        create_user_and_log_in(app);                                  // changes the current user
12✔
4138
        SyncTestFile config2(app->current_user(), partition, schema); // uses the user created above
12✔
4139
        config2.automatic_change_notifications = false;
12✔
4140
        auto r2 = realm::Realm::get_shared_realm(config2);
12✔
4141
        Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source"));
12✔
4142

4143
        constexpr int64_t source_pk = 0;
12✔
4144
        constexpr int64_t dest_pk_1 = 1;
12✔
4145
        constexpr int64_t dest_pk_2 = 2;
12✔
4146
        constexpr int64_t dest_pk_3 = 3;
12✔
4147
        Object object;
12✔
4148

4149
        { // add a container collection with three valid links
12✔
4150
            REQUIRE(r1_source_objs.size() == 0);
12!
4151
            ObjLink dest1 = create_one_dest_object(r1, dest_pk_1);
12✔
4152
            ObjLink dest2 = create_one_dest_object(r1, dest_pk_2);
12✔
4153
            ObjLink dest3 = create_one_dest_object(r1, dest_pk_3);
12✔
4154
            object = create_one_source_object(r1, source_pk, {dest1, dest2, dest3});
12✔
4155
            REQUIRE(r1_source_objs.size() == 1);
12!
4156
            REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4157
            REQUIRE(r1_source_objs.get(0).get<String>("realm_id") == partition);
12!
4158
            require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4159
        }
12✔
4160

4161
        size_t expected_coll_size = 3;
12✔
4162
        std::vector<int64_t> remaining_dest_object_ids;
12✔
4163
        { // erase one of the destination objects
12✔
4164
            wait_for_num_objects_to_equal(r2, "class_source", 1);
12✔
4165
            wait_for_num_objects_to_equal(r2, "class_dest", 3);
12✔
4166
            REQUIRE(r2_source_objs.size() == 1);
12!
4167
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4168
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3);
12!
4169
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4170
            require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4171
            r2->begin_transaction();
12✔
4172
            linked_objects[0].remove();
12✔
4173
            r2->commit_transaction();
12✔
4174
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name),
12✔
4175
                                         linked_objects[2].template get<Int>(valid_pk_name)};
12✔
4176
            expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3;
12✔
4177
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4178
        }
12✔
4179

4180
        { // remove a link from the collection
12✔
4181
            wait_for_num_objects_to_equal(r1, "class_dest", 2);
12✔
4182
            REQUIRE(r1_source_objs.size() == 1);
12!
4183
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4184
            auto linked_objects = test_type.get_links(r1_source_objs.get(0));
12✔
4185
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4186
            r1->begin_transaction();
12✔
4187
            auto obj = r1_source_objs.get(0);
12✔
4188
            test_type.remove_link(obj,
12✔
4189
                                  ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()});
12✔
4190
            r1->commit_transaction();
12✔
4191
            --expected_coll_size;
12✔
4192
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name)};
12✔
4193
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4194
        }
12✔
4195
        bool coll_cleared = false;
12✔
4196
        advance_and_notify(*r1);
12✔
4197
        auto collection = test_type.get_collection(r1, r1_source_objs.get(0));
12✔
4198
        auto token = collection.add_notification_callback([&coll_cleared](CollectionChangeSet c) {
24✔
4199
            coll_cleared = c.collection_was_cleared;
24✔
4200
        });
24✔
4201

4202
        { // clear the collection
12✔
4203
            REQUIRE(r2_source_objs.size() == 1);
12!
4204
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4205
            wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size);
12✔
4206
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4207
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4208
            r2->begin_transaction();
12✔
4209
            test_type.clear_collection(r2_source_objs.get(0));
12✔
4210
            r2->commit_transaction();
12✔
4211
            expected_coll_size = 0;
12✔
4212
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4213
        }
12✔
4214

4215
        { // expect an empty collection
12✔
4216
            REQUIRE(!coll_cleared);
12!
4217
            REQUIRE(r1_source_objs.size() == 1);
12!
4218
            wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size);
12✔
4219
            advance_and_notify(*r1);
12✔
4220
            REQUIRE(coll_cleared);
12!
4221
        }
12✔
4222
    }
12✔
4223
}
12✔
4224

4225
TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID,
4226
                   cf::UUID, cf::BoxedOptional<cf::Int>, cf::UnboxedOptional<cf::String>, cf::BoxedOptional<cf::OID>,
4227
                   cf::BoxedOptional<cf::UUID>)
4228
{
16✔
4229
    const std::string valid_pk_name = "_id";
16✔
4230
    const std::string partition_key_col_name = "partition_key_prop";
16✔
4231
    const std::string table_name = "class_partition_test_type";
16✔
4232
    auto partition_property = Property(partition_key_col_name, TestType::property_type);
16✔
4233
    Schema schema = {{Group::table_name_to_class_name(table_name),
16✔
4234
                      {
16✔
4235
                          {valid_pk_name, PropertyType::Int, true},
16✔
4236
                          partition_property,
16✔
4237
                      }}};
16✔
4238
    auto server_app_config = minimal_app_config("partition_types_app_name", schema);
16✔
4239
    server_app_config.partition_key = partition_property;
16✔
4240
    TestAppSession test_session(create_app(server_app_config));
16✔
4241
    auto app = test_session.app();
16✔
4242

4243
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
48✔
4244
        timed_sleeping_wait_for([&]() -> bool {
3,827✔
4245
            r->refresh();
3,827✔
4246
            TableRef dest = r->read_group().get_table(table_name);
3,827✔
4247
            size_t cur_count = dest->size();
3,827✔
4248
            return cur_count == count;
3,827✔
4249
        });
3,827✔
4250
    };
48✔
4251
    using T = typename TestType::Type;
16✔
4252
    CppContext c;
16✔
4253
    auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) {
48✔
4254
        r->begin_transaction();
48✔
4255
        auto object = Object::create(
48✔
4256
            c, r, Group::table_name_to_class_name(table_name),
48✔
4257
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}),
48✔
4258
            CreatePolicy::ForceCreate);
48✔
4259
        r->commit_transaction();
48✔
4260
    };
48✔
4261

4262
    auto get_bson = [](T val) -> bson::Bson {
96✔
4263
        if constexpr (std::is_same_v<T, StringData>) {
96✔
4264
            return val.is_null() ? bson::Bson(util::none) : bson::Bson(val);
48✔
4265
        }
14✔
4266
        else if constexpr (TestType::is_optional) {
54✔
4267
            return val ? bson::Bson(*val) : bson::Bson(util::none);
40✔
4268
        }
20✔
4269
        else {
28✔
4270
            return bson::Bson(val);
28✔
4271
        }
28✔
4272
    };
96✔
4273

4274
    SECTION("can round trip an object") {
16✔
4275
        auto values = TestType::values();
16✔
4276
        auto user1 = app->current_user();
16✔
4277
        create_user_and_log_in(app);
16✔
4278
        auto user2 = app->current_user();
16✔
4279
        REQUIRE(user1);
16!
4280
        REQUIRE(user2);
16!
4281
        REQUIRE(user1 != user2);
16!
4282
        for (T partition_value : values) {
48✔
4283
            SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above
48✔
4284
            auto r1 = realm::Realm::get_shared_realm(config1);
48✔
4285
            Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name));
48✔
4286

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

4291
            const int64_t pk_value = random_int();
48✔
4292
            {
48✔
4293
                REQUIRE(r1_source_objs.size() == 0);
48!
4294
                create_object(r1, pk_value, TestType::to_any(partition_value));
48✔
4295
                REQUIRE(r1_source_objs.size() == 1);
48!
4296
                REQUIRE(r1_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4297
                REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4298
            }
48✔
4299
            {
48✔
4300
                wait_for_num_objects_to_equal(r2, table_name, 1);
48✔
4301
                REQUIRE(r2_source_objs.size() == 1);
48!
4302
                REQUIRE(r2_source_objs.size() == 1);
48!
4303
                REQUIRE(r2_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4304
                REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4305
            }
48✔
4306
        }
48✔
4307
    }
16✔
4308
}
16✔
4309

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

4313
    Schema schema{
4✔
4314
        {"TopLevel",
4✔
4315
         {
4✔
4316
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
4317
             {"full_text", Property::IsFulltextIndexed{true}},
4✔
4318
         }},
4✔
4319
    };
4✔
4320

4321
    auto server_app_config = minimal_app_config("full_text", schema);
4✔
4322
    auto app_session = create_app(server_app_config);
4✔
4323
    const auto partition = random_string(100);
4✔
4324
    TestAppSession test_session(app_session);
4✔
4325
    SyncTestFile config(test_session.app()->current_user(), partition, schema);
4✔
4326
    SharedRealm realm;
4✔
4327
    SECTION("sync open") {
4✔
4328
        INFO("realm opened without async open");
2✔
4329
        realm = Realm::get_shared_realm(config);
2✔
4330
    }
2✔
4331
    SECTION("async open") {
4✔
4332
        INFO("realm opened with async open");
2✔
4333
        auto async_open_task = Realm::get_synchronized_realm(config);
2✔
4334

4335
        auto realm_future = async_open_task->start();
2✔
4336
        realm = Realm::get_shared_realm(std::move(realm_future.get()));
2✔
4337
    }
2✔
4338

4339
    CppContext c(realm);
4✔
4340
    auto obj_id_1 = ObjectId::gen();
4✔
4341
    auto obj_id_2 = ObjectId::gen();
4✔
4342
    realm->begin_transaction();
4✔
4343
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}}));
4✔
4344
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}}));
4✔
4345
    realm->commit_transaction();
4✔
4346

4347
    auto table = realm->read_group().get_table("class_TopLevel");
4✔
4348
    REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext);
4!
4349
    Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world"));
4✔
4350
    REQUIRE(world_results.size() == 1);
4!
4351
    REQUIRE(world_results.get<Obj>(0).get_primary_key() == Mixed{obj_id_1});
4!
4352
}
4✔
4353

4354
#endif // REALM_ENABLE_AUTH_TESTS
4355

4356
TEST_CASE("app: custom error handling", "[sync][app][custom errors]") {
2✔
4357
    class CustomErrorTransport : public GenericNetworkTransport {
2✔
4358
    public:
2✔
4359
        CustomErrorTransport(int code, const std::string& message)
2✔
4360
            : m_code(code)
2✔
4361
            , m_message(message)
2✔
4362
        {
2✔
4363
        }
2✔
4364

4365
        void send_request_to_server(const Request&, util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4366
        {
2✔
4367
            completion(Response{0, m_code, HttpHeaders(), m_message});
2✔
4368
        }
2✔
4369

4370
    private:
2✔
4371
        int m_code;
2✔
4372
        std::string m_message;
2✔
4373
    };
2✔
4374

4375
    SECTION("custom code and message is sent back") {
2✔
4376
        OfflineAppSession offline_session({std::make_shared<CustomErrorTransport>(1001, "Boom!")});
2✔
4377
        auto error = failed_log_in(offline_session.app());
2✔
4378
        CHECK(error.is_custom_error());
2!
4379
        CHECK(*error.additional_status_code == 1001);
2!
4380
        CHECK(error.reason() == "Boom!");
2!
4381
    }
2✔
4382
}
2✔
4383

4384
// MARK: - Unit Tests
4385

4386
static const std::string bad_access_token = "lolwut";
4387
static const std::string dummy_device_id = "123400000000000000000000";
4388

4389
TEST_CASE("subscribable unit tests", "[sync][app]") {
8✔
4390
    struct Foo : public Subscribable<Foo> {
8✔
4391
        void event()
8✔
4392
        {
18✔
4393
            emit_change_to_subscribers(*this);
18✔
4394
        }
18✔
4395
    };
8✔
4396

4397
    auto foo = Foo();
8✔
4398

4399
    SECTION("subscriber receives events") {
8✔
4400
        auto event_count = 0;
2✔
4401
        auto token = foo.subscribe([&event_count](auto&) {
6✔
4402
            event_count++;
6✔
4403
        });
6✔
4404

4405
        foo.event();
2✔
4406
        foo.event();
2✔
4407
        foo.event();
2✔
4408

4409
        CHECK(event_count == 3);
2!
4410
    }
2✔
4411

4412
    SECTION("subscriber can unsubscribe") {
8✔
4413
        auto event_count = 0;
2✔
4414
        auto token = foo.subscribe([&event_count](auto&) {
2✔
4415
            event_count++;
2✔
4416
        });
2✔
4417

4418
        foo.event();
2✔
4419
        CHECK(event_count == 1);
2!
4420

4421
        foo.unsubscribe(token);
2✔
4422
        foo.event();
2✔
4423
        CHECK(event_count == 1);
2!
4424
    }
2✔
4425

4426
    SECTION("subscriber is unsubscribed on dtor") {
8✔
4427
        auto event_count = 0;
2✔
4428
        {
2✔
4429
            auto token = foo.subscribe([&event_count](auto&) {
2✔
4430
                event_count++;
2✔
4431
            });
2✔
4432

4433
            foo.event();
2✔
4434
            CHECK(event_count == 1);
2!
4435
        }
2✔
4436
        foo.event();
2✔
4437
        CHECK(event_count == 1);
2!
4438
    }
2✔
4439

4440
    SECTION("multiple subscribers receive events") {
8✔
4441
        auto event_count = 0;
2✔
4442
        {
2✔
4443
            auto token1 = foo.subscribe([&event_count](auto&) {
2✔
4444
                event_count++;
2✔
4445
            });
2✔
4446
            auto token2 = foo.subscribe([&event_count](auto&) {
2✔
4447
                event_count++;
2✔
4448
            });
2✔
4449

4450
            foo.event();
2✔
4451
            CHECK(event_count == 2);
2!
4452
        }
2✔
4453
        foo.event();
2✔
4454
        CHECK(event_count == 2);
2!
4455
    }
2✔
4456
}
8✔
4457

4458
TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") {
6✔
4459
    auto transport = std::make_shared<UnitTestTransport>();
6✔
4460
    OfflineAppSession::Config config{transport};
6✔
4461
    transport->set_profile(profile_0);
6✔
4462

4463
    SECTION("login_anonymous good") {
6✔
4464
        config.storage_path = util::make_temp_dir();
2✔
4465
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
2✔
4466
        {
2✔
4467
            config.delete_storage = false;
2✔
4468
            OfflineAppSession oas(config);
2✔
4469
            auto app = oas.app();
2✔
4470
            auto user = log_in(app);
2✔
4471

4472
            REQUIRE(user->identities().size() == 1);
2!
4473
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4474
            UserProfile user_profile = user->user_profile();
2✔
4475

4476
            CHECK(user_profile.name() == profile_0_name);
2!
4477
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4478
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4479
            CHECK(user_profile.email() == profile_0_email);
2!
4480
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4481
            CHECK(user_profile.gender() == profile_0_gender);
2!
4482
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4483
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4484
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4485
        }
2✔
4486
        // assert everything is stored properly between runs
4487
        {
2✔
4488
            config.delete_storage = true; // clean up after this session
2✔
4489
            OfflineAppSession oas(config);
2✔
4490
            auto app = oas.app();
2✔
4491
            REQUIRE(app->all_users().size() == 1);
2!
4492
            auto user = app->all_users()[0];
2✔
4493
            REQUIRE(user->identities().size() == 1);
2!
4494
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4495
            UserProfile user_profile = user->user_profile();
2✔
4496

4497
            CHECK(user_profile.name() == profile_0_name);
2!
4498
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4499
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4500
            CHECK(user_profile.email() == profile_0_email);
2!
4501
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4502
            CHECK(user_profile.gender() == profile_0_gender);
2!
4503
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4504
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4505
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4506
        }
2✔
4507
    }
2✔
4508

4509
    SECTION("login_anonymous bad") {
6✔
4510
        struct transport : UnitTestTransport {
2✔
4511
            void send_request_to_server(const Request& request,
2✔
4512
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4513
            {
4✔
4514
                if (request.url.find("/login") != std::string::npos) {
4✔
4515
                    completion({200, 0, {}, user_json(bad_access_token).dump()});
2✔
4516
                }
2✔
4517
                else {
2✔
4518
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
2✔
4519
                }
2✔
4520
            }
4✔
4521
        };
2✔
4522

4523
        config.transport = instance_of<transport>;
2✔
4524
        OfflineAppSession oas(config);
2✔
4525
        auto error = failed_log_in(oas.app());
2✔
4526
        CHECK(error.reason() == std::string("Could not log in user: received malformed JWT"));
2!
4527
        CHECK(error.code_string() == "BadToken");
2!
4528
        CHECK(error.is_json_error());
2!
4529
        CHECK(error.code() == ErrorCodes::BadToken);
2!
4530
    }
2✔
4531

4532
    SECTION("login_anonynous multiple users") {
6✔
4533
        OfflineAppSession oas(config);
2✔
4534
        auto app = oas.app();
2✔
4535

4536
        auto user1 = log_in(app);
2✔
4537
        auto user2 = log_in(app, AppCredentials::anonymous(false));
2✔
4538
        CHECK(user1 != user2);
2!
4539
    }
2✔
4540
}
6✔
4541

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

4546
    auto logged_in_user = oas.make_user();
6✔
4547
    bool processed = false;
6✔
4548
    ObjectId obj_id(UnitTestTransport::api_key_id.c_str());
6✔
4549

4550
    SECTION("create api key") {
6✔
4551
        client.create_api_key(UnitTestTransport::api_key_name, logged_in_user,
2✔
4552
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4553
                                  REQUIRE_FALSE(error);
2!
4554
                                  CHECK(user_api_key.disabled == false);
2!
4555
                                  CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4556
                                  CHECK(user_api_key.key == UnitTestTransport::api_key);
2!
4557
                                  CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4558
                              });
2✔
4559
    }
2✔
4560

4561
    SECTION("fetch api key") {
6✔
4562
        client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4563
            REQUIRE_FALSE(error);
2!
4564
            CHECK(user_api_key.disabled == false);
2!
4565
            CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4566
            CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4567
        });
2✔
4568
    }
2✔
4569

4570
    SECTION("fetch api keys") {
6✔
4571
        client.fetch_api_keys(logged_in_user,
2✔
4572
                              [&](std::vector<App::UserAPIKey> user_api_keys, Optional<AppError> error) {
2✔
4573
                                  REQUIRE_FALSE(error);
2!
4574
                                  CHECK(user_api_keys.size() == 2);
2!
4575
                                  for (auto user_api_key : user_api_keys) {
4✔
4576
                                      CHECK(user_api_key.disabled == false);
4!
4577
                                      CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
4!
4578
                                      CHECK(user_api_key.name == UnitTestTransport::api_key_name);
4!
4579
                                  }
4✔
4580
                                  processed = true;
2✔
4581
                              });
2✔
4582
        CHECK(processed);
2!
4583
    }
2✔
4584
}
6✔
4585

4586
TEST_CASE("app: user_semantics", "[sync][app][user]") {
12✔
4587
    OfflineAppSession oas;
12✔
4588
    auto app = oas.app();
12✔
4589

4590
    const auto login_user_email_pass = [=] {
12✔
4591
        return log_in(app, AppCredentials::username_password("bob", "thompson"));
6✔
4592
    };
6✔
4593
    const auto login_user_anonymous = [=] {
18✔
4594
        return log_in(app, AppCredentials::anonymous());
18✔
4595
    };
18✔
4596

4597
    CHECK(!app->current_user());
12!
4598

4599
    int event_processed = 0;
12✔
4600
    auto token = app->subscribe([&](auto&) {
28✔
4601
        event_processed++;
28✔
4602
        // Read the current user to verify that doing so does not deadlock
4603
        app->current_user();
28✔
4604
    });
28✔
4605

4606
    SECTION("current user is populated") {
12✔
4607
        const auto user1 = login_user_anonymous();
2✔
4608
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4609
        CHECK(event_processed == 1);
2!
4610
    }
2✔
4611

4612
    SECTION("current user is updated on login") {
12✔
4613
        const auto user1 = login_user_anonymous();
2✔
4614
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4615
        const auto user2 = login_user_email_pass();
2✔
4616
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4617
        CHECK(user1->user_id() != user2->user_id());
2!
4618
        CHECK(event_processed == 2);
2!
4619
    }
2✔
4620

4621
    SECTION("current user is updated to last used user on logout") {
12✔
4622
        const auto user1 = login_user_anonymous();
2✔
4623
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4624
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4625

4626
        const auto user2 = login_user_email_pass();
2✔
4627
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4628
        CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn);
2!
4629
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4630
        CHECK(user1 != user2);
2!
4631

4632
        // should reuse existing session
4633
        const auto user3 = login_user_anonymous();
2✔
4634
        CHECK(user3 == user1);
2!
4635

4636
        auto user_events_processed = 0;
2✔
4637
        auto _ = user3->subscribe([&user_events_processed](auto&) {
2✔
4638
            user_events_processed++;
2✔
4639
        });
2✔
4640

4641
        app->log_out([](auto) {});
2✔
4642
        CHECK(user_events_processed == 1);
2!
4643
        REQUIRE(app->current_user());
2!
4644
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4645

4646
        CHECK(app->all_users().size() == 1);
2!
4647
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4648

4649
        CHECK(event_processed == 4);
2!
4650
    }
2✔
4651

4652
    SECTION("anon users are removed on logout") {
12✔
4653
        const auto user1 = login_user_anonymous();
2✔
4654
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4655
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4656

4657
        const auto user2 = login_user_anonymous();
2✔
4658
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4659
        CHECK(app->all_users().size() == 1);
2!
4660
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4661
        CHECK(user1->user_id() == user2->user_id());
2!
4662

4663
        app->log_out([](auto) {});
2✔
4664
        CHECK(app->all_users().size() == 0);
2!
4665

4666
        CHECK(event_processed == 3);
2!
4667
    }
2✔
4668

4669
    SECTION("logout user") {
12✔
4670
        auto user1 = login_user_email_pass();
2✔
4671
        auto user2 = login_user_anonymous();
2✔
4672

4673
        // Anonymous users are special
4674
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4675
            REQUIRE_FALSE(error);
2!
4676
        });
2✔
4677
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4678

4679
        // Other users can be LoggedOut
4680
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4681
            REQUIRE_FALSE(error);
2!
4682
        });
2✔
4683
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4684

4685
        // Logging out already logged out users does nothing
4686
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4687
            REQUIRE_FALSE(error);
2!
4688
        });
2✔
4689
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4690

4691
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4692
            REQUIRE_FALSE(error);
2!
4693
        });
2✔
4694
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4695

4696
        CHECK(event_processed == 4);
2!
4697
    }
2✔
4698

4699
    SECTION("unsubscribed observers no longer process events") {
12✔
4700
        app->unsubscribe(token);
2✔
4701

4702
        const auto user1 = login_user_anonymous();
2✔
4703
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4704
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4705

4706
        const auto user2 = login_user_anonymous();
2✔
4707
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4708
        CHECK(app->all_users().size() == 1);
2!
4709
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4710
        CHECK(user1->user_id() == user2->user_id());
2!
4711

4712
        app->log_out([](auto) {});
2✔
4713
        CHECK(app->all_users().size() == 0);
2!
4714

4715
        CHECK(event_processed == 0);
2!
4716
    }
2✔
4717
}
12✔
4718

4719
namespace {
4720
struct ErrorCheckingTransport : public GenericNetworkTransport {
4721
    ErrorCheckingTransport(Response* r)
4722
        : m_response(r)
5✔
4723
    {
10✔
4724
    }
10✔
4725
    void send_request_to_server(const Request& request,
4726
                                util::UniqueFunction<void(const Response&)>&& completion) override
4727
    {
20✔
4728
        // Make sure to return a valid location response
4729
        if (request.url.find("/location") != std::string::npos) {
20✔
4730
            completion(Response{200,
10✔
4731
                                0,
10✔
4732
                                {{"content-type", "application/json"}},
10✔
4733
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
10✔
4734
                                "\"http://some.fake.url\",\"ws_hostname\":\"ws://some.fake.url\"}"});
10✔
4735
            return;
10✔
4736
        }
10✔
4737

4738
        completion(Response(*m_response));
10✔
4739
    }
10✔
4740

4741
private:
4742
    Response* m_response;
4743
};
4744
} // namespace
4745

4746
TEST_CASE("app: response error handling", "[sync][app]") {
10✔
4747
    std::string response_body = nlohmann::json({{"access_token", good_access_token},
10✔
4748
                                                {"refresh_token", good_access_token},
10✔
4749
                                                {"user_id", "Brown Bear"},
10✔
4750
                                                {"device_id", "Panda Bear"}})
10✔
4751
                                    .dump();
10✔
4752

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

4755
    OfflineAppSession oas({std::make_shared<ErrorCheckingTransport>(&response)});
10✔
4756
    auto app = oas.app();
10✔
4757

4758
    SECTION("http 404") {
10✔
4759
        response.http_status_code = 404;
2✔
4760
        auto error = failed_log_in(app);
2✔
4761
        CHECK(!error.is_json_error());
2!
4762
        CHECK(!error.is_custom_error());
2!
4763
        CHECK(!error.is_service_error());
2!
4764
        CHECK(error.is_http_error());
2!
4765
        CHECK(*error.additional_status_code == 404);
2!
4766
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4767
    }
2✔
4768
    SECTION("http 500") {
10✔
4769
        response.http_status_code = 500;
2✔
4770
        auto error = failed_log_in(app);
2✔
4771
        CHECK(!error.is_json_error());
2!
4772
        CHECK(!error.is_custom_error());
2!
4773
        CHECK(!error.is_service_error());
2!
4774
        CHECK(error.is_http_error());
2!
4775
        CHECK(*error.additional_status_code == 500);
2!
4776
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4777
        CHECK(error.link_to_server_logs.empty());
2!
4778
    }
2✔
4779

4780
    SECTION("custom error code") {
10✔
4781
        response.custom_status_code = 42;
2✔
4782
        response.body = "Custom error message";
2✔
4783
        auto error = failed_log_in(app);
2✔
4784
        CHECK(!error.is_http_error());
2!
4785
        CHECK(!error.is_json_error());
2!
4786
        CHECK(!error.is_service_error());
2!
4787
        CHECK(error.is_custom_error());
2!
4788
        CHECK(*error.additional_status_code == 42);
2!
4789
        CHECK(error.reason() == std::string("Custom error message"));
2!
4790
        CHECK(error.link_to_server_logs.empty());
2!
4791
    }
2✔
4792

4793
    SECTION("session error code") {
10✔
4794
        response.headers = HttpHeaders{{"Content-Type", "application/json"}};
2✔
4795
        response.http_status_code = 400;
2✔
4796
        response.body = nlohmann::json({{"error_code", "MongoDBError"},
2✔
4797
                                        {"error", "a fake MongoDB error message!"},
2✔
4798
                                        {"access_token", good_access_token},
2✔
4799
                                        {"refresh_token", good_access_token},
2✔
4800
                                        {"user_id", "Brown Bear"},
2✔
4801
                                        {"device_id", "Panda Bear"},
2✔
4802
                                        {"link", "http://...whatever the server passes us"}})
2✔
4803
                            .dump();
2✔
4804
        auto error = failed_log_in(app);
2✔
4805
        CHECK(!error.is_http_error());
2!
4806
        CHECK(!error.is_json_error());
2!
4807
        CHECK(!error.is_custom_error());
2!
4808
        CHECK(error.is_service_error());
2!
4809
        CHECK(error.code() == ErrorCodes::MongoDBError);
2!
4810
        CHECK(error.reason() == std::string("a fake MongoDB error message!"));
2!
4811
        CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us"));
2!
4812
    }
2✔
4813

4814
    SECTION("json error code") {
10✔
4815
        response.body = "this: is not{} a valid json body!";
2✔
4816
        auto error = failed_log_in(app);
2✔
4817
        CHECK(!error.is_http_error());
2!
4818
        CHECK(error.is_json_error());
2!
4819
        CHECK(!error.is_custom_error());
2!
4820
        CHECK(!error.is_service_error());
2!
4821
        CHECK(error.code() == ErrorCodes::MalformedJson);
2!
4822
        CHECK(error.reason() ==
2!
4823
              std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error "
2✔
4824
                          "while parsing value - invalid literal; last read: 'th'"));
2✔
4825
        CHECK(error.code_string() == "MalformedJson");
2!
4826
    }
2✔
4827
}
10✔
4828

4829
TEST_CASE("app: switch user", "[sync][app][user]") {
4✔
4830
    OfflineAppSession oas;
4✔
4831
    auto app = oas.app();
4✔
4832

4833
    bool processed = false;
4✔
4834

4835
    SECTION("switch user expect success") {
4✔
4836
        CHECK(app->all_users().size() == 0);
2!
4837

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

4842
        // Log in user 2
4843
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
4844
        CHECK(app->current_user() == user_b);
2!
4845

4846
        CHECK(app->all_users().size() == 2);
2!
4847

4848
        app->switch_user(user_a);
2✔
4849
        CHECK(app->current_user() == user_a);
2!
4850

4851
        app->switch_user(user_b);
2✔
4852

4853
        CHECK(app->current_user() == user_b);
2!
4854
        processed = true;
2✔
4855
        CHECK(processed);
2!
4856
    }
2✔
4857

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

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

4865
        app->log_out([&](Optional<AppError> error) {
2✔
4866
            REQUIRE_FALSE(error);
2!
4867
        });
2✔
4868

4869
        CHECK(app->current_user() == nullptr);
2!
4870
        CHECK(user_a->state() == SyncUser::State::LoggedOut);
2!
4871

4872
        // Log in user 2
4873
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
4874
        CHECK(app->current_user() == user_b);
2!
4875
        CHECK(app->all_users().size() == 2);
2!
4876

4877
        REQUIRE_THROWS_AS(app->switch_user(user_a), AppError);
2✔
4878
        CHECK(app->current_user() == user_b);
2!
4879
    }
2✔
4880
}
4✔
4881

4882
TEST_CASE("app: remove user", "[sync][app][user]") {
4✔
4883
    OfflineAppSession oas;
4✔
4884
    auto app = oas.app();
4✔
4885

4886
    SECTION("remove anonymous user") {
4✔
4887
        CHECK(app->all_users().size() == 0);
2!
4888

4889
        // Log in user 1
4890
        auto user_a = log_in(app);
2✔
4891
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
4892

4893
        app->log_out(user_a, [&](Optional<AppError> error) {
2✔
4894
            REQUIRE_FALSE(error);
2!
4895
            // a logged out anon user will be marked as Removed, not LoggedOut
4896
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
4897
        });
2✔
4898
        CHECK(app->all_users().empty());
2!
4899

4900
        app->remove_user(user_a, [&](Optional<AppError> error) {
2✔
4901
            CHECK(error->reason() == "User has already been removed");
2!
4902
            CHECK(app->all_users().size() == 0);
2!
4903
        });
2✔
4904

4905
        // Log in user 2
4906
        auto user_b = log_in(app);
2✔
4907
        CHECK(app->current_user() == user_b);
2!
4908
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
4909
        CHECK(app->all_users().size() == 1);
2!
4910

4911
        app->remove_user(user_b, [&](Optional<AppError> error) {
2✔
4912
            REQUIRE_FALSE(error);
2!
4913
            CHECK(app->all_users().size() == 0);
2!
4914
        });
2✔
4915

4916
        CHECK(app->current_user() == nullptr);
2!
4917

4918
        // check both handles are no longer valid
4919
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
4920
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
4921
    }
2✔
4922

4923
    SECTION("remove user with credentials") {
4✔
4924
        CHECK(app->all_users().size() == 0);
2!
4925
        CHECK(app->current_user() == nullptr);
2!
4926

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

4929
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
4930

4931
        app->log_out(user, [&](Optional<AppError> error) {
2✔
4932
            REQUIRE_FALSE(error);
2!
4933
        });
2✔
4934

4935
        CHECK(user->state() == SyncUser::State::LoggedOut);
2!
4936

4937
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
4938
            REQUIRE_FALSE(error);
2!
4939
        });
2✔
4940
        CHECK(app->all_users().size() == 0);
2!
4941

4942
        Optional<AppError> error;
2✔
4943
        app->remove_user(user, [&](Optional<AppError> err) {
2✔
4944
            error = err;
2✔
4945
        });
2✔
4946
        CHECK(error->code() > 0);
2!
4947
        CHECK(app->all_users().size() == 0);
2!
4948
        CHECK(user->state() == SyncUser::State::Removed);
2!
4949
    }
2✔
4950
}
4✔
4951

4952
TEST_CASE("app: link_user", "[sync][app][user]") {
4✔
4953
    OfflineAppSession oas;
4✔
4954
    auto app = oas.app();
4✔
4955

4956
    auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10));
4✔
4957
    auto password = random_string(10);
4✔
4958

4959
    auto custom_credentials = AppCredentials::facebook("a_token");
4✔
4960
    auto email_pass_credentials = AppCredentials::username_password(email, password);
4✔
4961

4962
    auto sync_user = log_in(app, email_pass_credentials);
4✔
4963
    REQUIRE(sync_user->identities().size() == 2);
4!
4964
    CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword);
4!
4965

4966
    SECTION("successful link") {
4✔
4967
        bool processed = false;
2✔
4968
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
4969
            REQUIRE_FALSE(error);
2!
4970
            REQUIRE(user);
2!
4971
            CHECK(user->user_id() == sync_user->user_id());
2!
4972
            processed = true;
2✔
4973
        });
2✔
4974
        CHECK(processed);
2!
4975
    }
2✔
4976

4977
    SECTION("link_user should fail when logged out") {
4✔
4978
        app->log_out([&](Optional<AppError> error) {
2✔
4979
            REQUIRE_FALSE(error);
2!
4980
        });
2✔
4981

4982
        bool processed = false;
2✔
4983
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
4984
            CHECK(error->reason() == "The specified user is not logged in.");
2!
4985
            CHECK(!user);
2!
4986
            processed = true;
2✔
4987
        });
2✔
4988
        CHECK(processed);
2!
4989
    }
2✔
4990
}
4✔
4991

4992
TEST_CASE("app: auth providers", "[sync][app][user]") {
20✔
4993
    SECTION("auth providers facebook") {
20✔
4994
        auto credentials = AppCredentials::facebook("a_token");
2✔
4995
        CHECK(credentials.provider() == AuthProvider::FACEBOOK);
2!
4996
        CHECK(credentials.provider_as_string() == IdentityProviderFacebook);
2!
4997
        CHECK(credentials.serialize_as_bson() ==
2!
4998
              bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}});
2✔
4999
    }
2✔
5000

5001
    SECTION("auth providers anonymous") {
20✔
5002
        auto credentials = AppCredentials::anonymous();
2✔
5003
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS);
2!
5004
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5005
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5006
    }
2✔
5007

5008
    SECTION("auth providers anonymous no reuse") {
20✔
5009
        auto credentials = AppCredentials::anonymous(false);
2✔
5010
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE);
2!
5011
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5012
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5013
    }
2✔
5014

5015
    SECTION("auth providers google authCode") {
20✔
5016
        auto credentials = AppCredentials::google(AuthCode("a_token"));
2✔
5017
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5018
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5019
        CHECK(credentials.serialize_as_bson() ==
2!
5020
              bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}});
2✔
5021
    }
2✔
5022

5023
    SECTION("auth providers google idToken") {
20✔
5024
        auto credentials = AppCredentials::google(IdToken("a_token"));
2✔
5025
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5026
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5027
        CHECK(credentials.serialize_as_bson() ==
2!
5028
              bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}});
2✔
5029
    }
2✔
5030

5031
    SECTION("auth providers apple") {
20✔
5032
        auto credentials = AppCredentials::apple("a_token");
2✔
5033
        CHECK(credentials.provider() == AuthProvider::APPLE);
2!
5034
        CHECK(credentials.provider_as_string() == IdentityProviderApple);
2!
5035
        CHECK(credentials.serialize_as_bson() ==
2!
5036
              bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}});
2✔
5037
    }
2✔
5038

5039
    SECTION("auth providers custom") {
20✔
5040
        auto credentials = AppCredentials::custom("a_token");
2✔
5041
        CHECK(credentials.provider() == AuthProvider::CUSTOM);
2!
5042
        CHECK(credentials.provider_as_string() == IdentityProviderCustom);
2!
5043
        CHECK(credentials.serialize_as_bson() ==
2!
5044
              bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}});
2✔
5045
    }
2✔
5046

5047
    SECTION("auth providers username password") {
20✔
5048
        auto credentials = AppCredentials::username_password("user", "pass");
2✔
5049
        CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD);
2!
5050
        CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword);
2!
5051
        CHECK(credentials.serialize_as_bson() ==
2!
5052
              bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}});
2✔
5053
    }
2✔
5054

5055
    SECTION("auth providers function") {
20✔
5056
        bson::BsonDocument function_params{{"name", "mongo"}};
2✔
5057
        auto credentials = AppCredentials::function(function_params);
2✔
5058
        CHECK(credentials.provider() == AuthProvider::FUNCTION);
2!
5059
        CHECK(credentials.provider_as_string() == IdentityProviderFunction);
2!
5060
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}});
2!
5061
    }
2✔
5062

5063
    SECTION("auth providers api key") {
20✔
5064
        auto credentials = AppCredentials::api_key("a key");
2✔
5065
        CHECK(credentials.provider() == AuthProvider::API_KEY);
2!
5066
        CHECK(credentials.provider_as_string() == IdentityProviderAPIKey);
2!
5067
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}});
2!
5068
        CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY);
2!
5069
    }
2✔
5070
}
20✔
5071

5072
TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") {
6✔
5073
    SECTION("refresh custom data happy path") {
6✔
5074
        static bool session_route_hit = false;
2✔
5075

5076
        struct transport : UnitTestTransport {
2✔
5077
            void send_request_to_server(const Request& request,
2✔
5078
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5079
            {
10✔
5080
                if (request.url.find("/session") != std::string::npos) {
10✔
5081
                    session_route_hit = true;
2✔
5082
                    nlohmann::json json{{"access_token", good_access_token}};
2✔
5083
                    completion({200, 0, {}, json.dump()});
2✔
5084
                }
2✔
5085
                else {
8✔
5086
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5087
                }
8✔
5088
            }
10✔
5089
        };
2✔
5090
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5091
        auto app = oas.app();
2✔
5092
        oas.make_user();
2✔
5093

5094
        bool processed = false;
2✔
5095
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5096
            REQUIRE_FALSE(error);
2!
5097
            CHECK(session_route_hit);
2!
5098
            processed = true;
2✔
5099
        });
2✔
5100
        CHECK(processed);
2!
5101
    }
2✔
5102

5103
    SECTION("refresh custom data sad path") {
6✔
5104
        static bool session_route_hit = false;
2✔
5105

5106
        struct transport : UnitTestTransport {
2✔
5107
            void send_request_to_server(const Request& request,
2✔
5108
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5109
            {
10✔
5110
                if (request.url.find("/session") != std::string::npos) {
10✔
5111
                    session_route_hit = true;
2✔
5112
                    nlohmann::json json{{"access_token", bad_access_token}};
2✔
5113
                    completion({200, 0, {}, json.dump()});
2✔
5114
                }
2✔
5115
                else {
8✔
5116
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5117
                }
8✔
5118
            }
10✔
5119
        };
2✔
5120

5121
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5122
        auto app = oas.app();
2✔
5123
        oas.make_user();
2✔
5124

5125
        bool processed = false;
2✔
5126
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5127
            CHECK(error->reason() == "malformed JWT");
2!
5128
            CHECK(error->code() == ErrorCodes::BadToken);
2!
5129
            CHECK(session_route_hit);
2!
5130
            processed = true;
2✔
5131
        });
2✔
5132
        CHECK(processed);
2!
5133
    }
2✔
5134

5135
    SECTION("refresh token ensure flow is correct") {
6✔
5136
        /*
5137
         Expected flow:
5138
         Login - this gets access and refresh tokens
5139
         Get profile - throw back a 401 error
5140
         Refresh token - get a new token for the user
5141
         Get profile - get the profile with the new token
5142
         */
5143
        struct transport : GenericNetworkTransport {
2✔
5144
            enum class TestState { unknown, location, login, profile_1, refresh, profile_2 };
2✔
5145
            TestingStateMachine<TestState> state{TestState::unknown};
2✔
5146
            void send_request_to_server(const Request& request,
2✔
5147
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5148
            {
10✔
5149
                if (request.url.find("/login") != std::string::npos) {
10✔
5150
                    CHECK(state.get() == TestState::location);
2!
5151
                    state.transition_to(TestState::login);
2✔
5152
                    completion({200, 0, {}, user_json(good_access_token).dump()});
2✔
5153
                }
2✔
5154
                else if (request.url.find("/profile") != std::string::npos) {
8✔
5155
                    auto item = AppUtils::find_header("Authorization", request.headers);
4✔
5156
                    CHECK(item);
4!
5157
                    auto access_token = item->second;
4✔
5158
                    // simulated bad token request
5159
                    if (access_token.find(good_access_token2) != std::string::npos) {
4✔
5160
                        CHECK(state.get() == TestState::refresh);
2!
5161
                        state.transition_to(TestState::profile_2);
2✔
5162
                        completion({200, 0, {}, user_profile_json().dump()});
2✔
5163
                    }
2✔
5164
                    else if (access_token.find(good_access_token) != std::string::npos) {
2✔
5165
                        CHECK(state.get() == TestState::login);
2!
5166
                        state.transition_to(TestState::profile_1);
2✔
5167
                        completion({401, 0, {}});
2✔
5168
                    }
2✔
5169
                }
4✔
5170
                else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) {
4✔
5171
                    CHECK(state.get() == TestState::profile_1);
2!
5172
                    state.transition_to(TestState::refresh);
2✔
5173
                    nlohmann::json json{{"access_token", good_access_token2}};
2✔
5174
                    completion({200, 0, {}, json.dump()});
2✔
5175
                }
2✔
5176
                else if (request.url.find("/location") != std::string::npos) {
2✔
5177
                    CHECK(state.get() == TestState::unknown);
2!
5178
                    state.transition_to(TestState::location);
2✔
5179
                    CHECK(request.method == HttpMethod::get);
2!
5180
                    completion({200,
2✔
5181
                                0,
2✔
5182
                                {},
2✔
5183
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
2✔
5184
                                "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"});
2✔
5185
                }
2✔
5186
                else {
×
5187
                    FAIL("Unexpected request in test code" + request.url);
×
5188
                }
×
5189
            }
10✔
5190
        };
2✔
5191

5192
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5193
        auto app = oas.app();
2✔
5194
        REQUIRE(log_in(app));
2!
5195
    }
2✔
5196
}
6✔
5197

5198
TEST_CASE("app: app released during async operation", "[app][user]") {
10✔
5199
    struct Transport : public UnitTestTransport {
10✔
5200
        std::string endpoint_to_hook;
10✔
5201
        std::optional<Request> stored_request;
10✔
5202
        util::UniqueFunction<void(const Response&)> stored_completion;
10✔
5203

5204
        void send_request_to_server(const Request& request,
10✔
5205
                                    util::UniqueFunction<void(const Response&)>&& completion) override
10✔
5206
        {
38✔
5207
            // Store the completion handler for the chosen endpoint so that we can
5208
            // invoke it after releasing the test's references to the App to
5209
            // verify that it doesn't crash
5210
            if (request.url.find(endpoint_to_hook) != std::string::npos) {
38✔
5211
                REQUIRE_FALSE(stored_request);
10!
5212
                REQUIRE_FALSE(stored_completion);
10!
5213
                stored_request = request;
10✔
5214
                stored_completion = std::move(completion);
10✔
5215
                return;
10✔
5216
            }
10✔
5217

5218
            UnitTestTransport::send_request_to_server(request, std::move(completion));
28✔
5219
        }
28✔
5220

5221
        bool has_stored() const
10✔
5222
        {
20✔
5223
            return !!stored_completion;
20✔
5224
        }
20✔
5225

5226
        void send_stored()
10✔
5227
        {
10✔
5228
            REQUIRE(stored_request);
10!
5229
            REQUIRE(stored_completion);
10!
5230
            UnitTestTransport::send_request_to_server(*stored_request, std::move(stored_completion));
10✔
5231
            stored_request.reset();
10✔
5232
            stored_completion = nullptr;
10✔
5233
        }
10✔
5234
    };
10✔
5235
    auto transport = std::make_shared<Transport>();
10✔
5236
    test_util::TestDirGuard base_path(util::make_temp_dir(), false);
10✔
5237
    AppConfig app_config;
10✔
5238
    set_app_config_defaults(app_config, transport);
10✔
5239
    app_config.base_file_path = base_path;
10✔
5240

5241
    SECTION("login") {
10✔
5242
        transport->endpoint_to_hook = GENERATE("/location", "/login", "/profile");
6✔
5243
        bool called = false;
6✔
5244
        {
6✔
5245
            auto app = App::get_app(App::CacheMode::Disabled, app_config);
6✔
5246
            app->log_in_with_credentials(AppCredentials::anonymous(),
6✔
5247
                                         [&](std::shared_ptr<SyncUser> user, util::Optional<AppError> error) mutable {
6✔
5248
                                             REQUIRE_FALSE(error);
6!
5249
                                             REQUIRE(user);
6!
5250
                                             REQUIRE(user->is_logged_in());
6!
5251
                                             called = true;
6✔
5252
                                         });
6✔
5253
            REQUIRE(transport->has_stored());
6!
5254
        }
6✔
5255
        REQUIRE_FALSE(called);
6!
5256
        transport->send_stored();
6✔
5257
        REQUIRE(called);
6!
5258
    }
6✔
5259

5260
    SECTION("access token refresh") {
10✔
5261
        transport->endpoint_to_hook = "/auth/session";
4✔
5262
        SECTION("directly via user") {
4✔
5263
            bool completion_called = false;
2✔
5264
            {
2✔
5265
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5266
                create_user_and_log_in(app);
2✔
5267
                app->current_user()->refresh_custom_data([&](std::optional<app::AppError> error) {
2✔
5268
                    REQUIRE_FALSE(error);
2!
5269
                    completion_called = true;
2✔
5270
                });
2✔
5271
                REQUIRE(transport->has_stored());
2!
5272
            }
2✔
5273

5274
            REQUIRE_FALSE(completion_called);
2!
5275
            transport->send_stored();
2✔
5276
            REQUIRE(completion_called);
2!
5277
        }
2✔
5278

5279
        SECTION("via sync session") {
4✔
5280
            {
2✔
5281
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5282
                create_user_and_log_in(app);
2✔
5283
                auto user = app->current_user();
2✔
5284
                SyncTestFile config(user, bson::Bson("test"));
2✔
5285
                // give the user an expired access token so that the first use will try to refresh it
5286
                user->update_data_for_testing([](auto& data) {
2✔
5287
                    data.access_token = RealmJWT(encode_fake_jwt("token", 123, 456));
2✔
5288
                });
2✔
5289
                REQUIRE_FALSE(transport->stored_completion);
2!
5290
                auto realm = Realm::get_shared_realm(config);
2✔
5291
                REQUIRE(transport->has_stored());
2!
5292
            }
2✔
5293
            transport->send_stored();
2✔
5294
        }
2✔
5295
    }
4✔
5296

5297
    REQUIRE_FALSE(transport->has_stored());
10!
5298
}
10✔
5299

5300
TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") {
8✔
5301
    constexpr uint64_t timeout_ms = 60000; // this is the default
8✔
5302
    OfflineAppSession oas({std::make_shared<UnitTestTransport>(timeout_ms)});
8✔
5303
    auto app = oas.app();
8✔
5304

5305
    auto user = log_in(app);
8✔
5306

5307
    using Headers = decltype(Request().headers);
8✔
5308

5309
    const auto url_prefix = "https://some.fake.url/api/client/v2.0/app/app_id/functions/call?baas_request="sv;
8✔
5310
    const auto get_request_args = [&](const Request& req) {
8✔
5311
        REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix);
8!
5312
        auto args = req.url.substr(url_prefix.size());
8✔
5313
        if (auto amp = args.find('&'); amp != std::string::npos) {
8✔
5314
            args.resize(amp);
2✔
5315
        }
2✔
5316

5317
        auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args));
8✔
5318
        REQUIRE(!!vec);
8!
5319
        auto parsed = bson::parse({vec->data(), vec->size()});
8✔
5320
        REQUIRE(parsed.type() == bson::Bson::Type::Document);
8!
5321
        auto out = parsed.operator const bson::BsonDocument&();
8✔
5322
        CHECK(out.size() == 3);
8!
5323
        return out;
8✔
5324
    };
8✔
5325

5326
    const auto make_request = [&](std::shared_ptr<User> user, auto&&... args) {
8✔
5327
        auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"});
8✔
5328
        CHECK(req.method == HttpMethod::get);
8!
5329
        CHECK(req.body == "");
8!
5330
        CHECK(req.headers == Headers{{"Accept", "text/event-stream"}});
8!
5331
        CHECK(req.timeout_ms == timeout_ms);
8!
5332

5333
        auto req_args = get_request_args(req);
8✔
5334
        CHECK(req_args["name"] == "func");
8!
5335
        CHECK(req_args["service"] == "svc");
8!
5336
        CHECK(req_args["arguments"] == bson::BsonArray{args...});
8!
5337

5338
        return req;
8✔
5339
    };
8✔
5340

5341
    SECTION("no args") {
8✔
5342
        auto req = make_request(nullptr);
2✔
5343
        CHECK(req.url.find('&') == std::string::npos);
2!
5344
    }
2✔
5345
    SECTION("args") {
8✔
5346
        auto req = make_request(nullptr, "arg1", "arg2");
2✔
5347
        CHECK(req.url.find('&') == std::string::npos);
2!
5348
    }
2✔
5349
    SECTION("percent encoding") {
8✔
5350
        // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded.
5351
        auto req = make_request(nullptr, ">>>>>?????");
2✔
5352

5353
        CHECK(req.url.find('&') == std::string::npos);
2!
5354
        CHECK_THAT(req.url, ContainsSubstring("%2B"));     // + (from >)
2✔
5355
        CHECK_THAT(req.url, ContainsSubstring("%2F"));     // / (from ?)
2✔
5356
        CHECK_THAT(req.url, ContainsSubstring("%3D"));     // = (tail padding)
2✔
5357
        CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding)
2!
5358
    }
2✔
5359
    SECTION("with user") {
8✔
5360
        auto req = make_request(user, "arg1", "arg2");
2✔
5361

5362
        auto amp = req.url.find('&');
2✔
5363
        REQUIRE(amp != std::string::npos);
2!
5364
        auto tail = req.url.substr(amp);
2✔
5365
        REQUIRE(tail == ("&baas_at=" + user->access_token()));
2!
5366
    }
2✔
5367
}
8✔
5368

5369
TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") {
4✔
5370
    SECTION("with empty map") {
4✔
5371
        auto profile = UserProfile(bson::BsonDocument());
2✔
5372
        CHECK(profile.name() == util::none);
2!
5373
        CHECK(profile.email() == util::none);
2!
5374
        CHECK(profile.picture_url() == util::none);
2!
5375
        CHECK(profile.first_name() == util::none);
2!
5376
        CHECK(profile.last_name() == util::none);
2!
5377
        CHECK(profile.gender() == util::none);
2!
5378
        CHECK(profile.birthday() == util::none);
2!
5379
        CHECK(profile.min_age() == util::none);
2!
5380
        CHECK(profile.max_age() == util::none);
2!
5381
    }
2✔
5382
    SECTION("with full map") {
4✔
5383
        auto profile = UserProfile(bson::BsonDocument({
2✔
5384
            {"first_name", "Jan"},
2✔
5385
            {"last_name", "Jaanson"},
2✔
5386
            {"name", "Jan Jaanson"},
2✔
5387
            {"email", "jan.jaanson@jaanson.com"},
2✔
5388
            {"gender", "none"},
2✔
5389
            {"birthday", "January 1, 1970"},
2✔
5390
            {"min_age", "0"},
2✔
5391
            {"max_age", "100"},
2✔
5392
            {"picture_url", "some"},
2✔
5393
        }));
2✔
5394
        CHECK(profile.name() == "Jan Jaanson");
2!
5395
        CHECK(profile.email() == "jan.jaanson@jaanson.com");
2!
5396
        CHECK(profile.picture_url() == "some");
2!
5397
        CHECK(profile.first_name() == "Jan");
2!
5398
        CHECK(profile.last_name() == "Jaanson");
2!
5399
        CHECK(profile.gender() == "none");
2!
5400
        CHECK(profile.birthday() == "January 1, 1970");
2!
5401
        CHECK(profile.min_age() == "0");
2!
5402
        CHECK(profile.max_age() == "100");
2!
5403
    }
2✔
5404
}
4✔
5405

5406
TEST_CASE("app: shared instances", "[sync][app]") {
2✔
5407
    test_util::TestDirGuard test_dir(util::make_temp_dir(), false);
2✔
5408

5409
    AppConfig base_config;
2✔
5410
    set_app_config_defaults(base_config, instance_of<UnitTestTransport>);
2✔
5411
    base_config.base_file_path = test_dir;
2✔
5412

5413
    auto config1 = base_config;
2✔
5414
    config1.app_id = "app1";
2✔
5415

5416
    auto config2 = base_config;
2✔
5417
    config2.app_id = "app1";
2✔
5418
    config2.base_url = std::string(App::default_base_url());
2✔
5419

5420
    auto config3 = base_config;
2✔
5421
    config3.app_id = "app2";
2✔
5422

5423
    auto config4 = base_config;
2✔
5424
    config4.app_id = "app2";
2✔
5425
    config4.base_url = "http://localhost:9090";
2✔
5426

5427
    // should all point to same underlying app
5428
    auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5429
    auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5430
    auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url);
2✔
5431
    auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2);
2✔
5432
    auto app1_5 = App::get_cached_app(config1.app_id);
2✔
5433

5434
    CHECK(app1_1 == app1_2);
2!
5435
    CHECK(app1_1 == app1_3);
2!
5436
    CHECK(app1_1 == app1_4);
2!
5437
    CHECK(app1_1 == app1_5);
2!
5438

5439
    // config3 and config4 should point to different apps
5440
    auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3);
2✔
5441
    auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url);
2✔
5442
    auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4);
2✔
5443
    auto app2_4 = App::get_cached_app(config3.app_id);
2✔
5444
    auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url");
2✔
5445

5446
    CHECK(app2_1 == app2_2);
2!
5447
    CHECK(app2_1 != app2_3);
2!
5448
    CHECK(app2_4 != nullptr);
2!
5449
    CHECK(app2_5 == nullptr);
2!
5450

5451
    CHECK(app1_1 != app2_1);
2!
5452
    CHECK(app1_1 != app2_3);
2!
5453
    CHECK(app1_1 != app2_4);
2!
5454
}
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