|
45 | 45 |
|
46 | 46 | #if REALM_ENABLE_SYNC |
47 | 47 | #include <util/sync/flx_sync_harness.hpp> |
| 48 | +#include <util/sync/sync_test_utils.hpp> |
| 49 | +#include <util/test_file.hpp> |
| 50 | +#ifdef REALM_ENABLE_AUTH_TESTS |
| 51 | +#include <util/sync/baas_admin_api.hpp> |
| 52 | +#endif // REALM_ENABLE_AUTH_TESTS |
48 | 53 |
|
49 | 54 | #include <realm/object-store/sync/async_open_task.hpp> |
50 | 55 | #include <realm/object-store/sync/impl/app_metadata.hpp> |
51 | 56 | #include <realm/object-store/sync/sync_session.hpp> |
52 | 57 |
|
53 | 58 | #include <realm/sync/noinst/client_history_impl.hpp> |
54 | 59 | #include <realm/sync/subscriptions.hpp> |
55 | | -#endif |
| 60 | +#endif // REALM_ENABLE_SYNC |
56 | 61 |
|
57 | 62 | #include <catch2/catch_all.hpp> |
58 | 63 | #include <catch2/matchers/catch_matchers_string.hpp> |
|
62 | 67 | #include <array> |
63 | 68 | #if REALM_HAVE_UV |
64 | 69 | #include <uv.h> |
65 | | -#endif |
| 70 | +#endif // REALM_HAVE_UV |
66 | 71 |
|
67 | 72 | namespace realm { |
68 | 73 | class TestHelper { |
@@ -1215,6 +1220,148 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { |
1215 | 1220 | REQUIRE(got_error); |
1216 | 1221 | } |
1217 | 1222 |
|
| 1223 | +#if REALM_APP_SERVICES |
| 1224 | + |
| 1225 | + SECTION("waiters are cancelled if cancel_waits_on_nonfatal_error") { |
| 1226 | + auto logger = util::Logger::get_default_logger(); |
| 1227 | + auto transport = std::make_shared<HookedTransport<UnitTestTransport>>(); |
| 1228 | + auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent"); |
| 1229 | + enum TestMode { expired_at_start, expired_by_websocket, websocket_fails }; |
| 1230 | + enum FailureMode { location_fails, token_fails, token_not_authorized }; |
| 1231 | + auto txt_test_mode = [](TestMode mode) { |
| 1232 | + switch (mode) { |
| 1233 | + case TestMode::expired_at_start: |
| 1234 | + return "access token expired when realm is opened"; |
| 1235 | + case TestMode::expired_by_websocket: |
| 1236 | + return "access token expired by websocket"; |
| 1237 | + case TestMode::websocket_fails: |
| 1238 | + return "websocket returns connection failed"; |
| 1239 | + default: |
| 1240 | + return "Unknown TestMode"; |
| 1241 | + } |
| 1242 | + }; |
| 1243 | + auto txt_failure_mode = [](FailureMode mode) { |
| 1244 | + switch (mode) { |
| 1245 | + case FailureMode::location_fails: |
| 1246 | + return "location update fails"; |
| 1247 | + case FailureMode::token_fails: |
| 1248 | + return "access token refresh fails"; |
| 1249 | + case FailureMode::token_not_authorized: |
| 1250 | + return "websocket connect not authorized"; |
| 1251 | + default: |
| 1252 | + return "Unknown FailureMode"; |
| 1253 | + } |
| 1254 | + }; |
| 1255 | + |
| 1256 | + app::AppConfig app_config; |
| 1257 | + set_app_config_defaults(app_config, transport); |
| 1258 | + app_config.sync_client_config.socket_provider = socket_provider; |
| 1259 | + app_config.base_file_path = util::make_temp_dir(); |
| 1260 | + app_config.metadata_mode = app::AppConfig::MetadataMode::NoEncryption; |
| 1261 | + |
| 1262 | + auto the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config); |
| 1263 | + create_user_and_log_in(the_app); |
| 1264 | + auto user = the_app->current_user(); |
| 1265 | + // User should be logged in at this point |
| 1266 | + REQUIRE(user->is_logged_in()); |
| 1267 | + |
| 1268 | + bool not_authorized = false; |
| 1269 | + bool token_refresh_called = false; |
| 1270 | + bool location_refresh_called = false; |
| 1271 | + |
| 1272 | + TestMode test_mode = GENERATE(expired_at_start, expired_by_websocket, websocket_fails); |
| 1273 | + FailureMode failure = GENERATE(location_fails, token_fails, token_not_authorized); |
| 1274 | + |
| 1275 | + DYNAMIC_SECTION(txt_test_mode(test_mode) << " - " << txt_failure_mode(failure)) { |
| 1276 | + logger->info("TEST: %1 - %2", txt_test_mode(test_mode), txt_failure_mode(failure)); |
| 1277 | + if (test_mode == TestMode::expired_at_start) { |
| 1278 | + // invalidate the user's cached access token |
| 1279 | + auto app_user = the_app->current_user(); |
| 1280 | + app_user->update_data_for_testing([&](app::UserData& data) { |
| 1281 | + data.access_token = RealmJWT(expired_token); |
| 1282 | + }); |
| 1283 | + } |
| 1284 | + else if (test_mode == TestMode::expired_by_websocket) { |
| 1285 | + // tell websocket to return not authorized to refresh access token |
| 1286 | + not_authorized = true; |
| 1287 | + } |
| 1288 | + } |
| 1289 | + |
| 1290 | + the_app.reset(); |
| 1291 | + |
| 1292 | + auto err_handler = [](std::shared_ptr<SyncSession> session, SyncError error) { |
| 1293 | + auto logger = util::Logger::get_default_logger(); |
| 1294 | + logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path()); |
| 1295 | + // Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors |
| 1296 | + if (error.status.code() == ErrorCodes::SyncConnectFailed) { |
| 1297 | + REQUIRE_FALSE(error.is_fatal); |
| 1298 | + return; |
| 1299 | + } |
| 1300 | + // If it's not SyncConnectFailed, then it should be AuthError |
| 1301 | + REQUIRE(error.status.code() == ErrorCodes::AuthError); |
| 1302 | + REQUIRE(error.is_fatal); |
| 1303 | + }; |
| 1304 | + |
| 1305 | + transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> { |
| 1306 | + static constexpr int CURLE_OPERATION_TIMEDOUT = 28; |
| 1307 | + std::lock_guard<std::mutex> lock(mutex); |
| 1308 | + if (req.url.find("/auth/session") != std::string::npos) { |
| 1309 | + token_refresh_called = true; |
| 1310 | + if (failure == FailureMode::token_not_authorized) { |
| 1311 | + return app::Response{403, 0, {}, "403 not authorized"}; |
| 1312 | + } |
| 1313 | + if (failure == FailureMode::token_fails) { |
| 1314 | + return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"}; |
| 1315 | + } |
| 1316 | + } |
| 1317 | + else if (req.url.find("/location") != std::string::npos) { |
| 1318 | + location_refresh_called = true; |
| 1319 | + if (failure == FailureMode::location_fails) { |
| 1320 | + // Fake "offline/request timed out" custom error response |
| 1321 | + return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"}; |
| 1322 | + } |
| 1323 | + } |
| 1324 | + return std::nullopt; |
| 1325 | + }; |
| 1326 | + |
| 1327 | + socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> { |
| 1328 | + if (not_authorized) { |
| 1329 | + not_authorized = false; // one shot |
| 1330 | + return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, |
| 1331 | + "403 not authorized"); |
| 1332 | + } |
| 1333 | + return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, |
| 1334 | + "Operation timed out"); |
| 1335 | + }; |
| 1336 | + |
| 1337 | + the_app = app::App::get_app(app::App::CacheMode::Disabled, app_config); |
| 1338 | + SyncTestFile config(the_app->current_user(), "realm"); |
| 1339 | + config.sync_config->cancel_waits_on_nonfatal_error = true; |
| 1340 | + config.sync_config->error_handler = err_handler; |
| 1341 | + |
| 1342 | + // User should be logged in at this point |
| 1343 | + REQUIRE(config.sync_config->user->is_logged_in()); |
| 1344 | + |
| 1345 | + auto task = Realm::get_synchronized_realm(config); |
| 1346 | + auto pf = util::make_promise_future<std::exception_ptr>(); |
| 1347 | + task->start([&pf](auto ref, auto error) mutable { |
| 1348 | + REQUIRE(!ref); |
| 1349 | + REQUIRE(error); |
| 1350 | + pf.promise.emplace_value(error); |
| 1351 | + }); |
| 1352 | + |
| 1353 | + auto result = pf.future.get_no_throw(); |
| 1354 | + REQUIRE(result.is_ok()); |
| 1355 | + REQUIRE(result.get_value()); |
| 1356 | + std::lock_guard<std::mutex> lock(mutex); |
| 1357 | + REQUIRE(location_refresh_called); |
| 1358 | + if (failure != FailureMode::location_fails) { |
| 1359 | + REQUIRE(token_refresh_called); |
| 1360 | + } |
| 1361 | + } |
| 1362 | + |
| 1363 | +#endif // REALM_APP_SERVICES |
| 1364 | + |
1218 | 1365 | SECTION("read-only mode sets the schema version") { |
1219 | 1366 | { |
1220 | 1367 | SharedRealm realm = Realm::get_shared_realm(config); |
@@ -1348,6 +1495,109 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { |
1348 | 1495 | } |
1349 | 1496 | } |
1350 | 1497 |
|
| 1498 | +#if REALM_ENABLE_AUTH_TESTS |
| 1499 | + |
| 1500 | +TEST_CASE("Syhcnronized realm: AutoOpen", "[sync][baas][pbs][async open]") { |
| 1501 | + const auto partition = random_string(100); |
| 1502 | + auto schema = get_default_schema(); |
| 1503 | + enum TestMode { expired_at_start, expired_by_websocket, websocket_fails }; |
| 1504 | + enum FailureMode { location_fails, token_fails, token_not_authorized }; |
| 1505 | + |
| 1506 | + auto logger = util::Logger::get_default_logger(); |
| 1507 | + auto transport = std::make_shared<HookedTransport<>>(); |
| 1508 | + auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent"); |
| 1509 | + std::mutex mutex; |
| 1510 | + |
| 1511 | + // Create the app session and get the logged in user identity |
| 1512 | + auto server_app_config = minimal_app_config("autoopen-realm", schema); |
| 1513 | + TestAppSession session(create_app(server_app_config), transport, DeleteApp{true}, realm::ReconnectMode::normal, |
| 1514 | + socket_provider); |
| 1515 | + auto user = session.app()->current_user(); |
| 1516 | + std::string identity = user->user_id(); |
| 1517 | + REQUIRE(user->is_logged_in()); |
| 1518 | + REQUIRE(!identity.empty()); |
| 1519 | + // Reopen the App instance and retrieve the cached user |
| 1520 | + session.reopen(false); |
| 1521 | + user = session.app()->get_existing_logged_in_user(identity); |
| 1522 | + |
| 1523 | + SyncTestFile config(user, partition, schema); |
| 1524 | + config.sync_config->cancel_waits_on_nonfatal_error = true; |
| 1525 | + config.sync_config->error_handler = [&logger](std::shared_ptr<SyncSession> session, SyncError error) { |
| 1526 | + logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path()); |
| 1527 | + // Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors |
| 1528 | + if (error.status.code() == ErrorCodes::SyncConnectFailed) { |
| 1529 | + REQUIRE_FALSE(error.is_fatal); |
| 1530 | + return; |
| 1531 | + } |
| 1532 | + // If it's not SyncConnectFailed, then it should be AuthError |
| 1533 | + REQUIRE(error.status.code() == ErrorCodes::AuthError); |
| 1534 | + REQUIRE(error.is_fatal); |
| 1535 | + }; |
| 1536 | + |
| 1537 | + bool not_authorized = false; |
| 1538 | + bool token_refresh_called = false; |
| 1539 | + bool location_refresh_called = false; |
| 1540 | + |
| 1541 | + FailureMode failure = FailureMode::location_fails; |
| 1542 | + |
| 1543 | + transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> { |
| 1544 | + static constexpr int CURLE_OPERATION_TIMEDOUT = 28; |
| 1545 | + std::lock_guard<std::mutex> lock(mutex); |
| 1546 | + if (req.url.find("/auth/session") != std::string::npos) { |
| 1547 | + token_refresh_called = true; |
| 1548 | + if (failure == FailureMode::token_not_authorized) { |
| 1549 | + return app::Response{403, 0, {}, "403 not authorized"}; |
| 1550 | + } |
| 1551 | + if (failure == FailureMode::token_fails) { |
| 1552 | + return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"}; |
| 1553 | + } |
| 1554 | + } |
| 1555 | + else if (req.url.find("/location") != std::string::npos) { |
| 1556 | + location_refresh_called = true; |
| 1557 | + if (failure == FailureMode::location_fails) { |
| 1558 | + // Fake "offline/request timed out" custom error response |
| 1559 | + return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"}; |
| 1560 | + } |
| 1561 | + } |
| 1562 | + return std::nullopt; |
| 1563 | + }; |
| 1564 | + |
| 1565 | + socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> { |
| 1566 | + if (not_authorized) { |
| 1567 | + not_authorized = false; // one shot |
| 1568 | + return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, "403 not authorized"); |
| 1569 | + } |
| 1570 | + return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, |
| 1571 | + "Operation timed out"); |
| 1572 | + }; |
| 1573 | + |
| 1574 | + auto task = Realm::get_synchronized_realm(config); |
| 1575 | + auto pf = util::make_promise_future<std::exception_ptr>(); |
| 1576 | + task->start([&pf](auto ref, auto error) mutable { |
| 1577 | + REQUIRE(!ref); |
| 1578 | + REQUIRE(error); |
| 1579 | + pf.promise.emplace_value(error); |
| 1580 | + }); |
| 1581 | + |
| 1582 | + auto result = pf.future.get_no_throw(); |
| 1583 | + REQUIRE(result.is_ok()); |
| 1584 | + REQUIRE(result.get_value()); |
| 1585 | + { |
| 1586 | + std::lock_guard<std::mutex> lock(mutex); |
| 1587 | + REQUIRE(location_refresh_called); |
| 1588 | + if (failure != FailureMode::location_fails) { |
| 1589 | + REQUIRE(token_refresh_called); |
| 1590 | + } |
| 1591 | + } |
| 1592 | + |
| 1593 | + transport->request_hook = nullptr; |
| 1594 | + socket_provider->websocket_connect_func = nullptr; |
| 1595 | + auto r = Realm::get_shared_realm(config); |
| 1596 | + wait_for_download(*r); |
| 1597 | +} |
| 1598 | + |
| 1599 | +#endif // REALM_ENABLE_AUTH_TESTS |
| 1600 | + |
1351 | 1601 | TEST_CASE("SharedRealm: convert", "[sync][pbs][convert]") { |
1352 | 1602 | TestSyncManager tsm; |
1353 | 1603 | ObjectSchema object_schema = {"object", |
|
0 commit comments