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

realm / realm-core / jorgen.edelbo_402

21 Aug 2024 11:10AM UTC coverage: 91.054% (-0.03%) from 91.085%
jorgen.edelbo_402

Pull #7803

Evergreen

jedelbo
Small fix to Table::typed_write

When writing the realm to a new file from a write transaction,
the Table may be COW so that the top ref is changed. So don't
use the ref that is present in the group when the operation starts.
Pull Request #7803: Feature/string compression

103494 of 181580 branches covered (57.0%)

1929 of 1999 new or added lines in 46 files covered. (96.5%)

695 existing lines in 51 files now uncovered.

220142 of 241772 relevant lines covered (91.05%)

7344461.76 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
                [&] {
46,642,633✔
2833
                    return did_receive_valid_token.load();
46,642,633✔
2834
                },
46,642,633✔
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([&] {
22,746✔
2913
                    return called.load();
22,746✔
2914
                });
22,746✔
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,404,166✔
3097
                std::lock_guard lk(mutex);
6,404,166✔
3098
                return done;
6,404,166✔
3099
            },
6,404,166✔
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([&] {
11,347✔
3235
                return error_did_occur.load();
11,347✔
3236
            });
11,347✔
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
        {
116✔
3290
            auto formatted_line = util::format("%1 %2 %3", cat.get_name(), level, msg);
116✔
3291
            std::lock_guard lk(mtx);
116✔
3292
            log_messages.emplace_back(std::move(formatted_line));
116✔
3293
        }
116✔
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
    TestAppSession app_session(get_runtime_app_session(), nullptr, DeleteApp{false}, ReconnectMode::normal, nullptr,
2✔
3309
                               in_mem_logger);
2✔
3310

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

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

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

3330

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4252
        REQUIRE(logged_in_once);
2!
4253

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4553
#endif // REALM_ENABLE_AUTH_TESTS
4554

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

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

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

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

4583
// MARK: - Unit Tests
4584

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

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

4596
    auto foo = Foo();
8✔
4597

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5032
    bool processed = false;
4✔
5033

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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