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

realm / realm-core / jorgen.edelbo_389

12 Aug 2024 02:13PM UTC coverage: 91.085% (-0.02%) from 91.107%
jorgen.edelbo_389

Pull #7826

Evergreen

jedelbo
Bump file format version
Pull Request #7826: Merge Next major

103458 of 182206 branches covered (56.78%)

3138 of 3500 new or added lines in 53 files covered. (89.66%)

175 existing lines in 17 files now uncovered.

219944 of 241471 relevant lines covered (91.09%)

6840929.52 hits per line

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

99.0
/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]") {
8✔
605
    SECTION("find_header") {
8✔
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") {
8✔
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

669
    SECTION("is_redirect_status_code") {
8✔
670
        // Only MovedPermanently(301) and PermanentRedirect(308) return true
671
        CHECK(AppUtils::is_redirect_status_code(301));
2!
672
        CHECK(AppUtils::is_redirect_status_code(308));
2!
673
        CHECK(!AppUtils::is_redirect_status_code(0));
2!
674
        CHECK(!AppUtils::is_redirect_status_code(200));
2!
675
        CHECK(!AppUtils::is_redirect_status_code(300));
2!
676
        CHECK(!AppUtils::is_redirect_status_code(403));
2!
677
        CHECK(!AppUtils::is_redirect_status_code(99999));
2!
678
    }
2✔
679

680
    SECTION("extract_redir_location") {
8✔
681
        auto comp = AppUtils::extract_redir_location(
2✔
682
            {{"Content-Type", "application/json"}, {"Location", "http://redirect.host"}});
2✔
683
        CHECK(comp == "http://redirect.host");
2!
684
        comp = AppUtils::extract_redir_location({{"location", "http://redirect.host"}});
2✔
685
        CHECK(comp == "http://redirect.host");
2!
686
        comp = AppUtils::extract_redir_location({{"LoCaTiOn", "http://redirect.host/"}});
2✔
687
        CHECK(comp == "http://redirect.host/");
2!
688
        comp = AppUtils::extract_redir_location({{"LOCATION", "http://redirect.host/includes/path"}});
2✔
689
        CHECK(comp == "http://redirect.host/includes/path");
2!
690
        comp = AppUtils::extract_redir_location({{"Content-Type", "application/json"}});
2✔
691
        CHECK(!comp);
2!
692
        comp = AppUtils::extract_redir_location({{"some-location", "http://redirect.host"}});
2✔
693
        CHECK(!comp);
2!
694
        comp = AppUtils::extract_redir_location({{"location", ""}});
2✔
695
        CHECK(!comp);
2!
696
        comp = AppUtils::extract_redir_location({});
2✔
697
        CHECK(!comp);
2!
698
        comp = AppUtils::extract_redir_location({{"location", "bad-server-url"}});
2✔
699
        CHECK(!comp);
2!
700
    }
2✔
701
}
8✔
702

703
// MARK: - Login with Credentials Tests
704

705
TEST_CASE("app: login_with_credentials integration", "[sync][app][user][baas]") {
2✔
706
    SECTION("login") {
2✔
707
        TestAppSession session;
2✔
708
        auto app = session.app();
2✔
709
        app->log_out([](auto) {});
2✔
710

711
        int subscribe_processed = 0;
2✔
712

713
        auto token = app->subscribe([&subscribe_processed](auto&) {
4✔
714
            subscribe_processed++;
4✔
715
        });
4✔
716

717
        REQUIRE_FALSE(app->current_user());
2!
718
        auto user = log_in(app);
2✔
719
        CHECK(!user->device_id().empty());
2!
720
        CHECK(user->has_device_id());
2!
721
        REQUIRE(app->current_user());
2!
722
        CHECK(subscribe_processed == 1);
2!
723

724
        bool processed = false;
2✔
725
        app->log_out([&](auto error) {
2✔
726
            REQUIRE_FALSE(error);
2!
727
            processed = true;
2✔
728
        });
2✔
729
        REQUIRE_FALSE(app->current_user());
2!
730
        CHECK(processed);
2!
731
        CHECK(subscribe_processed == 2);
2!
732

733
        app->unsubscribe(token);
2✔
734
    }
2✔
735
}
2✔
736

737
// MARK: - UsernamePasswordProviderClient Tests
738

739
TEST_CASE("app: UsernamePasswordProviderClient integration", "[sync][app][user][baas]") {
26✔
740
    const std::string base_url = get_real_base_url();
26✔
741
    AutoVerifiedEmailCredentials creds;
26✔
742
    auto email = creds.email;
26✔
743
    auto password = creds.password;
26✔
744

745
    TestAppSession session;
26✔
746
    auto app = session.app();
26✔
747
    auto client = app->provider_client<App::UsernamePasswordProviderClient>();
26✔
748

749
    bool processed = false;
26✔
750

751
    client.register_email(email, password, [&](Optional<AppError> error) {
26✔
752
        CAPTURE(email);
26✔
753
        CAPTURE(password);
26✔
754
        REQUIRE_FALSE(error); // first registration success
26!
755
    });
26✔
756

757
    SECTION("double registration should fail") {
26✔
758
        client.register_email(email, password, [&](Optional<AppError> error) {
2✔
759
            // Error returned states the account has already been created
760
            REQUIRE(error);
2!
761
            CHECK(error->reason() == "name already in use");
2!
762
            CHECK(error->code() == ErrorCodes::AccountNameInUse);
2!
763
            CHECK(!error->link_to_server_logs.empty());
2!
764
            CHECK_THAT(error->link_to_server_logs, ContainsSubstring(base_url));
2✔
765
            processed = true;
2✔
766
        });
2✔
767
        CHECK(processed);
2!
768
    }
2✔
769

770
    SECTION("double registration should fail") {
26✔
771
        // the server registration function will reject emails that do not contain "realm_tests_do_autoverify"
772
        std::string email_to_reject = util::format("%1@%2.com", random_string(10), random_string(10));
2✔
773
        client.register_email(email_to_reject, password, [&](Optional<AppError> error) {
2✔
774
            REQUIRE(error);
2!
775
            CHECK(error->reason() == util::format("failed to confirm user \"%1\"", email_to_reject));
2!
776
            CHECK(error->code() == ErrorCodes::BadRequest);
2!
777
            processed = true;
2✔
778
        });
2✔
779
        CHECK(processed);
2!
780
    }
2✔
781

782
    SECTION("can login with registered account") {
26✔
783
        auto user = log_in(app, creds);
2✔
784
        CHECK(user->user_profile().email() == email);
2!
785
    }
2✔
786

787
    SECTION("cannot login with wrong password") {
26✔
788
        app->log_in_with_credentials(AppCredentials::username_password(email, "boogeyman"),
2✔
789
                                     [&](std::shared_ptr<User> user, Optional<AppError> error) {
2✔
790
                                         CHECK(!user);
2!
791
                                         REQUIRE(error);
2!
792
                                         REQUIRE(error->code() == ErrorCodes::InvalidPassword);
2!
793
                                         processed = true;
2✔
794
                                     });
2✔
795
        CHECK(processed);
2!
796
    }
2✔
797

798
    SECTION("confirm user") {
26✔
799
        client.confirm_user("a_token", "a_token_id", [&](Optional<AppError> error) {
2✔
800
            REQUIRE(error);
2!
801
            CHECK(error->reason() == "invalid token data");
2!
802
            processed = true;
2✔
803
        });
2✔
804
        CHECK(processed);
2!
805
    }
2✔
806

807
    SECTION("resend confirmation email") {
26✔
808
        client.resend_confirmation_email(email, [&](Optional<AppError> error) {
2✔
809
            REQUIRE(error);
2!
810
            CHECK(error->reason() == "already confirmed");
2!
811
            processed = true;
2✔
812
        });
2✔
813
        CHECK(processed);
2!
814
    }
2✔
815

816
    SECTION("reset password invalid tokens") {
26✔
817
        client.reset_password(password, "token_sample", "token_id_sample", [&](Optional<AppError> error) {
2✔
818
            REQUIRE(error);
2!
819
            CHECK(error->reason() == "invalid token data");
2!
820
            CHECK(!error->link_to_server_logs.empty());
2!
821
            CHECK_THAT(error->link_to_server_logs, ContainsSubstring(base_url));
2✔
822
            processed = true;
2✔
823
        });
2✔
824
        CHECK(processed);
2!
825
    }
2✔
826

827
    SECTION("reset password function success") {
26✔
828
        // the imported test app will accept password reset if the password contains "realm_tests_do_reset" via a
829
        // function
830
        std::string accepted_new_password = util::format("realm_tests_do_reset%1", random_string(10));
2✔
831
        client.call_reset_password_function(email, accepted_new_password, {}, [&](Optional<AppError> error) {
2✔
832
            REQUIRE_FALSE(error);
2!
833
            processed = true;
2✔
834
        });
2✔
835
        CHECK(processed);
2!
836
    }
2✔
837

838
    SECTION("reset password function failure") {
26✔
839
        std::string rejected_password = util::format("%1", random_string(10));
2✔
840
        client.call_reset_password_function(email, rejected_password, {"foo", "bar"}, [&](Optional<AppError> error) {
2✔
841
            REQUIRE(error);
2!
842
            CHECK(error->reason() == util::format("failed to reset password for user \"%1\"", email));
2!
843
            CHECK(error->is_service_error());
2!
844
            processed = true;
2✔
845
        });
2✔
846
        CHECK(processed);
2!
847
    }
2✔
848

849
    SECTION("reset password function for invalid user fails") {
26✔
850
        client.call_reset_password_function(util::format("%1@%2.com", random_string(5), random_string(5)), password,
2✔
851
                                            {"foo", "bar"}, [&](Optional<AppError> error) {
2✔
852
                                                REQUIRE(error);
2!
853
                                                CHECK(error->reason() == "user not found");
2!
854
                                                CHECK(error->is_service_error());
2!
855
                                                CHECK(error->code() == ErrorCodes::UserNotFound);
2!
856
                                                processed = true;
2✔
857
                                            });
2✔
858
        CHECK(processed);
2!
859
    }
2✔
860

861
    SECTION("retry custom confirmation") {
26✔
862
        client.retry_custom_confirmation(email, [&](Optional<AppError> error) {
2✔
863
            REQUIRE(error);
2!
864
            CHECK(error->reason() == "already confirmed");
2!
865
            processed = true;
2✔
866
        });
2✔
867
        CHECK(processed);
2!
868
    }
2✔
869

870
    SECTION("retry custom confirmation for invalid user fails") {
26✔
871
        client.retry_custom_confirmation(util::format("%1@%2.com", random_string(5), random_string(5)),
2✔
872
                                         [&](Optional<AppError> error) {
2✔
873
                                             REQUIRE(error);
2!
874
                                             CHECK(error->reason() == "user not found");
2!
875
                                             CHECK(error->is_service_error());
2!
876
                                             CHECK(error->code() == ErrorCodes::UserNotFound);
2!
877
                                             processed = true;
2✔
878
                                         });
2✔
879
        CHECK(processed);
2!
880
    }
2✔
881

882
    SECTION("log in, remove, log in") {
26✔
883
        app->remove_user(app->current_user(), [](auto) {});
2✔
884
        CHECK(app->all_users().size() == 0);
2!
885
        CHECK(app->current_user() == nullptr);
2!
886

887
        auto user = log_in(app, AppCredentials::username_password(email, password));
2✔
888
        CHECK(user->user_profile().email() == email);
2!
889
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
890

891
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
892
            REQUIRE_FALSE(error);
2!
893
        });
2✔
894
        CHECK(user->state() == SyncUser::State::Removed);
2!
895

896
        log_in(app, AppCredentials::username_password(email, password));
2✔
897
        CHECK(user->state() == SyncUser::State::Removed);
2!
898
        CHECK(app->current_user() != user);
2!
899
        user = app->current_user();
2✔
900
        CHECK(user->user_profile().email() == email);
2!
901
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
902

903
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
904
            REQUIRE(!error);
2!
905
            CHECK(app->all_users().size() == 0);
2!
906
            processed = true;
2✔
907
        });
2✔
908

909
        CHECK(user->state() == SyncUser::State::Removed);
2!
910
        CHECK(processed);
2!
911
        CHECK(app->all_users().size() == 0);
2!
912
    }
2✔
913
}
26✔
914

915
// MARK: - UserAPIKeyProviderClient Tests
916

917
TEST_CASE("app: UserAPIKeyProviderClient integration", "[sync][app][api key][baas]") {
6✔
918
    TestAppSession session;
6✔
919
    auto app = session.app();
6✔
920
    auto client = app->provider_client<App::UserAPIKeyProviderClient>();
6✔
921

922
    bool processed = false;
6✔
923
    App::UserAPIKey api_key;
6✔
924

925
    SECTION("api-key") {
6✔
926
        std::shared_ptr<User> logged_in_user = app->current_user();
2✔
927
        auto api_key_name = util::format("%1", random_string(15));
2✔
928
        client.create_api_key(api_key_name, logged_in_user,
2✔
929
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
930
                                  REQUIRE_FALSE(error);
2!
931
                                  CHECK(user_api_key.name == api_key_name);
2!
932
                                  api_key = user_api_key;
2✔
933
                              });
2✔
934

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

941
        client.fetch_api_keys(logged_in_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
942
            CHECK(api_keys.size() == 1);
2!
943
            for (auto key : api_keys) {
2✔
944
                CHECK(key.id.to_string() == api_key.id.to_string());
2!
945
                CHECK(api_key.name == api_key_name);
2!
946
                CHECK(key.id == api_key.id);
2!
947
            }
2✔
948
            REQUIRE_FALSE(error);
2!
949
        });
2✔
950

951
        client.enable_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
952
            REQUIRE_FALSE(error);
2!
953
        });
2✔
954

955
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
956
            REQUIRE_FALSE(error);
2!
957
            CHECK(user_api_key.disabled == false);
2!
958
            CHECK(user_api_key.name == api_key_name);
2!
959
            CHECK(user_api_key.id == api_key.id);
2!
960
        });
2✔
961

962
        client.disable_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
963
            REQUIRE_FALSE(error);
2!
964
        });
2✔
965

966
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
967
            REQUIRE_FALSE(error);
2!
968
            CHECK(user_api_key.disabled == true);
2!
969
            CHECK(user_api_key.name == api_key_name);
2!
970
        });
2✔
971

972
        client.delete_api_key(api_key.id, logged_in_user, [&](Optional<AppError> error) {
2✔
973
            REQUIRE_FALSE(error);
2!
974
        });
2✔
975

976
        client.fetch_api_key(api_key.id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
977
            CHECK(user_api_key.name == "");
2!
978
            CHECK(error);
2!
979
            processed = true;
2✔
980
        });
2✔
981

982
        CHECK(processed);
2!
983
    }
2✔
984

985
    SECTION("api-key without a user") {
6✔
986
        std::shared_ptr<User> no_user = nullptr;
2✔
987
        auto api_key_name = util::format("%1", random_string(15));
2✔
988
        client.create_api_key(api_key_name, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
989
            REQUIRE(error);
2!
990
            CHECK(error->is_service_error());
2!
991
            CHECK(error->reason() == "must authenticate first");
2!
992
            CHECK(user_api_key.name == "");
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.fetch_api_keys(no_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
1003
            REQUIRE(error);
2!
1004
            CHECK(error->is_service_error());
2!
1005
            CHECK(error->reason() == "must authenticate first");
2!
1006
            CHECK(api_keys.size() == 0);
2!
1007
        });
2✔
1008

1009
        client.enable_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
1010
            REQUIRE(error);
2!
1011
            CHECK(error->is_service_error());
2!
1012
            CHECK(error->reason() == "must authenticate first");
2!
1013
        });
2✔
1014

1015
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1016
            REQUIRE(error);
2!
1017
            CHECK(error->is_service_error());
2!
1018
            CHECK(error->reason() == "must authenticate first");
2!
1019
            CHECK(user_api_key.name == "");
2!
1020
        });
2✔
1021

1022
        client.disable_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
1023
            REQUIRE(error);
2!
1024
            CHECK(error->is_service_error());
2!
1025
            CHECK(error->reason() == "must authenticate first");
2!
1026
        });
2✔
1027

1028
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1029
            REQUIRE(error);
2!
1030
            CHECK(error->is_service_error());
2!
1031
            CHECK(error->reason() == "must authenticate first");
2!
1032
            CHECK(user_api_key.name == "");
2!
1033
        });
2✔
1034

1035
        client.delete_api_key(api_key.id, no_user, [&](Optional<AppError> error) {
2✔
1036
            REQUIRE(error);
2!
1037
            CHECK(error->is_service_error());
2!
1038
            CHECK(error->reason() == "must authenticate first");
2!
1039
        });
2✔
1040

1041
        client.fetch_api_key(api_key.id, no_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1042
            CHECK(user_api_key.name == "");
2!
1043
            REQUIRE(error);
2!
1044
            CHECK(error->is_service_error());
2!
1045
            CHECK(error->reason() == "must authenticate first");
2!
1046
            processed = true;
2✔
1047
        });
2✔
1048
        CHECK(processed);
2!
1049
    }
2✔
1050

1051
    SECTION("api-key against the wrong user") {
6✔
1052
        std::shared_ptr<User> first_user = app->current_user();
2✔
1053
        create_user_and_log_in(app);
2✔
1054
        std::shared_ptr<User> second_user = app->current_user();
2✔
1055
        REQUIRE(first_user != second_user);
2!
1056
        auto api_key_name = util::format("%1", random_string(15));
2✔
1057
        App::UserAPIKey api_key;
2✔
1058
        App::UserAPIKeyProviderClient provider = app->provider_client<App::UserAPIKeyProviderClient>();
2✔
1059

1060
        provider.create_api_key(api_key_name, first_user,
2✔
1061
                                [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1062
                                    REQUIRE_FALSE(error);
2!
1063
                                    CHECK(user_api_key.name == api_key_name);
2!
1064
                                    api_key = user_api_key;
2✔
1065
                                });
2✔
1066

1067
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1068
            REQUIRE_FALSE(error);
2!
1069
            CHECK(user_api_key.name == api_key_name);
2!
1070
            CHECK(user_api_key.id.to_string() == user_api_key.id.to_string());
2!
1071
        });
2✔
1072

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

1081
        provider.fetch_api_keys(first_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
1082
            CHECK(api_keys.size() == 1);
2!
1083
            for (auto api_key : api_keys) {
2✔
1084
                CHECK(api_key.name == api_key_name);
2!
1085
            }
2✔
1086
            REQUIRE_FALSE(error);
2!
1087
        });
2✔
1088

1089
        provider.fetch_api_keys(second_user, [&](std::vector<App::UserAPIKey> api_keys, Optional<AppError> error) {
2✔
1090
            CHECK(api_keys.size() == 0);
2!
1091
            REQUIRE_FALSE(error);
2!
1092
        });
2✔
1093

1094
        provider.enable_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
1095
            REQUIRE_FALSE(error);
2!
1096
        });
2✔
1097

1098
        provider.enable_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
1099
            REQUIRE(error);
2!
1100
            CHECK(error->reason() == "API key not found");
2!
1101
            CHECK(error->is_service_error());
2!
1102
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1103
        });
2✔
1104

1105
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1106
            REQUIRE_FALSE(error);
2!
1107
            CHECK(user_api_key.disabled == false);
2!
1108
            CHECK(user_api_key.name == api_key_name);
2!
1109
        });
2✔
1110

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

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

1123
        provider.disable_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
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
        });
2✔
1129

1130
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1131
            REQUIRE_FALSE(error);
2!
1132
            CHECK(user_api_key.disabled == true);
2!
1133
            CHECK(user_api_key.name == api_key_name);
2!
1134
        });
2✔
1135

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

1144
        provider.delete_api_key(api_key.id, second_user, [&](Optional<AppError> error) {
2✔
1145
            REQUIRE(error);
2!
1146
            CHECK(error->reason() == "API key not found");
2!
1147
            CHECK(error->is_service_error());
2!
1148
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1149
        });
2✔
1150

1151
        provider.delete_api_key(api_key.id, first_user, [&](Optional<AppError> error) {
2✔
1152
            REQUIRE_FALSE(error);
2!
1153
        });
2✔
1154

1155
        provider.fetch_api_key(api_key.id, first_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1156
            CHECK(user_api_key.name == "");
2!
1157
            REQUIRE(error);
2!
1158
            CHECK(error->reason() == "API key not found");
2!
1159
            CHECK(error->is_service_error());
2!
1160
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1161
            processed = true;
2✔
1162
        });
2✔
1163

1164
        provider.fetch_api_key(api_key.id, second_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
1165
            CHECK(user_api_key.name == "");
2!
1166
            REQUIRE(error);
2!
1167
            CHECK(error->reason() == "API key not found");
2!
1168
            CHECK(error->is_service_error());
2!
1169
            CHECK(error->code() == ErrorCodes::APIKeyNotFound);
2!
1170
            processed = true;
2✔
1171
        });
2✔
1172

1173
        CHECK(processed);
2!
1174
    }
2✔
1175
}
6✔
1176

1177
// MARK: - Auth Providers Function Tests
1178

1179
TEST_CASE("app: auth providers function integration", "[sync][app][user][baas]") {
2✔
1180
    TestAppSession session;
2✔
1181
    auto app = session.app();
2✔
1182

1183
    SECTION("auth providers function integration") {
2✔
1184
        bson::BsonDocument function_params{{"realmCustomAuthFuncUserId", "123456"}};
2✔
1185
        auto credentials = AppCredentials::function(function_params);
2✔
1186
        auto user = log_in(app, credentials);
2✔
1187
        REQUIRE(user->identities()[0].provider_type == IdentityProviderFunction);
2!
1188
    }
2✔
1189
}
2✔
1190

1191
// MARK: - Link User Tests
1192

1193
TEST_CASE("app: Linking user identities", "[sync][app][user][baas]") {
8✔
1194
    TestAppSession session;
8✔
1195
    auto app = session.app();
8✔
1196
    auto user = log_in(app);
8✔
1197

1198
    AutoVerifiedEmailCredentials creds;
8✔
1199
    app->provider_client<App::UsernamePasswordProviderClient>().register_email(creds.email, creds.password,
8✔
1200
                                                                               [&](Optional<AppError> error) {
8✔
1201
                                                                                   REQUIRE_FALSE(error);
8!
1202
                                                                               });
8✔
1203

1204
    SECTION("anonymous users are reused before they are linked to an identity") {
8✔
1205
        REQUIRE(user == log_in(app));
2!
1206
    }
2✔
1207

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

1212
        app->link_user(user, creds, [&](std::shared_ptr<User> user2, Optional<AppError> error) {
2✔
1213
            REQUIRE_FALSE(error);
2!
1214
            REQUIRE(user == user2);
2!
1215
            REQUIRE(user->identities().size() == 2);
2!
1216
            CHECK(user->identities()[0].provider_type == IdentityProviderAnonymous);
2!
1217
            CHECK(user->identities()[1].provider_type == IdentityProviderUsernamePassword);
2!
1218
        });
2✔
1219
    }
2✔
1220

1221
    SECTION("linking an identity makes the user no longer returned by anonymous logins") {
8✔
1222
        app->link_user(user, creds, [&](std::shared_ptr<User>, Optional<AppError> error) {
2✔
1223
            REQUIRE_FALSE(error);
2!
1224
        });
2✔
1225
        auto user2 = log_in(app);
2✔
1226
        REQUIRE(user != user2);
2!
1227
    }
2✔
1228

1229
    SECTION("existing users are reused when logging in via linked identities") {
8✔
1230
        app->link_user(user, creds, [](std::shared_ptr<User>, Optional<AppError> error) {
2✔
1231
            REQUIRE_FALSE(error);
2!
1232
        });
2✔
1233
        app->log_out([](auto error) {
2✔
1234
            REQUIRE_FALSE(error);
2!
1235
        });
2✔
1236
        REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
1237
        // Should give us the same user instance despite logging in with a
1238
        // different identity
1239
        REQUIRE(user == log_in(app, creds));
2!
1240
        REQUIRE(user->state() == SyncUser::State::LoggedIn);
2!
1241
    }
2✔
1242
}
8✔
1243

1244
// MARK: - Delete User Tests
1245

1246
TEST_CASE("app: delete anonymous user integration", "[sync][app][user][baas]") {
2✔
1247
    TestAppSession session;
2✔
1248
    auto app = session.app();
2✔
1249

1250
    SECTION("delete user expect success") {
2✔
1251
        CHECK(app->all_users().size() == 1);
2!
1252

1253
        // Log in user 1
1254
        auto user_a = app->current_user();
2✔
1255
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
1256
        app->delete_user(user_a, [&](Optional<app::AppError> error) {
2✔
1257
            REQUIRE_FALSE(error);
2!
1258
            // a logged out anon user will be marked as Removed, not LoggedOut
1259
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
1260
        });
2✔
1261
        CHECK(app->all_users().empty());
2!
1262
        CHECK(app->current_user() == nullptr);
2!
1263

1264
        app->delete_user(user_a, [&](Optional<app::AppError> error) {
2✔
1265
            CHECK(error->reason() == "User must be logged in to be deleted.");
2!
1266
            CHECK(app->all_users().size() == 0);
2!
1267
        });
2✔
1268

1269
        // Log in user 2
1270
        auto user_b = log_in(app);
2✔
1271
        CHECK(app->current_user() == user_b);
2!
1272
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
1273
        CHECK(app->all_users().size() == 1);
2!
1274

1275
        app->delete_user(user_b, [&](Optional<app::AppError> error) {
2✔
1276
            REQUIRE_FALSE(error);
2!
1277
            CHECK(app->all_users().size() == 0);
2!
1278
        });
2✔
1279

1280
        CHECK(app->current_user() == nullptr);
2!
1281

1282
        // check both handles are no longer valid
1283
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
1284
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
1285
    }
2✔
1286
}
2✔
1287

1288
TEST_CASE("app: delete user with credentials integration", "[sync][app][user][baas]") {
2✔
1289
    TestAppSession session;
2✔
1290
    auto app = session.app();
2✔
1291
    app->remove_user(app->current_user(), [](auto) {});
2✔
1292

1293
    SECTION("log in and delete") {
2✔
1294
        CHECK(app->all_users().size() == 0);
2!
1295
        CHECK(app->current_user() == nullptr);
2!
1296

1297
        auto credentials = create_user_and_log_in(app);
2✔
1298
        auto user = app->current_user();
2✔
1299

1300
        CHECK(app->current_user() == user);
2!
1301
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
1302
        app->delete_user(user, [&](Optional<app::AppError> error) {
2✔
1303
            REQUIRE_FALSE(error);
2!
1304
            CHECK(app->all_users().size() == 0);
2!
1305
        });
2✔
1306
        CHECK(user->state() == SyncUser::State::Removed);
2!
1307
        CHECK(app->current_user() == nullptr);
2!
1308

1309
        app->log_in_with_credentials(credentials, [](std::shared_ptr<User> user, util::Optional<AppError> error) {
2✔
1310
            CHECK(!user);
2!
1311
            REQUIRE(error);
2!
1312
            REQUIRE(error->code() == ErrorCodes::InvalidPassword);
2!
1313
        });
2✔
1314
        CHECK(app->current_user() == nullptr);
2!
1315

1316
        CHECK(app->all_users().size() == 0);
2!
1317
        app->delete_user(user, [](Optional<app::AppError> err) {
2✔
1318
            CHECK(err->code() > 0);
2!
1319
        });
2✔
1320

1321
        CHECK(app->current_user() == nullptr);
2!
1322
        CHECK(app->all_users().size() == 0);
2!
1323
        CHECK(user->state() == SyncUser::State::Removed);
2!
1324
    }
2✔
1325
}
2✔
1326

1327
// MARK: - Call Function Tests
1328

1329
TEST_CASE("app: call function", "[sync][app][function][baas]") {
2✔
1330
    TestAppSession session;
2✔
1331
    auto app = session.app();
2✔
1332

1333
    bson::BsonArray toSum(5);
2✔
1334
    std::iota(toSum.begin(), toSum.end(), static_cast<int64_t>(1));
2✔
1335
    const auto checkFn = [](Optional<int64_t>&& sum, Optional<AppError>&& error) {
4✔
1336
        REQUIRE(!error);
4!
1337
        CHECK(*sum == 15);
4!
1338
    };
4✔
1339
    app->call_function<int64_t>("sumFunc", toSum, checkFn);
2✔
1340
    app->call_function<int64_t>(app->current_user(), "sumFunc", toSum, checkFn);
2✔
1341
}
2✔
1342

1343
// MARK: - Remote Mongo Client Tests
1344

1345
TEST_CASE("app: remote mongo client", "[sync][app][mongo][baas]") {
16✔
1346
    TestAppSession session;
16✔
1347
    auto app = session.app();
16✔
1348

1349
    auto remote_client = app->current_user()->mongo_client("BackingDB");
16✔
1350
    auto app_session = get_runtime_app_session();
16✔
1351
    auto db = remote_client.db(app_session.config.mongo_dbname);
16✔
1352
    auto dog_collection = db["Dog"];
16✔
1353
    auto cat_collection = db["Cat"];
16✔
1354
    auto person_collection = db["Person"];
16✔
1355

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

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

1360
    auto dog3_object_id = ObjectId::gen();
16✔
1361
    bson::BsonDocument dog_document3{
16✔
1362
        {"_id", dog3_object_id},
16✔
1363
        {"name", "petunia"},
16✔
1364
        {"breed", "french bulldog"},
16✔
1365
    };
16✔
1366

1367
    auto cat_id_string = random_string(10);
16✔
1368
    bson::BsonDocument cat_document{
16✔
1369
        {"_id", cat_id_string},
16✔
1370
        {"name", "luna"},
16✔
1371
        {"breed", "scottish fold"},
16✔
1372
    };
16✔
1373

1374
    bson::BsonDocument person_document{
16✔
1375
        {"firstName", "John"},
16✔
1376
        {"lastName", "Johnson"},
16✔
1377
        {"age", 30},
16✔
1378
    };
16✔
1379

1380
    bson::BsonDocument person_document2{
16✔
1381
        {"firstName", "Bob"},
16✔
1382
        {"lastName", "Johnson"},
16✔
1383
        {"age", 30},
16✔
1384
    };
16✔
1385

1386
    bson::BsonDocument bad_document{{"bad", "value"}};
16✔
1387

1388
    dog_collection.delete_many(dog_document, [&](uint64_t, Optional<AppError> error) {
16✔
1389
        REQUIRE_FALSE(error);
16!
1390
    });
16✔
1391

1392
    dog_collection.delete_many(dog_document2, [&](uint64_t, Optional<AppError> error) {
16✔
1393
        REQUIRE_FALSE(error);
16!
1394
    });
16✔
1395

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

1400
    dog_collection.delete_many(person_document, [&](uint64_t, Optional<AppError> error) {
16✔
1401
        REQUIRE_FALSE(error);
16!
1402
    });
16✔
1403

1404
    dog_collection.delete_many(person_document2, [&](uint64_t, Optional<AppError> error) {
16✔
1405
        REQUIRE_FALSE(error);
16!
1406
    });
16✔
1407

1408
    SECTION("insert") {
16✔
1409
        bool processed = false;
2✔
1410
        ObjectId dog_object_id;
2✔
1411
        ObjectId dog2_object_id;
2✔
1412

1413
        dog_collection.insert_one_bson(bad_document, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1414
            CHECK(error);
2!
1415
            CHECK(!bson);
2!
1416
        });
2✔
1417

1418
        dog_collection.insert_one_bson(dog_document3, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1419
            REQUIRE_FALSE(error);
2!
1420
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1421
            CHECK(static_cast<ObjectId>(bson["insertedId"]) == dog3_object_id);
2!
1422
        });
2✔
1423

1424
        cat_collection.insert_one_bson(cat_document, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1425
            REQUIRE_FALSE(error);
2!
1426
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1427
            CHECK(static_cast<std::string>(bson["insertedId"]) == cat_id_string);
2!
1428
        });
2✔
1429

1430
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1431
            REQUIRE_FALSE(error);
2!
1432
        });
2✔
1433

1434
        cat_collection.delete_one(cat_document, [&](uint64_t, Optional<AppError> error) {
2✔
1435
            REQUIRE_FALSE(error);
2!
1436
        });
2✔
1437

1438
        dog_collection.insert_one(bad_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1439
            CHECK(error);
2!
1440
            CHECK(!object_id);
2!
1441
        });
2✔
1442

1443
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1444
            REQUIRE_FALSE(error);
2!
1445
            CHECK((*object_id).to_string() != "");
2!
1446
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1447
        });
2✔
1448

1449
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1450
            REQUIRE_FALSE(error);
2!
1451
            CHECK((*object_id).to_string() != "");
2!
1452
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1453
        });
2✔
1454

1455
        dog_collection.insert_one(dog_document3, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1456
            REQUIRE_FALSE(error);
2!
1457
            CHECK(object_id->type() == bson::Bson::Type::ObjectId);
2!
1458
            CHECK(static_cast<ObjectId>(*object_id) == dog3_object_id);
2!
1459
        });
2✔
1460

1461
        cat_collection.insert_one(cat_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1462
            REQUIRE_FALSE(error);
2!
1463
            CHECK(object_id->type() == bson::Bson::Type::String);
2!
1464
            CHECK(static_cast<std::string>(*object_id) == cat_id_string);
2!
1465
        });
2✔
1466

1467
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id, dog3_object_id});
2✔
1468
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1469
            REQUIRE_FALSE(error);
2!
1470
            CHECK((*object_id).to_string() != "");
2!
1471
        });
2✔
1472

1473
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1474
            REQUIRE_FALSE(error);
2!
1475
        });
2✔
1476

1477
        cat_collection.delete_one(cat_document, [&](uint64_t, Optional<AppError> error) {
2✔
1478
            REQUIRE_FALSE(error);
2!
1479
        });
2✔
1480

1481
        bson::BsonArray documents{
2✔
1482
            dog_document,
2✔
1483
            dog_document2,
2✔
1484
            dog_document3,
2✔
1485
        };
2✔
1486

1487
        dog_collection.insert_many_bson(documents, [&](Optional<bson::Bson> value, Optional<AppError> error) {
2✔
1488
            REQUIRE_FALSE(error);
2!
1489
            auto bson = static_cast<bson::BsonDocument>(*value);
2✔
1490
            auto insertedIds = static_cast<bson::BsonArray>(bson["insertedIds"]);
2✔
1491
        });
2✔
1492

1493
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1494
            REQUIRE_FALSE(error);
2!
1495
        });
2✔
1496

1497
        dog_collection.insert_many(documents, [&](bson::BsonArray inserted_docs, Optional<AppError> error) {
2✔
1498
            REQUIRE_FALSE(error);
2!
1499
            CHECK(inserted_docs.size() == 3);
2!
1500
            CHECK(inserted_docs[0].type() == bson::Bson::Type::ObjectId);
2!
1501
            CHECK(inserted_docs[1].type() == bson::Bson::Type::ObjectId);
2!
1502
            CHECK(inserted_docs[2].type() == bson::Bson::Type::ObjectId);
2!
1503
            CHECK(static_cast<ObjectId>(inserted_docs[2]) == dog3_object_id);
2!
1504
            processed = true;
2✔
1505
        });
2✔
1506

1507
        CHECK(processed);
2!
1508
    }
2✔
1509

1510
    SECTION("find") {
16✔
1511
        bool processed = false;
2✔
1512

1513
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1514
            REQUIRE_FALSE(error);
2!
1515
            CHECK((*document_array).size() == 0);
2!
1516
        });
2✔
1517

1518
        dog_collection.find_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1519
            REQUIRE_FALSE(error);
2!
1520
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 0);
2!
1521
        });
2✔
1522

1523
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1524
            REQUIRE_FALSE(error);
2!
1525
            CHECK(!document);
2!
1526
        });
2✔
1527

1528
        dog_collection.find_one_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1529
            REQUIRE_FALSE(error);
2!
1530
            CHECK((!bson || bson::holds_alternative<util::None>(*bson)));
2!
1531
        });
2✔
1532

1533
        ObjectId dog_object_id;
2✔
1534
        ObjectId dog2_object_id;
2✔
1535

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

1542
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1543
            REQUIRE_FALSE(error);
2!
1544
            CHECK((*object_id).to_string() != "");
2!
1545
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1546
        });
2✔
1547

1548
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id});
2✔
1549
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1550
            REQUIRE_FALSE(error);
2!
1551
            CHECK((*object_id).to_string() != "");
2!
1552
        });
2✔
1553

1554
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1555
            REQUIRE_FALSE(error);
2!
1556
            CHECK((*documents).size() == 1);
2!
1557
        });
2✔
1558

1559
        dog_collection.find_bson(dog_document, {}, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1560
            REQUIRE_FALSE(error);
2!
1561
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 1);
2!
1562
        });
2✔
1563

1564
        person_collection.find(person_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1565
            REQUIRE_FALSE(error);
2!
1566
            CHECK((*documents).size() == 1);
2!
1567
        });
2✔
1568

1569
        MongoCollection::FindOptions options{
2✔
1570
            2,                                                         // document limit
2✔
1571
            Optional<bson::BsonDocument>({{"name", 1}, {"breed", 1}}), // project
2✔
1572
            Optional<bson::BsonDocument>({{"breed", 1}})               // sort
2✔
1573
        };
2✔
1574

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

1581
        dog_collection.find({{"name", "fido"}}, options,
2✔
1582
                            [&](Optional<bson::BsonArray> document_array, Optional<AppError> error) {
2✔
1583
                                REQUIRE_FALSE(error);
2!
1584
                                CHECK((*document_array).size() == 1);
2!
1585
                                auto king_charles = static_cast<bson::BsonDocument>((*document_array)[0]);
2✔
1586
                                CHECK(king_charles["breed"] == "king charles");
2!
1587
                            });
2✔
1588

1589
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1590
            REQUIRE_FALSE(error);
2!
1591
            auto name = (*document)["name"];
2✔
1592
            CHECK(name == "fido");
2!
1593
        });
2✔
1594

1595
        dog_collection.find_one(dog_document, options,
2✔
1596
                                [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1597
                                    REQUIRE_FALSE(error);
2!
1598
                                    auto name = (*document)["name"];
2✔
1599
                                    CHECK(name == "fido");
2!
1600
                                });
2✔
1601

1602
        dog_collection.find_one_bson(dog_document, options, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1603
            REQUIRE_FALSE(error);
2!
1604
            auto name = (static_cast<bson::BsonDocument>(*bson))["name"];
2✔
1605
            CHECK(name == "fido");
2!
1606
        });
2✔
1607

1608
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1609
            REQUIRE_FALSE(error);
2!
1610
            CHECK((*documents).size() == 1);
2!
1611
        });
2✔
1612

1613
        dog_collection.find_one_and_delete(dog_document,
2✔
1614
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1615
                                               REQUIRE_FALSE(error);
2!
1616
                                               REQUIRE(document);
2!
1617
                                           });
2✔
1618

1619
        dog_collection.find_one_and_delete({{}},
2✔
1620
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1621
                                               REQUIRE_FALSE(error);
2!
1622
                                               REQUIRE(document);
2!
1623
                                           });
2✔
1624

1625
        dog_collection.find_one_and_delete({{"invalid", "key"}},
2✔
1626
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1627
                                               REQUIRE_FALSE(error);
2!
1628
                                               CHECK(!document);
2!
1629
                                           });
2✔
1630

1631
        dog_collection.find_one_and_delete_bson({{"invalid", "key"}}, {},
2✔
1632
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1633
                                                    REQUIRE_FALSE(error);
2!
1634
                                                    CHECK((!bson || bson::holds_alternative<util::None>(*bson)));
2!
1635
                                                });
2✔
1636

1637
        dog_collection.find(dog_document, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1638
            REQUIRE_FALSE(error);
2!
1639
            CHECK((*documents).size() == 0);
2!
1640
            processed = true;
2✔
1641
        });
2✔
1642

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

1646
    SECTION("count and aggregate") {
16✔
1647
        bool processed = false;
2✔
1648

1649
        ObjectId dog_object_id;
2✔
1650
        ObjectId dog2_object_id;
2✔
1651

1652
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1653
            REQUIRE_FALSE(error);
2!
1654
            CHECK((*object_id).to_string() != "");
2!
1655
        });
2✔
1656

1657
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1658
            REQUIRE_FALSE(error);
2!
1659
            CHECK((*object_id).to_string() != "");
2!
1660
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1661
        });
2✔
1662

1663
        dog_collection.insert_one(dog_document2, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1664
            REQUIRE_FALSE(error);
2!
1665
            CHECK((*object_id).to_string() != "");
2!
1666
            dog2_object_id = static_cast<ObjectId>(*object_id);
2✔
1667
        });
2✔
1668

1669
        person_document["dogs"] = bson::BsonArray({dog_object_id, dog2_object_id});
2✔
1670
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1671
            REQUIRE_FALSE(error);
2!
1672
            CHECK((*object_id).to_string() != "");
2!
1673
        });
2✔
1674

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

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

1679
        bson::BsonArray pipeline{match, group};
2✔
1680

1681
        dog_collection.aggregate(pipeline, [&](Optional<bson::BsonArray> documents, Optional<AppError> error) {
2✔
1682
            REQUIRE_FALSE(error);
2!
1683
            CHECK((*documents).size() == 1);
2!
1684
        });
2✔
1685

1686
        dog_collection.aggregate_bson(pipeline, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1687
            REQUIRE_FALSE(error);
2!
1688
            CHECK(static_cast<bson::BsonArray>(*bson).size() == 1);
2!
1689
        });
2✔
1690

1691
        dog_collection.count({{"breed", "king charles"}}, [&](uint64_t count, Optional<AppError> error) {
2✔
1692
            REQUIRE_FALSE(error);
2!
1693
            CHECK(count == 2);
2!
1694
        });
2✔
1695

1696
        dog_collection.count_bson({{"breed", "king charles"}}, 0,
2✔
1697
                                  [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1698
                                      REQUIRE_FALSE(error);
2!
1699
                                      CHECK(static_cast<int64_t>(*bson) == 2);
2!
1700
                                  });
2✔
1701

1702
        dog_collection.count({{"breed", "french bulldog"}}, [&](uint64_t count, Optional<AppError> error) {
2✔
1703
            REQUIRE_FALSE(error);
2!
1704
            CHECK(count == 1);
2!
1705
        });
2✔
1706

1707
        dog_collection.count({{"breed", "king charles"}}, 1, [&](uint64_t count, Optional<AppError> error) {
2✔
1708
            REQUIRE_FALSE(error);
2!
1709
            CHECK(count == 1);
2!
1710
        });
2✔
1711

1712
        person_collection.count(
2✔
1713
            {{"firstName", "John"}, {"lastName", "Johnson"}, {"age", bson::BsonDocument({{"$gt", 25}})}}, 1,
2✔
1714
            [&](uint64_t count, Optional<AppError> error) {
2✔
1715
                REQUIRE_FALSE(error);
2!
1716
                CHECK(count == 1);
2!
1717
                processed = true;
2✔
1718
            });
2✔
1719

1720
        CHECK(processed);
2!
1721
    }
2✔
1722

1723
    SECTION("find and update") {
16✔
1724
        bool processed = false;
2✔
1725

1726
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1727
            Optional<bson::BsonDocument>({{"name", 1}, {"breed", 1}}), // project
2✔
1728
            Optional<bson::BsonDocument>({{"name", 1}}),               // sort,
2✔
1729
            true,                                                      // upsert
2✔
1730
            true                                                       // return new doc
2✔
1731
        };
2✔
1732

1733
        dog_collection.find_one_and_update(dog_document, dog_document2,
2✔
1734
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1735
                                               REQUIRE_FALSE(error);
2!
1736
                                               CHECK(!document);
2!
1737
                                           });
2✔
1738

1739
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1740
            REQUIRE_FALSE(error);
2!
1741
            CHECK((*object_id).to_string() != "");
2!
1742
        });
2✔
1743

1744
        dog_collection.find_one_and_update(dog_document, dog_document2, find_and_modify_options,
2✔
1745
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1746
                                               REQUIRE_FALSE(error);
2!
1747
                                               auto breed = static_cast<std::string>((*document)["breed"]);
2✔
1748
                                               CHECK(breed == "french bulldog");
2!
1749
                                           });
2✔
1750

1751
        dog_collection.find_one_and_update(dog_document2, dog_document, find_and_modify_options,
2✔
1752
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1753
                                               REQUIRE_FALSE(error);
2!
1754
                                               auto breed = static_cast<std::string>((*document)["breed"]);
2✔
1755
                                               CHECK(breed == "king charles");
2!
1756
                                           });
2✔
1757

1758
        dog_collection.find_one_and_update_bson(dog_document, dog_document2, find_and_modify_options,
2✔
1759
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1760
                                                    REQUIRE_FALSE(error);
2!
1761
                                                    auto breed = static_cast<std::string>(
2✔
1762
                                                        static_cast<bson::BsonDocument>(*bson)["breed"]);
2✔
1763
                                                    CHECK(breed == "french bulldog");
2!
1764
                                                });
2✔
1765

1766
        dog_collection.find_one_and_update_bson(dog_document2, dog_document, find_and_modify_options,
2✔
1767
                                                [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1768
                                                    REQUIRE_FALSE(error);
2!
1769
                                                    auto breed = static_cast<std::string>(
2✔
1770
                                                        static_cast<bson::BsonDocument>(*bson)["breed"]);
2✔
1771
                                                    CHECK(breed == "king charles");
2!
1772
                                                });
2✔
1773

1774
        dog_collection.find_one_and_update({{"name", "invalid name"}}, {{"name", "some name"}},
2✔
1775
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1776
                                               REQUIRE_FALSE(error);
2!
1777
                                               CHECK(!document);
2!
1778
                                               processed = true;
2✔
1779
                                           });
2✔
1780
        CHECK(processed);
2!
1781
        processed = false;
2✔
1782

1783
        dog_collection.find_one_and_update({{"name", "invalid name"}}, {{}}, find_and_modify_options,
2✔
1784
                                           [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1785
                                               REQUIRE(error);
2!
1786
                                               CHECK(error->reason() == "insert not permitted");
2!
1787
                                               CHECK(!document);
2!
1788
                                               processed = true;
2✔
1789
                                           });
2✔
1790
        CHECK(processed);
2!
1791
    }
2✔
1792

1793
    SECTION("update") {
16✔
1794
        bool processed = false;
2✔
1795
        ObjectId dog_object_id;
2✔
1796

1797
        dog_collection.update_one(dog_document, dog_document2, true,
2✔
1798
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1799
                                      REQUIRE_FALSE(error);
2!
1800
                                      CHECK((*result.upserted_id).to_string() != "");
2!
1801
                                  });
2✔
1802

1803
        dog_collection.update_one(dog_document2, dog_document,
2✔
1804
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1805
                                      REQUIRE_FALSE(error);
2!
1806
                                      CHECK(!result.upserted_id);
2!
1807
                                  });
2✔
1808

1809
        cat_collection.update_one({}, cat_document, true,
2✔
1810
                                  [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1811
                                      REQUIRE_FALSE(error);
2!
1812
                                      CHECK(result.upserted_id->type() == bson::Bson::Type::String);
2!
1813
                                      CHECK(result.upserted_id == cat_id_string);
2!
1814
                                  });
2✔
1815

1816
        dog_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1817
            REQUIRE_FALSE(error);
2!
1818
        });
2✔
1819

1820
        cat_collection.delete_many({}, [&](uint64_t, Optional<AppError> error) {
2✔
1821
            REQUIRE_FALSE(error);
2!
1822
        });
2✔
1823

1824
        dog_collection.update_one_bson(dog_document, dog_document2, true,
2✔
1825
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1826
                                           REQUIRE_FALSE(error);
2!
1827
                                           auto upserted_id = static_cast<bson::BsonDocument>(*bson)["upsertedId"];
2✔
1828

1829
                                           REQUIRE(upserted_id.type() == bson::Bson::Type::ObjectId);
2!
1830
                                       });
2✔
1831

1832
        dog_collection.update_one_bson(dog_document2, dog_document, true,
2✔
1833
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1834
                                           REQUIRE_FALSE(error);
2!
1835
                                           auto document = static_cast<bson::BsonDocument>(*bson);
2✔
1836
                                           auto foundUpsertedId = document.find("upsertedId");
2✔
1837
                                           REQUIRE(!foundUpsertedId);
2!
1838
                                       });
2✔
1839

1840
        cat_collection.update_one_bson({}, cat_document, true,
2✔
1841
                                       [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
1842
                                           REQUIRE_FALSE(error);
2!
1843
                                           auto upserted_id = static_cast<bson::BsonDocument>(*bson)["upsertedId"];
2✔
1844
                                           REQUIRE(upserted_id.type() == bson::Bson::Type::String);
2!
1845
                                           REQUIRE(upserted_id == cat_id_string);
2!
1846
                                       });
2✔
1847

1848
        person_document["dogs"] = bson::BsonArray();
2✔
1849
        bson::BsonDocument person_document_copy = bson::BsonDocument(person_document);
2✔
1850
        person_document_copy["dogs"] = bson::BsonArray({dog_object_id});
2✔
1851
        person_collection.update_one(person_document, person_document, true,
2✔
1852
                                     [&](MongoCollection::UpdateResult, Optional<AppError> error) {
2✔
1853
                                         REQUIRE_FALSE(error);
2!
1854
                                         processed = true;
2✔
1855
                                     });
2✔
1856

1857
        CHECK(processed);
2!
1858
    }
2✔
1859

1860
    SECTION("update many") {
16✔
1861
        bool processed = false;
2✔
1862

1863
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1864
            REQUIRE_FALSE(error);
2!
1865
            CHECK((*object_id).to_string() != "");
2!
1866
        });
2✔
1867

1868
        dog_collection.update_many(dog_document2, dog_document, true,
2✔
1869
                                   [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1870
                                       REQUIRE_FALSE(error);
2!
1871
                                       CHECK((*result.upserted_id).to_string() != "");
2!
1872
                                   });
2✔
1873

1874
        dog_collection.update_many(dog_document2, dog_document,
2✔
1875
                                   [&](MongoCollection::UpdateResult result, Optional<AppError> error) {
2✔
1876
                                       REQUIRE_FALSE(error);
2!
1877
                                       CHECK(!result.upserted_id);
2!
1878
                                       processed = true;
2✔
1879
                                   });
2✔
1880

1881
        CHECK(processed);
2!
1882
    }
2✔
1883

1884
    SECTION("find and replace") {
16✔
1885
        bool processed = false;
2✔
1886
        ObjectId dog_object_id;
2✔
1887
        ObjectId person_object_id;
2✔
1888

1889
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1890
            Optional<bson::BsonDocument>({{"name", "fido"}}), // project
2✔
1891
            Optional<bson::BsonDocument>({{"name", 1}}),      // sort,
2✔
1892
            true,                                             // upsert
2✔
1893
            true                                              // return new doc
2✔
1894
        };
2✔
1895

1896
        dog_collection.find_one_and_replace(dog_document, dog_document2,
2✔
1897
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1898
                                                REQUIRE_FALSE(error);
2!
1899
                                                CHECK(!document);
2!
1900
                                            });
2✔
1901

1902
        dog_collection.insert_one(dog_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1903
            REQUIRE_FALSE(error);
2!
1904
            CHECK((*object_id).to_string() != "");
2!
1905
            dog_object_id = static_cast<ObjectId>(*object_id);
2✔
1906
        });
2✔
1907

1908
        dog_collection.find_one_and_replace(dog_document, dog_document2,
2✔
1909
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1910
                                                REQUIRE_FALSE(error);
2!
1911
                                                auto name = static_cast<std::string>((*document)["name"]);
2✔
1912
                                                CHECK(name == "fido");
2!
1913
                                            });
2✔
1914

1915
        dog_collection.find_one_and_replace(dog_document2, dog_document, find_and_modify_options,
2✔
1916
                                            [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1917
                                                REQUIRE_FALSE(error);
2!
1918
                                                auto name = static_cast<std::string>((*document)["name"]);
2✔
1919
                                                CHECK(static_cast<std::string>(name) == "fido");
2!
1920
                                            });
2✔
1921

1922
        person_document["dogs"] = bson::BsonArray({dog_object_id});
2✔
1923
        person_document2["dogs"] = bson::BsonArray({dog_object_id});
2✔
1924
        person_collection.insert_one(person_document, [&](Optional<bson::Bson> object_id, Optional<AppError> error) {
2✔
1925
            REQUIRE_FALSE(error);
2!
1926
            CHECK((*object_id).to_string() != "");
2!
1927
            person_object_id = static_cast<ObjectId>(*object_id);
2✔
1928
        });
2✔
1929

1930
        MongoCollection::FindOneAndModifyOptions person_find_and_modify_options{
2✔
1931
            Optional<bson::BsonDocument>({{"firstName", 1}}), // project
2✔
1932
            Optional<bson::BsonDocument>({{"firstName", 1}}), // sort,
2✔
1933
            false,                                            // upsert
2✔
1934
            true                                              // return new doc
2✔
1935
        };
2✔
1936

1937
        person_collection.find_one_and_replace(person_document, person_document2,
2✔
1938
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1939
                                                   REQUIRE_FALSE(error);
2!
1940
                                                   auto name = static_cast<std::string>((*document)["firstName"]);
2✔
1941
                                                   // Should return the old document
1942
                                                   CHECK(name == "John");
2!
1943
                                                   processed = true;
2✔
1944
                                               });
2✔
1945

1946
        person_collection.find_one_and_replace(person_document2, person_document, person_find_and_modify_options,
2✔
1947
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1948
                                                   REQUIRE_FALSE(error);
2!
1949
                                                   auto name = static_cast<std::string>((*document)["firstName"]);
2✔
1950
                                                   // Should return new document, Bob -> John
1951
                                                   CHECK(name == "John");
2!
1952
                                               });
2✔
1953

1954
        person_collection.find_one_and_replace({{"invalid", "item"}}, {{}},
2✔
1955
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1956
                                                   // If a document is not found then null will be returned for the
1957
                                                   // document and no error will be returned
1958
                                                   REQUIRE_FALSE(error);
2!
1959
                                                   CHECK(!document);
2!
1960
                                               });
2✔
1961

1962
        person_collection.find_one_and_replace({{"invalid", "item"}}, {{}}, person_find_and_modify_options,
2✔
1963
                                               [&](Optional<bson::BsonDocument> document, Optional<AppError> error) {
2✔
1964
                                                   REQUIRE_FALSE(error);
2!
1965
                                                   CHECK(!document);
2!
1966
                                                   processed = true;
2✔
1967
                                               });
2✔
1968

1969
        CHECK(processed);
2!
1970
    }
2✔
1971

1972
    SECTION("delete") {
16✔
1973

1974
        bool processed = false;
2✔
1975

1976
        bson::BsonArray documents;
2✔
1977
        documents.push_back(dog_document);
2✔
1978
        documents.push_back(dog_document);
2✔
1979
        documents.push_back(dog_document);
2✔
1980

1981
        dog_collection.insert_many(documents, [&](bson::BsonArray inserted_docs, Optional<AppError> error) {
2✔
1982
            REQUIRE_FALSE(error);
2!
1983
            CHECK(inserted_docs.size() == 3);
2!
1984
        });
2✔
1985

1986
        MongoCollection::FindOneAndModifyOptions find_and_modify_options{
2✔
1987
            Optional<bson::BsonDocument>({{"name", "fido"}}), // project
2✔
1988
            Optional<bson::BsonDocument>({{"name", 1}}),      // sort,
2✔
1989
            true,                                             // upsert
2✔
1990
            true                                              // return new doc
2✔
1991
        };
2✔
1992

1993
        dog_collection.delete_one(dog_document, [&](uint64_t deleted_count, Optional<AppError> error) {
2✔
1994
            REQUIRE_FALSE(error);
2!
1995
            CHECK(deleted_count >= 1);
2!
1996
        });
2✔
1997

1998
        dog_collection.delete_many(dog_document, [&](uint64_t deleted_count, Optional<AppError> error) {
2✔
1999
            REQUIRE_FALSE(error);
2!
2000
            CHECK(deleted_count >= 1);
2!
2001
            processed = true;
2✔
2002
        });
2✔
2003

2004
        person_collection.delete_many_bson(person_document, [&](Optional<bson::Bson> bson, Optional<AppError> error) {
2✔
2005
            REQUIRE_FALSE(error);
2!
2006
            CHECK(static_cast<int32_t>(static_cast<bson::BsonDocument>(*bson)["deletedCount"]) >= 1);
2!
2007
            processed = true;
2✔
2008
        });
2✔
2009

2010
        CHECK(processed);
2!
2011
    }
2✔
2012
}
16✔
2013

2014
// MARK: - Push Notifications Tests
2015

2016
TEST_CASE("app: push notifications", "[sync][app][notifications][baas]") {
8✔
2017
    TestAppSession session;
8✔
2018
    auto app = session.app();
8✔
2019
    std::shared_ptr<User> sync_user = app->current_user();
8✔
2020

2021
    SECTION("register") {
8✔
2022
        bool processed;
2✔
2023

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

2029
        CHECK(processed);
2!
2030
    }
2✔
2031
    /*
2032
        // FIXME: It seems this test fails when the two register_device calls are invoked too quickly,
2033
        // The error returned will be 'Device not found' on the second register_device call.
2034
        SECTION("register twice") {
2035
            // registering the same device twice should not result in an error
2036
            bool processed;
2037

2038
            app->push_notification_client("gcm").register_device("hello",
2039
                                                                 sync_user,
2040
                                                                 [&](Optional<AppError> error) {
2041
                REQUIRE_FALSE(error);
2042
            });
2043

2044
            app->push_notification_client("gcm").register_device("hello",
2045
                                                                 sync_user,
2046
                                                                 [&](Optional<AppError> error) {
2047
                REQUIRE_FALSE(error);
2048
                processed = true;
2049
            });
2050

2051
            CHECK(processed);
2052
        }
2053
    */
2054
    SECTION("deregister") {
8✔
2055
        bool processed;
2✔
2056

2057
        app->push_notification_client("gcm").deregister_device(sync_user, [&](Optional<AppError> error) {
2✔
2058
            REQUIRE_FALSE(error);
2!
2059
            processed = true;
2✔
2060
        });
2✔
2061
        CHECK(processed);
2!
2062
    }
2✔
2063

2064
    SECTION("register with unavailable service") {
8✔
2065
        bool processed;
2✔
2066

2067
        app->push_notification_client("gcm_blah").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
2068
            REQUIRE(error);
2!
2069
            CHECK(error->reason() == "service not found: 'gcm_blah'");
2!
2070
            processed = true;
2✔
2071
        });
2✔
2072
        CHECK(processed);
2!
2073
    }
2✔
2074

2075
    SECTION("register with logged out user") {
8✔
2076
        bool processed;
2✔
2077

2078
        app->log_out([=](Optional<AppError> error) {
2✔
2079
            REQUIRE_FALSE(error);
2!
2080
        });
2✔
2081

2082
        app->push_notification_client("gcm").register_device("hello", sync_user, [&](Optional<AppError> error) {
2✔
2083
            REQUIRE(error);
2!
2084
            processed = true;
2✔
2085
        });
2✔
2086

2087
        app->push_notification_client("gcm").register_device("hello", nullptr, [&](Optional<AppError> error) {
2✔
2088
            REQUIRE(error);
2!
2089
            processed = true;
2✔
2090
        });
2✔
2091

2092
        CHECK(processed);
2!
2093
    }
2✔
2094
}
8✔
2095

2096
// MARK: - Token refresh
2097

2098
TEST_CASE("app: token refresh", "[sync][app][token][baas]") {
2✔
2099
    TestAppSession session;
2✔
2100
    auto app = session.app();
2✔
2101
    std::shared_ptr<User> sync_user = app->current_user();
2✔
2102
    sync_user->update_data_for_testing([](UserData& data) {
2✔
2103
        data.access_token = RealmJWT(ENCODE_FAKE_JWT("fake_access_token"));
2✔
2104
    });
2✔
2105

2106
    auto remote_client = app->current_user()->mongo_client("BackingDB");
2✔
2107
    auto app_session = get_runtime_app_session();
2✔
2108
    auto db = remote_client.db(app_session.config.mongo_dbname);
2✔
2109
    auto dog_collection = db["Dog"];
2✔
2110
    bson::BsonDocument dog_document{{"name", "fido"}, {"breed", "king charles"}};
2✔
2111

2112
    SECTION("access token should refresh") {
2✔
2113
        /*
2114
         Expected sequence of events:
2115
         - `find_one` tries to hit the server with a bad access token
2116
         - Server returns an error because of the bad token, error should be something like:
2117
            {\"error\":\"json: cannot unmarshal array into Go value of type map[string]interface
2118
         {}\",\"link\":\"http://localhost:9090/groups/5f84167e776aa0f9dc27081a/apps/5f841686776aa0f9dc270876/logs?co_id=5f844c8c776aa0f9dc273db6\"}
2119
            http_status_code = 401
2120
            custom_status_code = 0
2121
         - App::handle_auth_failure is then called and an attempt to refresh the access token will be peformed.
2122
         - If the token refresh was successful, the original request will retry and we should expect no error in the
2123
         callback of `find_one`
2124
         */
2125
        dog_collection.find_one(dog_document, [&](Optional<bson::BsonDocument>, Optional<AppError> error) {
2✔
2126
            REQUIRE_FALSE(error);
2!
2127
        });
2✔
2128
    }
2✔
2129
}
2✔
2130

2131
// MARK: - Sync Tests
2132

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

2136
    Schema schema{
2✔
2137
        {"TopLevel",
2✔
2138
         {
2✔
2139
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2140
             {"mixed_array", PropertyType::Mixed | PropertyType::Array | PropertyType::Nullable},
2✔
2141
         }},
2✔
2142
        {"Target",
2✔
2143
         {
2✔
2144
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2145
             {"value", PropertyType::Int},
2✔
2146
         }},
2✔
2147
    };
2✔
2148

2149
    auto server_app_config = minimal_app_config("set_new_embedded_object", schema);
2✔
2150
    auto app_session = create_app(server_app_config);
2✔
2151
    auto partition = random_string(100);
2✔
2152

2153
    auto obj_id = ObjectId::gen();
2✔
2154
    auto target_id = ObjectId::gen();
2✔
2155
    auto mixed_list_values = AnyVector{
2✔
2156
        Mixed{int64_t(1234)},
2✔
2157
        Mixed{},
2✔
2158
        Mixed{target_id},
2✔
2159
    };
2✔
2160
    {
2✔
2161
        TestAppSession test_session(app_session, nullptr, DeleteApp{false});
2✔
2162
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2163
        auto realm = Realm::get_shared_realm(config);
2✔
2164

2165
        CppContext c(realm);
2✔
2166
        realm->begin_transaction();
2✔
2167
        auto target_obj = Object::create(
2✔
2168
            c, realm, "Target", std::any(AnyDict{{valid_pk_name, target_id}, {"value", static_cast<int64_t>(1234)}}));
2✔
2169
        mixed_list_values.push_back(Mixed(target_obj.get_obj().get_link()));
2✔
2170

2171
        Object::create(c, realm, "TopLevel",
2✔
2172
                       std::any(AnyDict{
2✔
2173
                           {valid_pk_name, obj_id},
2✔
2174
                           {"mixed_array", mixed_list_values},
2✔
2175
                       }),
2✔
2176
                       CreatePolicy::ForceCreate);
2✔
2177
        realm->commit_transaction();
2✔
2178
        CHECK(!wait_for_upload(*realm));
2!
2179
    }
2✔
2180

2181
    {
2✔
2182
        TestAppSession test_session(app_session);
2✔
2183
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2184
        auto realm = Realm::get_shared_realm(config);
2✔
2185

2186
        CHECK(!wait_for_download(*realm));
2!
2187
        CppContext c(realm);
2✔
2188
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{obj_id});
2✔
2189
        auto list = util::any_cast<List&&>(obj.get_property_value<std::any>(c, "mixed_array"));
2✔
2190
        for (size_t idx = 0; idx < list.size(); ++idx) {
10✔
2191
            Mixed mixed = list.get_any(idx);
8✔
2192
            if (idx == 3) {
8✔
2193
                CHECK(mixed.is_type(type_TypedLink));
2!
2194
                auto link = mixed.get<ObjLink>();
2✔
2195
                auto link_table = realm->read_group().get_table(link.get_table_key());
2✔
2196
                CHECK(link_table->get_name() == "class_Target");
2!
2197
                auto link_obj = link_table->get_object(link.get_obj_key());
2✔
2198
                CHECK(link_obj.get_primary_key() == target_id);
2!
2199
            }
2✔
2200
            else {
6✔
2201
                CHECK(mixed == util::any_cast<Mixed>(mixed_list_values[idx]));
6!
2202
            }
6✔
2203
        }
8✔
2204
    }
2✔
2205
}
2✔
2206

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

2210
    Schema schema{
2✔
2211
        {"TopLevel",
2✔
2212
         {
2✔
2213
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2214
             {"decimal", PropertyType::Decimal | PropertyType::Nullable},
2✔
2215
         }},
2✔
2216
    };
2✔
2217

2218
    auto server_app_config = minimal_app_config("roundtrip_values", schema);
2✔
2219
    auto app_session = create_app(server_app_config);
2✔
2220
    auto partition = random_string(100);
2✔
2221

2222
    Decimal128 large_significand = Decimal128(70) / Decimal128(1.09);
2✔
2223
    auto obj_id = ObjectId::gen();
2✔
2224
    {
2✔
2225
        TestAppSession test_session(app_session, nullptr, DeleteApp{false});
2✔
2226
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2227
        auto realm = Realm::get_shared_realm(config);
2✔
2228

2229
        CppContext c(realm);
2✔
2230
        realm->begin_transaction();
2✔
2231
        Object::create(c, realm, "TopLevel",
2✔
2232
                       util::Any(AnyDict{
2✔
2233
                           {valid_pk_name, obj_id},
2✔
2234
                           {"decimal", large_significand},
2✔
2235
                       }),
2✔
2236
                       CreatePolicy::ForceCreate);
2✔
2237
        realm->commit_transaction();
2✔
2238
        CHECK(!wait_for_upload(*realm, std::chrono::seconds(600)));
2!
2239
    }
2✔
2240

2241
    {
2✔
2242
        TestAppSession test_session(app_session);
2✔
2243
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2244
        auto realm = Realm::get_shared_realm(config);
2✔
2245

2246
        CHECK(!wait_for_download(*realm));
2!
2247
        CppContext c(realm);
2✔
2248
        auto obj = Object::get_for_primary_key(c, realm, "TopLevel", util::Any{obj_id});
2✔
2249
        auto val = obj.get_column_value<Decimal128>("decimal");
2✔
2250
        CHECK(val == large_significand);
2!
2251
    }
2✔
2252
}
2✔
2253

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

2257
    Schema schema{
4✔
2258
        {"origin",
4✔
2259
         {{valid_pk_name, PropertyType::Int, Property::IsPrimary{true}},
4✔
2260
          {"link", PropertyType::Object | PropertyType::Nullable, "target"},
4✔
2261
          {"embedded_link", PropertyType::Object | PropertyType::Nullable, "embedded"}}},
4✔
2262
        {"target",
4✔
2263
         {{valid_pk_name, PropertyType::String, Property::IsPrimary{true}},
4✔
2264
          {"value", PropertyType::Int},
4✔
2265
          {"name", PropertyType::String}}},
4✔
2266
        {"other_origin",
4✔
2267
         {{valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
2268
          {"array", PropertyType::Array | PropertyType::Object, "other_target"}}},
4✔
2269
        {"other_target",
4✔
2270
         {{valid_pk_name, PropertyType::UUID, Property::IsPrimary{true}}, {"value", PropertyType::Int}}},
4✔
2271
        {"embedded", ObjectSchema::ObjectType::Embedded, {{"name", PropertyType::String | PropertyType::Nullable}}},
4✔
2272
    };
4✔
2273

2274
    /*             Create local realm             */
2275
    TestFile local_config;
4✔
2276
    local_config.schema = schema;
4✔
2277
    auto local_realm = Realm::get_shared_realm(local_config);
4✔
2278
    {
4✔
2279
        auto origin = local_realm->read_group().get_table("class_origin");
4✔
2280
        auto target = local_realm->read_group().get_table("class_target");
4✔
2281
        auto other_origin = local_realm->read_group().get_table("class_other_origin");
4✔
2282
        auto other_target = local_realm->read_group().get_table("class_other_target");
4✔
2283

2284
        local_realm->begin_transaction();
4✔
2285
        auto o = target->create_object_with_primary_key("Foo").set("name", "Egon");
4✔
2286
        // 'embedded_link' property is null.
2287
        origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
2288
        // 'embedded_link' property is not null.
2289
        auto obj = origin->create_object_with_primary_key(42);
4✔
2290
        auto col_key = origin->get_column_key("embedded_link");
4✔
2291
        obj.create_and_set_linked_object(col_key);
4✔
2292
        other_target->create_object_with_primary_key(UUID("3b241101-e2bb-4255-8caf-4136c566a961"));
4✔
2293
        other_origin->create_object_with_primary_key(ObjectId::gen());
4✔
2294
        local_realm->commit_transaction();
4✔
2295
    }
4✔
2296

2297
    /* Create a synced realm and upload some data */
2298
    auto server_app_config = minimal_app_config("upgrade_from_local", schema);
4✔
2299
    TestAppSession test_session(create_app(server_app_config));
4✔
2300
    auto partition = random_string(100);
4✔
2301
    auto user1 = test_session.app()->current_user();
4✔
2302
    SyncTestFile config1(user1, partition, schema);
4✔
2303

2304
    auto r1 = Realm::get_shared_realm(config1);
4✔
2305

2306
    auto origin = r1->read_group().get_table("class_origin");
4✔
2307
    auto target = r1->read_group().get_table("class_target");
4✔
2308
    auto other_origin = r1->read_group().get_table("class_other_origin");
4✔
2309
    auto other_target = r1->read_group().get_table("class_other_target");
4✔
2310

2311
    r1->begin_transaction();
4✔
2312
    auto o = target->create_object_with_primary_key("Baa").set("name", "Børge");
4✔
2313
    origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
2314
    other_target->create_object_with_primary_key(UUID("01234567-89ab-cdef-edcb-a98765432101"));
4✔
2315
    other_origin->create_object_with_primary_key(ObjectId::gen());
4✔
2316
    r1->commit_transaction();
4✔
2317
    CHECK(!wait_for_upload(*r1));
4!
2318

2319
    /* Copy local realm data over in a synced one*/
2320
    create_user_and_log_in(test_session.app());
4✔
2321
    auto user2 = test_session.app()->current_user();
4✔
2322
    REQUIRE(user1 != user2);
4!
2323

2324
    SyncTestFile config2(user1, partition, schema);
4✔
2325

2326
    SharedRealm r2;
4✔
2327
    SECTION("Copy before connecting to server") {
4✔
2328
        local_realm->convert(config2);
2✔
2329
        r2 = Realm::get_shared_realm(config2);
2✔
2330
    }
2✔
2331

2332
    SECTION("Open synced realm first") {
4✔
2333
        r2 = Realm::get_shared_realm(config2);
2✔
2334
        CHECK(!wait_for_download(*r2));
2!
2335
        local_realm->convert(config2);
2✔
2336
        CHECK(!wait_for_upload(*r2));
2!
2337
    }
2✔
2338

2339
    CHECK(!wait_for_download(*r2));
4!
2340
    advance_and_notify(*r2);
4✔
2341
    Group& g = r2->read_group();
4✔
2342
    // g.to_json(std::cout);
2343
    REQUIRE(g.get_table("class_origin")->size() == 2);
4!
2344
    REQUIRE(g.get_table("class_target")->size() == 2);
4!
2345
    REQUIRE(g.get_table("class_other_origin")->size() == 2);
4!
2346
    REQUIRE(g.get_table("class_other_target")->size() == 2);
4!
2347

2348
    CHECK(!wait_for_upload(*r2));
4!
2349
    CHECK(!wait_for_download(*r1));
4!
2350
    advance_and_notify(*r1);
4✔
2351
    // r1->read_group().to_json(std::cout);
2352
}
4✔
2353

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

2357
    Schema schema{
2✔
2358
        {"TopLevel",
2✔
2359
         {
2✔
2360
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
2✔
2361
             {"array_of_objs", PropertyType::Object | PropertyType::Array, "TopLevel_array_of_objs"},
2✔
2362
             {"embedded_obj", PropertyType::Object | PropertyType::Nullable, "TopLevel_embedded_obj"},
2✔
2363
             {"embedded_dict", PropertyType::Object | PropertyType::Dictionary | PropertyType::Nullable,
2✔
2364
              "TopLevel_embedded_dict"},
2✔
2365
         }},
2✔
2366
        {"TopLevel_array_of_objs",
2✔
2367
         ObjectSchema::ObjectType::Embedded,
2✔
2368
         {
2✔
2369
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2370
         }},
2✔
2371
        {"TopLevel_embedded_obj",
2✔
2372
         ObjectSchema::ObjectType::Embedded,
2✔
2373
         {
2✔
2374
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2375
         }},
2✔
2376
        {"TopLevel_embedded_dict",
2✔
2377
         ObjectSchema::ObjectType::Embedded,
2✔
2378
         {
2✔
2379
             {"array", PropertyType::Int | PropertyType::Array},
2✔
2380
         }},
2✔
2381
    };
2✔
2382

2383
    auto server_app_config = minimal_app_config("set_new_embedded_object", schema);
2✔
2384
    TestAppSession test_session(create_app(server_app_config));
2✔
2385
    auto partition = random_string(100);
2✔
2386

2387
    auto array_of_objs_id = ObjectId::gen();
2✔
2388
    auto embedded_obj_id = ObjectId::gen();
2✔
2389
    auto dict_obj_id = ObjectId::gen();
2✔
2390

2391
    {
2✔
2392
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2393
        auto realm = Realm::get_shared_realm(config);
2✔
2394

2395
        CppContext c(realm);
2✔
2396
        realm->begin_transaction();
2✔
2397
        auto array_of_objs =
2✔
2398
            Object::create(c, realm, "TopLevel",
2✔
2399
                           std::any(AnyDict{
2✔
2400
                               {valid_pk_name, array_of_objs_id},
2✔
2401
                               {"array_of_objs", AnyVector{AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}},
2✔
2402
                           }),
2✔
2403
                           CreatePolicy::ForceCreate);
2✔
2404

2405
        auto embedded_obj =
2✔
2406
            Object::create(c, realm, "TopLevel",
2✔
2407
                           std::any(AnyDict{
2✔
2408
                               {valid_pk_name, embedded_obj_id},
2✔
2409
                               {"embedded_obj", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}},
2✔
2410
                           }),
2✔
2411
                           CreatePolicy::ForceCreate);
2✔
2412

2413
        auto dict_obj = Object::create(
2✔
2414
            c, realm, "TopLevel",
2✔
2415
            std::any(AnyDict{
2✔
2416
                {valid_pk_name, dict_obj_id},
2✔
2417
                {"embedded_dict", AnyDict{{"foo", AnyDict{{"array", AnyVector{INT64_C(1), INT64_C(2)}}}}}},
2✔
2418
            }),
2✔
2419
            CreatePolicy::ForceCreate);
2✔
2420

2421
        realm->commit_transaction();
2✔
2422
        {
2✔
2423
            realm->begin_transaction();
2✔
2424
            embedded_obj.set_property_value(c, "embedded_obj",
2✔
2425
                                            std::any(AnyDict{{
2✔
2426
                                                "array",
2✔
2427
                                                AnyVector{INT64_C(3), INT64_C(4)},
2✔
2428
                                            }}),
2✔
2429
                                            CreatePolicy::UpdateAll);
2✔
2430
            realm->commit_transaction();
2✔
2431
        }
2✔
2432

2433
        {
2✔
2434
            realm->begin_transaction();
2✔
2435
            List array(array_of_objs, array_of_objs.get_object_schema().property_for_name("array_of_objs"));
2✔
2436
            CppContext c2(realm, &array.get_object_schema());
2✔
2437
            array.set(c2, 0, std::any{AnyDict{{"array", AnyVector{INT64_C(5), INT64_C(6)}}}});
2✔
2438
            realm->commit_transaction();
2✔
2439
        }
2✔
2440

2441
        {
2✔
2442
            realm->begin_transaction();
2✔
2443
            object_store::Dictionary dict(dict_obj, dict_obj.get_object_schema().property_for_name("embedded_dict"));
2✔
2444
            CppContext c2(realm, &dict.get_object_schema());
2✔
2445
            dict.insert(c2, "foo", std::any{AnyDict{{"array", AnyVector{INT64_C(7), INT64_C(8)}}}});
2✔
2446
            realm->commit_transaction();
2✔
2447
        }
2✔
2448
        CHECK(!wait_for_upload(*realm));
2!
2449
    }
2✔
2450

2451
    {
2✔
2452
        SyncTestFile config(test_session.app()->current_user(), partition, schema);
2✔
2453
        auto realm = Realm::get_shared_realm(config);
2✔
2454

2455
        CHECK(!wait_for_download(*realm));
2!
2456
        CppContext c(realm);
2✔
2457
        {
2✔
2458
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{embedded_obj_id});
2✔
2459
            auto embedded_obj = util::any_cast<Object&&>(obj.get_property_value<std::any>(c, "embedded_obj"));
2✔
2460
            auto array_list = util::any_cast<List&&>(embedded_obj.get_property_value<std::any>(c, "array"));
2✔
2461
            CHECK(array_list.size() == 2);
2!
2462
            CHECK(array_list.get<int64_t>(0) == int64_t(3));
2!
2463
            CHECK(array_list.get<int64_t>(1) == int64_t(4));
2!
2464
        }
2✔
2465

2466
        {
2✔
2467
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{array_of_objs_id});
2✔
2468
            auto embedded_list = util::any_cast<List&&>(obj.get_property_value<std::any>(c, "array_of_objs"));
2✔
2469
            CppContext c2(realm, &embedded_list.get_object_schema());
2✔
2470
            auto embedded_array_obj = util::any_cast<Object&&>(embedded_list.get(c2, 0));
2✔
2471
            auto array_list = util::any_cast<List&&>(embedded_array_obj.get_property_value<std::any>(c2, "array"));
2✔
2472
            CHECK(array_list.size() == 2);
2!
2473
            CHECK(array_list.get<int64_t>(0) == int64_t(5));
2!
2474
            CHECK(array_list.get<int64_t>(1) == int64_t(6));
2!
2475
        }
2✔
2476

2477
        {
2✔
2478
            auto obj = Object::get_for_primary_key(c, realm, "TopLevel", std::any{dict_obj_id});
2✔
2479
            object_store::Dictionary dict(obj, obj.get_object_schema().property_for_name("embedded_dict"));
2✔
2480
            CppContext c2(realm, &dict.get_object_schema());
2✔
2481
            auto embedded_obj = util::any_cast<Object&&>(dict.get(c2, "foo"));
2✔
2482
            auto array_list = util::any_cast<List&&>(embedded_obj.get_property_value<std::any>(c2, "array"));
2✔
2483
            CHECK(array_list.size() == 2);
2!
2484
            CHECK(array_list.get<int64_t>(0) == int64_t(7));
2!
2485
            CHECK(array_list.get<int64_t>(1) == int64_t(8));
2!
2486
        }
2✔
2487
    }
2✔
2488
}
2✔
2489

2490
TEST_CASE("app: make distributable client file", "[sync][pbs][app][baas]") {
2✔
2491
    TestAppSession session;
2✔
2492
    auto app = session.app();
2✔
2493

2494
    auto schema = get_default_schema();
2✔
2495
    SyncTestFile original_config(app->current_user(), bson::Bson("foo"), schema);
2✔
2496
    create_user_and_log_in(app);
2✔
2497
    SyncTestFile target_config(app->current_user(), bson::Bson("foo"), schema);
2✔
2498

2499
    // Create realm file without client file id
2500
    {
2✔
2501
        auto realm = Realm::get_shared_realm(original_config);
2✔
2502

2503
        // Write some data
2504
        realm->begin_transaction();
2✔
2505
        CppContext c;
2✔
2506
        Object::create(c, realm, "Person",
2✔
2507
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2508
                                               {"age", INT64_C(64)},
2✔
2509
                                               {"firstName", std::string("Paul")},
2✔
2510
                                               {"lastName", std::string("McCartney")}}));
2✔
2511
        realm->commit_transaction();
2✔
2512
        wait_for_upload(*realm);
2✔
2513
        wait_for_download(*realm);
2✔
2514

2515
        realm->convert(target_config);
2✔
2516

2517
        // Write some additional data
2518
        realm->begin_transaction();
2✔
2519
        Object::create(c, realm, "Dog",
2✔
2520
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2521
                                               {"breed", std::string("stabyhoun")},
2✔
2522
                                               {"name", std::string("albert")},
2✔
2523
                                               {"realm_id", std::string("foo")}}));
2✔
2524
        realm->commit_transaction();
2✔
2525
        wait_for_upload(*realm);
2✔
2526
    }
2✔
2527
    // Starting a new session based on the copy
2528
    {
2✔
2529
        auto realm = Realm::get_shared_realm(target_config);
2✔
2530
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2531
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 0);
2!
2532

2533
        // Should be able to download the object created in the source Realm
2534
        // after writing the copy
2535
        wait_for_download(*realm);
2✔
2536
        realm->refresh();
2✔
2537
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2538
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1);
2!
2539

2540
        // Check that we can continue committing to this realm
2541
        realm->begin_transaction();
2✔
2542
        CppContext c;
2✔
2543
        Object::create(c, realm, "Dog",
2✔
2544
                       std::any(realm::AnyDict{{"_id", std::any(ObjectId::gen())},
2✔
2545
                                               {"breed", std::string("bulldog")},
2✔
2546
                                               {"name", std::string("fido")},
2✔
2547
                                               {"realm_id", std::string("foo")}}));
2✔
2548
        realm->commit_transaction();
2✔
2549
        wait_for_upload(*realm);
2✔
2550
    }
2✔
2551
    // Original Realm should be able to read the object which was written to the copy
2552
    {
2✔
2553
        auto realm = Realm::get_shared_realm(original_config);
2✔
2554
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2555
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 1);
2!
2556

2557
        wait_for_download(*realm);
2✔
2558
        realm->refresh();
2✔
2559
        REQUIRE(realm->read_group().get_table("class_Person")->size() == 1);
2!
2560
        REQUIRE(realm->read_group().get_table("class_Dog")->size() == 2);
2!
2561
    }
2✔
2562
}
2✔
2563

2564
TEST_CASE("app: sync integration", "[sync][pbs][app][baas]") {
38✔
2565
    auto logger = util::Logger::get_default_logger();
38✔
2566

2567
    const auto schema = get_default_schema();
38✔
2568

2569
    auto get_dogs = [](SharedRealm r) -> Results {
44✔
2570
        wait_for_upload(*r, std::chrono::seconds(10));
44✔
2571
        wait_for_download(*r, std::chrono::seconds(10));
44✔
2572
        return Results(r, r->read_group().get_table("class_Dog"));
44✔
2573
    };
44✔
2574

2575
    auto create_one_dog = [](SharedRealm r) {
38✔
2576
        r->begin_transaction();
16✔
2577
        CppContext c;
16✔
2578
        Object::create(c, r, "Dog",
16✔
2579
                       std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
16✔
2580
                                        {"breed", std::string("bulldog")},
16✔
2581
                                        {"name", std::string("fido")}}),
16✔
2582
                       CreatePolicy::ForceCreate);
16✔
2583
        r->commit_transaction();
16✔
2584
    };
16✔
2585

2586
    TestAppSession session;
38✔
2587
    auto app = session.app();
38✔
2588
    const auto partition = random_string(100);
38✔
2589

2590
    SECTION("Add Objects") {
38✔
2591
        {
2✔
2592
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2593
            auto r = Realm::get_shared_realm(config);
2✔
2594

2595
            REQUIRE(get_dogs(r).size() == 0);
2!
2596
            create_one_dog(r);
2✔
2597
            REQUIRE(get_dogs(r).size() == 1);
2!
2598
        }
2✔
2599

2600
        {
2✔
2601
            create_user_and_log_in(app);
2✔
2602
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2603
            auto r = Realm::get_shared_realm(config);
2✔
2604
            Results dogs = get_dogs(r);
2✔
2605
            REQUIRE(dogs.size() == 1);
2!
2606
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2607
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2608
        }
2✔
2609
    }
2✔
2610

2611
    SECTION("MemOnly durability") {
38✔
2612
        {
2✔
2613
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2614
            config.in_memory = true;
2✔
2615
            config.encryption_key = std::vector<char>();
2✔
2616

2617
            REQUIRE(config.options().durability == DBOptions::Durability::MemOnly);
2!
2618
            auto r = Realm::get_shared_realm(config);
2✔
2619

2620
            REQUIRE(get_dogs(r).size() == 0);
2!
2621
            create_one_dog(r);
2✔
2622
            REQUIRE(get_dogs(r).size() == 1);
2!
2623
        }
2✔
2624

2625
        {
2✔
2626
            create_user_and_log_in(app);
2✔
2627
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2628
            config.in_memory = true;
2✔
2629
            config.encryption_key = std::vector<char>();
2✔
2630
            auto r = Realm::get_shared_realm(config);
2✔
2631
            Results dogs = get_dogs(r);
2✔
2632
            REQUIRE(dogs.size() == 1);
2!
2633
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2634
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2635
        }
2✔
2636
    }
2✔
2637

2638
    SECTION("Fast clock on client") {
38✔
2639
        {
2✔
2640
            SyncTestFile config(app->current_user(), partition, schema);
2✔
2641
            auto r = Realm::get_shared_realm(config);
2✔
2642

2643
            REQUIRE(get_dogs(r).size() == 0);
2!
2644
            create_one_dog(r);
2✔
2645
            REQUIRE(get_dogs(r).size() == 1);
2!
2646
        }
2✔
2647

2648
        auto transport = std::make_shared<HookedTransport<>>();
2✔
2649
        TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false});
2✔
2650
        auto app = hooked_session.app();
2✔
2651
        std::shared_ptr<User> user = app->current_user();
2✔
2652
        REQUIRE(user);
2!
2653
        REQUIRE(!user->access_token_refresh_required());
2!
2654
        // Make the User behave as if the client clock is 31 minutes fast, so the token looks expired locally
2655
        // (access tokens have an lifetime of 30 minutes today).
2656
        user->set_seconds_to_adjust_time_for_testing(31 * 60);
2✔
2657
        REQUIRE(user->access_token_refresh_required());
2!
2658

2659
        // This assumes that we make an http request for the new token while
2660
        // already in the WaitingForAccessToken state.
2661
        bool seen_waiting_for_access_token = false;
2✔
2662
        transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
2663
            auto user = app->current_user();
2✔
2664
            REQUIRE(user);
2!
2665
            for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) {
2✔
2666
                // Prior to the fix for #4941, this callback would be called from an infinite loop, always in the
2667
                // WaitingForAccessToken state.
2668
                if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2669
                    REQUIRE(!seen_waiting_for_access_token);
2!
2670
                    seen_waiting_for_access_token = true;
2✔
2671
                }
2✔
2672
            }
2✔
2673
            return std::nullopt;
2✔
2674
        };
2✔
2675
        SyncTestFile config(user, partition, schema);
2✔
2676
        auto r = Realm::get_shared_realm(config);
2✔
2677
        REQUIRE(seen_waiting_for_access_token);
2!
2678
        Results dogs = get_dogs(r);
2✔
2679
        REQUIRE(dogs.size() == 1);
2!
2680
        REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2681
        REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2682
    }
2✔
2683

2684
    SECTION("Expired Tokens") {
38✔
2685
        sync::AccessToken token;
8✔
2686
        {
8✔
2687
            std::shared_ptr<User> user = app->current_user();
8✔
2688
            SyncTestFile config(user, partition, schema);
8✔
2689
            auto r = Realm::get_shared_realm(config);
8✔
2690

2691
            REQUIRE(get_dogs(r).size() == 0);
8!
2692
            create_one_dog(r);
8✔
2693

2694
            REQUIRE(get_dogs(r).size() == 1);
8!
2695
            sync::AccessToken::ParseError error_state = realm::sync::AccessToken::ParseError::none;
8✔
2696
            sync::AccessToken::parse(user->access_token(), token, error_state, nullptr);
8✔
2697
            REQUIRE(error_state == sync::AccessToken::ParseError::none);
8!
2698
            REQUIRE(token.timestamp);
8!
2699
            REQUIRE(token.expires);
8!
2700
            REQUIRE(token.timestamp < token.expires);
8!
2701
            std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
8✔
2702
            token.expires = std::chrono::system_clock::to_time_t(now - 30s);
8✔
2703
            REQUIRE(token.expired(now));
8!
2704
        }
8✔
2705

2706
        auto transport = std::make_shared<HookedTransport<>>();
8✔
2707
        TestAppSession hooked_session(session.app_session(), transport, DeleteApp{false});
8✔
2708
        auto app = hooked_session.app();
8✔
2709
        std::shared_ptr<User> user = app->current_user();
8✔
2710
        REQUIRE(user);
8!
2711
        REQUIRE(!user->access_token_refresh_required());
8!
2712
        // Set a bad access token, with an expired time. This will trigger a refresh initiated by the client.
2713
        user->update_data_for_testing([&token](UserData& data) {
8✔
2714
            data.access_token = RealmJWT(encode_fake_jwt("fake_access_token", token.expires, token.timestamp));
8✔
2715
        });
8✔
2716
        REQUIRE(user->access_token_refresh_required());
8!
2717

2718
        SECTION("Expired Access Token is Refreshed") {
8✔
2719
            // This assumes that we make an http request for the new token while
2720
            // already in the WaitingForAccessToken state.
2721
            bool seen_waiting_for_access_token = false;
2✔
2722
            transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
2723
                auto user = app->current_user();
2✔
2724
                REQUIRE(user);
2!
2725
                for (auto& session : app->sync_manager()->get_all_sessions_for(*user)) {
2✔
2726
                    if (session->state() == SyncSession::State::WaitingForAccessToken) {
2✔
2727
                        REQUIRE(!seen_waiting_for_access_token);
2!
2728
                        seen_waiting_for_access_token = true;
2✔
2729
                    }
2✔
2730
                }
2✔
2731
                return std::nullopt;
2✔
2732
            };
2✔
2733
            SyncTestFile config(user, partition, schema);
2✔
2734
            auto r = Realm::get_shared_realm(config);
2✔
2735
            REQUIRE(seen_waiting_for_access_token);
2!
2736
            Results dogs = get_dogs(r);
2✔
2737
            REQUIRE(dogs.size() == 1);
2!
2738
            REQUIRE(dogs.get(0).get<String>("breed") == "bulldog");
2!
2739
            REQUIRE(dogs.get(0).get<String>("name") == "fido");
2!
2740
        }
2✔
2741

2742
        SECTION("User is logged out if the refresh request is denied") {
8✔
2743
            REQUIRE(user->is_logged_in());
2!
2744
            size_t hook_count = 0;
2✔
2745
            transport->response_hook = [&](const Request& request, Response& response) {
2✔
2746
                auto user = app->current_user();
2✔
2747
                if (hook_count++ == 0) {
2✔
2748
                    // the initial request should have a current user and log it out
2749
                    REQUIRE(user);
2!
2750
                    REQUIRE(user->is_logged_in());
2!
2751
                }
2✔
2752
                else {
×
2753
                    INFO(request.url);
×
2754
                    // any later requests (eg. redirect) won't have a current user
2755
                    REQUIRE(!user);
×
2756
                }
×
2757
                // simulate the server denying the refresh
2758
                if (request.url.find("/session") != std::string::npos) {
2✔
2759
                    response.http_status_code = 401;
2✔
2760
                    response.body = "fake: refresh token could not be refreshed";
2✔
2761
                }
2✔
2762
            };
2✔
2763
            SyncTestFile config(user, partition, schema);
2✔
2764
            std::atomic<bool> sync_error_handler_called{false};
2✔
2765
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2766
                sync_error_handler_called.store(true);
2✔
2767
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
2!
2768
                REQUIRE_THAT(std::string{error.status.reason()},
2✔
2769
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
2✔
2770
            };
2✔
2771
            auto r = Realm::get_shared_realm(config);
2✔
2772
            timed_wait_for([&] {
3✔
2773
                return sync_error_handler_called.load();
3✔
2774
            });
3✔
2775
            // the failed refresh logs out the user
2776
            REQUIRE(!user->is_logged_in());
2!
2777
        }
2✔
2778

2779
        SECTION("User is left logged out if logged out while the refresh is in progress") {
8✔
2780
            REQUIRE(user->is_logged_in());
2!
2781
            transport->request_hook = [&](const Request&) -> std::optional<Response> {
4✔
2782
                user->log_out();
4✔
2783
                return std::nullopt;
4✔
2784
            };
4✔
2785
            SyncTestFile config(user, partition, schema);
2✔
2786
            std::atomic<bool> sync_error_handler_called{false};
2✔
2787
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2788
                sync_error_handler_called.store(true);
2✔
2789
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
2!
2790
                REQUIRE_THAT(std::string{error.status.reason()},
2✔
2791
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
2✔
2792
            };
2✔
2793
            auto r = Realm::get_shared_realm(config);
2✔
2794
            timed_wait_for([&] {
3✔
2795
                return sync_error_handler_called.load();
3✔
2796
            });
3✔
2797
            REQUIRE_FALSE(user->is_logged_in());
2!
2798
            REQUIRE(user->state() == SyncUser::State::LoggedOut);
2!
2799
        }
2✔
2800

2801
        SECTION("Requests that receive an error are retried on a backoff") {
8✔
2802
            using namespace std::chrono;
2✔
2803
            std::vector<time_point<steady_clock>> response_times;
2✔
2804
            std::atomic<bool> did_receive_valid_token{false};
2✔
2805
            constexpr size_t num_error_responses = 6;
2✔
2806

2807
            transport->response_hook = [&](const Request& request, Response& response) {
12✔
2808
                // simulate the server experiencing an internal server error
2809
                if (request.url.find("/session") != std::string::npos) {
12✔
2810
                    if (response_times.size() >= num_error_responses) {
12✔
2811
                        did_receive_valid_token.store(true);
2✔
2812
                        return;
2✔
2813
                    }
2✔
2814
                    response.http_status_code = 500;
10✔
2815
                }
10✔
2816
            };
12✔
2817
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
12✔
2818
                if (!did_receive_valid_token.load() && request.url.find("/session") != std::string::npos) {
12✔
2819
                    response_times.push_back(steady_clock::now());
12✔
2820
                }
12✔
2821
                return std::nullopt;
12✔
2822
            };
12✔
2823
            SyncTestFile config(user, partition, schema);
2✔
2824
            config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
2✔
2825
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
×
2826
                REQUIRE_THAT(std::string{error.status.reason()},
×
2827
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
×
2828
            };
×
2829
            auto r = Realm::get_shared_realm(config);
2✔
2830
            create_one_dog(r);
2✔
2831
            timed_wait_for(
2✔
2832
                [&] {
42,249,890✔
2833
                    return did_receive_valid_token.load();
42,249,890✔
2834
                },
42,249,890✔
2835
                30s);
2✔
2836
            REQUIRE(user->is_logged_in());
2!
2837
            REQUIRE(response_times.size() >= num_error_responses);
2!
2838
            std::vector<uint64_t> delay_times;
2✔
2839
            for (size_t i = 1; i < response_times.size(); ++i) {
12✔
2840
                delay_times.push_back(duration_cast<milliseconds>(response_times[i] - response_times[i - 1]).count());
10✔
2841
            }
10✔
2842

2843
            // sync delays start at 1000ms minus a random number of up to 25%.
2844
            // the subsequent delay is double the previous one minus a random 25% again.
2845
            // this calculation happens in Connection::initiate_reconnect_wait()
2846
            bool increasing_delay = true;
2✔
2847
            for (size_t i = 1; i < delay_times.size(); ++i) {
10✔
2848
                if (delay_times[i - 1] >= delay_times[i]) {
8✔
2849
                    increasing_delay = false;
×
2850
                }
×
2851
            }
8✔
2852
            // fail if the first delay isn't longer than half a second
2853
            if (delay_times.size() <= 1 || delay_times[1] < 500) {
2✔
2854
                increasing_delay = false;
×
2855
            }
×
2856
            if (!increasing_delay) {
2✔
2857
                std::cerr << "delay times are not increasing: ";
×
2858
                for (auto& delay : delay_times) {
×
2859
                    std::cerr << delay << ", ";
×
2860
                }
×
2861
                std::cerr << std::endl;
×
2862
            }
×
2863
            REQUIRE(increasing_delay);
2!
2864
        }
2✔
2865
    }
8✔
2866

2867
    SECTION("Invalid refresh token") {
38✔
2868
        auto& app_session = session.app_session();
8✔
2869
        std::mutex mtx;
8✔
2870
        auto verify_error_on_sync_with_invalid_refresh_token = [&](std::shared_ptr<User> user, Realm::Config config) {
8✔
2871
            REQUIRE(user);
6!
2872
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
6!
2873

2874
            // requesting a new access token fails because the refresh token used for this request is revoked
2875
            user->refresh_custom_data([&](Optional<AppError> error) {
6✔
2876
                REQUIRE(error);
6!
2877
                REQUIRE(error->additional_status_code == 401);
6!
2878
                REQUIRE(error->code() == ErrorCodes::InvalidSession);
6!
2879
            });
6✔
2880

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

2891
            auto [sync_error_promise, sync_error] = util::make_promise_future<SyncError>();
6✔
2892
            config.sync_config->error_handler =
6✔
2893
                [promise = util::CopyablePromiseHolder(std::move(sync_error_promise))](std::shared_ptr<SyncSession>,
6✔
2894
                                                                                       SyncError error) mutable {
6✔
2895
                    promise.get_promise().emplace_value(std::move(error));
6✔
2896
                };
6✔
2897

2898
            auto transport = static_cast<SynchronousTestTransport*>(session.transport());
6✔
2899
            transport->block(); // don't let the token refresh happen until we're ready for it
6✔
2900
            auto r = Realm::get_shared_realm(config);
6✔
2901
            auto session = app->sync_manager()->get_existing_session(config.path);
6✔
2902
            REQUIRE(user->is_logged_in());
6!
2903
            REQUIRE(!sync_error.is_ready());
6!
2904
            {
6✔
2905
                std::atomic<bool> called{false};
6✔
2906
                session->wait_for_upload_completion([&](Status stat) {
6✔
2907
                    std::lock_guard lock(mtx);
6✔
2908
                    called.store(true);
6✔
2909
                    REQUIRE(stat.code() == ErrorCodes::InvalidSession);
6!
2910
                });
6✔
2911
                transport->unblock();
6✔
2912
                timed_wait_for([&] {
21,153✔
2913
                    return called.load();
21,153✔
2914
                });
21,153✔
2915
                std::lock_guard lock(mtx);
6✔
2916
                REQUIRE(called);
6!
2917
            }
6✔
2918

2919
            auto sync_error_res = wait_for_future(std::move(sync_error)).get();
6✔
2920
            REQUIRE(sync_error_res.status == ErrorCodes::AuthError);
6!
2921
            REQUIRE_THAT(std::string{sync_error_res.status.reason()},
6✔
2922
                         Catch::Matchers::StartsWith("Unable to refresh the user access token"));
6✔
2923

2924
            // the failed refresh logs out the user
2925
            std::lock_guard lock(mtx);
6✔
2926
            REQUIRE(!user->is_logged_in());
6!
2927
        };
6✔
2928

2929
        SECTION("Disabled user results in a sync error") {
8✔
2930
            auto creds = create_user_and_log_in(app);
2✔
2931
            auto user = app->current_user();
2✔
2932
            REQUIRE(user);
2!
2933
            SyncTestFile config(user, partition, schema);
2✔
2934
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
2935
            app_session.admin_api.disable_user_sessions(app->current_user()->user_id(), app_session.server_app_id);
2✔
2936

2937
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2938

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

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

2946
            // logging in now works properly
2947
            log_in(app, creds);
2✔
2948

2949
            // still referencing the same user
2950
            REQUIRE(user == app->current_user());
2!
2951
            REQUIRE(user->is_logged_in());
2!
2952

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

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

2970
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2971

2972
            // logging in again succeeds and generates a new and valid refresh token
2973
            log_in(app, creds);
2✔
2974

2975
            // still referencing the same user and now the user is logged in
2976
            REQUIRE(user == app->current_user());
2!
2977
            REQUIRE(user->is_logged_in());
2!
2978

2979
            // new requests for an access token succeed again
2980
            user->refresh_custom_data([&](Optional<AppError> error) {
2✔
2981
                REQUIRE_FALSE(error);
2!
2982
            });
2✔
2983

2984
            {
2✔
2985
                // check that there are no errors initiating a new sync session by making sure upload/download
2986
                // succeeds
2987
                auto r = Realm::get_shared_realm(config);
2✔
2988
                Results dogs = get_dogs(r);
2✔
2989
            }
2✔
2990
        }
2✔
2991

2992
        SECTION("Revoked refresh token on an anonymous user results in a sync error") {
8✔
2993
            app->current_user()->log_out();
2✔
2994
            auto anon_user = log_in(app);
2✔
2995
            REQUIRE(app->current_user() == anon_user);
2!
2996
            SyncTestFile config(anon_user, partition, schema);
2✔
2997
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
2998
            app_session.admin_api.revoke_user_sessions(anon_user->user_id(), app_session.server_app_id);
2✔
2999
            // revoking a user session only affects the refresh token, so the access token should still continue to
3000
            // work.
3001
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
3002

3003
            verify_error_on_sync_with_invalid_refresh_token(anon_user, config);
2✔
3004

3005
            // the user has been logged out, and current user is reset
3006
            REQUIRE(!app->current_user());
2!
3007
            REQUIRE(!anon_user->is_logged_in());
2!
3008
            REQUIRE(anon_user->state() == SyncUser::State::Removed);
2!
3009

3010
            // new requests for an access token do not work for anon users
3011
            anon_user->refresh_custom_data([&](Optional<AppError> error) {
2✔
3012
                REQUIRE(error);
2!
3013
                REQUIRE(error->reason() ==
2!
3014
                        util::format("Cannot initiate a refresh on user '%1' because the user has been removed",
2✔
3015
                                     anon_user->user_id()));
2✔
3016
            });
2✔
3017

3018
            REQUIRE_EXCEPTION(
2✔
3019
                Realm::get_shared_realm(config), ClientUserNotFound,
2✔
3020
                util::format("Cannot start a sync session for user '%1' because this user has been removed.",
2✔
3021
                             anon_user->user_id()));
2✔
3022
        }
2✔
3023

3024
        SECTION("Opening a Realm with a removed email user results produces an exception") {
8✔
3025
            auto creds = create_user_and_log_in(app);
2✔
3026
            auto email_user = app->current_user();
2✔
3027
            const std::string user_ident = email_user->user_id();
2✔
3028
            REQUIRE(email_user);
2!
3029
            SyncTestFile config(email_user, partition, schema);
2✔
3030
            REQUIRE(email_user->is_logged_in());
2!
3031
            {
2✔
3032
                // sync works on a valid user
3033
                auto r = Realm::get_shared_realm(config);
2✔
3034
                Results dogs = get_dogs(r);
2✔
3035
            }
2✔
3036
            app->remove_user(email_user, [](util::Optional<AppError> err) {
2✔
3037
                REQUIRE(!err);
2!
3038
            });
2✔
3039
            REQUIRE_FALSE(email_user->is_logged_in());
2!
3040
            REQUIRE(email_user->state() == SyncUser::State::Removed);
2!
3041

3042
            // should not be able to open a synced Realm with an invalid user
3043
            REQUIRE_EXCEPTION(
2✔
3044
                Realm::get_shared_realm(config), ClientUserNotFound,
2✔
3045
                util::format("Cannot start a sync session for user '%1' because this user has been removed.",
2✔
3046
                             user_ident));
2✔
3047

3048
            std::shared_ptr<User> new_user_instance = log_in(app, creds);
2✔
3049
            // the previous instance is still invalid
3050
            REQUIRE_FALSE(email_user->is_logged_in());
2!
3051
            REQUIRE(email_user->state() == SyncUser::State::Removed);
2!
3052
            // but the new instance will work and has the same server issued ident
3053
            REQUIRE(new_user_instance);
2!
3054
            REQUIRE(new_user_instance->is_logged_in());
2!
3055
            REQUIRE(new_user_instance->user_id() == user_ident);
2!
3056
            {
2✔
3057
                // sync works again if the same user is logged back in
3058
                config.sync_config->user = new_user_instance;
2✔
3059
                auto r = Realm::get_shared_realm(config);
2✔
3060
                Results dogs = get_dogs(r);
2✔
3061
            }
2✔
3062
        }
2✔
3063
    }
8✔
3064

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

3068
        std::mutex mutex;
2✔
3069
        bool done = false;
2✔
3070
        auto r = Realm::get_shared_realm(config);
2✔
3071
        r->sync_session()->pause();
2✔
3072

3073
        // Create 26 MB worth of dogs in 26 transactions, which should work but
3074
        // will result in an error from the server if the changesets are batched
3075
        // for upload.
3076
        CppContext c;
2✔
3077
        for (auto i = 'a'; i < 'z'; ++i) {
52✔
3078
            r->begin_transaction();
50✔
3079
            Object::create(c, r, "Dog",
50✔
3080
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
50✔
3081
                                            {"breed", std::string("bulldog")},
50✔
3082
                                            {"name", random_string(1024 * 1024)}}),
50✔
3083
                           CreatePolicy::ForceCreate);
50✔
3084
            r->commit_transaction();
50✔
3085
        }
50✔
3086
        r->sync_session()->wait_for_upload_completion([&](Status status) {
2✔
3087
            std::lock_guard lk(mutex);
2✔
3088
            REQUIRE(status.is_ok());
2!
3089
            done = true;
2✔
3090
        });
2✔
3091
        r->sync_session()->resume();
2✔
3092

3093
        // If we haven't gotten an error in more than 5 minutes, then something has gone wrong
3094
        // and we should fail the test.
3095
        timed_wait_for(
2✔
3096
            [&] {
6,246,422✔
3097
                std::lock_guard lk(mutex);
6,246,422✔
3098
                return done;
6,246,422✔
3099
            },
6,246,422✔
3100
            std::chrono::minutes(5));
2✔
3101
    }
2✔
3102

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

3106
        auto pf = util::make_promise_future<SyncError>();
2✔
3107
        config.sync_config->error_handler =
2✔
3108
            [sp = util::CopyablePromiseHolder(std::move(pf.promise))](auto, SyncError error) mutable {
2✔
3109
                sp.get_promise().emplace_value(std::move(error));
2✔
3110
            };
2✔
3111
        auto r = Realm::get_shared_realm(config);
2✔
3112

3113
        // Create 26 MB worth of dogs in a single transaction - this should all get put into one changeset
3114
        // and get uploaded at once, which for now is an error on the server.
3115
        r->begin_transaction();
2✔
3116
        CppContext c;
2✔
3117
        for (auto i = 'a'; i < 'z'; ++i) {
52✔
3118
            Object::create(c, r, "Dog",
50✔
3119
                           std::any(AnyDict{{"_id", std::any(ObjectId::gen())},
50✔
3120
                                            {"breed", std::string("bulldog")},
50✔
3121
                                            {"name", random_string(1024 * 1024)}}),
50✔
3122
                           CreatePolicy::ForceCreate);
50✔
3123
        }
50✔
3124
        r->commit_transaction();
2✔
3125

3126
#if defined(TEST_TIMEOUT_EXTRA) && TEST_TIMEOUT_EXTRA > 0
3127
        // It may take 30 minutes to transfer 16MB at 10KB/s
3128
        auto delay = std::chrono::minutes(35);
3129
#else
3130
        auto delay = std::chrono::minutes(5);
2✔
3131
#endif
2✔
3132

3133
        auto error = wait_for_future(std::move(pf.future), delay).get();
2✔
3134
        REQUIRE(error.status == ErrorCodes::LimitExceeded);
2!
3135
        REQUIRE(error.status.reason() ==
2!
3136
                "Sync websocket closed because the server received a message that was too large: "
2✔
3137
                "read limited at 16777217 bytes");
2✔
3138
        REQUIRE(error.is_client_reset_requested());
2!
3139
        REQUIRE(error.server_requests_action == sync::ProtocolErrorInfo::Action::ClientReset);
2!
3140
    }
2✔
3141

3142
    SECTION("freezing realm does not resume session") {
38✔
3143
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3144
        auto realm = Realm::get_shared_realm(config);
2✔
3145
        wait_for_download(*realm);
2✔
3146

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

3150
        realm->sync_session()->pause();
2✔
3151
        state = realm->sync_session()->state();
2✔
3152
        REQUIRE(state == SyncSession::State::Paused);
2!
3153

3154
        realm->read_group();
2✔
3155

3156
        {
2✔
3157
            auto frozen = realm->freeze();
2✔
3158
            REQUIRE(realm->sync_session() == realm->sync_session());
2!
3159
            REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused);
2!
3160
        }
2✔
3161

3162
        {
2✔
3163
            auto frozen = Realm::get_frozen_realm(config, realm->read_transaction_version());
2✔
3164
            REQUIRE(realm->sync_session() == realm->sync_session());
2!
3165
            REQUIRE(realm->sync_session()->state() == SyncSession::State::Paused);
2!
3166
        }
2✔
3167
    }
2✔
3168

3169
    SECTION("pausing a session does not hold the DB open") {
38✔
3170
        auto logger = util::Logger::get_default_logger();
2✔
3171
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3172
        DBRef dbref;
2✔
3173
        std::shared_ptr<SyncSession> sync_sess_ext_ref;
2✔
3174
        {
2✔
3175
            auto realm = Realm::get_shared_realm(config);
2✔
3176
            wait_for_download(*realm);
2✔
3177

3178
            auto state = realm->sync_session()->state();
2✔
3179
            REQUIRE(state == SyncSession::State::Active);
2!
3180

3181
            sync_sess_ext_ref = realm->sync_session()->external_reference();
2✔
3182
            dbref = TestHelper::get_db(*realm);
2✔
3183
            // An active PBS realm should have one ref each for:
3184
            // - RealmCoordinator
3185
            // - SyncSession
3186
            // - MigrationStore
3187
            // - SessionWrapper
3188
            // - local dbref
3189
            logger->trace("DBRef ACTIVE use count: %1", dbref.use_count());
2✔
3190
            REQUIRE(dbref.use_count() >= 5);
2!
3191

3192
            realm->sync_session()->pause();
2✔
3193
            state = realm->sync_session()->state();
2✔
3194
            REQUIRE(state == SyncSession::State::Paused);
2!
3195
            logger->trace("DBRef PAUSING called use count: %1", dbref.use_count());
2✔
3196
        }
2✔
3197

3198
        // Closing the realm should leave one ref each for:
3199
        // - SyncSession
3200
        // - MigrationStore
3201
        // - local dbref
3202
        REQUIRE_THAT(
2✔
3203
            [&] {
2✔
3204
                logger->trace("DBRef PAUSED use count: %1", dbref.use_count());
2✔
3205
                return dbref.use_count() < 4;
2✔
3206
            },
2✔
3207
            ReturnsTrueWithinTimeLimit{});
2✔
3208

3209
        // Releasing the external reference should leave one ref for:
3210
        // - local dbref
3211
        sync_sess_ext_ref.reset();
2✔
3212
        REQUIRE_THAT(
2✔
3213
            [&] {
2✔
3214
                logger->trace("DBRef TEARDOWN use count: %1", dbref.use_count());
2✔
3215
                return dbref.use_count() == 1;
2✔
3216
            },
2✔
3217
            ReturnsTrueWithinTimeLimit{});
2✔
3218
    }
2✔
3219

3220
    SECTION("validation") {
38✔
3221
        SyncTestFile config(app->current_user(), partition, schema);
6✔
3222

3223
        SECTION("invalid partition error handling") {
6✔
3224
            config.sync_config->partition_value = "not a bson serialized string";
2✔
3225
            std::atomic<bool> error_did_occur = false;
2✔
3226
            config.sync_config->error_handler = [&error_did_occur](std::shared_ptr<SyncSession>, SyncError error) {
2✔
3227
                CHECK(error.status.reason().find(
2!
3228
                          "Illegal Realm path (BIND): serialized partition 'not a bson serialized "
2✔
3229
                          "string' is invalid") != std::string::npos);
2✔
3230
                error_did_occur.store(true);
2✔
3231
            };
2✔
3232
            auto r = Realm::get_shared_realm(config);
2✔
3233
            auto session = app->sync_manager()->get_existing_session(r->config().path);
2✔
3234
            timed_wait_for([&] {
9,296✔
3235
                return error_did_occur.load();
9,296✔
3236
            });
9,296✔
3237
            REQUIRE(error_did_occur.load());
2!
3238
        }
2✔
3239

3240
        SECTION("invalid pk schema error handling") {
6✔
3241
            const std::string invalid_pk_name = "my_primary_key";
2✔
3242
            auto it = config.schema->find("Dog");
2✔
3243
            REQUIRE(it != config.schema->end());
2!
3244
            REQUIRE(it->primary_key_property());
2!
3245
            REQUIRE(it->primary_key_property()->name == "_id");
2!
3246
            it->primary_key_property()->name = invalid_pk_name;
2✔
3247
            it->primary_key = invalid_pk_name;
2✔
3248
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3249
                                      "The primary key property on a synchronized Realm must be named '_id' but "
2✔
3250
                                      "found 'my_primary_key' for type 'Dog'");
2✔
3251
        }
2✔
3252

3253
        SECTION("missing pk schema error handling") {
6✔
3254
            auto it = config.schema->find("Dog");
2✔
3255
            REQUIRE(it != config.schema->end());
2!
3256
            REQUIRE(it->primary_key_property());
2!
3257
            it->primary_key_property()->is_primary = false;
2✔
3258
            it->primary_key = "";
2✔
3259
            REQUIRE(!it->primary_key_property());
2!
3260
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3261
                                      "There must be a primary key property named '_id' on a synchronized "
2✔
3262
                                      "Realm but none was found for type 'Dog'");
2✔
3263
        }
2✔
3264
    }
6✔
3265

3266
    SECTION("get_file_ident") {
38✔
3267
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3268
        config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
3269
        auto r = Realm::get_shared_realm(config);
2✔
3270
        wait_for_download(*r);
2✔
3271

3272
        auto first_ident = r->sync_session()->get_file_ident();
2✔
3273
        REQUIRE(first_ident.ident != 0);
2!
3274
        REQUIRE(first_ident.salt != 0);
2!
3275

3276
        reset_utils::trigger_client_reset(session.app_session(), r);
2✔
3277
        r->sync_session()->restart_session();
2✔
3278
        wait_for_download(*r);
2✔
3279

3280
        REQUIRE(first_ident.ident != r->sync_session()->get_file_ident().ident);
2!
3281
        REQUIRE(first_ident.salt != r->sync_session()->get_file_ident().salt);
2!
3282
    }
2✔
3283
}
38✔
3284

3285
TEST_CASE("app: sync logs contain baas coid", "[sync][app][baas]") {
2✔
3286
    class InMemoryLogger : public util::Logger {
2✔
3287
    public:
2✔
3288
        void do_log(const util::LogCategory& cat, Level level, const std::string& msg) final
2✔
3289
        {
138✔
3290
            auto formatted_line = util::format("%1 %2 %3", cat.get_name(), level, msg);
138✔
3291
            std::lock_guard lk(mtx);
138✔
3292
            log_messages.emplace_back(std::move(formatted_line));
138✔
3293
        }
138✔
3294

3295
        std::vector<std::string> get_log_messages()
2✔
3296
        {
2✔
3297
            std::lock_guard lk(mtx);
2✔
3298
            std::vector<std::string> ret;
2✔
3299
            std::swap(ret, log_messages);
2✔
3300
            return ret;
2✔
3301
        }
2✔
3302

3303
        std::mutex mtx;
2✔
3304
        std::vector<std::string> log_messages;
2✔
3305
    };
2✔
3306

3307
    auto in_mem_logger = std::make_shared<InMemoryLogger>();
2✔
3308
    in_mem_logger->set_level_threshold(InMemoryLogger::Level::all);
2✔
3309
    TestAppSession app_session(get_runtime_app_session(), nullptr, DeleteApp{false}, ReconnectMode::normal, nullptr,
2✔
3310
                               in_mem_logger);
2✔
3311

3312
    const auto partition = random_string(100);
2✔
3313
    SyncTestFile config(app_session.app()->current_user(), partition, util::none);
2✔
3314
    auto realm = successfully_async_open_realm(config);
2✔
3315
    auto sync_session = realm->sync_session();
2✔
3316
    auto coid = SyncSession::OnlyForTesting::get_appservices_connection_id(*sync_session);
2✔
3317

3318
    auto transition_log_msg =
2✔
3319
        util::format("Connection[1] Connected to app services with request id: \"%1\". Further log entries for this "
2✔
3320
                     "connection will be prefixed with \"Connection[1:%1]\" instead of \"Connection[1]\"",
2✔
3321
                     coid);
2✔
3322
    auto bind_send_msg = util::format("Connection[1:%1] Session[1]: Sending: BIND", coid);
2✔
3323
    auto ping_send_msg = util::format("Connection[1:%1] Will emit a ping in", coid);
2✔
3324

3325
    auto log_messages = in_mem_logger->get_log_messages();
2✔
3326
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(transition_log_msg)));
2✔
3327
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(bind_send_msg)));
2✔
3328
    REQUIRE_THAT(log_messages, AnyMatch(ContainsSubstring(ping_send_msg)));
2✔
3329
}
2✔
3330

3331

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

3335
    const auto schema = get_default_schema();
2✔
3336

3337
    SyncServer server({});
2✔
3338
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
2✔
3339
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
2✔
3340
    OfflineAppSession::Config oas_config(transport);
2✔
3341
    oas_config.base_url = util::format("http://localhost:%1/", server.port());
2✔
3342
    oas_config.socket_provider = socket_provider;
2✔
3343
    OfflineAppSession oas(oas_config);
2✔
3344
    AutoVerifiedEmailCredentials creds;
2✔
3345
    auto app = oas.app();
2✔
3346
    const auto partition = random_string(100);
2✔
3347

3348
    transport->request_hook = [&](const Request& req) -> std::optional<Response> {
8✔
3349
        if (req.url.find("/location") == std::string::npos) {
8✔
3350
            return std::nullopt;
6✔
3351
        }
6✔
3352

3353
        REQUIRE(req.url == util::format("http://localhost:%1/api/client/v2.0/app/app_id/location", server.port()));
2!
3354
        return Response{
2✔
3355
            200,
2✔
3356
            0,
2✔
3357
            {},
2✔
3358
            nlohmann::json(nlohmann::json::object({
2✔
3359
                               {"hostname", util::format("http://localhost:%1", server.port())},
2✔
3360
                               {"ws_hostname", util::format("ws://localhost:%1", server.port())},
2✔
3361
                               {"sync_route", util::format("ws://localhost:%1/realm-sync", server.port())},
2✔
3362
                           }))
2✔
3363
                .dump(),
2✔
3364
        };
2✔
3365
    };
2✔
3366

3367
    SyncTestFile realm_config(oas, "test");
2✔
3368

3369
    auto r = Realm::get_shared_realm(realm_config);
2✔
3370
    REQUIRE(!wait_for_download(*r));
2!
3371
}
2✔
3372

3373
TEST_CASE("app: redirect handling", "[sync][pbs][app]") {
14✔
3374
    auto logger = util::Logger::get_default_logger();
14✔
3375

3376
    const auto schema = get_default_schema();
14✔
3377

3378
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
14✔
3379
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
14✔
3380
    OfflineAppSession::Config oas_config(transport);
14✔
3381
    oas_config.base_url = "http://original.invalid:9090";
14✔
3382
    oas_config.socket_provider = socket_provider;
14✔
3383
    OfflineAppSession oas(oas_config);
14✔
3384
    AutoVerifiedEmailCredentials creds;
14✔
3385
    auto app = oas.app();
14✔
3386
    const auto partition = random_string(100);
14✔
3387

3388
    SECTION("invalid redirect response reports and error") {
14✔
3389
        int request_count = 0;
2✔
3390

3391
        // This will fail due to no Location header
3392
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
2✔
3393
            logger->trace("request.url (%1): %2", request_count, request.url);
2✔
3394
            REQUIRE(request_count++ == 0);
2!
3395
            return Response{301, 0, {{"Content-Type", "application/json"}}, "Some body data"};
2✔
3396
        };
2✔
3397
        app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3398
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
3399
                REQUIRE(error);
2!
3400
                REQUIRE(error->is_client_error());
2!
3401
                REQUIRE(error->code() == ErrorCodes::ClientRedirectError);
2!
3402
                REQUIRE(error->reason() == "Redirect response missing location header");
2!
3403
            });
2✔
3404

3405
        // This will fail due to empty Location header
3406
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
2✔
3407
            logger->trace("request.url (%1): %2", request_count, request.url);
2✔
3408
            REQUIRE(request_count++ == 1);
2!
3409
            return Response{301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"};
2✔
3410
        };
2✔
3411

3412
        app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3413
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
3414
                REQUIRE(error);
2!
3415
                REQUIRE(error->is_client_error());
2!
3416
                REQUIRE(error->code() == ErrorCodes::ClientRedirectError);
2!
3417
                REQUIRE(error->reason() == "Redirect response missing location header");
2!
3418
            });
2✔
3419
    }
2✔
3420

3421
    SECTION("valid redirect response") {
14✔
3422
        int request_count = 0;
2✔
3423
        const std::string second_host = "http://second.invalid:9091";
2✔
3424
        const std::string third_host = "http://third.invalid:9092";
2✔
3425

3426
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
10✔
3427
            logger->trace("Received request[%1]: %2", request_count, request.url);
10✔
3428
            switch (request_count++) {
10✔
3429
                case 0:
2✔
3430
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3431
                    REQUIRE_THAT(request.url, ContainsSubstring(*oas_config.base_url));
2✔
3432
                    return Response{301, 0, {{"Location", second_host}, {"Content-Type", "application/json"}}, ""};
2✔
3433

3434
                case 1:
2✔
3435
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3436
                    REQUIRE_THAT(request.url, ContainsSubstring(second_host));
2✔
3437
                    return Response{301, 0, {{"Location", third_host}, {"Content-Type", "application/json"}}, ""};
2✔
3438

3439
                case 2:
2✔
3440
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3441
                    REQUIRE_THAT(request.url, ContainsSubstring(third_host));
2✔
3442
                    return Response{301, 0, {{"Location", second_host}, {"Content-Type", "application/json"}}, ""};
2✔
3443

3444
                case 3:
2✔
3445
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3446
                    REQUIRE_THAT(request.url, ContainsSubstring(second_host));
2✔
3447
                    return std::nullopt;
2✔
3448

3449
                default:
2✔
3450
                    // some.fake.url is the location reported by UnitTestTransport
3451
                    REQUIRE_THAT(request.url, ContainsSubstring("https://some.fake.url"));
2✔
3452
                    return std::nullopt;
2✔
3453
            }
10✔
3454
        };
10✔
3455

3456
        // This will be successful after a couple of retries due to the redirect response
3457
        app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3458
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
3459
                REQUIRE(!error);
2!
3460
            });
2✔
3461
    }
2✔
3462

3463
    SECTION("too many redirects eventually reports an error") {
14✔
3464
        int request_count = 0;
2✔
3465
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
42✔
3466
            logger->trace("request.url (%1): %2", request_count, request.url);
42✔
3467
            REQUIRE(request_count < 21);
42!
3468
            ++request_count;
42✔
3469
            return Response{request_count % 2 == 1 ? 308 : 301,
42✔
3470
                            0,
42✔
3471
                            {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}},
42✔
3472
                            "Some body data"};
42✔
3473
        };
42✔
3474

3475
        app->log_in_with_credentials(app::AppCredentials::username_password(creds.email, creds.password),
2✔
3476
                                     [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
3477
                                         REQUIRE(!user);
2!
3478
                                         REQUIRE(error);
2!
3479
                                         REQUIRE(error->is_client_error());
2!
3480
                                         REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects);
2!
3481
                                         REQUIRE(error->reason() == "number of redirections exceeded 20");
2!
3482
                                     });
2✔
3483
        REQUIRE(request_count == 21);
2!
3484
    }
2✔
3485

3486
    SECTION("server in maintenance reports error") {
14✔
3487
        transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
3488
            nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"},
2✔
3489
                                                {"error", "This service is currently undergoing maintenance"},
2✔
3490
                                                {"link", "https://link.to/server_logs"}};
2✔
3491
            return Response{500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()};
2✔
3492
        };
2✔
3493

3494
        app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password),
2✔
3495
                                     [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
3496
                                         REQUIRE(!user);
2!
3497
                                         REQUIRE(error);
2!
3498
                                         REQUIRE(error->is_service_error());
2!
3499
                                         REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress);
2!
3500
                                         REQUIRE(error->reason() ==
2!
3501
                                                 "This service is currently undergoing maintenance");
2✔
3502
                                         REQUIRE(error->link_to_server_logs == "https://link.to/server_logs");
2!
3503
                                         REQUIRE(*error->additional_status_code == 500);
2!
3504
                                     });
2✔
3505
    }
2✔
3506

3507
    SECTION("websocket redirects update existing session") {
14✔
3508
        SyncServer server({});
6✔
3509

3510
        transport->request_hook = [&](const Request& req) -> std::optional<Response> {
24✔
3511
            if (req.url.find("/location") != std::string::npos) {
24✔
3512
                return Response{
6✔
3513
                    200,
6✔
3514
                    0,
6✔
3515
                    {},
6✔
3516
                    nlohmann::json({
6✔
3517
                                       {"hostname", "http://some.fake.url"},
6✔
3518
                                       {"ws_hostname", "ws://ws.some.fake.url"},
6✔
3519
                                       {"sync_route", "ws://some.fake.url/realm-sync"},
6✔
3520
                                   })
6✔
3521
                        .dump(),
6✔
3522
                };
6✔
3523
            }
6✔
3524
            return std::nullopt;
18✔
3525
        };
24✔
3526

3527
        // The location info is fake, so we need to override it with the actual
3528
        // server endpoint
3529
        socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
10✔
3530
            ep.address = "127.0.0.1";
10✔
3531
            ep.port = server.port();
10✔
3532
        };
10✔
3533

3534
        SyncTestFile realm_config(oas, "test");
6✔
3535

3536
        std::mutex logout_mutex;
6✔
3537
        std::condition_variable logout_cv;
6✔
3538
        bool logged_out = false;
6✔
3539
        realm_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
6✔
3540
            if (error.status == ErrorCodes::AuthError) {
4✔
3541
                {
4✔
3542
                    std::unique_lock lk(logout_mutex);
4✔
3543
                    logged_out = true;
4✔
3544
                }
4✔
3545
                logout_cv.notify_one();
4✔
3546
                return;
4✔
3547
            }
4✔
UNCOV
3548
            util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
3549
                         error.status);
×
3550
            abort();
×
3551
        };
4✔
3552

3553
        auto r = Realm::get_shared_realm(realm_config);
6✔
3554
        REQUIRE(!wait_for_download(*r));
6!
3555
        auto sync_session = r->sync_session();
6✔
3556
        sync_session->pause();
6✔
3557
        SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*oas.sync_manager());
6✔
3558

3559
        int connect_count = 0;
6✔
3560
        socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
8✔
3561
            // Report a 308 response the first time we try to reconnect the websocket,
3562
            // which should result in App performing a location update.
3563
            // The actual Location header isn't used when we get a redirect on
3564
            // the websocket, so we don't need to supply it here
3565
            if (connect_count++ > 0)
8✔
3566
                return std::nullopt;
2✔
3567
            return sync::HTTPStatus::PermanentRedirect;
6✔
3568
        };
8✔
3569

3570
        SECTION("valid websocket redirect") {
6✔
3571
            socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
4✔
3572
                logger->trace("resolve attempt %1: %2", connect_count, ep.address);
4✔
3573
                // First call happens after the call to the above hook which will
3574
                // force a 308 response. Second call happens after the redirect
3575
                // has been handled.
3576
                REQUIRE(connect_count <= 2);
4!
3577
                if (connect_count == 2) {
4✔
UNCOV
3578
                    REQUIRE(ep.address == "ws.invalid");
×
3579
                }
×
3580

3581
                // Overriding the handshake result happens after dns resolution,
3582
                // so we need to set it to a valid endpoint for even the first call
3583
                ep.address = "127.0.0.1";
4✔
3584
                ep.port = server.port();
4✔
3585
            };
4✔
3586

3587
            int request_count = 0;
2✔
3588
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
6✔
3589
                logger->trace("request.url (%1): %2", request_count, request.url);
6✔
3590
                ++request_count;
6✔
3591

3592
                // First request should be a location request against the original URL
3593
                if (request_count == 1) {
6✔
3594
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3595
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3596
                    return Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
2✔
3597
                                    0,
2✔
3598
                                    {{"Location", "http://asdf.invalid"}},
2✔
3599
                                    ""};
2✔
3600
                }
2✔
3601

3602
                // Second request should be a location request against the new URL
3603
                if (request_count == 2) {
4✔
3604
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3605
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
2✔
3606
                    return Response{200,
2✔
3607
                                    0,
2✔
3608
                                    {},
2✔
3609
                                    nlohmann::json({
2✔
3610
                                                       {"hostname", "http://http.invalid"},
2✔
3611
                                                       {"ws_hostname", "ws://ws.invalid"},
2✔
3612
                                                       {"sync_route", "ws://ws.invalid/realm-sync"},
2✔
3613
                                                   })
2✔
3614
                                        .dump()};
2✔
3615
                }
2✔
3616

3617
                // Rest of the requests get handled normally
3618
                return std::nullopt;
2✔
3619
            };
4✔
3620

3621
            sync_session->resume();
2✔
3622
            REQUIRE(!wait_for_download(*r));
2!
3623
            REQUIRE(request_count > 1);
2!
3624
            REQUIRE(realm_config.sync_config->user->is_logged_in());
2!
3625

3626
            // Verify session is using the updated server url from the redirect
3627
            auto server_url = sync_session->full_realm_url();
2✔
3628
            REQUIRE_THAT(server_url, ContainsSubstring("ws.invalid"));
2✔
3629
        }
2✔
3630

3631
        SECTION("websocket redirect into auth error logs out user") {
6✔
3632
            int request_count = 0;
2✔
3633
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
6✔
3634
                logger->trace("request.url (%1): %2", request_count, request.url);
6✔
3635
                ++request_count;
6✔
3636

3637
                if (request_count == 1) {
6✔
3638
                    // First request should be a location request against the original URL
3639
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3640
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3641
                    return Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
2✔
3642
                                    0,
2✔
3643
                                    {{"Location", "http://asdf.invalid"}},
2✔
3644
                                    ""};
2✔
3645
                }
2✔
3646

3647
                // Second request should be a location request against the new URL
3648
                if (request_count == 2) {
4✔
3649
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3650
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
2✔
3651
                    return Response{200,
2✔
3652
                                    0,
2✔
3653
                                    {},
2✔
3654
                                    nlohmann::json({
2✔
3655
                                                       {"hostname", "http://http.invalid"},
2✔
3656
                                                       {"ws_hostname", "ws://ws.invalid"},
2✔
3657
                                                   })
2✔
3658
                                        .dump()};
2✔
3659
                }
2✔
3660

3661
                // Third request should be for an acccess token, which we reject
3662
                REQUIRE(request_count == 3);
2!
3663
                REQUIRE_THAT(request.url, ContainsSubstring("auth/session"));
2✔
3664
                return Response{static_cast<int>(sync::HTTPStatus::Unauthorized), 0, {}, ""};
2✔
3665
            };
2✔
3666

3667
            sync_session->resume();
2✔
3668
            REQUIRE(wait_for_download(*r));
2!
3669
            std::unique_lock lk(logout_mutex);
2✔
3670
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
3671
                return logged_out;
4✔
3672
            });
4✔
3673
            REQUIRE(result);
2!
3674
            REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in());
2!
3675
        }
2✔
3676

3677
        SECTION("too many websocket redirects logs out user") {
6✔
3678
            int request_count = 0;
2✔
3679
            const int max_http_redirects = 20; // from app.cpp in object-store
2✔
3680
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
42✔
3681
                logger->trace("request.url (%1): %2", request_count, request.url);
42✔
3682

3683
                // The test should never request anything other than /location
3684
                // even though the user is set to the logged-out state as trying
3685
                // to log out on the server needs to go through /location first too
3686
                REQUIRE_THAT(request.url, ContainsSubstring("/location"));
42✔
3687
                REQUIRE(request_count <= max_http_redirects);
42!
3688

3689
                // First request should be a location request against the original URL
3690
                // and rest should use the redirect url
3691
                if (request_count++ == 0) {
42✔
3692
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3693
                }
2✔
3694
                else {
40✔
3695
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
40✔
3696
                }
40✔
3697
                // Keep returning the redirected response
3698
                return Response{static_cast<int>(sync::HTTPStatus::MovedPermanently),
42✔
3699
                                0,
42✔
3700
                                {{"Location", "http://asdf.invalid"}},
42✔
3701
                                ""};
42✔
3702
            };
42✔
3703

3704
            sync_session->resume();
2✔
3705
            REQUIRE(wait_for_download(*r));
2!
3706
            std::unique_lock lk(logout_mutex);
2✔
3707
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
3708
                return logged_out;
4✔
3709
            });
4✔
3710
            REQUIRE(result);
2!
3711
            REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in());
2!
3712
        }
2✔
3713
    }
6✔
3714
}
14✔
3715

3716
TEST_CASE("app: base_url", "[sync][app][base_url]") {
22✔
3717
    struct BaseUrlTransport : UnitTestTransport {
22✔
3718
        std::string expected_url;
22✔
3719
        std::optional<std::string_view> redirect_url;
22✔
3720
        bool location_requested = false;
22✔
3721
        bool location_returns_error = false;
22✔
3722

3723
        void reset(std::string_view expect_url, std::optional<std::string_view> redir_url = std::nullopt)
22✔
3724
        {
60✔
3725
            expected_url = std::string(expect_url);
60✔
3726
            redirect_url = redir_url;
60✔
3727
            location_requested = false;
60✔
3728
            location_returns_error = false;
60✔
3729
        }
60✔
3730

3731
        void send_request_to_server(const Request& request,
22✔
3732
                                    util::UniqueFunction<void(const Response&)>&& completion) override
22✔
3733
        {
178✔
3734
            if (request.url.find("/location") != std::string::npos) {
178✔
3735
                CHECK(request.method == HttpMethod::get);
76!
3736
                CHECK_THAT(request.url, ContainsSubstring(expected_url));
76✔
3737
                location_requested = true;
76✔
3738
                if (location_returns_error) {
76✔
3739
                    completion(app::Response{static_cast<int>(sync::HTTPStatus::NotFound), 0, {}, "404 not found"});
18✔
3740
                    return;
18✔
3741
                }
18✔
3742
                if (redirect_url) {
58✔
3743
                    // Update the expected url to be the redirect url
3744
                    expected_url = std::string(*redirect_url);
16✔
3745
                    redirect_url.reset();
16✔
3746

3747
                    completion(app::Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
16✔
3748
                                             0,
16✔
3749
                                             {{"location", expected_url}},
16✔
3750
                                             "308 permanent redirect"});
16✔
3751
                    return;
16✔
3752
                }
16✔
3753
                auto ws_url = App::create_ws_host_url(expected_url);
42✔
3754
                completion(
42✔
3755
                    app::Response{static_cast<int>(sync::HTTPStatus::Ok),
42✔
3756
                                  0,
42✔
3757
                                  {},
42✔
3758
                                  util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
42✔
3759
                                               "\"%1\",\"ws_hostname\":\"%2\"}",
42✔
3760
                                               expected_url, ws_url)});
42✔
3761
                return;
42✔
3762
            }
58✔
3763

3764
            UnitTestTransport::send_request_to_server(request, std::move(completion));
102✔
3765
        }
102✔
3766
    };
22✔
3767

3768
    auto logger = util::Logger::get_default_logger();
22✔
3769

3770
    auto redir_transport = std::make_shared<BaseUrlTransport>();
22✔
3771
    auto get_config_with_base_url = [&](std::optional<std::string> base_url = std::nullopt) {
26✔
3772
        OfflineAppSession::Config config(redir_transport);
26✔
3773
        config.base_url = base_url;
26✔
3774
        return config;
26✔
3775
    };
26✔
3776

3777
    SECTION("Test App::create_ws_host_url") {
22✔
3778
        auto result = App::create_ws_host_url("blah");
2✔
3779
        CHECK(result == "blah");
2!
3780
        result = App::create_ws_host_url("http://localhost:9090");
2✔
3781
        CHECK(result == "ws://localhost:9090");
2!
3782
        result = App::create_ws_host_url("https://localhost:9090");
2✔
3783
        CHECK(result == "wss://localhost:9090");
2!
3784
        result = App::create_ws_host_url("https://localhost:9090/some/extra/stuff");
2✔
3785
        CHECK(result == "wss://localhost:9090/some/extra/stuff");
2!
3786
        result = App::create_ws_host_url("http://172.0.0.1:9090");
2✔
3787
        CHECK(result == "ws://172.0.0.1:9090");
2!
3788
        result = App::create_ws_host_url("https://172.0.0.1:9090");
2✔
3789
        CHECK(result == "wss://172.0.0.1:9090");
2!
3790
        // Old default base url
3791
        result = App::create_ws_host_url("http://realm.mongodb.com");
2✔
3792
        CHECK(result == "ws://ws.realm.mongodb.com");
2!
3793
        result = App::create_ws_host_url("https://realm.mongodb.com");
2✔
3794
        CHECK(result == "wss://ws.realm.mongodb.com");
2!
3795
        result = App::create_ws_host_url("https://realm.mongodb.com/some/extra/stuff");
2✔
3796
        CHECK(result == "wss://ws.realm.mongodb.com/some/extra/stuff");
2!
3797
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3798
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3799
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3800
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3801
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2✔
3802
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2!
3803
        // New default base url
3804
        result = App::create_ws_host_url("http://services.cloud.mongodb.com");
2✔
3805
        CHECK(result == "ws://ws.services.cloud.mongodb.com");
2!
3806
        result = App::create_ws_host_url("https://services.cloud.mongodb.com");
2✔
3807
        CHECK(result == "wss://ws.services.cloud.mongodb.com");
2!
3808
        result = App::create_ws_host_url("https://services.cloud.mongodb.com/some/extra/stuff");
2✔
3809
        CHECK(result == "wss://ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3810
        result = App::create_ws_host_url("http://us-east-1.aws.services.cloud.mongodb.com");
2✔
3811
        CHECK(result == "ws://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3812
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com");
2✔
3813
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3814
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com/some/extra/stuff");
2✔
3815
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3816
    }
2✔
3817

3818
    SECTION("Test app config baseurl") {
22✔
3819
        {
2✔
3820
            // First time through, base_url is empty; https://services.cloud.mongodb.com is expected
3821
            redir_transport->reset(App::default_base_url());
2✔
3822
            auto config = get_config_with_base_url();
2✔
3823
            OfflineAppSession oas(config);
2✔
3824
            auto app = oas.app();
2✔
3825

3826
            // Location is not requested until first app services request
3827
            CHECK(!redir_transport->location_requested);
2!
3828
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3829
            CHECK(app->get_host_url() == App::default_base_url());
2!
3830
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3831

3832
            oas.make_user();
2✔
3833
            CHECK(redir_transport->location_requested);
2!
3834
            CHECK(app->get_base_url() == App::default_base_url());
2!
3835
            CHECK(app->get_host_url() == App::default_base_url());
2!
3836
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3837
        }
2✔
3838
        {
2✔
3839
            // Second time through, base_url is set to https://alternate.someurl.fake is expected
3840
            redir_transport->reset("https://alternate.someurl.fake");
2✔
3841
            auto config = get_config_with_base_url("https://alternate.someurl.fake");
2✔
3842
            OfflineAppSession oas(config);
2✔
3843
            auto app = oas.app();
2✔
3844

3845
            // Location is not requested until first app services request
3846
            CHECK(!redir_transport->location_requested);
2!
3847
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3848
            CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3849
            CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3850

3851
            oas.make_user();
2✔
3852
            CHECK(redir_transport->location_requested);
2!
3853
            CHECK(app->get_base_url() == "https://alternate.someurl.fake");
2!
3854
            CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3855
            CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3856
        }
2✔
3857
        {
2✔
3858
            // Third time through, base_url is not set, expect https://services.cloud.mongodb.com,
3859
            // since metadata is no longer used
3860
            std::string expected_url = std::string(App::default_base_url());
2✔
3861
            std::string expected_wsurl = App::create_ws_host_url(App::default_base_url());
2✔
3862
            redir_transport->reset(expected_url);
2✔
3863
            auto config = get_config_with_base_url();
2✔
3864
            OfflineAppSession oas(config);
2✔
3865
            auto app = oas.app();
2✔
3866

3867
            // Location is not requested until first app services request
3868
            CHECK(!redir_transport->location_requested);
2!
3869
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3870
            CHECK(app->get_host_url() == expected_url);
2!
3871
            CHECK(app->get_ws_host_url() == expected_wsurl);
2!
3872

3873
            oas.make_user();
2✔
3874
            CHECK(redir_transport->location_requested);
2!
3875
            CHECK(app->get_base_url() == expected_url);
2!
3876
            CHECK(app->get_host_url() == expected_url);
2!
3877
            CHECK(app->get_ws_host_url() == expected_wsurl);
2!
3878
        }
2✔
3879
        {
2✔
3880
            // Fourth time through, base_url is set to https://some-other.someurl.fake, with a redirect
3881
            redir_transport->reset("https://some-other.someurl.fake", "http://redirect.someurl.fake");
2✔
3882
            auto config = get_config_with_base_url("https://some-other.someurl.fake");
2✔
3883
            OfflineAppSession oas(config);
2✔
3884
            auto app = oas.app();
2✔
3885

3886
            // Location is not requested until first app services request
3887
            CHECK(!redir_transport->location_requested);
2!
3888
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
3889
            CHECK(app->get_host_url() == "https://some-other.someurl.fake");
2!
3890
            CHECK(app->get_ws_host_url() == "wss://some-other.someurl.fake");
2!
3891

3892
            oas.make_user();
2✔
3893
            CHECK(redir_transport->location_requested);
2!
3894
            // Base URL is still set to the original value
3895
            CHECK(app->get_base_url() == "https://some-other.someurl.fake");
2!
3896
            // Hostname and ws hostname use the redirect URL values
3897
            CHECK(app->get_host_url() == "http://redirect.someurl.fake");
2!
3898
            CHECK(app->get_ws_host_url() == "ws://redirect.someurl.fake");
2!
3899
        }
2✔
3900
    }
2✔
3901

3902
    SECTION("Test update_baseurl") {
22✔
3903
        redir_transport->reset("https://alternate.someurl.fake");
2✔
3904
        auto config = get_config_with_base_url("https://alternate.someurl.fake");
2✔
3905
        OfflineAppSession oas(config);
2✔
3906
        auto app = oas.app();
2✔
3907

3908
        // Location is not requested until first app services request
3909
        CHECK(!redir_transport->location_requested);
2!
3910

3911
        oas.make_user();
2✔
3912
        CHECK(redir_transport->location_requested);
2!
3913
        CHECK(app->get_base_url() == "https://alternate.someurl.fake");
2!
3914
        CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3915
        CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3916

3917
        redir_transport->reset(App::default_base_url());
2✔
3918

3919
        // Revert the base URL to the default URL value using the empty string
3920
        app->update_base_url("", [](util::Optional<app::AppError> error) {
2✔
3921
            CHECK(!error);
2!
3922
        });
2✔
3923
        CHECK(redir_transport->location_requested);
2!
3924
        CHECK(app->get_base_url() == App::default_base_url());
2!
3925
        CHECK(app->get_host_url() == App::default_base_url());
2!
3926
        CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3927
        oas.make_user();
2✔
3928
    }
2✔
3929

3930
    SECTION("Test update_baseurl with redirect") {
22✔
3931
        redir_transport->reset("https://alternate.someurl.fake");
2✔
3932
        auto config = get_config_with_base_url("https://alternate.someurl.fake");
2✔
3933
        OfflineAppSession oas(config);
2✔
3934
        auto app = oas.app();
2✔
3935

3936
        // Location is not requested until first app services request
3937
        CHECK(!redir_transport->location_requested);
2!
3938

3939
        oas.make_user();
2✔
3940
        CHECK(redir_transport->location_requested);
2!
3941
        CHECK(app->get_base_url() == "https://alternate.someurl.fake");
2!
3942
        CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3943
        CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3944

3945
        redir_transport->reset("http://some-other.someurl.fake", "https://redirect.otherurl.fake");
2✔
3946

3947
        app->update_base_url("http://some-other.someurl.fake", [](util::Optional<app::AppError> error) {
2✔
3948
            CHECK(!error);
2!
3949
        });
2✔
3950
        CHECK(redir_transport->location_requested);
2!
3951
        CHECK(app->get_base_url() == "http://some-other.someurl.fake");
2!
3952
        CHECK(app->get_host_url() == "https://redirect.otherurl.fake");
2!
3953
        CHECK(app->get_ws_host_url() == "wss://redirect.otherurl.fake");
2!
3954
        // Expected URL is still "https://redirect.otherurl.fake" after redirect
3955
        oas.make_user();
2✔
3956
    }
2✔
3957

3958
    SECTION("Test update_baseurl returns error") {
22✔
3959
        redir_transport->reset("http://alternate.someurl.fake");
2✔
3960
        auto config = get_config_with_base_url("http://alternate.someurl.fake");
2✔
3961
        OfflineAppSession oas(config);
2✔
3962
        auto app = oas.app();
2✔
3963

3964
        // Location is not requested until first app services request
3965
        CHECK(!redir_transport->location_requested);
2!
3966

3967
        oas.make_user();
2✔
3968
        CHECK(redir_transport->location_requested);
2!
3969
        CHECK(app->get_base_url() == "http://alternate.someurl.fake");
2!
3970
        CHECK(app->get_host_url() == "http://alternate.someurl.fake");
2!
3971
        CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake");
2!
3972

3973
        redir_transport->reset("https://some-other.someurl.fake");
2✔
3974
        redir_transport->location_returns_error = true;
2✔
3975

3976
        app->update_base_url("https://some-other.someurl.fake", [](util::Optional<app::AppError> error) {
2✔
3977
            CHECK(error);
2!
3978
        });
2✔
3979
        CHECK(redir_transport->location_requested);
2!
3980
        // Verify original url values are still being used
3981
        CHECK(app->get_base_url() == "http://alternate.someurl.fake");
2!
3982
        CHECK(app->get_host_url() == "http://alternate.someurl.fake");
2!
3983
        CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake");
2!
3984
    }
2✔
3985

3986
    // Verify new sync session updates location when created with cached user
3987
    SECTION("Verify new sync session updates location") {
22✔
3988
        bool use_ssl = GENERATE(true, false);
12✔
3989
        std::string initial_host = "alternate.someurl.fake";
12✔
3990
        unsigned initial_port = use_ssl ? 443 : 80;
12✔
3991
        std::string expected_host = "redirect.someurl.fake";
12✔
3992
        unsigned expected_port = 8081;
12✔
3993
        std::string init_url = util::format("http%1://%2", use_ssl ? "s" : "", initial_host);
12✔
3994
        std::string init_wsurl = util::format("ws%1://%2", use_ssl ? "s" : "", initial_host);
12✔
3995
        std::string redir_url = util::format("http%1://%2:%3", use_ssl ? "s" : "", expected_host, expected_port);
12✔
3996
        std::string redir_wsurl = util::format("ws%1://%2:%3", use_ssl ? "s" : "", expected_host, expected_port);
12✔
3997

3998
        auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
12✔
3999
        socket_provider->websocket_connect_func = []() -> std::optional<SocketProviderError> {
12✔
4000
            return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, "404 not found");
8✔
4001
        };
8✔
4002

4003
        auto config = get_config_with_base_url(init_url);
12✔
4004
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
12✔
4005
        config.socket_provider = socket_provider;
12✔
4006
        config.storage_path = util::make_temp_dir();
12✔
4007
        config.delete_storage = false; // persist the current user
12✔
4008

4009
        // Log in to get a cached user
4010
        {
12✔
4011
            redir_transport->reset(init_url);
12✔
4012
            OfflineAppSession oas(config);
12✔
4013
            auto app = oas.app();
12✔
4014

4015
            {
12✔
4016
                auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
4017
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
12✔
4018
                CHECK_FALSE(verified);
12!
4019
            }
12✔
4020

4021
            oas.make_user();
12✔
4022
            CHECK(redir_transport->location_requested);
12!
4023
            CHECK(app->get_base_url() == init_url);
12!
4024
            CHECK(app->get_host_url() == init_url);
12!
4025
            CHECK(app->get_ws_host_url() == init_wsurl);
12!
4026
            auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
4027
            CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
12✔
4028
            CHECK_THAT(sync_route, ContainsSubstring(init_wsurl));
12✔
4029
            CHECK(verified);
12!
4030
        }
12✔
4031

4032
        // the next instance can clean up the files
4033
        config.delete_storage = true;
12✔
4034
        // Recreate the app using the cached user and start a sync session, which will is set to fail on connect
4035
        SECTION("Sync Session fails on connect after updating location") {
12✔
4036
            enum class TestState { start, session_started };
4✔
4037
            TestingStateMachine<TestState> state(TestState::start);
4✔
4038
            redir_transport->reset(init_url, redir_url);
4✔
4039

4040
            OfflineAppSession oas(config);
4✔
4041
            auto app = oas.app();
4✔
4042

4043
            // Verify the default sync route, which has not been verified
4044
            {
4✔
4045
                auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
4046
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
4✔
4047
                CHECK_FALSE(verified);
4!
4048
            }
4✔
4049
            REQUIRE(app->current_user());
4!
4050

4051
            std::atomic<int> connect_attempts = 0;
4✔
4052
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
8✔
4053
                // First connection attempt is to the originally specified endpoint. Since
4054
                // it hasn't been verified, we swallow the error and do a location update,
4055
                // which will then try to connect to the redir target
4056
                auto attempt = connect_attempts++;
8✔
4057
                if (attempt == 0) {
8✔
4058
                    CHECK(ep.address == initial_host);
4!
4059
                    CHECK(ep.port == initial_port);
4!
4060
                    CHECK(ep.is_ssl == use_ssl);
4!
4061
                }
4✔
4062
                else {
4✔
4063
                    CHECK(ep.address == expected_host);
4!
4064
                    CHECK(ep.port == expected_port);
4!
4065
                    CHECK(ep.is_ssl == use_ssl);
4!
4066
                }
4✔
4067
            };
8✔
4068

4069
            RealmConfig r_config;
4✔
4070
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
4✔
4071
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
4✔
4072
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
4✔
4073
                // Websocket is forcing a 404 failure so it won't actually start
4074
                logger->debug("Received expected error: %1", error.status);
4✔
4075
                CHECK(!error.status.is_ok());
4!
4076
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
4!
4077
                CHECK(!error.is_fatal);
4!
4078
                state.transition_to(TestState::session_started);
4✔
4079
            };
4✔
4080
            auto realm = Realm::get_shared_realm(r_config);
4✔
4081
            state.wait_for(TestState::session_started);
4✔
4082

4083
            CHECK(redir_transport->location_requested);
4!
4084
            CHECK(app->get_base_url() == init_url);
4!
4085
            CHECK(app->get_host_url() == redir_url);
4!
4086
            CHECK(app->get_ws_host_url() == redir_wsurl);
4!
4087
            auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
4088
            CHECK_THAT(sync_route, ContainsSubstring(redir_wsurl));
4✔
4089
            CHECK(verified);
4!
4090
        }
4✔
4091

4092
        SECTION("Sync Session retries after initial location failure") {
12✔
4093
            enum class TestState { start, location_failed, session_started };
8✔
4094
            TestingStateMachine<TestState> state(TestState::start);
8✔
4095
            const int retry_count = GENERATE(1, 3);
8✔
4096

4097
            redir_transport->reset(init_url);
8✔
4098
            redir_transport->location_returns_error = true;
8✔
4099

4100
            OfflineAppSession oas(config);
8✔
4101
            auto app = oas.app();
8✔
4102
            REQUIRE(app->current_user());
8!
4103
            // Verify the default sync route, which has not been verified
4104
            {
8✔
4105
                auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
4106
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
8✔
4107
                CHECK_FALSE(verified);
8!
4108
            }
8✔
4109

4110
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
24✔
4111
                CHECK(ep.address == initial_host);
24!
4112
                CHECK(ep.port == initial_port);
24!
4113
                CHECK(ep.is_ssl == use_ssl);
24!
4114
            };
24✔
4115

4116
            socket_provider->websocket_connect_func = [&, request_count =
8✔
4117
                                                              0]() mutable -> std::optional<SocketProviderError> {
32✔
4118
                if (request_count == 0) {
32✔
4119
                    // First connection attempt is to the unverified initial URL
4120
                    // since we have a valid access token but have never successfully
4121
                    // connected. This failing will trigger a location update.
4122
                    CHECK_FALSE(redir_transport->location_requested);
8!
4123
                }
8✔
4124
                else {
24✔
4125
                    // All attempts after the first should have requested location
4126
                    CHECK(redir_transport->location_requested);
24!
4127
                    redir_transport->location_requested = false;
24✔
4128
                }
24✔
4129

4130
                // Until we allow a location request to succeed we should keep
4131
                // getting the original unverified route
4132
                if (redir_transport->location_returns_error) {
32✔
4133
                    CHECK(app->get_base_url() == init_url);
24!
4134
                    CHECK(app->get_host_url() == init_url);
24!
4135
                    CHECK(app->get_ws_host_url() == init_wsurl);
24!
4136
                    {
24✔
4137
                        auto [sync_route, verified] = app->sync_manager()->sync_route();
24✔
4138
                        CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
24✔
4139
                        CHECK_FALSE(verified);
24!
4140
                    }
24✔
4141
                }
24✔
4142

4143
                // After the chosen number of attempts let the location request succeed
4144
                if (request_count++ >= retry_count) {
32✔
4145
                    redir_transport->reset(init_url, redir_url);
16✔
4146
                    socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
16✔
4147
                        CHECK(ep.address == expected_host);
8!
4148
                        CHECK(ep.port == expected_port);
8!
4149
                        CHECK(ep.is_ssl == use_ssl);
8!
4150
                        state.transition_to(TestState::location_failed);
8✔
4151
                    };
8✔
4152
                }
16✔
4153

4154
                return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
32✔
4155
                                           "404 not found");
32✔
4156
            };
32✔
4157

4158
            RealmConfig r_config;
8✔
4159
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
8✔
4160
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
8✔
4161
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
8✔
4162
                // An error will only be reported if the websocket fails after updating the location and access token
4163
                logger->debug("Received expected error: %1", error.status);
8✔
4164
                CHECK(!error.status.is_ok());
8!
4165
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
8!
4166
                CHECK(!error.is_fatal);
8!
4167
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
8✔
4168
                    if (cur_state == TestState::location_failed) {
8✔
4169
                        // This time, the session was being started, and the location was successful
4170
                        // Websocket is forcing a 404 failure so it won't actually start
4171
                        return TestState::session_started;
8✔
4172
                    }
8✔
UNCOV
4173
                    return std::nullopt;
×
4174
                });
8✔
4175
            };
8✔
4176
            auto realm = Realm::get_shared_realm(r_config);
8✔
4177
            state.wait_for(TestState::session_started);
8✔
4178

4179
            CHECK(app->get_base_url() == init_url);
8!
4180
            CHECK(app->get_host_url() == redir_url);
8!
4181
            CHECK(app->get_ws_host_url() == redir_wsurl);
8!
4182
            auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
4183
            CHECK_THAT(sync_route, ContainsSubstring(redir_wsurl));
8✔
4184
            CHECK(verified);
8!
4185
        }
8✔
4186
    }
12✔
4187
}
22✔
4188

4189
TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") {
2✔
4190
    TestAppSession session;
2✔
4191
    auto app = session.app();
2✔
4192
    auto user = app->current_user();
2✔
4193

4194
    SECTION("custom user data happy path") {
2✔
4195
        bool processed = false;
2✔
4196
        app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})},
2✔
4197
                           [&](auto response, auto error) {
2✔
4198
                               CHECK(error == none);
2!
4199
                               CHECK(response);
2!
4200
                               CHECK(*response == true);
2!
4201
                               processed = true;
2✔
4202
                           });
2✔
4203
        CHECK(processed);
2!
4204
        processed = false;
2✔
4205
        app->refresh_custom_data(user, [&](auto) {
2✔
4206
            processed = true;
2✔
4207
        });
2✔
4208
        CHECK(processed);
2!
4209
        auto data = *user->custom_data();
2✔
4210
        CHECK(data["favorite_color"] == "green");
2!
4211
    }
2✔
4212
}
2✔
4213

4214
TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") {
2✔
4215
    TestAppSession session;
2✔
4216
    auto app = session.app();
2✔
4217
    auto jwt = create_jwt(session.app()->app_id());
2✔
4218

4219
    SECTION("jwt happy path") {
2✔
4220
        bool processed = false;
2✔
4221
        bool logged_in_once = false;
2✔
4222

4223
        auto token = app->subscribe([&logged_in_once, &app](auto&) {
2✔
4224
            REQUIRE(!logged_in_once);
2!
4225
            auto user = app->current_user();
2✔
4226
            auto metadata = user->user_profile();
2✔
4227

4228
            // Ensure that the JWT metadata fields are available when the callback is fired on login.
4229
            CHECK(metadata["name"] == "Foo Bar");
2!
4230
            logged_in_once = true;
2✔
4231
        });
2✔
4232

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

4235
        app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})},
2✔
4236
                           [&](auto response, auto error) {
2✔
4237
                               CHECK(error == none);
2!
4238
                               CHECK(response);
2!
4239
                               CHECK(*response == true);
2!
4240
                               processed = true;
2✔
4241
                           });
2✔
4242
        CHECK(processed);
2!
4243
        processed = false;
2✔
4244
        app->refresh_custom_data(user, [&](auto) {
2✔
4245
            processed = true;
2✔
4246
        });
2✔
4247
        CHECK(processed);
2!
4248
        auto metadata = user->user_profile();
2✔
4249
        auto custom_data = *user->custom_data();
2✔
4250
        CHECK(custom_data["name"] == "Not Foo Bar");
2!
4251
        CHECK(metadata["name"] == "Foo Bar");
2!
4252

4253
        REQUIRE(logged_in_once);
2!
4254

4255
        app->unsubscribe(token);
2✔
4256
    }
2✔
4257
}
2✔
4258

4259
namespace cf = realm::collection_fixtures;
4260
TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects,
4261
                   cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects,
4262
                   cf::DictionaryOfMixedLinks)
4263
{
12✔
4264
    const std::string valid_pk_name = "_id";
12✔
4265
    const auto partition = random_string(100);
12✔
4266
    TestType test_type("collection", "dest");
12✔
4267
    Schema schema = {{"source",
12✔
4268
                      {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4269
                       {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4270
                       test_type.property()}},
12✔
4271
                     {"dest",
12✔
4272
                      {
12✔
4273
                          {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4274
                          {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4275
                      }}};
12✔
4276
    auto server_app_config = minimal_app_config("collections_of_links", schema);
12✔
4277
    TestAppSession test_session(create_app(server_app_config));
12✔
4278

4279
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
36✔
4280
        timed_sleeping_wait_for([&]() -> bool {
2,361✔
4281
            r->refresh();
2,361✔
4282
            TableRef dest = r->read_group().get_table(table_name);
2,361✔
4283
            size_t cur_count = dest->size();
2,361✔
4284
            return cur_count == count;
2,361✔
4285
        });
2,361✔
4286
    };
36✔
4287
    auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) {
24✔
4288
        timed_sleeping_wait_for([&]() -> bool {
702✔
4289
            r->refresh();
702✔
4290
            return test_type.size_of_collection(obj) == count;
702✔
4291
        });
702✔
4292
    };
24✔
4293

4294
    CppContext c;
12✔
4295
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
12✔
4296
        r->begin_transaction();
12✔
4297
        auto object = Object::create(
12✔
4298
            c, r, "source",
12✔
4299
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
12✔
4300
            CreatePolicy::ForceCreate);
12✔
4301

4302
        for (auto link : links) {
36✔
4303
            auto& obj = object.get_obj();
36✔
4304
            test_type.add_link(obj, link);
36✔
4305
        }
36✔
4306
        r->commit_transaction();
12✔
4307
        return object;
12✔
4308
    };
12✔
4309

4310
    auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink {
36✔
4311
        r->begin_transaction();
36✔
4312
        auto obj = Object::create(
36✔
4313
            c, r, "dest",
36✔
4314
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
36✔
4315
            CreatePolicy::ForceCreate);
36✔
4316
        r->commit_transaction();
36✔
4317
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
36✔
4318
    };
36✔
4319

4320
    auto require_links_to_match_ids = [&](std::vector<Obj> links, std::vector<int64_t> expected) {
48✔
4321
        std::vector<int64_t> actual;
48✔
4322
        for (auto obj : links) {
108✔
4323
            actual.push_back(obj.get<Int>(valid_pk_name));
108✔
4324
        }
108✔
4325
        std::sort(actual.begin(), actual.end());
48✔
4326
        std::sort(expected.begin(), expected.end());
48✔
4327
        REQUIRE(actual == expected);
48!
4328
    };
48✔
4329

4330
    SECTION("integration testing") {
12✔
4331
        auto app = test_session.app();
12✔
4332
        SyncTestFile config1(app->current_user(), partition, schema); // uses the current user created above
12✔
4333
        config1.automatic_change_notifications = false;
12✔
4334
        auto r1 = realm::Realm::get_shared_realm(config1);
12✔
4335
        Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source"));
12✔
4336

4337
        create_user_and_log_in(app);                                  // changes the current user
12✔
4338
        SyncTestFile config2(app->current_user(), partition, schema); // uses the user created above
12✔
4339
        config2.automatic_change_notifications = false;
12✔
4340
        auto r2 = realm::Realm::get_shared_realm(config2);
12✔
4341
        Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source"));
12✔
4342

4343
        constexpr int64_t source_pk = 0;
12✔
4344
        constexpr int64_t dest_pk_1 = 1;
12✔
4345
        constexpr int64_t dest_pk_2 = 2;
12✔
4346
        constexpr int64_t dest_pk_3 = 3;
12✔
4347
        Object object;
12✔
4348

4349
        { // add a container collection with three valid links
12✔
4350
            REQUIRE(r1_source_objs.size() == 0);
12!
4351
            ObjLink dest1 = create_one_dest_object(r1, dest_pk_1);
12✔
4352
            ObjLink dest2 = create_one_dest_object(r1, dest_pk_2);
12✔
4353
            ObjLink dest3 = create_one_dest_object(r1, dest_pk_3);
12✔
4354
            object = create_one_source_object(r1, source_pk, {dest1, dest2, dest3});
12✔
4355
            REQUIRE(r1_source_objs.size() == 1);
12!
4356
            REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4357
            REQUIRE(r1_source_objs.get(0).get<String>("realm_id") == partition);
12!
4358
            require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4359
        }
12✔
4360

4361
        size_t expected_coll_size = 3;
12✔
4362
        std::vector<int64_t> remaining_dest_object_ids;
12✔
4363
        { // erase one of the destination objects
12✔
4364
            wait_for_num_objects_to_equal(r2, "class_source", 1);
12✔
4365
            wait_for_num_objects_to_equal(r2, "class_dest", 3);
12✔
4366
            REQUIRE(r2_source_objs.size() == 1);
12!
4367
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4368
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3);
12!
4369
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4370
            require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4371
            r2->begin_transaction();
12✔
4372
            linked_objects[0].remove();
12✔
4373
            r2->commit_transaction();
12✔
4374
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name),
12✔
4375
                                         linked_objects[2].template get<Int>(valid_pk_name)};
12✔
4376
            expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3;
12✔
4377
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4378
        }
12✔
4379

4380
        { // remove a link from the collection
12✔
4381
            wait_for_num_objects_to_equal(r1, "class_dest", 2);
12✔
4382
            REQUIRE(r1_source_objs.size() == 1);
12!
4383
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4384
            auto linked_objects = test_type.get_links(r1_source_objs.get(0));
12✔
4385
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4386
            r1->begin_transaction();
12✔
4387
            auto obj = r1_source_objs.get(0);
12✔
4388
            test_type.remove_link(obj,
12✔
4389
                                  ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()});
12✔
4390
            r1->commit_transaction();
12✔
4391
            --expected_coll_size;
12✔
4392
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name)};
12✔
4393
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4394
        }
12✔
4395
        bool coll_cleared = false;
12✔
4396
        advance_and_notify(*r1);
12✔
4397
        auto collection = test_type.get_collection(r1, r1_source_objs.get(0));
12✔
4398
        auto token = collection.add_notification_callback([&coll_cleared](CollectionChangeSet c) {
24✔
4399
            coll_cleared = c.collection_was_cleared;
24✔
4400
        });
24✔
4401

4402
        { // clear the collection
12✔
4403
            REQUIRE(r2_source_objs.size() == 1);
12!
4404
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4405
            wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size);
12✔
4406
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4407
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4408
            r2->begin_transaction();
12✔
4409
            test_type.clear_collection(r2_source_objs.get(0));
12✔
4410
            r2->commit_transaction();
12✔
4411
            expected_coll_size = 0;
12✔
4412
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4413
        }
12✔
4414

4415
        { // expect an empty collection
12✔
4416
            REQUIRE(!coll_cleared);
12!
4417
            REQUIRE(r1_source_objs.size() == 1);
12!
4418
            wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size);
12✔
4419
            advance_and_notify(*r1);
12✔
4420
            REQUIRE(coll_cleared);
12!
4421
        }
12✔
4422
    }
12✔
4423
}
12✔
4424

4425
TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID,
4426
                   cf::UUID, cf::BoxedOptional<cf::Int>, cf::UnboxedOptional<cf::String>, cf::BoxedOptional<cf::OID>,
4427
                   cf::BoxedOptional<cf::UUID>)
4428
{
16✔
4429
    const std::string valid_pk_name = "_id";
16✔
4430
    const std::string partition_key_col_name = "partition_key_prop";
16✔
4431
    const std::string table_name = "class_partition_test_type";
16✔
4432
    auto partition_property = Property(partition_key_col_name, TestType::property_type);
16✔
4433
    Schema schema = {{Group::table_name_to_class_name(table_name),
16✔
4434
                      {
16✔
4435
                          {valid_pk_name, PropertyType::Int, true},
16✔
4436
                          partition_property,
16✔
4437
                      }}};
16✔
4438
    auto server_app_config = minimal_app_config("partition_types_app_name", schema);
16✔
4439
    server_app_config.partition_key = partition_property;
16✔
4440
    TestAppSession test_session(create_app(server_app_config));
16✔
4441
    auto app = test_session.app();
16✔
4442

4443
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
48✔
4444
        timed_sleeping_wait_for([&]() -> bool {
3,350✔
4445
            r->refresh();
3,350✔
4446
            TableRef dest = r->read_group().get_table(table_name);
3,350✔
4447
            size_t cur_count = dest->size();
3,350✔
4448
            return cur_count == count;
3,350✔
4449
        });
3,350✔
4450
    };
48✔
4451
    using T = typename TestType::Type;
16✔
4452
    CppContext c;
16✔
4453
    auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) {
48✔
4454
        r->begin_transaction();
48✔
4455
        auto object = Object::create(
48✔
4456
            c, r, Group::table_name_to_class_name(table_name),
48✔
4457
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}),
48✔
4458
            CreatePolicy::ForceCreate);
48✔
4459
        r->commit_transaction();
48✔
4460
    };
48✔
4461

4462
    auto get_bson = [](T val) -> bson::Bson {
96✔
4463
        if constexpr (std::is_same_v<T, StringData>) {
96✔
4464
            return val.is_null() ? bson::Bson(util::none) : bson::Bson(val);
48✔
4465
        }
14✔
4466
        else if constexpr (TestType::is_optional) {
54✔
4467
            return val ? bson::Bson(*val) : bson::Bson(util::none);
40✔
4468
        }
20✔
4469
        else {
28✔
4470
            return bson::Bson(val);
28✔
4471
        }
28✔
4472
    };
96✔
4473

4474
    SECTION("can round trip an object") {
16✔
4475
        auto values = TestType::values();
16✔
4476
        auto user1 = app->current_user();
16✔
4477
        create_user_and_log_in(app);
16✔
4478
        auto user2 = app->current_user();
16✔
4479
        REQUIRE(user1);
16!
4480
        REQUIRE(user2);
16!
4481
        REQUIRE(user1 != user2);
16!
4482
        for (T partition_value : values) {
48✔
4483
            SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above
48✔
4484
            auto r1 = realm::Realm::get_shared_realm(config1);
48✔
4485
            Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name));
48✔
4486

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

4491
            const int64_t pk_value = random_int();
48✔
4492
            {
48✔
4493
                REQUIRE(r1_source_objs.size() == 0);
48!
4494
                create_object(r1, pk_value, TestType::to_any(partition_value));
48✔
4495
                REQUIRE(r1_source_objs.size() == 1);
48!
4496
                REQUIRE(r1_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4497
                REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4498
            }
48✔
4499
            {
48✔
4500
                wait_for_num_objects_to_equal(r2, table_name, 1);
48✔
4501
                REQUIRE(r2_source_objs.size() == 1);
48!
4502
                REQUIRE(r2_source_objs.size() == 1);
48!
4503
                REQUIRE(r2_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4504
                REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4505
            }
48✔
4506
        }
48✔
4507
    }
16✔
4508
}
16✔
4509

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

4513
    Schema schema{
4✔
4514
        {"TopLevel",
4✔
4515
         {
4✔
4516
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
4517
             {"full_text", Property::IsFulltextIndexed{true}},
4✔
4518
         }},
4✔
4519
    };
4✔
4520

4521
    auto server_app_config = minimal_app_config("full_text", schema);
4✔
4522
    auto app_session = create_app(server_app_config);
4✔
4523
    const auto partition = random_string(100);
4✔
4524
    TestAppSession test_session(app_session, nullptr);
4✔
4525
    SyncTestFile config(test_session.app()->current_user(), partition, schema);
4✔
4526
    SharedRealm realm;
4✔
4527
    SECTION("sync open") {
4✔
4528
        INFO("realm opened without async open");
2✔
4529
        realm = Realm::get_shared_realm(config);
2✔
4530
    }
2✔
4531
    SECTION("async open") {
4✔
4532
        INFO("realm opened with async open");
2✔
4533
        auto async_open_task = Realm::get_synchronized_realm(config);
2✔
4534

4535
        auto realm_future = async_open_task->start();
2✔
4536
        realm = Realm::get_shared_realm(std::move(realm_future.get()));
2✔
4537
    }
2✔
4538

4539
    CppContext c(realm);
4✔
4540
    auto obj_id_1 = ObjectId::gen();
4✔
4541
    auto obj_id_2 = ObjectId::gen();
4✔
4542
    realm->begin_transaction();
4✔
4543
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}}));
4✔
4544
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}}));
4✔
4545
    realm->commit_transaction();
4✔
4546

4547
    auto table = realm->read_group().get_table("class_TopLevel");
4✔
4548
    REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext);
4!
4549
    Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world"));
4✔
4550
    REQUIRE(world_results.size() == 1);
4!
4551
    REQUIRE(world_results.get<Obj>(0).get_primary_key() == Mixed{obj_id_1});
4!
4552
}
4✔
4553

4554
#endif // REALM_ENABLE_AUTH_TESTS
4555

4556
TEST_CASE("app: custom error handling", "[sync][app][custom errors]") {
2✔
4557
    class CustomErrorTransport : public GenericNetworkTransport {
2✔
4558
    public:
2✔
4559
        CustomErrorTransport(int code, const std::string& message)
2✔
4560
            : m_code(code)
2✔
4561
            , m_message(message)
2✔
4562
        {
2✔
4563
        }
2✔
4564

4565
        void send_request_to_server(const Request&, util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4566
        {
2✔
4567
            completion(Response{0, m_code, HttpHeaders(), m_message});
2✔
4568
        }
2✔
4569

4570
    private:
2✔
4571
        int m_code;
2✔
4572
        std::string m_message;
2✔
4573
    };
2✔
4574

4575
    SECTION("custom code and message is sent back") {
2✔
4576
        OfflineAppSession offline_session({std::make_shared<CustomErrorTransport>(1001, "Boom!")});
2✔
4577
        auto error = failed_log_in(offline_session.app());
2✔
4578
        CHECK(error.is_custom_error());
2!
4579
        CHECK(*error.additional_status_code == 1001);
2!
4580
        CHECK(error.reason() == "Boom!");
2!
4581
    }
2✔
4582
}
2✔
4583

4584
// MARK: - Unit Tests
4585

4586
static const std::string bad_access_token = "lolwut";
4587
static const std::string dummy_device_id = "123400000000000000000000";
4588

4589
TEST_CASE("subscribable unit tests", "[sync][app]") {
8✔
4590
    struct Foo : public Subscribable<Foo> {
8✔
4591
        void event()
8✔
4592
        {
18✔
4593
            emit_change_to_subscribers(*this);
18✔
4594
        }
18✔
4595
    };
8✔
4596

4597
    auto foo = Foo();
8✔
4598

4599
    SECTION("subscriber receives events") {
8✔
4600
        auto event_count = 0;
2✔
4601
        auto token = foo.subscribe([&event_count](auto&) {
6✔
4602
            event_count++;
6✔
4603
        });
6✔
4604

4605
        foo.event();
2✔
4606
        foo.event();
2✔
4607
        foo.event();
2✔
4608

4609
        CHECK(event_count == 3);
2!
4610
    }
2✔
4611

4612
    SECTION("subscriber can unsubscribe") {
8✔
4613
        auto event_count = 0;
2✔
4614
        auto token = foo.subscribe([&event_count](auto&) {
2✔
4615
            event_count++;
2✔
4616
        });
2✔
4617

4618
        foo.event();
2✔
4619
        CHECK(event_count == 1);
2!
4620

4621
        foo.unsubscribe(token);
2✔
4622
        foo.event();
2✔
4623
        CHECK(event_count == 1);
2!
4624
    }
2✔
4625

4626
    SECTION("subscriber is unsubscribed on dtor") {
8✔
4627
        auto event_count = 0;
2✔
4628
        {
2✔
4629
            auto token = foo.subscribe([&event_count](auto&) {
2✔
4630
                event_count++;
2✔
4631
            });
2✔
4632

4633
            foo.event();
2✔
4634
            CHECK(event_count == 1);
2!
4635
        }
2✔
4636
        foo.event();
2✔
4637
        CHECK(event_count == 1);
2!
4638
    }
2✔
4639

4640
    SECTION("multiple subscribers receive events") {
8✔
4641
        auto event_count = 0;
2✔
4642
        {
2✔
4643
            auto token1 = foo.subscribe([&event_count](auto&) {
2✔
4644
                event_count++;
2✔
4645
            });
2✔
4646
            auto token2 = foo.subscribe([&event_count](auto&) {
2✔
4647
                event_count++;
2✔
4648
            });
2✔
4649

4650
            foo.event();
2✔
4651
            CHECK(event_count == 2);
2!
4652
        }
2✔
4653
        foo.event();
2✔
4654
        CHECK(event_count == 2);
2!
4655
    }
2✔
4656
}
8✔
4657

4658
TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") {
6✔
4659
    auto transport = std::make_shared<UnitTestTransport>();
6✔
4660
    OfflineAppSession::Config config{transport};
6✔
4661
    transport->set_profile(profile_0);
6✔
4662

4663
    SECTION("login_anonymous good") {
6✔
4664
        config.storage_path = util::make_temp_dir();
2✔
4665
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
2✔
4666
        {
2✔
4667
            config.delete_storage = false;
2✔
4668
            OfflineAppSession oas(config);
2✔
4669
            auto app = oas.app();
2✔
4670
            auto user = log_in(app);
2✔
4671

4672
            REQUIRE(user->identities().size() == 1);
2!
4673
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4674
            UserProfile user_profile = user->user_profile();
2✔
4675

4676
            CHECK(user_profile.name() == profile_0_name);
2!
4677
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4678
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4679
            CHECK(user_profile.email() == profile_0_email);
2!
4680
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4681
            CHECK(user_profile.gender() == profile_0_gender);
2!
4682
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4683
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4684
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4685
        }
2✔
4686
        // assert everything is stored properly between runs
4687
        {
2✔
4688
            config.delete_storage = true; // clean up after this session
2✔
4689
            OfflineAppSession oas(config);
2✔
4690
            auto app = oas.app();
2✔
4691
            REQUIRE(app->all_users().size() == 1);
2!
4692
            auto user = app->all_users()[0];
2✔
4693
            REQUIRE(user->identities().size() == 1);
2!
4694
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4695
            UserProfile user_profile = user->user_profile();
2✔
4696

4697
            CHECK(user_profile.name() == profile_0_name);
2!
4698
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4699
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4700
            CHECK(user_profile.email() == profile_0_email);
2!
4701
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4702
            CHECK(user_profile.gender() == profile_0_gender);
2!
4703
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4704
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4705
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4706
        }
2✔
4707
    }
2✔
4708

4709
    SECTION("login_anonymous bad") {
6✔
4710
        struct transport : UnitTestTransport {
2✔
4711
            void send_request_to_server(const Request& request,
2✔
4712
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4713
            {
4✔
4714
                if (request.url.find("/login") != std::string::npos) {
4✔
4715
                    completion({200, 0, {}, user_json(bad_access_token).dump()});
2✔
4716
                }
2✔
4717
                else {
2✔
4718
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
2✔
4719
                }
2✔
4720
            }
4✔
4721
        };
2✔
4722

4723
        config.transport = instance_of<transport>;
2✔
4724
        OfflineAppSession oas(config);
2✔
4725
        auto error = failed_log_in(oas.app());
2✔
4726
        CHECK(error.reason() == std::string("Could not log in user: received malformed JWT"));
2!
4727
        CHECK(error.code_string() == "BadToken");
2!
4728
        CHECK(error.is_json_error());
2!
4729
        CHECK(error.code() == ErrorCodes::BadToken);
2!
4730
    }
2✔
4731

4732
    SECTION("login_anonynous multiple users") {
6✔
4733
        OfflineAppSession oas(config);
2✔
4734
        auto app = oas.app();
2✔
4735

4736
        auto user1 = log_in(app);
2✔
4737
        auto user2 = log_in(app, AppCredentials::anonymous(false));
2✔
4738
        CHECK(user1 != user2);
2!
4739
    }
2✔
4740
}
6✔
4741

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

4746
    auto logged_in_user = oas.make_user();
6✔
4747
    bool processed = false;
6✔
4748
    ObjectId obj_id(UnitTestTransport::api_key_id.c_str());
6✔
4749

4750
    SECTION("create api key") {
6✔
4751
        client.create_api_key(UnitTestTransport::api_key_name, logged_in_user,
2✔
4752
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4753
                                  REQUIRE_FALSE(error);
2!
4754
                                  CHECK(user_api_key.disabled == false);
2!
4755
                                  CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4756
                                  CHECK(user_api_key.key == UnitTestTransport::api_key);
2!
4757
                                  CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4758
                              });
2✔
4759
    }
2✔
4760

4761
    SECTION("fetch api key") {
6✔
4762
        client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4763
            REQUIRE_FALSE(error);
2!
4764
            CHECK(user_api_key.disabled == false);
2!
4765
            CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4766
            CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4767
        });
2✔
4768
    }
2✔
4769

4770
    SECTION("fetch api keys") {
6✔
4771
        client.fetch_api_keys(logged_in_user,
2✔
4772
                              [&](std::vector<App::UserAPIKey> user_api_keys, Optional<AppError> error) {
2✔
4773
                                  REQUIRE_FALSE(error);
2!
4774
                                  CHECK(user_api_keys.size() == 2);
2!
4775
                                  for (auto user_api_key : user_api_keys) {
4✔
4776
                                      CHECK(user_api_key.disabled == false);
4!
4777
                                      CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
4!
4778
                                      CHECK(user_api_key.name == UnitTestTransport::api_key_name);
4!
4779
                                  }
4✔
4780
                                  processed = true;
2✔
4781
                              });
2✔
4782
        CHECK(processed);
2!
4783
    }
2✔
4784
}
6✔
4785

4786
TEST_CASE("app: user_semantics", "[sync][app][user]") {
12✔
4787
    OfflineAppSession oas;
12✔
4788
    auto app = oas.app();
12✔
4789

4790
    const auto login_user_email_pass = [=] {
12✔
4791
        return log_in(app, AppCredentials::username_password("bob", "thompson"));
6✔
4792
    };
6✔
4793
    const auto login_user_anonymous = [=] {
18✔
4794
        return log_in(app, AppCredentials::anonymous());
18✔
4795
    };
18✔
4796

4797
    CHECK(!app->current_user());
12!
4798

4799
    int event_processed = 0;
12✔
4800
    auto token = app->subscribe([&](auto&) {
28✔
4801
        event_processed++;
28✔
4802
        // Read the current user to verify that doing so does not deadlock
4803
        app->current_user();
28✔
4804
    });
28✔
4805

4806
    SECTION("current user is populated") {
12✔
4807
        const auto user1 = login_user_anonymous();
2✔
4808
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4809
        CHECK(event_processed == 1);
2!
4810
    }
2✔
4811

4812
    SECTION("current user is updated on login") {
12✔
4813
        const auto user1 = login_user_anonymous();
2✔
4814
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4815
        const auto user2 = login_user_email_pass();
2✔
4816
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4817
        CHECK(user1->user_id() != user2->user_id());
2!
4818
        CHECK(event_processed == 2);
2!
4819
    }
2✔
4820

4821
    SECTION("current user is updated to last used user on logout") {
12✔
4822
        const auto user1 = login_user_anonymous();
2✔
4823
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4824
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4825

4826
        const auto user2 = login_user_email_pass();
2✔
4827
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4828
        CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn);
2!
4829
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4830
        CHECK(user1 != user2);
2!
4831

4832
        // should reuse existing session
4833
        const auto user3 = login_user_anonymous();
2✔
4834
        CHECK(user3 == user1);
2!
4835

4836
        auto user_events_processed = 0;
2✔
4837
        auto _ = user3->subscribe([&user_events_processed](auto&) {
2✔
4838
            user_events_processed++;
2✔
4839
        });
2✔
4840

4841
        app->log_out([](auto) {});
2✔
4842
        CHECK(user_events_processed == 1);
2!
4843
        REQUIRE(app->current_user());
2!
4844
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4845

4846
        CHECK(app->all_users().size() == 1);
2!
4847
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4848

4849
        CHECK(event_processed == 4);
2!
4850
    }
2✔
4851

4852
    SECTION("anon users are removed on logout") {
12✔
4853
        const auto user1 = login_user_anonymous();
2✔
4854
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4855
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4856

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

4863
        app->log_out([](auto) {});
2✔
4864
        CHECK(app->all_users().size() == 0);
2!
4865

4866
        CHECK(event_processed == 3);
2!
4867
    }
2✔
4868

4869
    SECTION("logout user") {
12✔
4870
        auto user1 = login_user_email_pass();
2✔
4871
        auto user2 = login_user_anonymous();
2✔
4872

4873
        // Anonymous users are special
4874
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4875
            REQUIRE_FALSE(error);
2!
4876
        });
2✔
4877
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4878

4879
        // Other users can be LoggedOut
4880
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4881
            REQUIRE_FALSE(error);
2!
4882
        });
2✔
4883
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4884

4885
        // Logging out already logged out users does nothing
4886
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4887
            REQUIRE_FALSE(error);
2!
4888
        });
2✔
4889
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4890

4891
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4892
            REQUIRE_FALSE(error);
2!
4893
        });
2✔
4894
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4895

4896
        CHECK(event_processed == 4);
2!
4897
    }
2✔
4898

4899
    SECTION("unsubscribed observers no longer process events") {
12✔
4900
        app->unsubscribe(token);
2✔
4901

4902
        const auto user1 = login_user_anonymous();
2✔
4903
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4904
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4905

4906
        const auto user2 = login_user_anonymous();
2✔
4907
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4908
        CHECK(app->all_users().size() == 1);
2!
4909
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4910
        CHECK(user1->user_id() == user2->user_id());
2!
4911

4912
        app->log_out([](auto) {});
2✔
4913
        CHECK(app->all_users().size() == 0);
2!
4914

4915
        CHECK(event_processed == 0);
2!
4916
    }
2✔
4917
}
12✔
4918

4919
namespace {
4920
struct ErrorCheckingTransport : public GenericNetworkTransport {
4921
    ErrorCheckingTransport(Response* r)
4922
        : m_response(r)
5✔
4923
    {
10✔
4924
    }
10✔
4925
    void send_request_to_server(const Request& request,
4926
                                util::UniqueFunction<void(const Response&)>&& completion) override
4927
    {
20✔
4928
        // Make sure to return a valid location response
4929
        if (request.url.find("/location") != std::string::npos) {
20✔
4930
            completion(Response{200,
10✔
4931
                                0,
10✔
4932
                                {{"content-type", "application/json"}},
10✔
4933
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
10✔
4934
                                "\"http://some.fake.url\",\"ws_hostname\":\"ws://some.fake.url\"}"});
10✔
4935
            return;
10✔
4936
        }
10✔
4937

4938
        completion(Response(*m_response));
10✔
4939
    }
10✔
4940

4941
private:
4942
    Response* m_response;
4943
};
4944
} // namespace
4945

4946
TEST_CASE("app: response error handling", "[sync][app]") {
10✔
4947
    std::string response_body = nlohmann::json({{"access_token", good_access_token},
10✔
4948
                                                {"refresh_token", good_access_token},
10✔
4949
                                                {"user_id", "Brown Bear"},
10✔
4950
                                                {"device_id", "Panda Bear"}})
10✔
4951
                                    .dump();
10✔
4952

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

4955
    OfflineAppSession oas({std::make_shared<ErrorCheckingTransport>(&response)});
10✔
4956
    auto app = oas.app();
10✔
4957

4958
    SECTION("http 404") {
10✔
4959
        response.http_status_code = 404;
2✔
4960
        auto error = failed_log_in(app);
2✔
4961
        CHECK(!error.is_json_error());
2!
4962
        CHECK(!error.is_custom_error());
2!
4963
        CHECK(!error.is_service_error());
2!
4964
        CHECK(error.is_http_error());
2!
4965
        CHECK(*error.additional_status_code == 404);
2!
4966
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4967
    }
2✔
4968
    SECTION("http 500") {
10✔
4969
        response.http_status_code = 500;
2✔
4970
        auto error = failed_log_in(app);
2✔
4971
        CHECK(!error.is_json_error());
2!
4972
        CHECK(!error.is_custom_error());
2!
4973
        CHECK(!error.is_service_error());
2!
4974
        CHECK(error.is_http_error());
2!
4975
        CHECK(*error.additional_status_code == 500);
2!
4976
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4977
        CHECK(error.link_to_server_logs.empty());
2!
4978
    }
2✔
4979

4980
    SECTION("custom error code") {
10✔
4981
        response.custom_status_code = 42;
2✔
4982
        response.body = "Custom error message";
2✔
4983
        auto error = failed_log_in(app);
2✔
4984
        CHECK(!error.is_http_error());
2!
4985
        CHECK(!error.is_json_error());
2!
4986
        CHECK(!error.is_service_error());
2!
4987
        CHECK(error.is_custom_error());
2!
4988
        CHECK(*error.additional_status_code == 42);
2!
4989
        CHECK(error.reason() == std::string("Custom error message"));
2!
4990
        CHECK(error.link_to_server_logs.empty());
2!
4991
    }
2✔
4992

4993
    SECTION("session error code") {
10✔
4994
        response.headers = HttpHeaders{{"Content-Type", "application/json"}};
2✔
4995
        response.http_status_code = 400;
2✔
4996
        response.body = nlohmann::json({{"error_code", "MongoDBError"},
2✔
4997
                                        {"error", "a fake MongoDB error message!"},
2✔
4998
                                        {"access_token", good_access_token},
2✔
4999
                                        {"refresh_token", good_access_token},
2✔
5000
                                        {"user_id", "Brown Bear"},
2✔
5001
                                        {"device_id", "Panda Bear"},
2✔
5002
                                        {"link", "http://...whatever the server passes us"}})
2✔
5003
                            .dump();
2✔
5004
        auto error = failed_log_in(app);
2✔
5005
        CHECK(!error.is_http_error());
2!
5006
        CHECK(!error.is_json_error());
2!
5007
        CHECK(!error.is_custom_error());
2!
5008
        CHECK(error.is_service_error());
2!
5009
        CHECK(error.code() == ErrorCodes::MongoDBError);
2!
5010
        CHECK(error.reason() == std::string("a fake MongoDB error message!"));
2!
5011
        CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us"));
2!
5012
    }
2✔
5013

5014
    SECTION("json error code") {
10✔
5015
        response.body = "this: is not{} a valid json body!";
2✔
5016
        auto error = failed_log_in(app);
2✔
5017
        CHECK(!error.is_http_error());
2!
5018
        CHECK(error.is_json_error());
2!
5019
        CHECK(!error.is_custom_error());
2!
5020
        CHECK(!error.is_service_error());
2!
5021
        CHECK(error.code() == ErrorCodes::MalformedJson);
2!
5022
        CHECK(error.reason() ==
2!
5023
              std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error "
2✔
5024
                          "while parsing value - invalid literal; last read: 'th'"));
2✔
5025
        CHECK(error.code_string() == "MalformedJson");
2!
5026
    }
2✔
5027
}
10✔
5028

5029
TEST_CASE("app: switch user", "[sync][app][user]") {
4✔
5030
    OfflineAppSession oas;
4✔
5031
    auto app = oas.app();
4✔
5032

5033
    bool processed = false;
4✔
5034

5035
    SECTION("switch user expect success") {
4✔
5036
        CHECK(app->all_users().size() == 0);
2!
5037

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

5042
        // Log in user 2
5043
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
5044
        CHECK(app->current_user() == user_b);
2!
5045

5046
        CHECK(app->all_users().size() == 2);
2!
5047

5048
        app->switch_user(user_a);
2✔
5049
        CHECK(app->current_user() == user_a);
2!
5050

5051
        app->switch_user(user_b);
2✔
5052

5053
        CHECK(app->current_user() == user_b);
2!
5054
        processed = true;
2✔
5055
        CHECK(processed);
2!
5056
    }
2✔
5057

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

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

5065
        app->log_out([&](Optional<AppError> error) {
2✔
5066
            REQUIRE_FALSE(error);
2!
5067
        });
2✔
5068

5069
        CHECK(app->current_user() == nullptr);
2!
5070
        CHECK(user_a->state() == SyncUser::State::LoggedOut);
2!
5071

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

5077
        REQUIRE_THROWS_AS(app->switch_user(user_a), AppError);
2✔
5078
        CHECK(app->current_user() == user_b);
2!
5079
    }
2✔
5080
}
4✔
5081

5082
TEST_CASE("app: remove user", "[sync][app][user]") {
4✔
5083
    OfflineAppSession oas;
4✔
5084
    auto app = oas.app();
4✔
5085

5086
    SECTION("remove anonymous user") {
4✔
5087
        CHECK(app->all_users().size() == 0);
2!
5088

5089
        // Log in user 1
5090
        auto user_a = log_in(app);
2✔
5091
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
5092

5093
        app->log_out(user_a, [&](Optional<AppError> error) {
2✔
5094
            REQUIRE_FALSE(error);
2!
5095
            // a logged out anon user will be marked as Removed, not LoggedOut
5096
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
5097
        });
2✔
5098
        CHECK(app->all_users().empty());
2!
5099

5100
        app->remove_user(user_a, [&](Optional<AppError> error) {
2✔
5101
            CHECK(error->reason() == "User has already been removed");
2!
5102
            CHECK(app->all_users().size() == 0);
2!
5103
        });
2✔
5104

5105
        // Log in user 2
5106
        auto user_b = log_in(app);
2✔
5107
        CHECK(app->current_user() == user_b);
2!
5108
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
5109
        CHECK(app->all_users().size() == 1);
2!
5110

5111
        app->remove_user(user_b, [&](Optional<AppError> error) {
2✔
5112
            REQUIRE_FALSE(error);
2!
5113
            CHECK(app->all_users().size() == 0);
2!
5114
        });
2✔
5115

5116
        CHECK(app->current_user() == nullptr);
2!
5117

5118
        // check both handles are no longer valid
5119
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
5120
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
5121
    }
2✔
5122

5123
    SECTION("remove user with credentials") {
4✔
5124
        CHECK(app->all_users().size() == 0);
2!
5125
        CHECK(app->current_user() == nullptr);
2!
5126

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

5129
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
5130

5131
        app->log_out(user, [&](Optional<AppError> error) {
2✔
5132
            REQUIRE_FALSE(error);
2!
5133
        });
2✔
5134

5135
        CHECK(user->state() == SyncUser::State::LoggedOut);
2!
5136

5137
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
5138
            REQUIRE_FALSE(error);
2!
5139
        });
2✔
5140
        CHECK(app->all_users().size() == 0);
2!
5141

5142
        Optional<AppError> error;
2✔
5143
        app->remove_user(user, [&](Optional<AppError> err) {
2✔
5144
            error = err;
2✔
5145
        });
2✔
5146
        CHECK(error->code() > 0);
2!
5147
        CHECK(app->all_users().size() == 0);
2!
5148
        CHECK(user->state() == SyncUser::State::Removed);
2!
5149
    }
2✔
5150
}
4✔
5151

5152
TEST_CASE("app: link_user", "[sync][app][user]") {
4✔
5153
    OfflineAppSession oas;
4✔
5154
    auto app = oas.app();
4✔
5155

5156
    auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10));
4✔
5157
    auto password = random_string(10);
4✔
5158

5159
    auto custom_credentials = AppCredentials::facebook("a_token");
4✔
5160
    auto email_pass_credentials = AppCredentials::username_password(email, password);
4✔
5161

5162
    auto sync_user = log_in(app, email_pass_credentials);
4✔
5163
    REQUIRE(sync_user->identities().size() == 2);
4!
5164
    CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword);
4!
5165

5166
    SECTION("successful link") {
4✔
5167
        bool processed = false;
2✔
5168
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
5169
            REQUIRE_FALSE(error);
2!
5170
            REQUIRE(user);
2!
5171
            CHECK(user->user_id() == sync_user->user_id());
2!
5172
            processed = true;
2✔
5173
        });
2✔
5174
        CHECK(processed);
2!
5175
    }
2✔
5176

5177
    SECTION("link_user should fail when logged out") {
4✔
5178
        app->log_out([&](Optional<AppError> error) {
2✔
5179
            REQUIRE_FALSE(error);
2!
5180
        });
2✔
5181

5182
        bool processed = false;
2✔
5183
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
5184
            CHECK(error->reason() == "The specified user is not logged in.");
2!
5185
            CHECK(!user);
2!
5186
            processed = true;
2✔
5187
        });
2✔
5188
        CHECK(processed);
2!
5189
    }
2✔
5190
}
4✔
5191

5192
TEST_CASE("app: auth providers", "[sync][app][user]") {
20✔
5193
    SECTION("auth providers facebook") {
20✔
5194
        auto credentials = AppCredentials::facebook("a_token");
2✔
5195
        CHECK(credentials.provider() == AuthProvider::FACEBOOK);
2!
5196
        CHECK(credentials.provider_as_string() == IdentityProviderFacebook);
2!
5197
        CHECK(credentials.serialize_as_bson() ==
2!
5198
              bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}});
2✔
5199
    }
2✔
5200

5201
    SECTION("auth providers anonymous") {
20✔
5202
        auto credentials = AppCredentials::anonymous();
2✔
5203
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS);
2!
5204
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5205
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5206
    }
2✔
5207

5208
    SECTION("auth providers anonymous no reuse") {
20✔
5209
        auto credentials = AppCredentials::anonymous(false);
2✔
5210
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE);
2!
5211
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5212
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5213
    }
2✔
5214

5215
    SECTION("auth providers google authCode") {
20✔
5216
        auto credentials = AppCredentials::google(AuthCode("a_token"));
2✔
5217
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5218
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5219
        CHECK(credentials.serialize_as_bson() ==
2!
5220
              bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}});
2✔
5221
    }
2✔
5222

5223
    SECTION("auth providers google idToken") {
20✔
5224
        auto credentials = AppCredentials::google(IdToken("a_token"));
2✔
5225
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5226
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5227
        CHECK(credentials.serialize_as_bson() ==
2!
5228
              bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}});
2✔
5229
    }
2✔
5230

5231
    SECTION("auth providers apple") {
20✔
5232
        auto credentials = AppCredentials::apple("a_token");
2✔
5233
        CHECK(credentials.provider() == AuthProvider::APPLE);
2!
5234
        CHECK(credentials.provider_as_string() == IdentityProviderApple);
2!
5235
        CHECK(credentials.serialize_as_bson() ==
2!
5236
              bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}});
2✔
5237
    }
2✔
5238

5239
    SECTION("auth providers custom") {
20✔
5240
        auto credentials = AppCredentials::custom("a_token");
2✔
5241
        CHECK(credentials.provider() == AuthProvider::CUSTOM);
2!
5242
        CHECK(credentials.provider_as_string() == IdentityProviderCustom);
2!
5243
        CHECK(credentials.serialize_as_bson() ==
2!
5244
              bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}});
2✔
5245
    }
2✔
5246

5247
    SECTION("auth providers username password") {
20✔
5248
        auto credentials = AppCredentials::username_password("user", "pass");
2✔
5249
        CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD);
2!
5250
        CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword);
2!
5251
        CHECK(credentials.serialize_as_bson() ==
2!
5252
              bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}});
2✔
5253
    }
2✔
5254

5255
    SECTION("auth providers function") {
20✔
5256
        bson::BsonDocument function_params{{"name", "mongo"}};
2✔
5257
        auto credentials = AppCredentials::function(function_params);
2✔
5258
        CHECK(credentials.provider() == AuthProvider::FUNCTION);
2!
5259
        CHECK(credentials.provider_as_string() == IdentityProviderFunction);
2!
5260
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}});
2!
5261
    }
2✔
5262

5263
    SECTION("auth providers api key") {
20✔
5264
        auto credentials = AppCredentials::api_key("a key");
2✔
5265
        CHECK(credentials.provider() == AuthProvider::API_KEY);
2!
5266
        CHECK(credentials.provider_as_string() == IdentityProviderAPIKey);
2!
5267
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}});
2!
5268
        CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY);
2!
5269
    }
2✔
5270
}
20✔
5271

5272
TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") {
6✔
5273
    SECTION("refresh custom data happy path") {
6✔
5274
        static bool session_route_hit = false;
2✔
5275

5276
        struct transport : UnitTestTransport {
2✔
5277
            void send_request_to_server(const Request& request,
2✔
5278
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5279
            {
10✔
5280
                if (request.url.find("/session") != std::string::npos) {
10✔
5281
                    session_route_hit = true;
2✔
5282
                    nlohmann::json json{{"access_token", good_access_token}};
2✔
5283
                    completion({200, 0, {}, json.dump()});
2✔
5284
                }
2✔
5285
                else {
8✔
5286
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5287
                }
8✔
5288
            }
10✔
5289
        };
2✔
5290
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5291
        auto app = oas.app();
2✔
5292
        oas.make_user();
2✔
5293

5294
        bool processed = false;
2✔
5295
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5296
            REQUIRE_FALSE(error);
2!
5297
            CHECK(session_route_hit);
2!
5298
            processed = true;
2✔
5299
        });
2✔
5300
        CHECK(processed);
2!
5301
    }
2✔
5302

5303
    SECTION("refresh custom data sad path") {
6✔
5304
        static bool session_route_hit = false;
2✔
5305

5306
        struct transport : UnitTestTransport {
2✔
5307
            void send_request_to_server(const Request& request,
2✔
5308
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5309
            {
10✔
5310
                if (request.url.find("/session") != std::string::npos) {
10✔
5311
                    session_route_hit = true;
2✔
5312
                    nlohmann::json json{{"access_token", bad_access_token}};
2✔
5313
                    completion({200, 0, {}, json.dump()});
2✔
5314
                }
2✔
5315
                else {
8✔
5316
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5317
                }
8✔
5318
            }
10✔
5319
        };
2✔
5320

5321
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5322
        auto app = oas.app();
2✔
5323
        oas.make_user();
2✔
5324

5325
        bool processed = false;
2✔
5326
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5327
            CHECK(error->reason() == "malformed JWT");
2!
5328
            CHECK(error->code() == ErrorCodes::BadToken);
2!
5329
            CHECK(session_route_hit);
2!
5330
            processed = true;
2✔
5331
        });
2✔
5332
        CHECK(processed);
2!
5333
    }
2✔
5334

5335
    SECTION("refresh token ensure flow is correct") {
6✔
5336
        /*
5337
         Expected flow:
5338
         Login - this gets access and refresh tokens
5339
         Get profile - throw back a 401 error
5340
         Refresh token - get a new token for the user
5341
         Get profile - get the profile with the new token
5342
         */
5343
        struct transport : GenericNetworkTransport {
2✔
5344
            enum class TestState { unknown, location, login, profile_1, refresh, profile_2 };
2✔
5345
            TestingStateMachine<TestState> state{TestState::unknown};
2✔
5346
            void send_request_to_server(const Request& request,
2✔
5347
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5348
            {
10✔
5349
                if (request.url.find("/login") != std::string::npos) {
10✔
5350
                    CHECK(state.get() == TestState::location);
2!
5351
                    state.transition_to(TestState::login);
2✔
5352
                    completion({200, 0, {}, user_json(good_access_token).dump()});
2✔
5353
                }
2✔
5354
                else if (request.url.find("/profile") != std::string::npos) {
8✔
5355
                    auto item = AppUtils::find_header("Authorization", request.headers);
4✔
5356
                    CHECK(item);
4!
5357
                    auto access_token = item->second;
4✔
5358
                    // simulated bad token request
5359
                    if (access_token.find(good_access_token2) != std::string::npos) {
4✔
5360
                        CHECK(state.get() == TestState::refresh);
2!
5361
                        state.transition_to(TestState::profile_2);
2✔
5362
                        completion({200, 0, {}, user_profile_json().dump()});
2✔
5363
                    }
2✔
5364
                    else if (access_token.find(good_access_token) != std::string::npos) {
2✔
5365
                        CHECK(state.get() == TestState::login);
2!
5366
                        state.transition_to(TestState::profile_1);
2✔
5367
                        completion({401, 0, {}});
2✔
5368
                    }
2✔
5369
                }
4✔
5370
                else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) {
4✔
5371
                    CHECK(state.get() == TestState::profile_1);
2!
5372
                    state.transition_to(TestState::refresh);
2✔
5373
                    nlohmann::json json{{"access_token", good_access_token2}};
2✔
5374
                    completion({200, 0, {}, json.dump()});
2✔
5375
                }
2✔
5376
                else if (request.url.find("/location") != std::string::npos) {
2✔
5377
                    CHECK(state.get() == TestState::unknown);
2!
5378
                    state.transition_to(TestState::location);
2✔
5379
                    CHECK(request.method == HttpMethod::get);
2!
5380
                    completion({200,
2✔
5381
                                0,
2✔
5382
                                {},
2✔
5383
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
2✔
5384
                                "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"});
2✔
5385
                }
2✔
UNCOV
5386
                else {
×
5387
                    FAIL("Unexpected request in test code" + request.url);
×
5388
                }
×
5389
            }
10✔
5390
        };
2✔
5391

5392
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5393
        auto app = oas.app();
2✔
5394
        REQUIRE(log_in(app));
2!
5395
    }
2✔
5396
}
6✔
5397

5398
TEST_CASE("app: app released during async operation", "[app][user]") {
10✔
5399
    struct Transport : public UnitTestTransport {
10✔
5400
        std::string endpoint_to_hook;
10✔
5401
        std::optional<Request> stored_request;
10✔
5402
        util::UniqueFunction<void(const Response&)> stored_completion;
10✔
5403

5404
        void send_request_to_server(const Request& request,
10✔
5405
                                    util::UniqueFunction<void(const Response&)>&& completion) override
10✔
5406
        {
38✔
5407
            // Store the completion handler for the chosen endpoint so that we can
5408
            // invoke it after releasing the test's references to the App to
5409
            // verify that it doesn't crash
5410
            if (request.url.find(endpoint_to_hook) != std::string::npos) {
38✔
5411
                REQUIRE_FALSE(stored_request);
10!
5412
                REQUIRE_FALSE(stored_completion);
10!
5413
                stored_request = request;
10✔
5414
                stored_completion = std::move(completion);
10✔
5415
                return;
10✔
5416
            }
10✔
5417

5418
            UnitTestTransport::send_request_to_server(request, std::move(completion));
28✔
5419
        }
28✔
5420

5421
        bool has_stored() const
10✔
5422
        {
20✔
5423
            return !!stored_completion;
20✔
5424
        }
20✔
5425

5426
        void send_stored()
10✔
5427
        {
10✔
5428
            REQUIRE(stored_request);
10!
5429
            REQUIRE(stored_completion);
10!
5430
            UnitTestTransport::send_request_to_server(*stored_request, std::move(stored_completion));
10✔
5431
            stored_request.reset();
10✔
5432
            stored_completion = nullptr;
10✔
5433
        }
10✔
5434
    };
10✔
5435
    auto transport = std::make_shared<Transport>();
10✔
5436
    test_util::TestDirGuard base_path(util::make_temp_dir(), false);
10✔
5437
    AppConfig app_config;
10✔
5438
    set_app_config_defaults(app_config, transport);
10✔
5439
    app_config.base_file_path = base_path;
10✔
5440

5441
    SECTION("login") {
10✔
5442
        transport->endpoint_to_hook = GENERATE("/location", "/login", "/profile");
6✔
5443
        bool called = false;
6✔
5444
        {
6✔
5445
            auto app = App::get_app(App::CacheMode::Disabled, app_config);
6✔
5446
            app->log_in_with_credentials(AppCredentials::anonymous(),
6✔
5447
                                         [&](std::shared_ptr<SyncUser> user, util::Optional<AppError> error) mutable {
6✔
5448
                                             REQUIRE_FALSE(error);
6!
5449
                                             REQUIRE(user);
6!
5450
                                             REQUIRE(user->is_logged_in());
6!
5451
                                             called = true;
6✔
5452
                                         });
6✔
5453
            REQUIRE(transport->has_stored());
6!
5454
        }
6✔
5455
        REQUIRE_FALSE(called);
6!
5456
        transport->send_stored();
6✔
5457
        REQUIRE(called);
6!
5458
    }
6✔
5459

5460
    SECTION("access token refresh") {
10✔
5461
        transport->endpoint_to_hook = "/auth/session";
4✔
5462
        SECTION("directly via user") {
4✔
5463
            bool completion_called = false;
2✔
5464
            {
2✔
5465
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5466
                create_user_and_log_in(app);
2✔
5467
                app->current_user()->refresh_custom_data([&](std::optional<app::AppError> error) {
2✔
5468
                    REQUIRE_FALSE(error);
2!
5469
                    completion_called = true;
2✔
5470
                });
2✔
5471
                REQUIRE(transport->has_stored());
2!
5472
            }
2✔
5473

5474
            REQUIRE_FALSE(completion_called);
2!
5475
            transport->send_stored();
2✔
5476
            REQUIRE(completion_called);
2!
5477
        }
2✔
5478

5479
        SECTION("via sync session") {
4✔
5480
            {
2✔
5481
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5482
                create_user_and_log_in(app);
2✔
5483
                auto user = app->current_user();
2✔
5484
                SyncTestFile config(user, bson::Bson("test"));
2✔
5485
                // give the user an expired access token so that the first use will try to refresh it
5486
                user->update_data_for_testing([](auto& data) {
2✔
5487
                    data.access_token = RealmJWT(encode_fake_jwt("token", 123, 456));
2✔
5488
                });
2✔
5489
                REQUIRE_FALSE(transport->stored_completion);
2!
5490
                auto realm = Realm::get_shared_realm(config);
2✔
5491
                REQUIRE(transport->has_stored());
2!
5492
            }
2✔
5493
            transport->send_stored();
2✔
5494
        }
2✔
5495
    }
4✔
5496

5497
    REQUIRE_FALSE(transport->has_stored());
10!
5498
}
10✔
5499

5500
TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") {
8✔
5501
    constexpr uint64_t timeout_ms = 60000; // this is the default
8✔
5502
    OfflineAppSession oas({std::make_shared<UnitTestTransport>(timeout_ms)});
8✔
5503
    auto app = oas.app();
8✔
5504

5505
    auto user = log_in(app);
8✔
5506

5507
    using Headers = decltype(Request().headers);
8✔
5508

5509
    const auto url_prefix = "https://some.fake.url/api/client/v2.0/app/app_id/functions/call?baas_request="sv;
8✔
5510
    const auto get_request_args = [&](const Request& req) {
8✔
5511
        REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix);
8!
5512
        auto args = req.url.substr(url_prefix.size());
8✔
5513
        if (auto amp = args.find('&'); amp != std::string::npos) {
8✔
5514
            args.resize(amp);
2✔
5515
        }
2✔
5516

5517
        auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args));
8✔
5518
        REQUIRE(!!vec);
8!
5519
        auto parsed = bson::parse({vec->data(), vec->size()});
8✔
5520
        REQUIRE(parsed.type() == bson::Bson::Type::Document);
8!
5521
        auto out = parsed.operator const bson::BsonDocument&();
8✔
5522
        CHECK(out.size() == 3);
8!
5523
        return out;
8✔
5524
    };
8✔
5525

5526
    const auto make_request = [&](std::shared_ptr<User> user, auto&&... args) {
8✔
5527
        auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"});
8✔
5528
        CHECK(req.method == HttpMethod::get);
8!
5529
        CHECK(req.body == "");
8!
5530
        CHECK(req.headers == Headers{{"Accept", "text/event-stream"}});
8!
5531
        CHECK(req.timeout_ms == timeout_ms);
8!
5532

5533
        auto req_args = get_request_args(req);
8✔
5534
        CHECK(req_args["name"] == "func");
8!
5535
        CHECK(req_args["service"] == "svc");
8!
5536
        CHECK(req_args["arguments"] == bson::BsonArray{args...});
8!
5537

5538
        return req;
8✔
5539
    };
8✔
5540

5541
    SECTION("no args") {
8✔
5542
        auto req = make_request(nullptr);
2✔
5543
        CHECK(req.url.find('&') == std::string::npos);
2!
5544
    }
2✔
5545
    SECTION("args") {
8✔
5546
        auto req = make_request(nullptr, "arg1", "arg2");
2✔
5547
        CHECK(req.url.find('&') == std::string::npos);
2!
5548
    }
2✔
5549
    SECTION("percent encoding") {
8✔
5550
        // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded.
5551
        auto req = make_request(nullptr, ">>>>>?????");
2✔
5552

5553
        CHECK(req.url.find('&') == std::string::npos);
2!
5554
        CHECK_THAT(req.url, ContainsSubstring("%2B"));     // + (from >)
2✔
5555
        CHECK_THAT(req.url, ContainsSubstring("%2F"));     // / (from ?)
2✔
5556
        CHECK_THAT(req.url, ContainsSubstring("%3D"));     // = (tail padding)
2✔
5557
        CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding)
2!
5558
    }
2✔
5559
    SECTION("with user") {
8✔
5560
        auto req = make_request(user, "arg1", "arg2");
2✔
5561

5562
        auto amp = req.url.find('&');
2✔
5563
        REQUIRE(amp != std::string::npos);
2!
5564
        auto tail = req.url.substr(amp);
2✔
5565
        REQUIRE(tail == ("&baas_at=" + user->access_token()));
2!
5566
    }
2✔
5567
}
8✔
5568

5569
TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") {
4✔
5570
    SECTION("with empty map") {
4✔
5571
        auto profile = UserProfile(bson::BsonDocument());
2✔
5572
        CHECK(profile.name() == util::none);
2!
5573
        CHECK(profile.email() == util::none);
2!
5574
        CHECK(profile.picture_url() == util::none);
2!
5575
        CHECK(profile.first_name() == util::none);
2!
5576
        CHECK(profile.last_name() == util::none);
2!
5577
        CHECK(profile.gender() == util::none);
2!
5578
        CHECK(profile.birthday() == util::none);
2!
5579
        CHECK(profile.min_age() == util::none);
2!
5580
        CHECK(profile.max_age() == util::none);
2!
5581
    }
2✔
5582
    SECTION("with full map") {
4✔
5583
        auto profile = UserProfile(bson::BsonDocument({
2✔
5584
            {"first_name", "Jan"},
2✔
5585
            {"last_name", "Jaanson"},
2✔
5586
            {"name", "Jan Jaanson"},
2✔
5587
            {"email", "jan.jaanson@jaanson.com"},
2✔
5588
            {"gender", "none"},
2✔
5589
            {"birthday", "January 1, 1970"},
2✔
5590
            {"min_age", "0"},
2✔
5591
            {"max_age", "100"},
2✔
5592
            {"picture_url", "some"},
2✔
5593
        }));
2✔
5594
        CHECK(profile.name() == "Jan Jaanson");
2!
5595
        CHECK(profile.email() == "jan.jaanson@jaanson.com");
2!
5596
        CHECK(profile.picture_url() == "some");
2!
5597
        CHECK(profile.first_name() == "Jan");
2!
5598
        CHECK(profile.last_name() == "Jaanson");
2!
5599
        CHECK(profile.gender() == "none");
2!
5600
        CHECK(profile.birthday() == "January 1, 1970");
2!
5601
        CHECK(profile.min_age() == "0");
2!
5602
        CHECK(profile.max_age() == "100");
2!
5603
    }
2✔
5604
}
4✔
5605

5606
TEST_CASE("app: shared instances", "[sync][app]") {
2✔
5607
    test_util::TestDirGuard test_dir(util::make_temp_dir(), false);
2✔
5608

5609
    AppConfig base_config;
2✔
5610
    set_app_config_defaults(base_config, instance_of<UnitTestTransport>);
2✔
5611
    base_config.base_file_path = test_dir;
2✔
5612

5613
    auto config1 = base_config;
2✔
5614
    config1.app_id = "app1";
2✔
5615

5616
    auto config2 = base_config;
2✔
5617
    config2.app_id = "app1";
2✔
5618
    config2.base_url = std::string(App::default_base_url());
2✔
5619

5620
    auto config3 = base_config;
2✔
5621
    config3.app_id = "app2";
2✔
5622

5623
    auto config4 = base_config;
2✔
5624
    config4.app_id = "app2";
2✔
5625
    config4.base_url = "http://localhost:9090";
2✔
5626

5627
    // should all point to same underlying app
5628
    auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5629
    auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5630
    auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url);
2✔
5631
    auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2);
2✔
5632
    auto app1_5 = App::get_cached_app(config1.app_id);
2✔
5633

5634
    CHECK(app1_1 == app1_2);
2!
5635
    CHECK(app1_1 == app1_3);
2!
5636
    CHECK(app1_1 == app1_4);
2!
5637
    CHECK(app1_1 == app1_5);
2!
5638

5639
    // config3 and config4 should point to different apps
5640
    auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3);
2✔
5641
    auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url);
2✔
5642
    auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4);
2✔
5643
    auto app2_4 = App::get_cached_app(config3.app_id);
2✔
5644
    auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url");
2✔
5645

5646
    CHECK(app2_1 == app2_2);
2!
5647
    CHECK(app2_1 != app2_3);
2!
5648
    CHECK(app2_4 != nullptr);
2!
5649
    CHECK(app2_5 == nullptr);
2!
5650

5651
    CHECK(app1_1 != app2_1);
2!
5652
    CHECK(app1_1 != app2_3);
2!
5653
    CHECK(app1_1 != app2_4);
2!
5654
}
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