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

realm / realm-core / 2214

10 Apr 2024 11:21PM UTC coverage: 91.813% (-0.8%) from 92.623%
2214

push

Evergreen

web-flow
Add missing availability checks for SecCopyErrorMessageString (#7577)

This requires iOS 11.3 and we currently target iOS 11.

94848 of 175770 branches covered (53.96%)

7 of 22 new or added lines in 2 files covered. (31.82%)

1815 existing lines in 77 files now uncovered.

242945 of 264608 relevant lines covered (91.81%)

6136478.37 hits per line

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

98.93
/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

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

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

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

1✔
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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

164✔
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

1✔
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✔
UNCOV
242
            return false;
×
UNCOV
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
163✔
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✔
UNCOV
250
                    return false;
×
251
                }
×
252
            }
4✔
253
            // If not provided, it's an error if the value is included in the json body
2✔
254
            else if (code != json_body.end()) {
4✔
UNCOV
255
                return false;
×
UNCOV
256
            }
×
257
            // If provided, check the message value against the 'error' value in the json body
163✔
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
            }
×
264
            // If not provided, it's an error if the value is included in the json body
UNCOV
265
            else if (message != json_body.end()) {
×
UNCOV
266
                return false;
×
UNCOV
267
            }
×
268
            // If provided, check the logs_link value against the 'link' value in the json body
163✔
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✔
UNCOV
272
                    return false;
×
273
                }
×
274
            }
2✔
275
            // If not provided, it's an error if the value is included in the json body
1✔
276
            else if (link != json_body.end()) {
2✔
UNCOV
277
                return false;
×
278
            }
×
279
        }
×
UNCOV
280
        catch (const nlohmann::json::exception&) {
×
281
            // It's also a failure if parsing the json body throws an exception
UNCOV
282
            return false;
×
UNCOV
283
        }
×
284
        return true;
326✔
285
    };
326✔
286

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

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

1✔
296
    // Empty error code
1✔
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

1✔
307
    // Re-compose back into a Response
1✔
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

1✔
318
    // Missing error code
1✔
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

1✔
329
    // Re-compose back into a Response
1✔
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

1✔
340
    // Missing error message
1✔
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

1✔
351
    // Re-compose back into a Response
1✔
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

1✔
362
    // Missing logs link
1✔
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

1✔
373
    // Re-compose back into a Response
1✔
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

1✔
384
    // Missing error code and error message with success http status
1✔
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

1✔
389
    for (auto [name, error] : error_codes) {
320✔
390
        // All error codes should not cause an exception
160✔
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

158✔
409
            // Re-compose back into a Response
158✔
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

1✔
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

1✔
433
    // Re-compose back into a Response
1✔
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

1✔
444
    // HTTPError with different status values
1✔
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
        }
10✔
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

5✔
466
        // Recompose back into a Response
5✔
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

1✔
475
    // Missing error code and error message with fatal http status
1✔
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

1✔
492
    // Re-compose back into a Response
1✔
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

1✔
500
    // Missing error code and error message contains period with redirect http status
1✔
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

1✔
517
    // Re-compose back into a Response
1✔
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

1✔
525
    // Valid client error code, with body, but no json
1✔
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

1✔
543
    // Re-compose back into a Response
1✔
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

1✔
551
    // Same response with client error code, but no body
1✔
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

1✔
557
    // Re-compose back into a Response
1✔
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

1✔
565
    // Valid custom status code, with body, but no json
1✔
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

1✔
580
    // Re-compose back into a Response
1✔
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

1✔
588
    // Same response with custom status code, but no body
1✔
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

1✔
594
    // Re-compose back into a Response
1✔
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

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

1✔
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

4✔
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

4✔
669
    SECTION("is_redirect_status_code") {
8✔
670
        // Only MovedPermanently(301) and PermanentRedirect(308) return true
1✔
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

4✔
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

1✔
711
        int subscribe_processed = 0;
2✔
712

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

1✔
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

1✔
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

1✔
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_base_url();
26✔
741
    AutoVerifiedEmailCredentials creds;
26✔
742
    auto email = creds.email;
26✔
743
    auto password = creds.password;
26✔
744

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

13✔
749
    bool processed = false;
26✔
750

13✔
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

13✔
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
1✔
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

13✔
770
    SECTION("double registration should fail") {
26✔
771
        // the server registration function will reject emails that do not contain "realm_tests_do_autoverify"
1✔
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

13✔
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

13✔
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

13✔
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

13✔
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

13✔
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

13✔
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
1✔
829
        // function
1✔
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

13✔
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

13✔
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

13✔
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

13✔
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

13✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

3✔
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

1✔
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

1✔
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

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

1✔
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

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

1✔
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

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

1✔
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

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

3✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

3✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

4✔
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

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

4✔
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

1✔
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

4✔
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

4✔
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
1✔
1238
        // different identity
1✔
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

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

1✔
1253
        // Log in user 1
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
1✔
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

1✔
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

1✔
1269
        // Log in user 2
1✔
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

1✔
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

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

1✔
1282
        // check both handles are no longer valid
1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

8✔
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

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

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

8✔
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

8✔
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

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

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

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

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

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

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

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

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

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

1✔
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

1✔
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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

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

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

1✔
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

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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

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

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

1✔
1542
        dog_collection.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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

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

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

1✔
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

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

1✔
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

1✔
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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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
1✔
1942
                                                   CHECK(name == "John");
2!
1943
                                                   processed = true;
2✔
1944
                                               });
2✔
1945

1✔
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
1✔
1951
                                                   CHECK(name == "John");
2!
1952
                                               });
2✔
1953

1✔
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
1✔
1957
                                                   // document and no error will be returned
1✔
1958
                                                   REQUIRE_FALSE(error);
2!
1959
                                                   CHECK(!document);
2!
1960
                                               });
2✔
1961

1✔
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

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

8✔
1972
    SECTION("delete") {
16✔
1973

1✔
1974
        bool processed = false;
2✔
1975

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

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

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

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

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

1✔
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

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

1✔
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

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

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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

2✔
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

2✔
2274
    /*             Create local realm             */
2✔
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

2✔
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.
2✔
2287
        origin->create_object_with_primary_key(47).set("link", o.get_key());
4✔
2288
        // 'embedded_link' property is not null.
2✔
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

2✔
2297
    /* Create a synced realm and upload some data */
2✔
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

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

2✔
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

2✔
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

2✔
2319
    /* Copy local realm data over in a synced one*/
2✔
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

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

2✔
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

2✔
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

2✔
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);
2✔
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

2✔
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);
2✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
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

1✔
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

1✔
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

1✔
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

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

1✔
2503
        // Write some data
1✔
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

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

1✔
2517
        // Write some additional data
1✔
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
1✔
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

1✔
2533
        // Should be able to download the object created in the source Realm
1✔
2534
        // after writing the copy
1✔
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

1✔
2540
        // Check that we can continue committing to this realm
1✔
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
1✔
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

1✔
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

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

19✔
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

19✔
2575
    auto create_one_dog = [](SharedRealm r) {
27✔
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

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

19✔
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

1✔
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

1✔
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

19✔
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

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

1✔
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

1✔
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

19✔
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

1✔
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

1✔
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
1✔
2655
        // (access tokens have an lifetime of 30 minutes today).
1✔
2656
        user->set_seconds_to_adjust_time_for_testing(31 * 60);
2✔
2657
        REQUIRE(user->access_token_refresh_required());
2!
2658

1✔
2659
        // This assumes that we make an http request for the new token while
1✔
2660
        // already in the WaitingForAccessToken state.
1✔
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
1✔
2667
                // WaitingForAccessToken state.
1✔
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

19✔
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

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

4✔
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

4✔
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.
4✔
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

4✔
2718
        SECTION("Expired Access Token is Refreshed") {
8✔
2719
            // This assumes that we make an http request for the new token while
1✔
2720
            // already in the WaitingForAccessToken state.
1✔
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

4✔
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
1✔
2749
                    REQUIRE(user);
2!
2750
                    REQUIRE(user->is_logged_in());
2!
2751
                }
2✔
UNCOV
2752
                else {
×
UNCOV
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
1✔
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
1✔
2776
            REQUIRE(!user->is_logged_in());
2!
2777
        }
2✔
2778

4✔
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

4✔
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

1✔
2807
            transport->response_hook = [&](const Request& request, Response& response) {
12✔
2808
                // simulate the server experiencing an internal server error
6✔
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) {
1✔
UNCOV
2825
                REQUIRE(error.status.code() == ErrorCodes::AuthError);
×
UNCOV
2826
                REQUIRE_THAT(std::string{error.status.reason()},
×
UNCOV
2827
                             Catch::Matchers::StartsWith("Unable to refresh the user access token"));
×
UNCOV
2828
            };
×
2829
            auto r = Realm::get_shared_realm(config);
2✔
2830
            create_one_dog(r);
2✔
2831
            timed_wait_for(
2✔
2832
                [&] {
16,946,462✔
2833
                    return did_receive_valid_token.load();
16,946,462✔
2834
                },
16,946,462✔
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

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

19✔
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) {
7✔
2871
            REQUIRE(user);
6!
2872
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
6!
2873

3✔
2874
            // requesting a new access token fails because the refresh token used for this request is revoked
3✔
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

3✔
2881
            // Set a bad access token. This will force a request for a new access token when the sync session opens
3✔
2882
            // this is only necessary because the server doesn't actually revoke previously issued access tokens
3✔
2883
            // instead allowing their session to time out as normal. So this simulates the access token expiring.
3✔
2884
            // see:
3✔
2885
            // https://github.com/10gen/baas/blob/05837cc3753218dfaf89229c6930277ef1616402/api/common/auth.go#L1380-L1386
3✔
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

3✔
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

3✔
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([&] {
13,835✔
2913
                    return called.load();
13,835✔
2914
                });
13,835✔
2915
                std::lock_guard lock(mtx);
6✔
2916
                REQUIRE(called);
6!
2917
            }
6✔
2918

3✔
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

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

4✔
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

1✔
2937
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2938

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

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

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

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

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

4✔
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
1✔
2967
            // work.
1✔
2968
            REQUIRE(app_session.admin_api.verify_access_token(user->access_token(), app_session.server_app_id));
2!
2969

1✔
2970
            verify_error_on_sync_with_invalid_refresh_token(user, config);
2✔
2971

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

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

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

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

4✔
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
1✔
3000
            // work.
1✔
3001
            REQUIRE(app_session.admin_api.verify_access_token(anon_user->access_token(), app_session.server_app_id));
2!
3002

1✔
3003
            verify_error_on_sync_with_invalid_refresh_token(anon_user, config);
2✔
3004

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

1✔
3010
            // new requests for an access token do not work for anon users
1✔
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

1✔
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

4✔
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
1✔
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

1✔
3042
            // should not be able to open a synced Realm with an invalid user
1✔
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

1✔
3048
            std::shared_ptr<User> new_user_instance = log_in(app, creds);
2✔
3049
            // the previous instance is still invalid
1✔
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
1✔
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
1✔
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

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

1✔
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

1✔
3073
        // Create 26 MB worth of dogs in 26 transactions, which should work but
1✔
3074
        // will result in an error from the server if the changesets are batched
1✔
3075
        // for upload.
1✔
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

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

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

1✔
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

1✔
3113
        // Create 26 MB worth of dogs in a single transaction - this should all get put into one changeset
1✔
3114
        // and get uploaded at once, which for now is an error on the server.
1✔
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

1✔
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

1✔
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

19✔
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

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

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

1✔
3154
        realm->read_group();
2✔
3155

1✔
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

1✔
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

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

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

1✔
3180
            sync_sess_ext_ref = realm->sync_session()->external_reference();
2✔
3181
            dbref = TestHelper::get_db(*realm);
2✔
3182
            // One ref each for the
1✔
3183
            // - RealmCoordinator
1✔
3184
            // - SyncSession
1✔
3185
            // - SessionWrapper
1✔
3186
            // - local dbref
1✔
3187
            REQUIRE(dbref.use_count() >= 4);
2!
3188

1✔
3189
            realm->sync_session()->pause();
2✔
3190
            state = realm->sync_session()->state();
2✔
3191
            REQUIRE(state == SyncSession::State::Paused);
2!
3192
        }
2✔
3193

1✔
3194
        // Closing the realm should leave one ref for the SyncSession and one for the local dbref.
1✔
3195
        REQUIRE_THAT(
2✔
3196
            [&] {
2✔
3197
                return dbref.use_count() < 4;
2✔
3198
            },
2✔
3199
            ReturnsTrueWithinTimeLimit{});
2✔
3200

1✔
3201
        // Releasing the external reference should leave one ref (the local dbref) only.
1✔
3202
        sync_sess_ext_ref.reset();
2✔
3203
        REQUIRE_THAT(
2✔
3204
            [&] {
2✔
3205
                return dbref.use_count() == 1;
2✔
3206
            },
2✔
3207
            ReturnsTrueWithinTimeLimit{});
2✔
3208
    }
2✔
3209

19✔
3210
    SECTION("validation") {
38✔
3211
        SyncTestFile config(app->current_user(), partition, schema);
6✔
3212

3✔
3213
        SECTION("invalid partition error handling") {
6✔
3214
            config.sync_config->partition_value = "not a bson serialized string";
2✔
3215
            std::atomic<bool> error_did_occur = false;
2✔
3216
            config.sync_config->error_handler = [&error_did_occur](std::shared_ptr<SyncSession>, SyncError error) {
2✔
3217
                CHECK(error.status.reason().find(
2!
3218
                          "Illegal Realm path (BIND): serialized partition 'not a bson serialized "
2✔
3219
                          "string' is invalid") != std::string::npos);
2✔
3220
                error_did_occur.store(true);
2✔
3221
            };
2✔
3222
            auto r = Realm::get_shared_realm(config);
2✔
3223
            auto session = app->sync_manager()->get_existing_session(r->config().path);
2✔
3224
            timed_wait_for([&] {
7,110✔
3225
                return error_did_occur.load();
7,110✔
3226
            });
7,110✔
3227
            REQUIRE(error_did_occur.load());
2!
3228
        }
2✔
3229

3✔
3230
        SECTION("invalid pk schema error handling") {
6✔
3231
            const std::string invalid_pk_name = "my_primary_key";
2✔
3232
            auto it = config.schema->find("Dog");
2✔
3233
            REQUIRE(it != config.schema->end());
2!
3234
            REQUIRE(it->primary_key_property());
2!
3235
            REQUIRE(it->primary_key_property()->name == "_id");
2!
3236
            it->primary_key_property()->name = invalid_pk_name;
2✔
3237
            it->primary_key = invalid_pk_name;
2✔
3238
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3239
                                      "The primary key property on a synchronized Realm must be named '_id' but "
2✔
3240
                                      "found 'my_primary_key' for type 'Dog'");
2✔
3241
        }
2✔
3242

3✔
3243
        SECTION("missing pk schema error handling") {
6✔
3244
            auto it = config.schema->find("Dog");
2✔
3245
            REQUIRE(it != config.schema->end());
2!
3246
            REQUIRE(it->primary_key_property());
2!
3247
            it->primary_key_property()->is_primary = false;
2✔
3248
            it->primary_key = "";
2✔
3249
            REQUIRE(!it->primary_key_property());
2!
3250
            REQUIRE_THROWS_CONTAINING(Realm::get_shared_realm(config),
2✔
3251
                                      "There must be a primary key property named '_id' on a synchronized "
2✔
3252
                                      "Realm but none was found for type 'Dog'");
2✔
3253
        }
2✔
3254
    }
6✔
3255

19✔
3256
    SECTION("get_file_ident") {
38✔
3257
        SyncTestFile config(app->current_user(), partition, schema);
2✔
3258
        config.sync_config->client_resync_mode = ClientResyncMode::RecoverOrDiscard;
2✔
3259
        auto r = Realm::get_shared_realm(config);
2✔
3260
        wait_for_download(*r);
2✔
3261

1✔
3262
        auto first_ident = r->sync_session()->get_file_ident();
2✔
3263
        REQUIRE(first_ident.ident != 0);
2!
3264
        REQUIRE(first_ident.salt != 0);
2!
3265

1✔
3266
        reset_utils::trigger_client_reset(session.app_session(), r);
2✔
3267
        r->sync_session()->restart_session();
2✔
3268
        wait_for_download(*r);
2✔
3269

1✔
3270
        REQUIRE(first_ident.ident != r->sync_session()->get_file_ident().ident);
2!
3271
        REQUIRE(first_ident.salt != r->sync_session()->get_file_ident().salt);
2!
3272
    }
2✔
3273
}
38✔
3274

3275
TEST_CASE("app: redirect handling", "[sync][pbs][app]") {
14✔
3276
    auto logger = util::Logger::get_default_logger();
14✔
3277

7✔
3278
    const auto schema = get_default_schema();
14✔
3279

7✔
3280
    auto transport = std::make_shared<HookedTransport<UnitTestTransport>>();
14✔
3281
    auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "");
14✔
3282
    OfflineAppSession::Config oas_config(transport);
14✔
3283
    oas_config.base_url = "http://original.invalid:9090";
14✔
3284
    oas_config.socket_provider = socket_provider;
14✔
3285
    OfflineAppSession oas(oas_config);
14✔
3286
    AutoVerifiedEmailCredentials creds;
14✔
3287
    auto app = oas.app();
14✔
3288
    const auto partition = random_string(100);
14✔
3289

7✔
3290
    SECTION("invalid redirect response reports and error") {
14✔
3291
        int request_count = 0;
2✔
3292

1✔
3293
        // This will fail due to no Location header
1✔
3294
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
2✔
3295
            logger->trace("request.url (%1): %2", request_count, request.url);
2✔
3296
            REQUIRE(request_count++ == 0);
2!
3297
            return Response{301, 0, {{"Content-Type", "application/json"}}, "Some body data"};
2✔
3298
        };
2✔
3299
        app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3300
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
3301
                REQUIRE(error);
2!
3302
                REQUIRE(error->is_client_error());
2!
3303
                REQUIRE(error->code() == ErrorCodes::ClientRedirectError);
2!
3304
                REQUIRE(error->reason() == "Redirect response missing location header");
2!
3305
            });
2✔
3306

1✔
3307
        // This will fail due to empty Location header
1✔
3308
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
2✔
3309
            logger->trace("request.url (%1): %2", request_count, request.url);
2✔
3310
            REQUIRE(request_count++ == 1);
2!
3311
            return Response{301, 0, {{"Location", ""}, {"Content-Type", "application/json"}}, "Some body data"};
2✔
3312
        };
2✔
3313

1✔
3314
        app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3315
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
3316
                REQUIRE(error);
2!
3317
                REQUIRE(error->is_client_error());
2!
3318
                REQUIRE(error->code() == ErrorCodes::ClientRedirectError);
2!
3319
                REQUIRE(error->reason() == "Redirect response missing location header");
2!
3320
            });
2✔
3321
    }
2✔
3322

7✔
3323
    SECTION("valid redirect response") {
14✔
3324
        int request_count = 0;
2✔
3325
        const std::string second_host = "http://second.invalid:9091";
2✔
3326
        const std::string third_host = "http://third.invalid:9092";
2✔
3327

1✔
3328
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
10✔
3329
            logger->trace("Received request[%1]: %2", request_count, request.url);
10✔
3330
            switch (request_count++) {
10✔
3331
                case 0:
2✔
3332
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3333
                    REQUIRE_THAT(request.url, ContainsSubstring(*oas_config.base_url));
2✔
3334
                    return Response{301, 0, {{"Location", second_host}, {"Content-Type", "application/json"}}, ""};
2✔
3335

1✔
3336
                case 1:
2✔
3337
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3338
                    REQUIRE_THAT(request.url, ContainsSubstring(second_host));
2✔
3339
                    return Response{301, 0, {{"Location", third_host}, {"Content-Type", "application/json"}}, ""};
2✔
3340

1✔
3341
                case 2:
2✔
3342
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3343
                    REQUIRE_THAT(request.url, ContainsSubstring(third_host));
2✔
3344
                    return Response{301, 0, {{"Location", second_host}, {"Content-Type", "application/json"}}, ""};
2✔
3345

1✔
3346
                case 3:
2✔
3347
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3348
                    REQUIRE_THAT(request.url, ContainsSubstring(second_host));
2✔
3349
                    return std::nullopt;
2✔
3350

1✔
3351
                default:
2✔
3352
                    // some.fake.url is the location reported by UnitTestTransport
1✔
3353
                    REQUIRE_THAT(request.url, ContainsSubstring("https://some.fake.url"));
2✔
3354
                    return std::nullopt;
2✔
3355
            }
10✔
3356
        };
10✔
3357

1✔
3358
        // This will be successful after a couple of retries due to the redirect response
1✔
3359
        app->provider_client<app::App::UsernamePasswordProviderClient>().register_email(
2✔
3360
            creds.email, creds.password, [&](util::Optional<app::AppError> error) {
2✔
3361
                REQUIRE(!error);
2!
3362
            });
2✔
3363
    }
2✔
3364

7✔
3365
    SECTION("too many redirects eventually reports an error") {
14✔
3366
        int request_count = 0;
2✔
3367
        transport->request_hook = [&](const Request& request) -> std::optional<Response> {
42✔
3368
            logger->trace("request.url (%1): %2", request_count, request.url);
42✔
3369
            REQUIRE(request_count < 21);
42!
3370
            ++request_count;
42✔
3371
            return Response{request_count % 2 == 1 ? 308 : 301,
32✔
3372
                            0,
42✔
3373
                            {{"Location", "http://somehost:9090"}, {"Content-Type", "application/json"}},
42✔
3374
                            "Some body data"};
42✔
3375
        };
42✔
3376

1✔
3377
        app->log_in_with_credentials(app::AppCredentials::username_password(creds.email, creds.password),
2✔
3378
                                     [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
3379
                                         REQUIRE(!user);
2!
3380
                                         REQUIRE(error);
2!
3381
                                         REQUIRE(error->is_client_error());
2!
3382
                                         REQUIRE(error->code() == ErrorCodes::ClientTooManyRedirects);
2!
3383
                                         REQUIRE(error->reason() == "number of redirections exceeded 20");
2!
3384
                                     });
2✔
3385
        REQUIRE(request_count == 21);
2!
3386
    }
2✔
3387

7✔
3388
    SECTION("server in maintenance reports error") {
14✔
3389
        transport->request_hook = [&](const Request&) -> std::optional<Response> {
2✔
3390
            nlohmann::json maintenance_error = {{"error_code", "MaintenanceInProgress"},
2✔
3391
                                                {"error", "This service is currently undergoing maintenance"},
2✔
3392
                                                {"link", "https://link.to/server_logs"}};
2✔
3393
            return Response{500, 0, {{"Content-Type", "application/json"}}, maintenance_error.dump()};
2✔
3394
        };
2✔
3395

1✔
3396
        app->log_in_with_credentials(realm::app::AppCredentials::username_password(creds.email, creds.password),
2✔
3397
                                     [&](std::shared_ptr<realm::SyncUser> user, util::Optional<app::AppError> error) {
2✔
3398
                                         REQUIRE(!user);
2!
3399
                                         REQUIRE(error);
2!
3400
                                         REQUIRE(error->is_service_error());
2!
3401
                                         REQUIRE(error->code() == ErrorCodes::MaintenanceInProgress);
2!
3402
                                         REQUIRE(error->reason() ==
2!
3403
                                                 "This service is currently undergoing maintenance");
2✔
3404
                                         REQUIRE(error->link_to_server_logs == "https://link.to/server_logs");
2!
3405
                                         REQUIRE(*error->additional_status_code == 500);
2!
3406
                                     });
2✔
3407
    }
2✔
3408

7✔
3409
    SECTION("websocket redirects update existing session") {
14✔
3410
        SyncServer server({});
6✔
3411

3✔
3412
        transport->request_hook = [&](const Request& req) -> std::optional<Response> {
24✔
3413
            if (req.url.find("/location") != std::string::npos) {
24✔
3414
                return Response{
6✔
3415
                    200,
6✔
3416
                    0,
6✔
3417
                    {},
6✔
3418
                    nlohmann::json({
6✔
3419
                                       {"hostname", "http://some.fake.url"},
6✔
3420
                                       {"ws_hostname", "ws://ws.some.fake.url"},
6✔
3421
                                       {"sync_route", "ws://some.fake.url/realm-sync"},
6✔
3422
                                   })
6✔
3423
                        .dump(),
6✔
3424
                };
6✔
3425
            }
6✔
3426
            return std::nullopt;
18✔
3427
        };
18✔
3428

3✔
3429
        // The location info is fake, so we need to override it with the actual
3✔
3430
        // server endpoint
3✔
3431
        socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
10✔
3432
            ep.address = "127.0.0.1";
10✔
3433
            ep.port = server.port();
10✔
3434
        };
10✔
3435

3✔
3436
        SyncTestFile realm_config(oas, "test");
6✔
3437

3✔
3438
        std::mutex logout_mutex;
6✔
3439
        std::condition_variable logout_cv;
6✔
3440
        bool logged_out = false;
6✔
3441
        realm_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) {
5✔
3442
            if (error.status == ErrorCodes::AuthError) {
4✔
3443
                {
4✔
3444
                    std::unique_lock lk(logout_mutex);
4✔
3445
                    logged_out = true;
4✔
3446
                }
4✔
3447
                logout_cv.notify_one();
4✔
3448
                return;
4✔
3449
            }
4✔
3450
            util::format(std::cerr, "An unexpected sync error was caught by the default SyncTestFile handler: '%1'\n",
×
3451
                         error.status);
×
3452
            abort();
×
3453
        };
×
3454

3✔
3455
        auto r = Realm::get_shared_realm(realm_config);
6✔
3456
        REQUIRE(!wait_for_download(*r));
6!
3457
        auto sync_session = r->sync_session();
6✔
3458
        sync_session->pause();
6✔
3459
        SyncManager::OnlyForTesting::voluntary_disconnect_all_connections(*oas.sync_manager());
6✔
3460

3✔
3461
        int connect_count = 0;
6✔
3462
        socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> {
8✔
3463
            // Report a 308 response the first time we try to reconnect the websocket,
4✔
3464
            // which should result in App performing a location update.
4✔
3465
            // The actual Location header isn't used when we get a redirect on
4✔
3466
            // the websocket, so we don't need to supply it here
4✔
3467
            if (connect_count++ > 0)
8✔
3468
                return std::nullopt;
2✔
3469
            return sync::HTTPStatus::PermanentRedirect;
6✔
3470
        };
6✔
3471

3✔
3472
        SECTION("valid websocket redirect") {
6✔
3473
            socket_provider->websocket_endpoint_resolver = [&](sync::WebSocketEndpoint& ep) {
4✔
3474
                logger->trace("resolve attempt %1: %2", connect_count, ep.address);
4✔
3475
                // First call happens after the call to the above hook which will
2✔
3476
                // force a 308 response. Second call happens after the redirect
2✔
3477
                // has been handled.
2✔
3478
                REQUIRE(connect_count <= 2);
4!
3479
                if (connect_count == 2) {
4✔
UNCOV
3480
                    REQUIRE(ep.address == "ws.invalid");
×
UNCOV
3481
                }
×
3482

2✔
3483
                // Overriding the handshake result happens after dns resolution,
2✔
3484
                // so we need to set it to a valid endpoint for even the first call
2✔
3485
                ep.address = "127.0.0.1";
4✔
3486
                ep.port = server.port();
4✔
3487
            };
4✔
3488

1✔
3489
            int request_count = 0;
2✔
3490
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
6✔
3491
                logger->trace("request.url (%1): %2", request_count, request.url);
6✔
3492
                ++request_count;
6✔
3493

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

2✔
3504
                // Second request should be a location request against the new URL
2✔
3505
                if (request_count == 2) {
4✔
3506
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3507
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
2✔
3508
                    return Response{200,
2✔
3509
                                    0,
2✔
3510
                                    {},
2✔
3511
                                    nlohmann::json({
2✔
3512
                                                       {"hostname", "http://http.invalid"},
2✔
3513
                                                       {"ws_hostname", "ws://ws.invalid"},
2✔
3514
                                                       {"sync_route", "ws://ws.invalid/realm-sync"},
2✔
3515
                                                   })
2✔
3516
                                        .dump()};
2✔
3517
                }
2✔
3518

1✔
3519
                // Rest of the requests get handled normally
1✔
3520
                return std::nullopt;
2✔
3521
            };
2✔
3522

1✔
3523
            sync_session->resume();
2✔
3524
            REQUIRE(!wait_for_download(*r));
2!
3525
            REQUIRE(request_count > 1);
2!
3526
            REQUIRE(realm_config.sync_config->user->is_logged_in());
2!
3527

1✔
3528
            // Verify session is using the updated server url from the redirect
1✔
3529
            auto server_url = sync_session->full_realm_url();
2✔
3530
            REQUIRE_THAT(server_url, ContainsSubstring("ws.invalid"));
2✔
3531
        }
2✔
3532

3✔
3533
        SECTION("websocket redirect into auth error logs out user") {
6✔
3534
            int request_count = 0;
2✔
3535
            transport->request_hook = [&](const Request& request) -> std::optional<Response> {
6✔
3536
                logger->trace("request.url (%1): %2", request_count, request.url);
6✔
3537
                ++request_count;
6✔
3538

3✔
3539
                if (request_count == 1) {
6✔
3540
                    // First request should be a location request against the original URL
1✔
3541
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3542
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3543
                    return Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
2✔
3544
                                    0,
2✔
3545
                                    {{"Location", "http://asdf.invalid"}},
2✔
3546
                                    ""};
2✔
3547
                }
4✔
3548

2✔
3549
                // Second request should be a location request against the new URL
2✔
3550
                if (request_count == 2) {
4✔
3551
                    REQUIRE_THAT(request.url, ContainsSubstring("/location"));
2✔
3552
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
2✔
3553
                    return Response{200,
2✔
3554
                                    0,
2✔
3555
                                    {},
2✔
3556
                                    nlohmann::json({
2✔
3557
                                                       {"hostname", "http://http.invalid"},
2✔
3558
                                                       {"ws_hostname", "ws://ws.invalid"},
2✔
3559
                                                   })
2✔
3560
                                        .dump()};
2✔
3561
                }
2✔
3562

1✔
3563
                // Third request should be for an acccess token, which we reject
1✔
3564
                REQUIRE(request_count == 3);
2!
3565
                REQUIRE_THAT(request.url, ContainsSubstring("auth/session"));
2✔
3566
                return Response{static_cast<int>(sync::HTTPStatus::Unauthorized), 0, {}, ""};
2✔
3567
            };
2✔
3568

1✔
3569
            sync_session->resume();
2✔
3570
            REQUIRE(wait_for_download(*r));
2!
3571
            std::unique_lock lk(logout_mutex);
2✔
3572
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
3573
                return logged_out;
4✔
3574
            });
4✔
3575
            REQUIRE(result);
2!
3576
            REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in());
2!
3577
        }
2✔
3578

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

21✔
3585
                // The test should never request anything other than /location
21✔
3586
                // even though the user is set to the logged-out state as trying
21✔
3587
                // to log out on the server needs to go through /location first too
21✔
3588
                REQUIRE_THAT(request.url, ContainsSubstring("/location"));
42✔
3589
                REQUIRE(request_count <= max_http_redirects);
42!
3590

21✔
3591
                // First request should be a location request against the original URL
21✔
3592
                // and rest should use the redirect url
21✔
3593
                if (request_count++ == 0) {
42✔
3594
                    REQUIRE_THAT(request.url, ContainsSubstring("some.fake.url"));
2✔
3595
                }
2✔
3596
                else {
40✔
3597
                    REQUIRE_THAT(request.url, ContainsSubstring("asdf.invalid"));
40✔
3598
                }
40✔
3599
                // Keep returning the redirected response
21✔
3600
                return Response{static_cast<int>(sync::HTTPStatus::MovedPermanently),
42✔
3601
                                0,
42✔
3602
                                {{"Location", "http://asdf.invalid"}},
42✔
3603
                                ""};
42✔
3604
            };
42✔
3605

1✔
3606
            sync_session->resume();
2✔
3607
            REQUIRE(wait_for_download(*r));
2!
3608
            std::unique_lock lk(logout_mutex);
2✔
3609
            auto result = logout_cv.wait_for(lk, std::chrono::seconds(15), [&]() {
4✔
3610
                return logged_out;
4✔
3611
            });
4✔
3612
            REQUIRE(result);
2!
3613
            REQUIRE_FALSE(realm_config.sync_config->user->is_logged_in());
2!
3614
        }
2✔
3615
    }
6✔
3616
}
14✔
3617

3618
TEST_CASE("app: base_url", "[sync][app][base_url]") {
22✔
3619
    struct BaseUrlTransport : UnitTestTransport {
22✔
3620
        std::string expected_url;
22✔
3621
        std::optional<std::string_view> redirect_url;
22✔
3622
        bool location_requested = false;
22✔
3623
        bool location_returns_error = false;
22✔
3624

11✔
3625
        void reset(std::string_view expect_url, std::optional<std::string_view> redir_url = std::nullopt)
22✔
3626
        {
60✔
3627
            expected_url = std::string(expect_url);
60✔
3628
            redirect_url = redir_url;
60✔
3629
            location_requested = false;
60✔
3630
            location_returns_error = false;
60✔
3631
        }
60✔
3632

11✔
3633
        void send_request_to_server(const Request& request,
22✔
3634
                                    util::UniqueFunction<void(const Response&)>&& completion) override
22✔
3635
        {
178✔
3636
            if (request.url.find("/location") != std::string::npos) {
178✔
3637
                CHECK(request.method == HttpMethod::get);
76!
3638
                CHECK_THAT(request.url, ContainsSubstring(expected_url));
76✔
3639
                location_requested = true;
76✔
3640
                if (location_returns_error) {
76✔
3641
                    completion(app::Response{static_cast<int>(sync::HTTPStatus::NotFound), 0, {}, "404 not found"});
18✔
3642
                    return;
18✔
3643
                }
18✔
3644
                if (redirect_url) {
58✔
3645
                    // Update the expected url to be the redirect url
8✔
3646
                    expected_url = std::string(*redirect_url);
16✔
3647
                    redirect_url.reset();
16✔
3648

8✔
3649
                    completion(app::Response{static_cast<int>(sync::HTTPStatus::PermanentRedirect),
16✔
3650
                                             0,
16✔
3651
                                             {{"location", expected_url}},
16✔
3652
                                             "308 permanent redirect"});
16✔
3653
                    return;
16✔
3654
                }
16✔
3655
                auto ws_url = App::create_ws_host_url(expected_url);
42✔
3656
                completion(
42✔
3657
                    app::Response{static_cast<int>(sync::HTTPStatus::Ok),
42✔
3658
                                  0,
42✔
3659
                                  {},
42✔
3660
                                  util::format("{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
42✔
3661
                                               "\"%1\",\"ws_hostname\":\"%2\"}",
42✔
3662
                                               expected_url, ws_url)});
42✔
3663
                return;
42✔
3664
            }
42✔
3665

51✔
3666
            UnitTestTransport::send_request_to_server(request, std::move(completion));
102✔
3667
        }
102✔
3668
    };
22✔
3669

11✔
3670
    auto logger = util::Logger::get_default_logger();
22✔
3671

11✔
3672
    auto redir_transport = std::make_shared<BaseUrlTransport>();
22✔
3673
    auto get_config_with_base_url = [&](std::optional<std::string> base_url = std::nullopt) {
26✔
3674
        OfflineAppSession::Config config(redir_transport);
26✔
3675
        config.base_url = base_url;
26✔
3676
        return config;
26✔
3677
    };
26✔
3678

11✔
3679
    SECTION("Test App::create_ws_host_url") {
22✔
3680
        auto result = App::create_ws_host_url("blah");
2✔
3681
        CHECK(result == "blah");
2!
3682
        result = App::create_ws_host_url("http://localhost:9090");
2✔
3683
        CHECK(result == "ws://localhost:9090");
2!
3684
        result = App::create_ws_host_url("https://localhost:9090");
2✔
3685
        CHECK(result == "wss://localhost:9090");
2!
3686
        result = App::create_ws_host_url("https://localhost:9090/some/extra/stuff");
2✔
3687
        CHECK(result == "wss://localhost:9090/some/extra/stuff");
2!
3688
        result = App::create_ws_host_url("http://172.0.0.1:9090");
2✔
3689
        CHECK(result == "ws://172.0.0.1:9090");
2!
3690
        result = App::create_ws_host_url("https://172.0.0.1:9090");
2✔
3691
        CHECK(result == "wss://172.0.0.1:9090");
2!
3692
        // Old default base url
1✔
3693
        result = App::create_ws_host_url("http://realm.mongodb.com");
2✔
3694
        CHECK(result == "ws://ws.realm.mongodb.com");
2!
3695
        result = App::create_ws_host_url("https://realm.mongodb.com");
2✔
3696
        CHECK(result == "wss://ws.realm.mongodb.com");
2!
3697
        result = App::create_ws_host_url("https://realm.mongodb.com/some/extra/stuff");
2✔
3698
        CHECK(result == "wss://ws.realm.mongodb.com/some/extra/stuff");
2!
3699
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3700
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3701
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com");
2✔
3702
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com");
2!
3703
        result = App::create_ws_host_url("https://us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2✔
3704
        CHECK(result == "wss://ws.us-east-1.aws.realm.mongodb.com/some/extra/stuff");
2!
3705
        // New default base url
1✔
3706
        result = App::create_ws_host_url("http://services.cloud.mongodb.com");
2✔
3707
        CHECK(result == "ws://ws.services.cloud.mongodb.com");
2!
3708
        result = App::create_ws_host_url("https://services.cloud.mongodb.com");
2✔
3709
        CHECK(result == "wss://ws.services.cloud.mongodb.com");
2!
3710
        result = App::create_ws_host_url("https://services.cloud.mongodb.com/some/extra/stuff");
2✔
3711
        CHECK(result == "wss://ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3712
        result = App::create_ws_host_url("http://us-east-1.aws.services.cloud.mongodb.com");
2✔
3713
        CHECK(result == "ws://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3714
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com");
2✔
3715
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com");
2!
3716
        result = App::create_ws_host_url("https://us-east-1.aws.services.cloud.mongodb.com/some/extra/stuff");
2✔
3717
        CHECK(result == "wss://us-east-1.aws.ws.services.cloud.mongodb.com/some/extra/stuff");
2!
3718
    }
2✔
3719

11✔
3720
    SECTION("Test app config baseurl") {
22✔
3721
        {
2✔
3722
            // First time through, base_url is empty; https://services.cloud.mongodb.com is expected
1✔
3723
            redir_transport->reset(App::default_base_url());
2✔
3724
            auto config = get_config_with_base_url();
2✔
3725
            OfflineAppSession oas(config);
2✔
3726
            auto app = oas.app();
2✔
3727

1✔
3728
            // Location is not requested until first app services request
1✔
3729
            CHECK(!redir_transport->location_requested);
2!
3730
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
1✔
3731
            CHECK(app->get_host_url() == App::default_base_url());
2!
3732
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3733

1✔
3734
            oas.make_user();
2✔
3735
            CHECK(redir_transport->location_requested);
2!
3736
            CHECK(app->get_base_url() == App::default_base_url());
2!
3737
            CHECK(app->get_host_url() == App::default_base_url());
2!
3738
            CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3739
        }
2✔
3740
        {
2✔
3741
            // Second time through, base_url is set to https://alternate.someurl.fake is expected
1✔
3742
            redir_transport->reset("https://alternate.someurl.fake");
2✔
3743
            auto config = get_config_with_base_url("https://alternate.someurl.fake");
2✔
3744
            OfflineAppSession oas(config);
2✔
3745
            auto app = oas.app();
2✔
3746

1✔
3747
            // Location is not requested until first app services request
1✔
3748
            CHECK(!redir_transport->location_requested);
2!
3749
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
1✔
3750
            CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3751
            CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3752

1✔
3753
            oas.make_user();
2✔
3754
            CHECK(redir_transport->location_requested);
2!
3755
            CHECK(app->get_base_url() == "https://alternate.someurl.fake");
2!
3756
            CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3757
            CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3758
        }
2✔
3759
        {
2✔
3760
            // Third time through, base_url is not set, expect https://services.cloud.mongodb.com,
1✔
3761
            // since metadata is no longer used
1✔
3762
            std::string expected_url = std::string(App::default_base_url());
2✔
3763
            std::string expected_wsurl = App::create_ws_host_url(App::default_base_url());
2✔
3764
            redir_transport->reset(expected_url);
2✔
3765
            auto config = get_config_with_base_url();
2✔
3766
            OfflineAppSession oas(config);
2✔
3767
            auto app = oas.app();
2✔
3768

1✔
3769
            // Location is not requested until first app services request
1✔
3770
            CHECK(!redir_transport->location_requested);
2!
3771
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
1✔
3772
            CHECK(app->get_host_url() == expected_url);
2!
3773
            CHECK(app->get_ws_host_url() == expected_wsurl);
2!
3774

1✔
3775
            oas.make_user();
2✔
3776
            CHECK(redir_transport->location_requested);
2!
3777
            CHECK(app->get_base_url() == expected_url);
2!
3778
            CHECK(app->get_host_url() == expected_url);
2!
3779
            CHECK(app->get_ws_host_url() == expected_wsurl);
2!
3780
        }
2✔
3781
        {
2✔
3782
            // Fourth time through, base_url is set to https://some-other.someurl.fake, with a redirect
1✔
3783
            redir_transport->reset("https://some-other.someurl.fake", "http://redirect.someurl.fake");
2✔
3784
            auto config = get_config_with_base_url("https://some-other.someurl.fake");
2✔
3785
            OfflineAppSession oas(config);
2✔
3786
            auto app = oas.app();
2✔
3787

1✔
3788
            // Location is not requested until first app services request
1✔
3789
            CHECK(!redir_transport->location_requested);
2!
3790
            // Initial hostname and ws hostname use base url, but aren't used until location is updated
1✔
3791
            CHECK(app->get_host_url() == "https://some-other.someurl.fake");
2!
3792
            CHECK(app->get_ws_host_url() == "wss://some-other.someurl.fake");
2!
3793

1✔
3794
            oas.make_user();
2✔
3795
            CHECK(redir_transport->location_requested);
2!
3796
            // Base URL is still set to the original value
1✔
3797
            CHECK(app->get_base_url() == "https://some-other.someurl.fake");
2!
3798
            // Hostname and ws hostname use the redirect URL values
1✔
3799
            CHECK(app->get_host_url() == "http://redirect.someurl.fake");
2!
3800
            CHECK(app->get_ws_host_url() == "ws://redirect.someurl.fake");
2!
3801
        }
2✔
3802
    }
2✔
3803

11✔
3804
    SECTION("Test update_baseurl") {
22✔
3805
        redir_transport->reset("https://alternate.someurl.fake");
2✔
3806
        auto config = get_config_with_base_url("https://alternate.someurl.fake");
2✔
3807
        OfflineAppSession oas(config);
2✔
3808
        auto app = oas.app();
2✔
3809

1✔
3810
        // Location is not requested until first app services request
1✔
3811
        CHECK(!redir_transport->location_requested);
2!
3812

1✔
3813
        oas.make_user();
2✔
3814
        CHECK(redir_transport->location_requested);
2!
3815
        CHECK(app->get_base_url() == "https://alternate.someurl.fake");
2!
3816
        CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3817
        CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3818

1✔
3819
        redir_transport->reset(App::default_base_url());
2✔
3820

1✔
3821
        // Revert the base URL to the default URL value using the empty string
1✔
3822
        app->update_base_url("", [](util::Optional<app::AppError> error) {
2✔
3823
            CHECK(!error);
2!
3824
        });
2✔
3825
        CHECK(redir_transport->location_requested);
2!
3826
        CHECK(app->get_base_url() == App::default_base_url());
2!
3827
        CHECK(app->get_host_url() == App::default_base_url());
2!
3828
        CHECK(app->get_ws_host_url() == App::create_ws_host_url(App::default_base_url()));
2!
3829
        oas.make_user();
2✔
3830
    }
2✔
3831

11✔
3832
    SECTION("Test update_baseurl with redirect") {
22✔
3833
        redir_transport->reset("https://alternate.someurl.fake");
2✔
3834
        auto config = get_config_with_base_url("https://alternate.someurl.fake");
2✔
3835
        OfflineAppSession oas(config);
2✔
3836
        auto app = oas.app();
2✔
3837

1✔
3838
        // Location is not requested until first app services request
1✔
3839
        CHECK(!redir_transport->location_requested);
2!
3840

1✔
3841
        oas.make_user();
2✔
3842
        CHECK(redir_transport->location_requested);
2!
3843
        CHECK(app->get_base_url() == "https://alternate.someurl.fake");
2!
3844
        CHECK(app->get_host_url() == "https://alternate.someurl.fake");
2!
3845
        CHECK(app->get_ws_host_url() == "wss://alternate.someurl.fake");
2!
3846

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

1✔
3849
        app->update_base_url("http://some-other.someurl.fake", [](util::Optional<app::AppError> error) {
2✔
3850
            CHECK(!error);
2!
3851
        });
2✔
3852
        CHECK(redir_transport->location_requested);
2!
3853
        CHECK(app->get_base_url() == "http://some-other.someurl.fake");
2!
3854
        CHECK(app->get_host_url() == "https://redirect.otherurl.fake");
2!
3855
        CHECK(app->get_ws_host_url() == "wss://redirect.otherurl.fake");
2!
3856
        // Expected URL is still "https://redirect.otherurl.fake" after redirect
1✔
3857
        oas.make_user();
2✔
3858
    }
2✔
3859

11✔
3860
    SECTION("Test update_baseurl returns error") {
22✔
3861
        redir_transport->reset("http://alternate.someurl.fake");
2✔
3862
        auto config = get_config_with_base_url("http://alternate.someurl.fake");
2✔
3863
        OfflineAppSession oas(config);
2✔
3864
        auto app = oas.app();
2✔
3865

1✔
3866
        // Location is not requested until first app services request
1✔
3867
        CHECK(!redir_transport->location_requested);
2!
3868

1✔
3869
        oas.make_user();
2✔
3870
        CHECK(redir_transport->location_requested);
2!
3871
        CHECK(app->get_base_url() == "http://alternate.someurl.fake");
2!
3872
        CHECK(app->get_host_url() == "http://alternate.someurl.fake");
2!
3873
        CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake");
2!
3874

1✔
3875
        redir_transport->reset("https://some-other.someurl.fake");
2✔
3876
        redir_transport->location_returns_error = true;
2✔
3877

1✔
3878
        app->update_base_url("https://some-other.someurl.fake", [](util::Optional<app::AppError> error) {
2✔
3879
            CHECK(error);
2!
3880
        });
2✔
3881
        CHECK(redir_transport->location_requested);
2!
3882
        // Verify original url values are still being used
1✔
3883
        CHECK(app->get_base_url() == "http://alternate.someurl.fake");
2!
3884
        CHECK(app->get_host_url() == "http://alternate.someurl.fake");
2!
3885
        CHECK(app->get_ws_host_url() == "ws://alternate.someurl.fake");
2!
3886
    }
2✔
3887

11✔
3888
    // Verify new sync session updates location when created with cached user
11✔
3889
    SECTION("Verify new sync session updates location") {
22✔
3890
        bool use_ssl = GENERATE(true, false);
12✔
3891
        std::string initial_host = "alternate.someurl.fake";
12✔
3892
        unsigned initial_port = use_ssl ? 443 : 80;
9✔
3893
        std::string expected_host = "redirect.someurl.fake";
12✔
3894
        unsigned expected_port = 8081;
12✔
3895
        std::string init_url = util::format("http%1://%2", use_ssl ? "s" : "", initial_host);
9✔
3896
        std::string init_wsurl = util::format("ws%1://%2", use_ssl ? "s" : "", initial_host);
9✔
3897
        std::string redir_url = util::format("http%1://%2:%3", use_ssl ? "s" : "", expected_host, expected_port);
9✔
3898
        std::string redir_wsurl = util::format("ws%1://%2:%3", use_ssl ? "s" : "", expected_host, expected_port);
9✔
3899

6✔
3900
        auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent");
12✔
3901
        socket_provider->websocket_connect_func = []() -> std::optional<SocketProviderError> {
10✔
3902
            return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, "404 not found");
8✔
3903
        };
8✔
3904

6✔
3905
        auto config = get_config_with_base_url(init_url);
12✔
3906
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
12✔
3907
        config.socket_provider = socket_provider;
12✔
3908
        config.storage_path = util::make_temp_dir();
12✔
3909
        config.delete_storage = false; // persist the current user
12✔
3910

6✔
3911
        // Log in to get a cached user
6✔
3912
        {
12✔
3913
            redir_transport->reset(init_url);
12✔
3914
            OfflineAppSession oas(config);
12✔
3915
            auto app = oas.app();
12✔
3916

6✔
3917
            {
12✔
3918
                auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
3919
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
12✔
3920
                CHECK_FALSE(verified);
12!
3921
            }
12✔
3922

6✔
3923
            oas.make_user();
12✔
3924
            CHECK(redir_transport->location_requested);
12!
3925
            CHECK(app->get_base_url() == init_url);
12!
3926
            CHECK(app->get_host_url() == init_url);
12!
3927
            CHECK(app->get_ws_host_url() == init_wsurl);
12!
3928
            auto [sync_route, verified] = app->sync_manager()->sync_route();
12✔
3929
            CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
12✔
3930
            CHECK_THAT(sync_route, ContainsSubstring(init_wsurl));
12✔
3931
            CHECK(verified);
12!
3932
        }
12✔
3933

6✔
3934
        // the next instance can clean up the files
6✔
3935
        config.delete_storage = true;
12✔
3936
        // Recreate the app using the cached user and start a sync session, which will is set to fail on connect
6✔
3937
        SECTION("Sync Session fails on connect after updating location") {
12✔
3938
            enum class TestState { start, session_started };
4✔
3939
            TestingStateMachine<TestState> state(TestState::start);
4✔
3940
            redir_transport->reset(init_url, redir_url);
4✔
3941

2✔
3942
            OfflineAppSession oas(config);
4✔
3943
            auto app = oas.app();
4✔
3944

2✔
3945
            // Verify the default sync route, which has not been verified
2✔
3946
            {
4✔
3947
                auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
3948
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
4✔
3949
                CHECK_FALSE(verified);
4!
3950
            }
4✔
3951
            REQUIRE(app->current_user());
4!
3952

2✔
3953
            std::atomic<int> connect_attempts = 0;
4✔
3954
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
8✔
3955
                // First connection attempt is to the originally specified endpoint. Since
4✔
3956
                // it hasn't been verified, we swallow the error and do a location update,
4✔
3957
                // which will then try to connect to the redir target
4✔
3958
                auto attempt = connect_attempts++;
8✔
3959
                if (attempt == 0) {
8✔
3960
                    CHECK(ep.address == initial_host);
4!
3961
                    CHECK(ep.port == initial_port);
4!
3962
                    CHECK(ep.is_ssl == use_ssl);
4!
3963
                }
4✔
3964
                else {
4✔
3965
                    CHECK(ep.address == expected_host);
4!
3966
                    CHECK(ep.port == expected_port);
4!
3967
                    CHECK(ep.is_ssl == use_ssl);
4!
3968
                }
4✔
3969
            };
8✔
3970

2✔
3971
            RealmConfig r_config;
4✔
3972
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
4✔
3973
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
4✔
3974
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
4✔
3975
                // Websocket is forcing a 404 failure so it won't actually start
2✔
3976
                logger->debug("Received expected error: %1", error.status);
4✔
3977
                CHECK(!error.status.is_ok());
4!
3978
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
4!
3979
                CHECK(!error.is_fatal);
4!
3980
                state.transition_to(TestState::session_started);
4✔
3981
            };
4✔
3982
            auto realm = Realm::get_shared_realm(r_config);
4✔
3983
            state.wait_for(TestState::session_started);
4✔
3984

2✔
3985
            CHECK(redir_transport->location_requested);
4!
3986
            CHECK(app->get_base_url() == init_url);
4!
3987
            CHECK(app->get_host_url() == redir_url);
4!
3988
            CHECK(app->get_ws_host_url() == redir_wsurl);
4!
3989
            auto [sync_route, verified] = app->sync_manager()->sync_route();
4✔
3990
            CHECK_THAT(sync_route, ContainsSubstring(redir_wsurl));
4✔
3991
            CHECK(verified);
4!
3992
        }
4✔
3993

6✔
3994
        SECTION("Sync Session retries after initial location failure") {
12✔
3995
            enum class TestState { start, location_failed, session_started };
8✔
3996
            TestingStateMachine<TestState> state(TestState::start);
8✔
3997
            const int retry_count = GENERATE(1, 3);
8✔
3998

4✔
3999
            redir_transport->reset(init_url);
8✔
4000
            redir_transport->location_returns_error = true;
8✔
4001

4✔
4002
            OfflineAppSession oas(config);
8✔
4003
            auto app = oas.app();
8✔
4004
            REQUIRE(app->current_user());
8!
4005
            // Verify the default sync route, which has not been verified
4✔
4006
            {
8✔
4007
                auto [sync_route, verified] = app->sync_manager()->sync_route();
8✔
4008
                CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
8✔
4009
                CHECK_FALSE(verified);
8!
4010
            }
8✔
4011

4✔
4012
            socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
24✔
4013
                CHECK(ep.address == initial_host);
24!
4014
                CHECK(ep.port == initial_port);
24!
4015
                CHECK(ep.is_ssl == use_ssl);
24!
4016
            };
24✔
4017

4✔
4018
            socket_provider->websocket_connect_func = [&, request_count =
8✔
4019
                                                              0]() mutable -> std::optional<SocketProviderError> {
32✔
4020
                if (request_count == 0) {
32✔
4021
                    // First connection attempt is to the unverified initial URL
4✔
4022
                    // since we have a valid access token but have never successfully
4✔
4023
                    // connected. This failing will trigger a location update.
4✔
4024
                    CHECK_FALSE(redir_transport->location_requested);
8!
4025
                }
8✔
4026
                else {
24✔
4027
                    // All attempts after the first should have requested location
12✔
4028
                    CHECK(redir_transport->location_requested);
24!
4029
                    redir_transport->location_requested = false;
24✔
4030
                }
24✔
4031

16✔
4032
                // Until we allow a location request to succeed we should keep
16✔
4033
                // getting the original unverified route
16✔
4034
                if (redir_transport->location_returns_error) {
32✔
4035
                    CHECK(app->get_base_url() == init_url);
24!
4036
                    CHECK(app->get_host_url() == init_url);
24!
4037
                    CHECK(app->get_ws_host_url() == init_wsurl);
24!
4038
                    {
24✔
4039
                        auto [sync_route, verified] = app->sync_manager()->sync_route();
24✔
4040
                        CHECK_THAT(sync_route, ContainsSubstring(app::App::create_ws_host_url(init_url)));
24✔
4041
                        CHECK_FALSE(verified);
24!
4042
                    }
24✔
4043
                }
24✔
4044

16✔
4045
                // After the chosen number of attempts let the location request succeed
16✔
4046
                if (request_count++ >= retry_count) {
32✔
4047
                    redir_transport->reset(init_url, redir_url);
16✔
4048
                    socket_provider->endpoint_verify_func = [&](const sync::WebSocketEndpoint& ep) {
12✔
4049
                        CHECK(ep.address == expected_host);
8!
4050
                        CHECK(ep.port == expected_port);
8!
4051
                        CHECK(ep.is_ssl == use_ssl);
8!
4052
                        state.transition_to(TestState::location_failed);
8✔
4053
                    };
8✔
4054
                }
16✔
4055

16✔
4056
                return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed,
32✔
4057
                                           "404 not found");
32✔
4058
            };
32✔
4059

4✔
4060
            RealmConfig r_config;
8✔
4061
            r_config.path = app->config().base_file_path + "/fakerealm.realm";
8✔
4062
            r_config.sync_config = std::make_shared<SyncConfig>(app->current_user(), SyncConfig::FLXSyncEnabled{});
8✔
4063
            r_config.sync_config->error_handler = [&](std::shared_ptr<SyncSession>, SyncError error) mutable {
8✔
4064
                // An error will only be reported if the websocket fails after updating the location and access token
4✔
4065
                logger->debug("Received expected error: %1", error.status);
8✔
4066
                CHECK(!error.status.is_ok());
8!
4067
                CHECK(error.status.code() == ErrorCodes::SyncConnectFailed);
8!
4068
                CHECK(!error.is_fatal);
8!
4069
                state.transition_with([&](TestState cur_state) -> std::optional<TestState> {
8✔
4070
                    if (cur_state == TestState::location_failed) {
8✔
4071
                        // This time, the session was being started, and the location was successful
4✔
4072
                        // Websocket is forcing a 404 failure so it won't actually start
4✔
4073
                        return TestState::session_started;
8✔
4074
                    }
8✔
UNCOV
4075
                    return std::nullopt;
×
UNCOV
4076
                });
×
4077
            };
8✔
4078
            auto realm = Realm::get_shared_realm(r_config);
8✔
4079
            state.wait_for(TestState::session_started);
8✔
4080

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

4091
TEST_CASE("app: custom user data integration tests", "[sync][app][user][function][baas]") {
2✔
4092
    TestAppSession session;
2✔
4093
    auto app = session.app();
2✔
4094
    auto user = app->current_user();
2✔
4095

1✔
4096
    SECTION("custom user data happy path") {
2✔
4097
        bool processed = false;
2✔
4098
        app->call_function("updateUserData", {bson::BsonDocument({{"favorite_color", "green"}})},
2✔
4099
                           [&](auto response, auto error) {
2✔
4100
                               CHECK(error == none);
2!
4101
                               CHECK(response);
2!
4102
                               CHECK(*response == true);
2!
4103
                               processed = true;
2✔
4104
                           });
2✔
4105
        CHECK(processed);
2!
4106
        processed = false;
2✔
4107
        app->refresh_custom_data(user, [&](auto) {
2✔
4108
            processed = true;
2✔
4109
        });
2✔
4110
        CHECK(processed);
2!
4111
        auto data = *user->custom_data();
2✔
4112
        CHECK(data["favorite_color"] == "green");
2!
4113
    }
2✔
4114
}
2✔
4115

4116
TEST_CASE("app: jwt login and metadata tests", "[sync][app][user][metadata][function][baas]") {
2✔
4117
    TestAppSession session;
2✔
4118
    auto app = session.app();
2✔
4119
    auto jwt = create_jwt(session.app()->app_id());
2✔
4120

1✔
4121
    SECTION("jwt happy path") {
2✔
4122
        bool processed = false;
2✔
4123

1✔
4124
        std::shared_ptr<User> user = log_in(app, AppCredentials::custom(jwt));
2✔
4125

1✔
4126
        app->call_function(user, "updateUserData", {bson::BsonDocument({{"name", "Not Foo Bar"}})},
2✔
4127
                           [&](auto response, auto error) {
2✔
4128
                               CHECK(error == none);
2!
4129
                               CHECK(response);
2!
4130
                               CHECK(*response == true);
2!
4131
                               processed = true;
2✔
4132
                           });
2✔
4133
        CHECK(processed);
2!
4134
        processed = false;
2✔
4135
        app->refresh_custom_data(user, [&](auto) {
2✔
4136
            processed = true;
2✔
4137
        });
2✔
4138
        CHECK(processed);
2!
4139
        auto metadata = user->user_profile();
2✔
4140
        auto custom_data = *user->custom_data();
2✔
4141
        CHECK(custom_data["name"] == "Not Foo Bar");
2!
4142
        CHECK(metadata["name"] == "Foo Bar");
2!
4143
    }
2✔
4144
}
2✔
4145

4146
namespace cf = realm::collection_fixtures;
4147
TEMPLATE_TEST_CASE("app: collections of links integration", "[sync][pbs][app][collections][baas]", cf::ListOfObjects,
4148
                   cf::ListOfMixedLinks, cf::SetOfObjects, cf::SetOfMixedLinks, cf::DictionaryOfObjects,
4149
                   cf::DictionaryOfMixedLinks)
4150
{
12✔
4151
    const std::string valid_pk_name = "_id";
12✔
4152
    const auto partition = random_string(100);
12✔
4153
    TestType test_type("collection", "dest");
12✔
4154
    Schema schema = {{"source",
12✔
4155
                      {{valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4156
                       {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4157
                       test_type.property()}},
12✔
4158
                     {"dest",
12✔
4159
                      {
12✔
4160
                          {valid_pk_name, PropertyType::Int | PropertyType::Nullable, true},
12✔
4161
                          {"realm_id", PropertyType::String | PropertyType::Nullable},
12✔
4162
                      }}};
12✔
4163
    auto server_app_config = minimal_app_config("collections_of_links", schema);
12✔
4164
    TestAppSession test_session(create_app(server_app_config));
12✔
4165

6✔
4166
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
36✔
4167
        timed_sleeping_wait_for([&]() -> bool {
1,948✔
4168
            r->refresh();
1,948✔
4169
            TableRef dest = r->read_group().get_table(table_name);
1,948✔
4170
            size_t cur_count = dest->size();
1,948✔
4171
            return cur_count == count;
1,948✔
4172
        });
1,948✔
4173
    };
36✔
4174
    auto wait_for_num_outgoing_links_to_equal = [&](realm::SharedRealm r, Obj obj, size_t count) {
24✔
4175
        timed_sleeping_wait_for([&]() -> bool {
1,088✔
4176
            r->refresh();
1,088✔
4177
            return test_type.size_of_collection(obj) == count;
1,088✔
4178
        });
1,088✔
4179
    };
24✔
4180

6✔
4181
    CppContext c;
12✔
4182
    auto create_one_source_object = [&](realm::SharedRealm r, int64_t val, std::vector<ObjLink> links = {}) {
12✔
4183
        r->begin_transaction();
12✔
4184
        auto object = Object::create(
12✔
4185
            c, r, "source",
12✔
4186
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
12✔
4187
            CreatePolicy::ForceCreate);
12✔
4188

6✔
4189
        for (auto link : links) {
36✔
4190
            auto& obj = object.get_obj();
36✔
4191
            test_type.add_link(obj, link);
36✔
4192
        }
36✔
4193
        r->commit_transaction();
12✔
4194
        return object;
12✔
4195
    };
12✔
4196

6✔
4197
    auto create_one_dest_object = [&](realm::SharedRealm r, int64_t val) -> ObjLink {
36✔
4198
        r->begin_transaction();
36✔
4199
        auto obj = Object::create(
36✔
4200
            c, r, "dest",
36✔
4201
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {"realm_id", std::string(partition)}}),
36✔
4202
            CreatePolicy::ForceCreate);
36✔
4203
        r->commit_transaction();
36✔
4204
        return ObjLink{obj.get_obj().get_table()->get_key(), obj.get_obj().get_key()};
36✔
4205
    };
36✔
4206

6✔
4207
    auto require_links_to_match_ids = [&](std::vector<Obj> links, std::vector<int64_t> expected) {
48✔
4208
        std::vector<int64_t> actual;
48✔
4209
        for (auto obj : links) {
108✔
4210
            actual.push_back(obj.get<Int>(valid_pk_name));
108✔
4211
        }
108✔
4212
        std::sort(actual.begin(), actual.end());
48✔
4213
        std::sort(expected.begin(), expected.end());
48✔
4214
        REQUIRE(actual == expected);
48!
4215
    };
48✔
4216

6✔
4217
    SECTION("integration testing") {
12✔
4218
        auto app = test_session.app();
12✔
4219
        SyncTestFile config1(app->current_user(), partition, schema); // uses the current user created above
12✔
4220
        config1.automatic_change_notifications = false;
12✔
4221
        auto r1 = realm::Realm::get_shared_realm(config1);
12✔
4222
        Results r1_source_objs = realm::Results(r1, r1->read_group().get_table("class_source"));
12✔
4223

6✔
4224
        create_user_and_log_in(app);                                  // changes the current user
12✔
4225
        SyncTestFile config2(app->current_user(), partition, schema); // uses the user created above
12✔
4226
        config2.automatic_change_notifications = false;
12✔
4227
        auto r2 = realm::Realm::get_shared_realm(config2);
12✔
4228
        Results r2_source_objs = realm::Results(r2, r2->read_group().get_table("class_source"));
12✔
4229

6✔
4230
        constexpr int64_t source_pk = 0;
12✔
4231
        constexpr int64_t dest_pk_1 = 1;
12✔
4232
        constexpr int64_t dest_pk_2 = 2;
12✔
4233
        constexpr int64_t dest_pk_3 = 3;
12✔
4234
        Object object;
12✔
4235

6✔
4236
        { // add a container collection with three valid links
12✔
4237
            REQUIRE(r1_source_objs.size() == 0);
12!
4238
            ObjLink dest1 = create_one_dest_object(r1, dest_pk_1);
12✔
4239
            ObjLink dest2 = create_one_dest_object(r1, dest_pk_2);
12✔
4240
            ObjLink dest3 = create_one_dest_object(r1, dest_pk_3);
12✔
4241
            object = create_one_source_object(r1, source_pk, {dest1, dest2, dest3});
12✔
4242
            REQUIRE(r1_source_objs.size() == 1);
12!
4243
            REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4244
            REQUIRE(r1_source_objs.get(0).get<String>("realm_id") == partition);
12!
4245
            require_links_to_match_ids(test_type.get_links(r1_source_objs.get(0)), {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4246
        }
12✔
4247

6✔
4248
        size_t expected_coll_size = 3;
12✔
4249
        std::vector<int64_t> remaining_dest_object_ids;
12✔
4250
        { // erase one of the destination objects
12✔
4251
            wait_for_num_objects_to_equal(r2, "class_source", 1);
12✔
4252
            wait_for_num_objects_to_equal(r2, "class_dest", 3);
12✔
4253
            REQUIRE(r2_source_objs.size() == 1);
12!
4254
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4255
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == 3);
12!
4256
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4257
            require_links_to_match_ids(linked_objects, {dest_pk_1, dest_pk_2, dest_pk_3});
12✔
4258
            r2->begin_transaction();
12✔
4259
            linked_objects[0].remove();
12✔
4260
            r2->commit_transaction();
12✔
4261
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name),
12✔
4262
                                         linked_objects[2].template get<Int>(valid_pk_name)};
12✔
4263
            expected_coll_size = test_type.will_erase_removed_object_links() ? 2 : 3;
10✔
4264
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4265
        }
12✔
4266

6✔
4267
        { // remove a link from the collection
12✔
4268
            wait_for_num_objects_to_equal(r1, "class_dest", 2);
12✔
4269
            REQUIRE(r1_source_objs.size() == 1);
12!
4270
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4271
            auto linked_objects = test_type.get_links(r1_source_objs.get(0));
12✔
4272
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4273
            r1->begin_transaction();
12✔
4274
            auto obj = r1_source_objs.get(0);
12✔
4275
            test_type.remove_link(obj,
12✔
4276
                                  ObjLink{linked_objects[0].get_table()->get_key(), linked_objects[0].get_key()});
12✔
4277
            r1->commit_transaction();
12✔
4278
            --expected_coll_size;
12✔
4279
            remaining_dest_object_ids = {linked_objects[1].template get<Int>(valid_pk_name)};
12✔
4280
            REQUIRE(test_type.size_of_collection(r1_source_objs.get(0)) == expected_coll_size);
12!
4281
        }
12✔
4282
        bool coll_cleared = false;
12✔
4283
        advance_and_notify(*r1);
12✔
4284
        auto collection = test_type.get_collection(r1, r1_source_objs.get(0));
12✔
4285
        auto token = collection.add_notification_callback([&coll_cleared](CollectionChangeSet c) {
24✔
4286
            coll_cleared = c.collection_was_cleared;
24✔
4287
        });
24✔
4288

6✔
4289
        { // clear the collection
12✔
4290
            REQUIRE(r2_source_objs.size() == 1);
12!
4291
            REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == source_pk);
12!
4292
            wait_for_num_outgoing_links_to_equal(r2, r2_source_objs.get(0), expected_coll_size);
12✔
4293
            auto linked_objects = test_type.get_links(r2_source_objs.get(0));
12✔
4294
            require_links_to_match_ids(linked_objects, remaining_dest_object_ids);
12✔
4295
            r2->begin_transaction();
12✔
4296
            test_type.clear_collection(r2_source_objs.get(0));
12✔
4297
            r2->commit_transaction();
12✔
4298
            expected_coll_size = 0;
12✔
4299
            REQUIRE(test_type.size_of_collection(r2_source_objs.get(0)) == expected_coll_size);
12!
4300
        }
12✔
4301

6✔
4302
        { // expect an empty collection
12✔
4303
            REQUIRE(!coll_cleared);
12!
4304
            REQUIRE(r1_source_objs.size() == 1);
12!
4305
            wait_for_num_outgoing_links_to_equal(r1, r1_source_objs.get(0), expected_coll_size);
12✔
4306
            advance_and_notify(*r1);
12✔
4307
            REQUIRE(coll_cleared);
12!
4308
        }
12✔
4309
    }
12✔
4310
}
12✔
4311

4312
TEMPLATE_TEST_CASE("app: partition types", "[sync][pbs][app][partition][baas]", cf::Int, cf::String, cf::OID,
4313
                   cf::UUID, cf::BoxedOptional<cf::Int>, cf::UnboxedOptional<cf::String>, cf::BoxedOptional<cf::OID>,
4314
                   cf::BoxedOptional<cf::UUID>)
4315
{
16✔
4316
    const std::string valid_pk_name = "_id";
16✔
4317
    const std::string partition_key_col_name = "partition_key_prop";
16✔
4318
    const std::string table_name = "class_partition_test_type";
16✔
4319
    auto partition_property = Property(partition_key_col_name, TestType::property_type);
16✔
4320
    Schema schema = {{Group::table_name_to_class_name(table_name),
16✔
4321
                      {
16✔
4322
                          {valid_pk_name, PropertyType::Int, true},
16✔
4323
                          partition_property,
16✔
4324
                      }}};
16✔
4325
    auto server_app_config = minimal_app_config("partition_types_app_name", schema);
16✔
4326
    server_app_config.partition_key = partition_property;
16✔
4327
    TestAppSession test_session(create_app(server_app_config));
16✔
4328
    auto app = test_session.app();
16✔
4329

8✔
4330
    auto wait_for_num_objects_to_equal = [](realm::SharedRealm r, const std::string& table_name, size_t count) {
48✔
4331
        timed_sleeping_wait_for([&]() -> bool {
5,055✔
4332
            r->refresh();
5,055✔
4333
            TableRef dest = r->read_group().get_table(table_name);
5,055✔
4334
            size_t cur_count = dest->size();
5,055✔
4335
            return cur_count == count;
5,055✔
4336
        });
5,055✔
4337
    };
48✔
4338
    using T = typename TestType::Type;
16✔
4339
    CppContext c;
16✔
4340
    auto create_object = [&](realm::SharedRealm r, int64_t val, std::any partition) {
48✔
4341
        r->begin_transaction();
48✔
4342
        auto object = Object::create(
48✔
4343
            c, r, Group::table_name_to_class_name(table_name),
48✔
4344
            std::any(realm::AnyDict{{valid_pk_name, std::any(val)}, {partition_key_col_name, partition}}),
48✔
4345
            CreatePolicy::ForceCreate);
48✔
4346
        r->commit_transaction();
48✔
4347
    };
48✔
4348

8✔
4349
    auto get_bson = [](T val) -> bson::Bson {
96✔
4350
        if constexpr (std::is_same_v<T, StringData>) {
96✔
4351
            return val.is_null() ? bson::Bson(util::none) : bson::Bson(val);
46✔
4352
        }
28✔
4353
        else if constexpr (TestType::is_optional) {
68✔
4354
            return val ? bson::Bson(*val) : bson::Bson(util::none);
28✔
4355
        }
40✔
4356
        else {
28✔
4357
            return bson::Bson(val);
28✔
4358
        }
28✔
4359
    };
96✔
4360

8✔
4361
    SECTION("can round trip an object") {
16✔
4362
        auto values = TestType::values();
16✔
4363
        auto user1 = app->current_user();
16✔
4364
        create_user_and_log_in(app);
16✔
4365
        auto user2 = app->current_user();
16✔
4366
        REQUIRE(user1);
16!
4367
        REQUIRE(user2);
16!
4368
        REQUIRE(user1 != user2);
16!
4369
        for (T partition_value : values) {
48✔
4370
            SyncTestFile config1(user1, get_bson(partition_value), schema); // uses the current user created above
48✔
4371
            auto r1 = realm::Realm::get_shared_realm(config1);
48✔
4372
            Results r1_source_objs = realm::Results(r1, r1->read_group().get_table(table_name));
48✔
4373

24✔
4374
            SyncTestFile config2(user2, get_bson(partition_value), schema); // uses the user created above
48✔
4375
            auto r2 = realm::Realm::get_shared_realm(config2);
48✔
4376
            Results r2_source_objs = realm::Results(r2, r2->read_group().get_table(table_name));
48✔
4377

24✔
4378
            const int64_t pk_value = random_int();
48✔
4379
            {
48✔
4380
                REQUIRE(r1_source_objs.size() == 0);
48!
4381
                create_object(r1, pk_value, TestType::to_any(partition_value));
48✔
4382
                REQUIRE(r1_source_objs.size() == 1);
48!
4383
                REQUIRE(r1_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4384
                REQUIRE(r1_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4385
            }
48✔
4386
            {
48✔
4387
                wait_for_num_objects_to_equal(r2, table_name, 1);
48✔
4388
                REQUIRE(r2_source_objs.size() == 1);
48!
4389
                REQUIRE(r2_source_objs.size() == 1);
48!
4390
                REQUIRE(r2_source_objs.get(0).get<T>(partition_key_col_name) == partition_value);
48!
4391
                REQUIRE(r2_source_objs.get(0).get<Int>(valid_pk_name) == pk_value);
48!
4392
            }
48✔
4393
        }
48✔
4394
    }
16✔
4395
}
16✔
4396

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

2✔
4400
    Schema schema{
4✔
4401
        {"TopLevel",
4✔
4402
         {
4✔
4403
             {valid_pk_name, PropertyType::ObjectId, Property::IsPrimary{true}},
4✔
4404
             {"full_text", Property::IsFulltextIndexed{true}},
4✔
4405
         }},
4✔
4406
    };
4✔
4407

2✔
4408
    auto server_app_config = minimal_app_config("full_text", schema);
4✔
4409
    auto app_session = create_app(server_app_config);
4✔
4410
    const auto partition = random_string(100);
4✔
4411
    TestAppSession test_session(app_session, nullptr);
4✔
4412
    SyncTestFile config(test_session.app()->current_user(), partition, schema);
4✔
4413
    SharedRealm realm;
4✔
4414
    SECTION("sync open") {
4✔
4415
        INFO("realm opened without async open");
2✔
4416
        realm = Realm::get_shared_realm(config);
2✔
4417
    }
2✔
4418
    SECTION("async open") {
4✔
4419
        INFO("realm opened with async open");
2✔
4420
        auto async_open_task = Realm::get_synchronized_realm(config);
2✔
4421

1✔
4422
        auto [realm_promise, realm_future] = util::make_promise_future<ThreadSafeReference>();
2✔
4423
        async_open_task->start(
2✔
4424
            [promise = std::move(realm_promise)](ThreadSafeReference ref, std::exception_ptr ouch) mutable {
2✔
4425
                if (ouch) {
2✔
UNCOV
4426
                    try {
×
UNCOV
4427
                        std::rethrow_exception(ouch);
×
UNCOV
4428
                    }
×
UNCOV
4429
                    catch (...) {
×
UNCOV
4430
                        promise.set_error(exception_to_status());
×
UNCOV
4431
                    }
×
UNCOV
4432
                }
×
4433
                else {
2✔
4434
                    promise.emplace_value(std::move(ref));
2✔
4435
                }
2✔
4436
            });
2✔
4437

1✔
4438
        realm = Realm::get_shared_realm(std::move(realm_future.get()));
2✔
4439
    }
2✔
4440

2✔
4441
    CppContext c(realm);
4✔
4442
    auto obj_id_1 = ObjectId::gen();
4✔
4443
    auto obj_id_2 = ObjectId::gen();
4✔
4444
    realm->begin_transaction();
4✔
4445
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_1}, {"full_text", "Hello, world!"s}}));
4✔
4446
    Object::create(c, realm, "TopLevel", std::any(AnyDict{{"_id", obj_id_2}, {"full_text", "Hello, everyone!"s}}));
4✔
4447
    realm->commit_transaction();
4✔
4448

2✔
4449
    auto table = realm->read_group().get_table("class_TopLevel");
4✔
4450
    REQUIRE(table->search_index_type(table->get_column_key("full_text")) == IndexType::Fulltext);
4!
4451
    Results world_results(realm, Query(table).fulltext(table->get_column_key("full_text"), "world"));
4✔
4452
    REQUIRE(world_results.size() == 1);
4!
4453
    REQUIRE(world_results.get<Obj>(0).get_primary_key() == Mixed{obj_id_1});
4!
4454
}
4✔
4455

4456
#endif // REALM_ENABLE_AUTH_TESTS
4457

4458
TEST_CASE("app: custom error handling", "[sync][app][custom errors]") {
2✔
4459
    class CustomErrorTransport : public GenericNetworkTransport {
2✔
4460
    public:
2✔
4461
        CustomErrorTransport(int code, const std::string& message)
2✔
4462
            : m_code(code)
2✔
4463
            , m_message(message)
2✔
4464
        {
2✔
4465
        }
2✔
4466

1✔
4467
        void send_request_to_server(const Request&, util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4468
        {
2✔
4469
            completion(Response{0, m_code, HttpHeaders(), m_message});
2✔
4470
        }
2✔
4471

1✔
4472
    private:
2✔
4473
        int m_code;
2✔
4474
        std::string m_message;
2✔
4475
    };
2✔
4476

1✔
4477
    SECTION("custom code and message is sent back") {
2✔
4478
        OfflineAppSession offline_session({std::make_shared<CustomErrorTransport>(1001, "Boom!")});
2✔
4479
        auto error = failed_log_in(offline_session.app());
2✔
4480
        CHECK(error.is_custom_error());
2!
4481
        CHECK(*error.additional_status_code == 1001);
2!
4482
        CHECK(error.reason() == "Boom!");
2!
4483
    }
2✔
4484
}
2✔
4485

4486
// MARK: - Unit Tests
4487

4488
static const std::string bad_access_token = "lolwut";
4489
static const std::string dummy_device_id = "123400000000000000000000";
4490

4491
TEST_CASE("subscribable unit tests", "[sync][app]") {
8✔
4492
    struct Foo : public Subscribable<Foo> {
8✔
4493
        void event()
8✔
4494
        {
18✔
4495
            emit_change_to_subscribers(*this);
18✔
4496
        }
18✔
4497
    };
8✔
4498

4✔
4499
    auto foo = Foo();
8✔
4500

4✔
4501
    SECTION("subscriber receives events") {
8✔
4502
        auto event_count = 0;
2✔
4503
        auto token = foo.subscribe([&event_count](auto&) {
6✔
4504
            event_count++;
6✔
4505
        });
6✔
4506

1✔
4507
        foo.event();
2✔
4508
        foo.event();
2✔
4509
        foo.event();
2✔
4510

1✔
4511
        CHECK(event_count == 3);
2!
4512
    }
2✔
4513

4✔
4514
    SECTION("subscriber can unsubscribe") {
8✔
4515
        auto event_count = 0;
2✔
4516
        auto token = foo.subscribe([&event_count](auto&) {
2✔
4517
            event_count++;
2✔
4518
        });
2✔
4519

1✔
4520
        foo.event();
2✔
4521
        CHECK(event_count == 1);
2!
4522

1✔
4523
        foo.unsubscribe(token);
2✔
4524
        foo.event();
2✔
4525
        CHECK(event_count == 1);
2!
4526
    }
2✔
4527

4✔
4528
    SECTION("subscriber is unsubscribed on dtor") {
8✔
4529
        auto event_count = 0;
2✔
4530
        {
2✔
4531
            auto token = foo.subscribe([&event_count](auto&) {
2✔
4532
                event_count++;
2✔
4533
            });
2✔
4534

1✔
4535
            foo.event();
2✔
4536
            CHECK(event_count == 1);
2!
4537
        }
2✔
4538
        foo.event();
2✔
4539
        CHECK(event_count == 1);
2!
4540
    }
2✔
4541

4✔
4542
    SECTION("multiple subscribers receive events") {
8✔
4543
        auto event_count = 0;
2✔
4544
        {
2✔
4545
            auto token1 = foo.subscribe([&event_count](auto&) {
2✔
4546
                event_count++;
2✔
4547
            });
2✔
4548
            auto token2 = foo.subscribe([&event_count](auto&) {
2✔
4549
                event_count++;
2✔
4550
            });
2✔
4551

1✔
4552
            foo.event();
2✔
4553
            CHECK(event_count == 2);
2!
4554
        }
2✔
4555
        foo.event();
2✔
4556
        CHECK(event_count == 2);
2!
4557
    }
2✔
4558
}
8✔
4559

4560
TEST_CASE("app: login_with_credentials unit_tests", "[sync][app][user]") {
6✔
4561
    auto transport = std::make_shared<UnitTestTransport>();
6✔
4562
    OfflineAppSession::Config config{transport};
6✔
4563
    transport->set_profile(profile_0);
6✔
4564

3✔
4565
    SECTION("login_anonymous good") {
6✔
4566
        config.storage_path = util::make_temp_dir();
2✔
4567
        config.metadata_mode = AppConfig::MetadataMode::NoEncryption;
2✔
4568
        {
2✔
4569
            config.delete_storage = false;
2✔
4570
            OfflineAppSession oas(config);
2✔
4571
            auto app = oas.app();
2✔
4572
            auto user = log_in(app);
2✔
4573

1✔
4574
            REQUIRE(user->identities().size() == 1);
2!
4575
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4576
            UserProfile user_profile = user->user_profile();
2✔
4577

1✔
4578
            CHECK(user_profile.name() == profile_0_name);
2!
4579
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4580
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4581
            CHECK(user_profile.email() == profile_0_email);
2!
4582
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4583
            CHECK(user_profile.gender() == profile_0_gender);
2!
4584
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4585
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4586
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4587
        }
2✔
4588
        // assert everything is stored properly between runs
1✔
4589
        {
2✔
4590
            config.delete_storage = true; // clean up after this session
2✔
4591
            OfflineAppSession oas(config);
2✔
4592
            auto app = oas.app();
2✔
4593
            REQUIRE(app->all_users().size() == 1);
2!
4594
            auto user = app->all_users()[0];
2✔
4595
            REQUIRE(user->identities().size() == 1);
2!
4596
            CHECK(user->identities()[0].id == UnitTestTransport::identity_0_id);
2!
4597
            UserProfile user_profile = user->user_profile();
2✔
4598

1✔
4599
            CHECK(user_profile.name() == profile_0_name);
2!
4600
            CHECK(user_profile.first_name() == profile_0_first_name);
2!
4601
            CHECK(user_profile.last_name() == profile_0_last_name);
2!
4602
            CHECK(user_profile.email() == profile_0_email);
2!
4603
            CHECK(user_profile.picture_url() == profile_0_picture_url);
2!
4604
            CHECK(user_profile.gender() == profile_0_gender);
2!
4605
            CHECK(user_profile.birthday() == profile_0_birthday);
2!
4606
            CHECK(user_profile.min_age() == profile_0_min_age);
2!
4607
            CHECK(user_profile.max_age() == profile_0_max_age);
2!
4608
        }
2✔
4609
    }
2✔
4610

3✔
4611
    SECTION("login_anonymous bad") {
6✔
4612
        struct transport : UnitTestTransport {
2✔
4613
            void send_request_to_server(const Request& request,
2✔
4614
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
4615
            {
4✔
4616
                if (request.url.find("/login") != std::string::npos) {
4✔
4617
                    completion({200, 0, {}, user_json(bad_access_token).dump()});
2✔
4618
                }
2✔
4619
                else {
2✔
4620
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
2✔
4621
                }
2✔
4622
            }
4✔
4623
        };
2✔
4624

1✔
4625
        config.transport = instance_of<transport>;
2✔
4626
        OfflineAppSession oas(config);
2✔
4627
        auto error = failed_log_in(oas.app());
2✔
4628
        CHECK(error.reason() == std::string("Could not log in user: received malformed JWT"));
2!
4629
        CHECK(error.code_string() == "BadToken");
2!
4630
        CHECK(error.is_json_error());
2!
4631
        CHECK(error.code() == ErrorCodes::BadToken);
2!
4632
    }
2✔
4633

3✔
4634
    SECTION("login_anonynous multiple users") {
6✔
4635
        OfflineAppSession oas(config);
2✔
4636
        auto app = oas.app();
2✔
4637

1✔
4638
        auto user1 = log_in(app);
2✔
4639
        auto user2 = log_in(app, AppCredentials::anonymous(false));
2✔
4640
        CHECK(user1 != user2);
2!
4641
    }
2✔
4642
}
6✔
4643

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

3✔
4648
    auto logged_in_user = oas.make_user();
6✔
4649
    bool processed = false;
6✔
4650
    ObjectId obj_id(UnitTestTransport::api_key_id.c_str());
6✔
4651

3✔
4652
    SECTION("create api key") {
6✔
4653
        client.create_api_key(UnitTestTransport::api_key_name, logged_in_user,
2✔
4654
                              [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4655
                                  REQUIRE_FALSE(error);
2!
4656
                                  CHECK(user_api_key.disabled == false);
2!
4657
                                  CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4658
                                  CHECK(user_api_key.key == UnitTestTransport::api_key);
2!
4659
                                  CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4660
                              });
2✔
4661
    }
2✔
4662

3✔
4663
    SECTION("fetch api key") {
6✔
4664
        client.fetch_api_key(obj_id, logged_in_user, [&](App::UserAPIKey user_api_key, Optional<AppError> error) {
2✔
4665
            REQUIRE_FALSE(error);
2!
4666
            CHECK(user_api_key.disabled == false);
2!
4667
            CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
2!
4668
            CHECK(user_api_key.name == UnitTestTransport::api_key_name);
2!
4669
        });
2✔
4670
    }
2✔
4671

3✔
4672
    SECTION("fetch api keys") {
6✔
4673
        client.fetch_api_keys(logged_in_user,
2✔
4674
                              [&](std::vector<App::UserAPIKey> user_api_keys, Optional<AppError> error) {
2✔
4675
                                  REQUIRE_FALSE(error);
2!
4676
                                  CHECK(user_api_keys.size() == 2);
2!
4677
                                  for (auto user_api_key : user_api_keys) {
4✔
4678
                                      CHECK(user_api_key.disabled == false);
4!
4679
                                      CHECK(user_api_key.id.to_string() == UnitTestTransport::api_key_id);
4!
4680
                                      CHECK(user_api_key.name == UnitTestTransport::api_key_name);
4!
4681
                                  }
4✔
4682
                                  processed = true;
2✔
4683
                              });
2✔
4684
        CHECK(processed);
2!
4685
    }
2✔
4686
}
6✔
4687

4688
TEST_CASE("app: user_semantics", "[sync][app][user]") {
12✔
4689
    OfflineAppSession oas;
12✔
4690
    auto app = oas.app();
12✔
4691

6✔
4692
    const auto login_user_email_pass = [=] {
9✔
4693
        return log_in(app, AppCredentials::username_password("bob", "thompson"));
6✔
4694
    };
6✔
4695
    const auto login_user_anonymous = [=] {
18✔
4696
        return log_in(app, AppCredentials::anonymous());
18✔
4697
    };
18✔
4698

6✔
4699
    CHECK(!app->current_user());
12!
4700

6✔
4701
    int event_processed = 0;
12✔
4702
    auto token = app->subscribe([&event_processed](auto&) {
28✔
4703
        event_processed++;
28✔
4704
    });
28✔
4705

6✔
4706
    SECTION("current user is populated") {
12✔
4707
        const auto user1 = login_user_anonymous();
2✔
4708
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4709
        CHECK(event_processed == 1);
2!
4710
    }
2✔
4711

6✔
4712
    SECTION("current user is updated on login") {
12✔
4713
        const auto user1 = login_user_anonymous();
2✔
4714
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4715
        const auto user2 = login_user_email_pass();
2✔
4716
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4717
        CHECK(user1->user_id() != user2->user_id());
2!
4718
        CHECK(event_processed == 2);
2!
4719
    }
2✔
4720

6✔
4721
    SECTION("current user is updated to last used user on logout") {
12✔
4722
        const auto user1 = login_user_anonymous();
2✔
4723
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4724
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4725

1✔
4726
        const auto user2 = login_user_email_pass();
2✔
4727
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4728
        CHECK(app->all_users()[1]->state() == SyncUser::State::LoggedIn);
2!
4729
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4730
        CHECK(user1 != user2);
2!
4731

1✔
4732
        // should reuse existing session
1✔
4733
        const auto user3 = login_user_anonymous();
2✔
4734
        CHECK(user3 == user1);
2!
4735

1✔
4736
        auto user_events_processed = 0;
2✔
4737
        auto _ = user3->subscribe([&user_events_processed](auto&) {
2✔
4738
            user_events_processed++;
2✔
4739
        });
2✔
4740

1✔
4741
        app->log_out([](auto) {});
2✔
4742
        CHECK(user_events_processed == 1);
2!
4743
        REQUIRE(app->current_user());
2!
4744
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4745

1✔
4746
        CHECK(app->all_users().size() == 1);
2!
4747
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4748

1✔
4749
        CHECK(event_processed == 4);
2!
4750
    }
2✔
4751

6✔
4752
    SECTION("anon users are removed on logout") {
12✔
4753
        const auto user1 = login_user_anonymous();
2✔
4754
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4755
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4756

1✔
4757
        const auto user2 = login_user_anonymous();
2✔
4758
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4759
        CHECK(app->all_users().size() == 1);
2!
4760
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4761
        CHECK(user1->user_id() == user2->user_id());
2!
4762

1✔
4763
        app->log_out([](auto) {});
2✔
4764
        CHECK(app->all_users().size() == 0);
2!
4765

1✔
4766
        CHECK(event_processed == 3);
2!
4767
    }
2✔
4768

6✔
4769
    SECTION("logout user") {
12✔
4770
        auto user1 = login_user_email_pass();
2✔
4771
        auto user2 = login_user_anonymous();
2✔
4772

1✔
4773
        // Anonymous users are special
1✔
4774
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4775
            REQUIRE_FALSE(error);
2!
4776
        });
2✔
4777
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4778

1✔
4779
        // Other users can be LoggedOut
1✔
4780
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4781
            REQUIRE_FALSE(error);
2!
4782
        });
2✔
4783
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4784

1✔
4785
        // Logging out already logged out users does nothing
1✔
4786
        app->log_out(user1, [](Optional<AppError> error) {
2✔
4787
            REQUIRE_FALSE(error);
2!
4788
        });
2✔
4789
        CHECK(user1->state() == SyncUser::State::LoggedOut);
2!
4790

1✔
4791
        app->log_out(user2, [](Optional<AppError> error) {
2✔
4792
            REQUIRE_FALSE(error);
2!
4793
        });
2✔
4794
        CHECK(user2->state() == SyncUser::State::Removed);
2!
4795

1✔
4796
        CHECK(event_processed == 4);
2!
4797
    }
2✔
4798

6✔
4799
    SECTION("unsubscribed observers no longer process events") {
12✔
4800
        app->unsubscribe(token);
2✔
4801

1✔
4802
        const auto user1 = login_user_anonymous();
2✔
4803
        CHECK(app->current_user()->user_id() == user1->user_id());
2!
4804
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4805

1✔
4806
        const auto user2 = login_user_anonymous();
2✔
4807
        CHECK(app->all_users()[0]->state() == SyncUser::State::LoggedIn);
2!
4808
        CHECK(app->all_users().size() == 1);
2!
4809
        CHECK(app->current_user()->user_id() == user2->user_id());
2!
4810
        CHECK(user1->user_id() == user2->user_id());
2!
4811

1✔
4812
        app->log_out([](auto) {});
2✔
4813
        CHECK(app->all_users().size() == 0);
2!
4814

1✔
4815
        CHECK(event_processed == 0);
2!
4816
    }
2✔
4817
}
12✔
4818

4819
namespace {
4820
struct ErrorCheckingTransport : public GenericNetworkTransport {
4821
    ErrorCheckingTransport(Response* r)
4822
        : m_response(r)
4823
    {
10✔
4824
    }
10✔
4825
    void send_request_to_server(const Request& request,
4826
                                util::UniqueFunction<void(const Response&)>&& completion) override
4827
    {
20✔
4828
        // Make sure to return a valid location response
10✔
4829
        if (request.url.find("/location") != std::string::npos) {
20✔
4830
            completion(Response{200,
10✔
4831
                                0,
10✔
4832
                                {{"content-type", "application/json"}},
10✔
4833
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
10✔
4834
                                "\"http://some.fake.url\",\"ws_hostname\":\"ws://some.fake.url\"}"});
10✔
4835
            return;
10✔
4836
        }
10✔
4837

5✔
4838
        completion(Response(*m_response));
10✔
4839
    }
10✔
4840

4841
private:
4842
    Response* m_response;
4843
};
4844
} // namespace
4845

4846
TEST_CASE("app: response error handling", "[sync][app]") {
10✔
4847
    std::string response_body = nlohmann::json({{"access_token", good_access_token},
10✔
4848
                                                {"refresh_token", good_access_token},
10✔
4849
                                                {"user_id", "Brown Bear"},
10✔
4850
                                                {"device_id", "Panda Bear"}})
10✔
4851
                                    .dump();
10✔
4852

5✔
4853
    Response response{200, 0, {{"Content-Type", "text/plain"}}, response_body};
10✔
4854

5✔
4855
    OfflineAppSession oas({std::make_shared<ErrorCheckingTransport>(&response)});
10✔
4856
    auto app = oas.app();
10✔
4857

5✔
4858
    SECTION("http 404") {
10✔
4859
        response.http_status_code = 404;
2✔
4860
        auto error = failed_log_in(app);
2✔
4861
        CHECK(!error.is_json_error());
2!
4862
        CHECK(!error.is_custom_error());
2!
4863
        CHECK(!error.is_service_error());
2!
4864
        CHECK(error.is_http_error());
2!
4865
        CHECK(*error.additional_status_code == 404);
2!
4866
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4867
    }
2✔
4868
    SECTION("http 500") {
10✔
4869
        response.http_status_code = 500;
2✔
4870
        auto error = failed_log_in(app);
2✔
4871
        CHECK(!error.is_json_error());
2!
4872
        CHECK(!error.is_custom_error());
2!
4873
        CHECK(!error.is_service_error());
2!
4874
        CHECK(error.is_http_error());
2!
4875
        CHECK(*error.additional_status_code == 500);
2!
4876
        CHECK_THAT(std::string(error.reason()), ContainsSubstring("http error code considered fatal"));
2✔
4877
        CHECK(error.link_to_server_logs.empty());
2!
4878
    }
2✔
4879

5✔
4880
    SECTION("custom error code") {
10✔
4881
        response.custom_status_code = 42;
2✔
4882
        response.body = "Custom error message";
2✔
4883
        auto error = failed_log_in(app);
2✔
4884
        CHECK(!error.is_http_error());
2!
4885
        CHECK(!error.is_json_error());
2!
4886
        CHECK(!error.is_service_error());
2!
4887
        CHECK(error.is_custom_error());
2!
4888
        CHECK(*error.additional_status_code == 42);
2!
4889
        CHECK(error.reason() == std::string("Custom error message"));
2!
4890
        CHECK(error.link_to_server_logs.empty());
2!
4891
    }
2✔
4892

5✔
4893
    SECTION("session error code") {
10✔
4894
        response.headers = HttpHeaders{{"Content-Type", "application/json"}};
2✔
4895
        response.http_status_code = 400;
2✔
4896
        response.body = nlohmann::json({{"error_code", "MongoDBError"},
2✔
4897
                                        {"error", "a fake MongoDB error message!"},
2✔
4898
                                        {"access_token", good_access_token},
2✔
4899
                                        {"refresh_token", good_access_token},
2✔
4900
                                        {"user_id", "Brown Bear"},
2✔
4901
                                        {"device_id", "Panda Bear"},
2✔
4902
                                        {"link", "http://...whatever the server passes us"}})
2✔
4903
                            .dump();
2✔
4904
        auto error = failed_log_in(app);
2✔
4905
        CHECK(!error.is_http_error());
2!
4906
        CHECK(!error.is_json_error());
2!
4907
        CHECK(!error.is_custom_error());
2!
4908
        CHECK(error.is_service_error());
2!
4909
        CHECK(error.code() == ErrorCodes::MongoDBError);
2!
4910
        CHECK(error.reason() == std::string("a fake MongoDB error message!"));
2!
4911
        CHECK(error.link_to_server_logs == std::string("http://...whatever the server passes us"));
2!
4912
    }
2✔
4913

5✔
4914
    SECTION("json error code") {
10✔
4915
        response.body = "this: is not{} a valid json body!";
2✔
4916
        auto error = failed_log_in(app);
2✔
4917
        CHECK(!error.is_http_error());
2!
4918
        CHECK(error.is_json_error());
2!
4919
        CHECK(!error.is_custom_error());
2!
4920
        CHECK(!error.is_service_error());
2!
4921
        CHECK(error.code() == ErrorCodes::MalformedJson);
2!
4922
        CHECK(error.reason() ==
2!
4923
              std::string("[json.exception.parse_error.101] parse error at line 1, column 2: syntax error "
2✔
4924
                          "while parsing value - invalid literal; last read: 'th'"));
2✔
4925
        CHECK(error.code_string() == "MalformedJson");
2!
4926
    }
2✔
4927
}
10✔
4928

4929
TEST_CASE("app: switch user", "[sync][app][user]") {
4✔
4930
    OfflineAppSession oas;
4✔
4931
    auto app = oas.app();
4✔
4932

2✔
4933
    bool processed = false;
4✔
4934

2✔
4935
    SECTION("switch user expect success") {
4✔
4936
        CHECK(app->all_users().size() == 0);
2!
4937

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

1✔
4942
        // Log in user 2
1✔
4943
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
4944
        CHECK(app->current_user() == user_b);
2!
4945

1✔
4946
        CHECK(app->all_users().size() == 2);
2!
4947

1✔
4948
        app->switch_user(user_a);
2✔
4949
        CHECK(app->current_user() == user_a);
2!
4950

1✔
4951
        app->switch_user(user_b);
2✔
4952

1✔
4953
        CHECK(app->current_user() == user_b);
2!
4954
        processed = true;
2✔
4955
        CHECK(processed);
2!
4956
    }
2✔
4957

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

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

1✔
4965
        app->log_out([&](Optional<AppError> error) {
2✔
4966
            REQUIRE_FALSE(error);
2!
4967
        });
2✔
4968

1✔
4969
        CHECK(app->current_user() == nullptr);
2!
4970
        CHECK(user_a->state() == SyncUser::State::LoggedOut);
2!
4971

1✔
4972
        // Log in user 2
1✔
4973
        auto user_b = log_in(app, AppCredentials::username_password("test2@10gen.com", "password"));
2✔
4974
        CHECK(app->current_user() == user_b);
2!
4975
        CHECK(app->all_users().size() == 2);
2!
4976

1✔
4977
        REQUIRE_THROWS_AS(app->switch_user(user_a), AppError);
2✔
4978
        CHECK(app->current_user() == user_b);
2!
4979
    }
2✔
4980
}
4✔
4981

4982
TEST_CASE("app: remove user", "[sync][app][user]") {
4✔
4983
    OfflineAppSession oas;
4✔
4984
    auto app = oas.app();
4✔
4985

2✔
4986
    SECTION("remove anonymous user") {
4✔
4987
        CHECK(app->all_users().size() == 0);
2!
4988

1✔
4989
        // Log in user 1
1✔
4990
        auto user_a = log_in(app);
2✔
4991
        CHECK(user_a->state() == SyncUser::State::LoggedIn);
2!
4992

1✔
4993
        app->log_out(user_a, [&](Optional<AppError> error) {
2✔
4994
            REQUIRE_FALSE(error);
2!
4995
            // a logged out anon user will be marked as Removed, not LoggedOut
1✔
4996
            CHECK(user_a->state() == SyncUser::State::Removed);
2!
4997
        });
2✔
4998
        CHECK(app->all_users().empty());
2!
4999

1✔
5000
        app->remove_user(user_a, [&](Optional<AppError> error) {
2✔
5001
            CHECK(error->reason() == "User has already been removed");
2!
5002
            CHECK(app->all_users().size() == 0);
2!
5003
        });
2✔
5004

1✔
5005
        // Log in user 2
1✔
5006
        auto user_b = log_in(app);
2✔
5007
        CHECK(app->current_user() == user_b);
2!
5008
        CHECK(user_b->state() == SyncUser::State::LoggedIn);
2!
5009
        CHECK(app->all_users().size() == 1);
2!
5010

1✔
5011
        app->remove_user(user_b, [&](Optional<AppError> error) {
2✔
5012
            REQUIRE_FALSE(error);
2!
5013
            CHECK(app->all_users().size() == 0);
2!
5014
        });
2✔
5015

1✔
5016
        CHECK(app->current_user() == nullptr);
2!
5017

1✔
5018
        // check both handles are no longer valid
1✔
5019
        CHECK(user_a->state() == SyncUser::State::Removed);
2!
5020
        CHECK(user_b->state() == SyncUser::State::Removed);
2!
5021
    }
2✔
5022

2✔
5023
    SECTION("remove user with credentials") {
4✔
5024
        CHECK(app->all_users().size() == 0);
2!
5025
        CHECK(app->current_user() == nullptr);
2!
5026

1✔
5027
        auto user = log_in(app, AppCredentials::username_password("email", "pass"));
2✔
5028

1✔
5029
        CHECK(user->state() == SyncUser::State::LoggedIn);
2!
5030

1✔
5031
        app->log_out(user, [&](Optional<AppError> error) {
2✔
5032
            REQUIRE_FALSE(error);
2!
5033
        });
2✔
5034

1✔
5035
        CHECK(user->state() == SyncUser::State::LoggedOut);
2!
5036

1✔
5037
        app->remove_user(user, [&](Optional<AppError> error) {
2✔
5038
            REQUIRE_FALSE(error);
2!
5039
        });
2✔
5040
        CHECK(app->all_users().size() == 0);
2!
5041

1✔
5042
        Optional<AppError> error;
2✔
5043
        app->remove_user(user, [&](Optional<AppError> err) {
2✔
5044
            error = err;
2✔
5045
        });
2✔
5046
        CHECK(error->code() > 0);
2!
5047
        CHECK(app->all_users().size() == 0);
2!
5048
        CHECK(user->state() == SyncUser::State::Removed);
2!
5049
    }
2✔
5050
}
4✔
5051

5052
TEST_CASE("app: link_user", "[sync][app][user]") {
4✔
5053
    OfflineAppSession oas;
4✔
5054
    auto app = oas.app();
4✔
5055

2✔
5056
    auto email = util::format("realm_tests_do_autoverify%1@%2.com", random_string(10), random_string(10));
4✔
5057
    auto password = random_string(10);
4✔
5058

2✔
5059
    auto custom_credentials = AppCredentials::facebook("a_token");
4✔
5060
    auto email_pass_credentials = AppCredentials::username_password(email, password);
4✔
5061

2✔
5062
    auto sync_user = log_in(app, email_pass_credentials);
4✔
5063
    REQUIRE(sync_user->identities().size() == 2);
4!
5064
    CHECK(sync_user->identities()[0].provider_type == IdentityProviderUsernamePassword);
4!
5065

2✔
5066
    SECTION("successful link") {
4✔
5067
        bool processed = false;
2✔
5068
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
5069
            REQUIRE_FALSE(error);
2!
5070
            REQUIRE(user);
2!
5071
            CHECK(user->user_id() == sync_user->user_id());
2!
5072
            processed = true;
2✔
5073
        });
2✔
5074
        CHECK(processed);
2!
5075
    }
2✔
5076

2✔
5077
    SECTION("link_user should fail when logged out") {
4✔
5078
        app->log_out([&](Optional<AppError> error) {
2✔
5079
            REQUIRE_FALSE(error);
2!
5080
        });
2✔
5081

1✔
5082
        bool processed = false;
2✔
5083
        app->link_user(sync_user, custom_credentials, [&](std::shared_ptr<SyncUser> user, Optional<AppError> error) {
2✔
5084
            CHECK(error->reason() == "The specified user is not logged in.");
2!
5085
            CHECK(!user);
2!
5086
            processed = true;
2✔
5087
        });
2✔
5088
        CHECK(processed);
2!
5089
    }
2✔
5090
}
4✔
5091

5092
TEST_CASE("app: auth providers", "[sync][app][user]") {
20✔
5093
    SECTION("auth providers facebook") {
20✔
5094
        auto credentials = AppCredentials::facebook("a_token");
2✔
5095
        CHECK(credentials.provider() == AuthProvider::FACEBOOK);
2!
5096
        CHECK(credentials.provider_as_string() == IdentityProviderFacebook);
2!
5097
        CHECK(credentials.serialize_as_bson() ==
2!
5098
              bson::BsonDocument{{"provider", "oauth2-facebook"}, {"accessToken", "a_token"}});
2✔
5099
    }
2✔
5100

10✔
5101
    SECTION("auth providers anonymous") {
20✔
5102
        auto credentials = AppCredentials::anonymous();
2✔
5103
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS);
2!
5104
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5105
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5106
    }
2✔
5107

10✔
5108
    SECTION("auth providers anonymous no reuse") {
20✔
5109
        auto credentials = AppCredentials::anonymous(false);
2✔
5110
        CHECK(credentials.provider() == AuthProvider::ANONYMOUS_NO_REUSE);
2!
5111
        CHECK(credentials.provider_as_string() == IdentityProviderAnonymous);
2!
5112
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "anon-user"}});
2!
5113
    }
2✔
5114

10✔
5115
    SECTION("auth providers google authCode") {
20✔
5116
        auto credentials = AppCredentials::google(AuthCode("a_token"));
2✔
5117
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5118
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5119
        CHECK(credentials.serialize_as_bson() ==
2!
5120
              bson::BsonDocument{{"provider", "oauth2-google"}, {"authCode", "a_token"}});
2✔
5121
    }
2✔
5122

10✔
5123
    SECTION("auth providers google idToken") {
20✔
5124
        auto credentials = AppCredentials::google(IdToken("a_token"));
2✔
5125
        CHECK(credentials.provider() == AuthProvider::GOOGLE);
2!
5126
        CHECK(credentials.provider_as_string() == IdentityProviderGoogle);
2!
5127
        CHECK(credentials.serialize_as_bson() ==
2!
5128
              bson::BsonDocument{{"provider", "oauth2-google"}, {"id_token", "a_token"}});
2✔
5129
    }
2✔
5130

10✔
5131
    SECTION("auth providers apple") {
20✔
5132
        auto credentials = AppCredentials::apple("a_token");
2✔
5133
        CHECK(credentials.provider() == AuthProvider::APPLE);
2!
5134
        CHECK(credentials.provider_as_string() == IdentityProviderApple);
2!
5135
        CHECK(credentials.serialize_as_bson() ==
2!
5136
              bson::BsonDocument{{"provider", "oauth2-apple"}, {"id_token", "a_token"}});
2✔
5137
    }
2✔
5138

10✔
5139
    SECTION("auth providers custom") {
20✔
5140
        auto credentials = AppCredentials::custom("a_token");
2✔
5141
        CHECK(credentials.provider() == AuthProvider::CUSTOM);
2!
5142
        CHECK(credentials.provider_as_string() == IdentityProviderCustom);
2!
5143
        CHECK(credentials.serialize_as_bson() ==
2!
5144
              bson::BsonDocument{{"provider", "custom-token"}, {"token", "a_token"}});
2✔
5145
    }
2✔
5146

10✔
5147
    SECTION("auth providers username password") {
20✔
5148
        auto credentials = AppCredentials::username_password("user", "pass");
2✔
5149
        CHECK(credentials.provider() == AuthProvider::USERNAME_PASSWORD);
2!
5150
        CHECK(credentials.provider_as_string() == IdentityProviderUsernamePassword);
2!
5151
        CHECK(credentials.serialize_as_bson() ==
2!
5152
              bson::BsonDocument{{"provider", "local-userpass"}, {"username", "user"}, {"password", "pass"}});
2✔
5153
    }
2✔
5154

10✔
5155
    SECTION("auth providers function") {
20✔
5156
        bson::BsonDocument function_params{{"name", "mongo"}};
2✔
5157
        auto credentials = AppCredentials::function(function_params);
2✔
5158
        CHECK(credentials.provider() == AuthProvider::FUNCTION);
2!
5159
        CHECK(credentials.provider_as_string() == IdentityProviderFunction);
2!
5160
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"name", "mongo"}});
2!
5161
    }
2✔
5162

10✔
5163
    SECTION("auth providers api key") {
20✔
5164
        auto credentials = AppCredentials::api_key("a key");
2✔
5165
        CHECK(credentials.provider() == AuthProvider::API_KEY);
2!
5166
        CHECK(credentials.provider_as_string() == IdentityProviderAPIKey);
2!
5167
        CHECK(credentials.serialize_as_bson() == bson::BsonDocument{{"provider", "api-key"}, {"key", "a key"}});
2!
5168
        CHECK(enum_from_provider_type(provider_type_from_enum(AuthProvider::API_KEY)) == AuthProvider::API_KEY);
2!
5169
    }
2✔
5170
}
20✔
5171

5172
TEST_CASE("app: refresh access token unit tests", "[sync][app][user][token]") {
6✔
5173
    SECTION("refresh custom data happy path") {
6✔
5174
        static bool session_route_hit = false;
2✔
5175

1✔
5176
        struct transport : UnitTestTransport {
2✔
5177
            void send_request_to_server(const Request& request,
2✔
5178
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5179
            {
10✔
5180
                if (request.url.find("/session") != std::string::npos) {
10✔
5181
                    session_route_hit = true;
2✔
5182
                    nlohmann::json json{{"access_token", good_access_token}};
2✔
5183
                    completion({200, 0, {}, json.dump()});
2✔
5184
                }
2✔
5185
                else {
8✔
5186
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5187
                }
8✔
5188
            }
10✔
5189
        };
2✔
5190
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5191
        auto app = oas.app();
2✔
5192
        oas.make_user();
2✔
5193

1✔
5194
        bool processed = false;
2✔
5195
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5196
            REQUIRE_FALSE(error);
2!
5197
            CHECK(session_route_hit);
2!
5198
            processed = true;
2✔
5199
        });
2✔
5200
        CHECK(processed);
2!
5201
    }
2✔
5202

3✔
5203
    SECTION("refresh custom data sad path") {
6✔
5204
        static bool session_route_hit = false;
2✔
5205

1✔
5206
        struct transport : UnitTestTransport {
2✔
5207
            void send_request_to_server(const Request& request,
2✔
5208
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5209
            {
10✔
5210
                if (request.url.find("/session") != std::string::npos) {
10✔
5211
                    session_route_hit = true;
2✔
5212
                    nlohmann::json json{{"access_token", bad_access_token}};
2✔
5213
                    completion({200, 0, {}, json.dump()});
2✔
5214
                }
2✔
5215
                else {
8✔
5216
                    UnitTestTransport::send_request_to_server(request, std::move(completion));
8✔
5217
                }
8✔
5218
            }
10✔
5219
        };
2✔
5220

1✔
5221
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5222
        auto app = oas.app();
2✔
5223
        oas.make_user();
2✔
5224

1✔
5225
        bool processed = false;
2✔
5226
        app->refresh_custom_data(app->current_user(), [&](const Optional<AppError>& error) {
2✔
5227
            CHECK(error->reason() == "malformed JWT");
2!
5228
            CHECK(error->code() == ErrorCodes::BadToken);
2!
5229
            CHECK(session_route_hit);
2!
5230
            processed = true;
2✔
5231
        });
2✔
5232
        CHECK(processed);
2!
5233
    }
2✔
5234

3✔
5235
    SECTION("refresh token ensure flow is correct") {
6✔
5236
        /*
1✔
5237
         Expected flow:
1✔
5238
         Login - this gets access and refresh tokens
1✔
5239
         Get profile - throw back a 401 error
1✔
5240
         Refresh token - get a new token for the user
1✔
5241
         Get profile - get the profile with the new token
1✔
5242
         */
1✔
5243
        struct transport : GenericNetworkTransport {
2✔
5244
            enum class TestState { unknown, location, login, profile_1, refresh, profile_2 };
2✔
5245
            TestingStateMachine<TestState> state{TestState::unknown};
2✔
5246
            void send_request_to_server(const Request& request,
2✔
5247
                                        util::UniqueFunction<void(const Response&)>&& completion) override
2✔
5248
            {
10✔
5249
                if (request.url.find("/login") != std::string::npos) {
10✔
5250
                    CHECK(state.get() == TestState::location);
2!
5251
                    state.transition_to(TestState::login);
2✔
5252
                    completion({200, 0, {}, user_json(good_access_token).dump()});
2✔
5253
                }
2✔
5254
                else if (request.url.find("/profile") != std::string::npos) {
8✔
5255
                    auto item = AppUtils::find_header("Authorization", request.headers);
4✔
5256
                    CHECK(item);
4!
5257
                    auto access_token = item->second;
4✔
5258
                    // simulated bad token request
2✔
5259
                    if (access_token.find(good_access_token2) != std::string::npos) {
4✔
5260
                        CHECK(state.get() == TestState::refresh);
2!
5261
                        state.transition_to(TestState::profile_2);
2✔
5262
                        completion({200, 0, {}, user_profile_json().dump()});
2✔
5263
                    }
2✔
5264
                    else if (access_token.find(good_access_token) != std::string::npos) {
2✔
5265
                        CHECK(state.get() == TestState::login);
2!
5266
                        state.transition_to(TestState::profile_1);
2✔
5267
                        completion({401, 0, {}});
2✔
5268
                    }
2✔
5269
                }
4✔
5270
                else if (request.url.find("/session") != std::string::npos && request.method == HttpMethod::post) {
4✔
5271
                    CHECK(state.get() == TestState::profile_1);
2!
5272
                    state.transition_to(TestState::refresh);
2✔
5273
                    nlohmann::json json{{"access_token", good_access_token2}};
2✔
5274
                    completion({200, 0, {}, json.dump()});
2✔
5275
                }
2✔
5276
                else if (request.url.find("/location") != std::string::npos) {
2✔
5277
                    CHECK(state.get() == TestState::unknown);
2!
5278
                    state.transition_to(TestState::location);
2✔
5279
                    CHECK(request.method == HttpMethod::get);
2!
5280
                    completion({200,
2✔
5281
                                0,
2✔
5282
                                {},
2✔
5283
                                "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":"
2✔
5284
                                "\"http://localhost:9090\",\"ws_hostname\":\"ws://localhost:9090\"}"});
2✔
5285
                }
2✔
UNCOV
5286
                else {
×
UNCOV
5287
                    FAIL("Unexpected request in test code" + request.url);
×
UNCOV
5288
                }
×
5289
            }
10✔
5290
        };
2✔
5291

1✔
5292
        OfflineAppSession oas(OfflineAppSession::Config{instance_of<transport>});
2✔
5293
        auto app = oas.app();
2✔
5294
        REQUIRE(log_in(app));
2!
5295
    }
2✔
5296
}
6✔
5297

5298
TEST_CASE("app: app released during async operation", "[app][user]") {
10✔
5299
    struct Transport : public UnitTestTransport {
10✔
5300
        std::string endpoint_to_hook;
10✔
5301
        std::optional<Request> stored_request;
10✔
5302
        util::UniqueFunction<void(const Response&)> stored_completion;
10✔
5303

5✔
5304
        void send_request_to_server(const Request& request,
10✔
5305
                                    util::UniqueFunction<void(const Response&)>&& completion) override
10✔
5306
        {
38✔
5307
            // Store the completion handler for the chosen endpoint so that we can
19✔
5308
            // invoke it after releasing the test's references to the App to
19✔
5309
            // verify that it doesn't crash
19✔
5310
            if (request.url.find(endpoint_to_hook) != std::string::npos) {
38✔
5311
                REQUIRE_FALSE(stored_request);
10!
5312
                REQUIRE_FALSE(stored_completion);
10!
5313
                stored_request = request;
10✔
5314
                stored_completion = std::move(completion);
10✔
5315
                return;
10✔
5316
            }
28✔
5317

14✔
5318
            UnitTestTransport::send_request_to_server(request, std::move(completion));
28✔
5319
        }
28✔
5320

5✔
5321
        bool has_stored() const
10✔
5322
        {
20✔
5323
            return !!stored_completion;
20✔
5324
        }
20✔
5325

5✔
5326
        void send_stored()
10✔
5327
        {
10✔
5328
            REQUIRE(stored_request);
10!
5329
            REQUIRE(stored_completion);
10!
5330
            UnitTestTransport::send_request_to_server(*stored_request, std::move(stored_completion));
10✔
5331
            stored_request.reset();
10✔
5332
            stored_completion = nullptr;
10✔
5333
        }
10✔
5334
    };
10✔
5335
    auto transport = std::make_shared<Transport>();
10✔
5336
    test_util::TestDirGuard base_path(util::make_temp_dir(), false);
10✔
5337
    AppConfig app_config;
10✔
5338
    set_app_config_defaults(app_config, transport);
10✔
5339
    app_config.base_file_path = base_path;
10✔
5340

5✔
5341
    SECTION("login") {
10✔
5342
        transport->endpoint_to_hook = GENERATE("/location", "/login", "/profile");
6✔
5343
        bool called = false;
6✔
5344
        {
6✔
5345
            auto app = App::get_app(App::CacheMode::Disabled, app_config);
6✔
5346
            app->log_in_with_credentials(AppCredentials::anonymous(),
6✔
5347
                                         [&](std::shared_ptr<SyncUser> user, util::Optional<AppError> error) mutable {
6✔
5348
                                             REQUIRE_FALSE(error);
6!
5349
                                             REQUIRE(user);
6!
5350
                                             REQUIRE(user->is_logged_in());
6!
5351
                                             called = true;
6✔
5352
                                         });
6✔
5353
            REQUIRE(transport->has_stored());
6!
5354
        }
6✔
5355
        REQUIRE_FALSE(called);
6!
5356
        transport->send_stored();
6✔
5357
        REQUIRE(called);
6!
5358
    }
6✔
5359

5✔
5360
    SECTION("access token refresh") {
10✔
5361
        transport->endpoint_to_hook = "/auth/session";
4✔
5362
        SECTION("directly via user") {
4✔
5363
            bool completion_called = false;
2✔
5364
            {
2✔
5365
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5366
                create_user_and_log_in(app);
2✔
5367
                app->current_user()->refresh_custom_data([&](std::optional<app::AppError> error) {
2✔
5368
                    REQUIRE_FALSE(error);
2!
5369
                    completion_called = true;
2✔
5370
                });
2✔
5371
                REQUIRE(transport->has_stored());
2!
5372
            }
2✔
5373

1✔
5374
            REQUIRE_FALSE(completion_called);
2!
5375
            transport->send_stored();
2✔
5376
            REQUIRE(completion_called);
2!
5377
        }
2✔
5378

2✔
5379
        SECTION("via sync session") {
4✔
5380
            {
2✔
5381
                auto app = App::get_app(App::CacheMode::Disabled, app_config);
2✔
5382
                create_user_and_log_in(app);
2✔
5383
                auto user = app->current_user();
2✔
5384
                SyncTestFile config(user, bson::Bson("test"));
2✔
5385
                // give the user an expired access token so that the first use will try to refresh it
1✔
5386
                user->update_data_for_testing([](auto& data) {
2✔
5387
                    data.access_token = RealmJWT(encode_fake_jwt("token", 123, 456));
2✔
5388
                });
2✔
5389
                REQUIRE_FALSE(transport->stored_completion);
2!
5390
                auto realm = Realm::get_shared_realm(config);
2✔
5391
                REQUIRE(transport->has_stored());
2!
5392
            }
2✔
5393
            transport->send_stored();
2✔
5394
        }
2✔
5395
    }
4✔
5396

5✔
5397
    REQUIRE_FALSE(transport->has_stored());
10!
5398
}
10✔
5399

5400
TEST_CASE("app: make_streaming_request", "[sync][app][streaming]") {
8✔
5401
    constexpr uint64_t timeout_ms = 60000; // this is the default
8✔
5402
    OfflineAppSession oas({std::make_shared<UnitTestTransport>(timeout_ms)});
8✔
5403
    auto app = oas.app();
8✔
5404

4✔
5405
    auto user = log_in(app);
8✔
5406

4✔
5407
    using Headers = decltype(Request().headers);
8✔
5408

4✔
5409
    const auto url_prefix = "https://some.fake.url/api/client/v2.0/app/app_id/functions/call?baas_request="sv;
8✔
5410
    const auto get_request_args = [&](const Request& req) {
8✔
5411
        REQUIRE(req.url.substr(0, url_prefix.size()) == url_prefix);
8!
5412
        auto args = req.url.substr(url_prefix.size());
8✔
5413
        if (auto amp = args.find('&'); amp != std::string::npos) {
8✔
5414
            args.resize(amp);
2✔
5415
        }
2✔
5416

4✔
5417
        auto vec = util::base64_decode_to_vector(util::uri_percent_decode(args));
8✔
5418
        REQUIRE(!!vec);
8!
5419
        auto parsed = bson::parse({vec->data(), vec->size()});
8✔
5420
        REQUIRE(parsed.type() == bson::Bson::Type::Document);
8!
5421
        auto out = parsed.operator const bson::BsonDocument&();
8✔
5422
        CHECK(out.size() == 3);
8!
5423
        return out;
8✔
5424
    };
8✔
5425

4✔
5426
    const auto make_request = [&](std::shared_ptr<User> user, auto&&... args) {
8✔
5427
        auto req = app->make_streaming_request(user, "func", bson::BsonArray{args...}, {"svc"});
8✔
5428
        CHECK(req.method == HttpMethod::get);
8!
5429
        CHECK(req.body == "");
8!
5430
        CHECK(req.headers == Headers{{"Accept", "text/event-stream"}});
8!
5431
        CHECK(req.timeout_ms == timeout_ms);
8!
5432

4✔
5433
        auto req_args = get_request_args(req);
8✔
5434
        CHECK(req_args["name"] == "func");
8!
5435
        CHECK(req_args["service"] == "svc");
8!
5436
        CHECK(req_args["arguments"] == bson::BsonArray{args...});
8!
5437

4✔
5438
        return req;
8✔
5439
    };
8✔
5440

4✔
5441
    SECTION("no args") {
8✔
5442
        auto req = make_request(nullptr);
2✔
5443
        CHECK(req.url.find('&') == std::string::npos);
2!
5444
    }
2✔
5445
    SECTION("args") {
8✔
5446
        auto req = make_request(nullptr, "arg1", "arg2");
2✔
5447
        CHECK(req.url.find('&') == std::string::npos);
2!
5448
    }
2✔
5449
    SECTION("percent encoding") {
8✔
5450
        // These force the base64 encoding to have + and / bytes and = padding, all of which are uri encoded.
1✔
5451
        auto req = make_request(nullptr, ">>>>>?????");
2✔
5452

1✔
5453
        CHECK(req.url.find('&') == std::string::npos);
2!
5454
        CHECK_THAT(req.url, ContainsSubstring("%2B"));     // + (from >)
2✔
5455
        CHECK_THAT(req.url, ContainsSubstring("%2F"));     // / (from ?)
2✔
5456
        CHECK_THAT(req.url, ContainsSubstring("%3D"));     // = (tail padding)
2✔
5457
        CHECK(req.url.rfind("%3D") == req.url.size() - 3); // = (tail padding)
2!
5458
    }
2✔
5459
    SECTION("with user") {
8✔
5460
        auto req = make_request(user, "arg1", "arg2");
2✔
5461

1✔
5462
        auto amp = req.url.find('&');
2✔
5463
        REQUIRE(amp != std::string::npos);
2!
5464
        auto tail = req.url.substr(amp);
2✔
5465
        REQUIRE(tail == ("&baas_at=" + user->access_token()));
2!
5466
    }
2✔
5467
}
8✔
5468

5469
TEST_CASE("app: sync_user_profile unit tests", "[sync][app][user]") {
4✔
5470
    SECTION("with empty map") {
4✔
5471
        auto profile = UserProfile(bson::BsonDocument());
2✔
5472
        CHECK(profile.name() == util::none);
2!
5473
        CHECK(profile.email() == util::none);
2!
5474
        CHECK(profile.picture_url() == util::none);
2!
5475
        CHECK(profile.first_name() == util::none);
2!
5476
        CHECK(profile.last_name() == util::none);
2!
5477
        CHECK(profile.gender() == util::none);
2!
5478
        CHECK(profile.birthday() == util::none);
2!
5479
        CHECK(profile.min_age() == util::none);
2!
5480
        CHECK(profile.max_age() == util::none);
2!
5481
    }
2✔
5482
    SECTION("with full map") {
4✔
5483
        auto profile = UserProfile(bson::BsonDocument({
2✔
5484
            {"first_name", "Jan"},
2✔
5485
            {"last_name", "Jaanson"},
2✔
5486
            {"name", "Jan Jaanson"},
2✔
5487
            {"email", "jan.jaanson@jaanson.com"},
2✔
5488
            {"gender", "none"},
2✔
5489
            {"birthday", "January 1, 1970"},
2✔
5490
            {"min_age", "0"},
2✔
5491
            {"max_age", "100"},
2✔
5492
            {"picture_url", "some"},
2✔
5493
        }));
2✔
5494
        CHECK(profile.name() == "Jan Jaanson");
2!
5495
        CHECK(profile.email() == "jan.jaanson@jaanson.com");
2!
5496
        CHECK(profile.picture_url() == "some");
2!
5497
        CHECK(profile.first_name() == "Jan");
2!
5498
        CHECK(profile.last_name() == "Jaanson");
2!
5499
        CHECK(profile.gender() == "none");
2!
5500
        CHECK(profile.birthday() == "January 1, 1970");
2!
5501
        CHECK(profile.min_age() == "0");
2!
5502
        CHECK(profile.max_age() == "100");
2!
5503
    }
2✔
5504
}
4✔
5505

5506
TEST_CASE("app: shared instances", "[sync][app]") {
2✔
5507
    test_util::TestDirGuard test_dir(util::make_temp_dir(), false);
2✔
5508

1✔
5509
    AppConfig base_config;
2✔
5510
    set_app_config_defaults(base_config, instance_of<UnitTestTransport>);
2✔
5511
    base_config.base_file_path = test_dir;
2✔
5512

1✔
5513
    auto config1 = base_config;
2✔
5514
    config1.app_id = "app1";
2✔
5515

1✔
5516
    auto config2 = base_config;
2✔
5517
    config2.app_id = "app1";
2✔
5518
    config2.base_url = std::string(App::default_base_url());
2✔
5519

1✔
5520
    auto config3 = base_config;
2✔
5521
    config3.app_id = "app2";
2✔
5522

1✔
5523
    auto config4 = base_config;
2✔
5524
    config4.app_id = "app2";
2✔
5525
    config4.base_url = "http://localhost:9090";
2✔
5526

1✔
5527
    // should all point to same underlying app
1✔
5528
    auto app1_1 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5529
    auto app1_2 = App::get_app(app::App::CacheMode::Enabled, config1);
2✔
5530
    auto app1_3 = App::get_cached_app(config1.app_id, config1.base_url);
2✔
5531
    auto app1_4 = App::get_app(app::App::CacheMode::Enabled, config2);
2✔
5532
    auto app1_5 = App::get_cached_app(config1.app_id);
2✔
5533

1✔
5534
    CHECK(app1_1 == app1_2);
2!
5535
    CHECK(app1_1 == app1_3);
2!
5536
    CHECK(app1_1 == app1_4);
2!
5537
    CHECK(app1_1 == app1_5);
2!
5538

1✔
5539
    // config3 and config4 should point to different apps
1✔
5540
    auto app2_1 = App::get_app(app::App::CacheMode::Enabled, config3);
2✔
5541
    auto app2_2 = App::get_cached_app(config3.app_id, config3.base_url);
2✔
5542
    auto app2_3 = App::get_app(app::App::CacheMode::Enabled, config4);
2✔
5543
    auto app2_4 = App::get_cached_app(config3.app_id);
2✔
5544
    auto app2_5 = App::get_cached_app(config4.app_id, "https://some.different.url");
2✔
5545

1✔
5546
    CHECK(app2_1 == app2_2);
2!
5547
    CHECK(app2_1 != app2_3);
2!
5548
    CHECK(app2_4 != nullptr);
2!
5549
    CHECK(app2_5 == nullptr);
2!
5550

1✔
5551
    CHECK(app1_1 != app2_1);
2!
5552
    CHECK(app1_1 != app2_3);
2!
5553
    CHECK(app1_1 != app2_4);
2!
5554
}
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