-
Notifications
You must be signed in to change notification settings - Fork 169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RCORE-2060 Enabling 'cancel_waits_on_nonfatal_error' does not cancel waits during location update while offline #7528
Changes from 12 commits
2990263
b5d2dc5
c060daf
91a54f1
5650107
ea76f65
0776dd8
7298787
5ac829b
b3bb528
3bad95e
3c158a9
6e5f0a4
3f29c30
a6d55e9
1ef3639
52a1d54
669d09c
b83a346
b2e7406
5830b46
c1a25c0
dce8409
a34d5f6
2e0a68f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ | |
|
||
#if REALM_ENABLE_SYNC | ||
#include <util/sync/flx_sync_harness.hpp> | ||
#include <util/sync/sync_test_utils.hpp> | ||
|
||
#include <realm/object-store/sync/async_open_task.hpp> | ||
#include <realm/object-store/sync/impl/sync_metadata.hpp> | ||
|
@@ -1206,6 +1207,117 @@ TEST_CASE("Get Realm using Async Open", "[sync][pbs][async open]") { | |
REQUIRE(got_error); | ||
} | ||
|
||
SECTION("waiters are cancelled if cancel_waits_on_nonfatal_error") { | ||
auto logger = util::Logger::get_default_logger(); | ||
auto transport = std::make_shared<HookedUnitTestTransport>(); | ||
auto socket_provider = std::make_shared<HookedSocketProvider>(logger, "some user agent"); | ||
enum TestMode { location_fails, token_fails, token_not_authorized }; | ||
auto txt_test_mode = [](TestMode mode) { | ||
switch (mode) { | ||
case TestMode::location_fails: | ||
return "location_fails"; | ||
case TestMode::token_fails: | ||
return "token_fails"; | ||
case TestMode::token_not_authorized: | ||
return "token_not_authorized"; | ||
default: | ||
return "Unknown TestMode"; | ||
} | ||
}; | ||
|
||
OfflineAppSession::Config oas_config; | ||
oas_config.transport = transport; | ||
oas_config.socket_provider = socket_provider; | ||
OfflineAppSession oas(oas_config); | ||
|
||
SyncTestFile config(oas, "realm"); | ||
auto user = config.sync_config->user; | ||
config.sync_config->cancel_waits_on_nonfatal_error = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add a test (maybe already have one?) when this is false? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's needed... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, what I meant is to test that behavior and expectations too (in a separate test not as a section). But I think you added a test in the previous PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the tests added in the previous PR should test the normal cases; although I could add an additional test specifically for async open if you think that is necessary. |
||
config.sync_config->error_handler = [&logger](std::shared_ptr<SyncSession> session, SyncError error) { | ||
logger->debug("The sync error handler caught an error: '%1' for '%2'", error.status, session->path()); | ||
// Ignore connection failed non-fatal errors and check for access token refresh unauthorized fatal errors | ||
if (error.status.code() == ErrorCodes::SyncConnectFailed) { | ||
REQUIRE_FALSE(error.is_fatal); | ||
return; | ||
} | ||
if (error.status.code() == ErrorCodes::AuthError) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looking at the websocket replies, these are the only two errors allowed. you could just:
|
||
REQUIRE(error.is_fatal); | ||
} | ||
}; | ||
|
||
// User should be logged in at this point | ||
michael-wb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
REQUIRE(user->is_logged_in()); | ||
bool not_authorized = false; | ||
bool token_refresh_called = false; | ||
bool location_refresh_called = false; | ||
|
||
TestMode mode = GENERATE(location_fails, token_fails, token_not_authorized); | ||
|
||
SECTION(util::format("access token expired when realm is opened - mode: %1", txt_test_mode(mode))) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catch has DYNAMIC_SECTION for this: This pattern of doing configuration in the SECTION and then the shared test afterwards has the problem that it results in catch not being able to tell us which section failed, and relying on logging for that is awkward. I think the intended way to do this with Catch would be using GENERATE on a second enum and switching on that instead of using sections. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good to know 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @tgoyne - I updated the tests to use the |
||
logger->trace(">>> access token expired when realm is opened - mode: %1", txt_test_mode(mode)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: if this is mainly for CI, I don't think we run the tests with trace logs (could use debug) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These were originally in for debugging the tests, but I ended up removing them from the test. |
||
// invalidate the user's cached access token | ||
user->update_access_token(std::move(expired_token)); | ||
} | ||
SECTION(util::format("access token expired by websocket - mode: %1", txt_test_mode(mode))) { | ||
logger->trace(">>> access token expired by websocket - mode: %1", txt_test_mode(mode)); | ||
// tell websocket to return not authorized to refresh access token | ||
not_authorized = true; | ||
} | ||
SECTION(util::format("websocket returns connection failed - mode: %1", txt_test_mode(mode))) { | ||
logger->trace(">>> websocket returns connection failed - mode: %1", txt_test_mode(mode)); | ||
// default case | ||
} | ||
|
||
transport->request_hook = [&](const app::Request& req) -> std::optional<app::Response> { | ||
static constexpr int CURLE_OPERATION_TIMEDOUT = 28; | ||
std::lock_guard<std::mutex> lock(mutex); | ||
if (req.url.find("/auth/session") != std::string::npos) { | ||
token_refresh_called = true; | ||
if (mode == token_not_authorized) { | ||
return app::Response{403, 0, {}, "403 not authorized"}; | ||
} | ||
if (mode == token_fails) { | ||
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"}; | ||
} | ||
} | ||
else if (req.url.find("/location") != std::string::npos) { | ||
location_refresh_called = true; | ||
if (mode == location_fails) { | ||
// Fake "offline/request timed out" custom error response | ||
return app::Response{0, CURLE_OPERATION_TIMEDOUT, {}, "Operation timed out"}; | ||
} | ||
} | ||
return std::nullopt; | ||
}; | ||
|
||
socket_provider->websocket_connect_func = [&]() -> std::optional<SocketProviderError> { | ||
if (not_authorized) { | ||
not_authorized = false; // one shot | ||
return SocketProviderError(sync::websocket::WebSocketError::websocket_unauthorized, | ||
"403 not authorized"); | ||
} | ||
return SocketProviderError(sync::websocket::WebSocketError::websocket_connection_failed, | ||
"Operation timed out"); | ||
}; | ||
|
||
auto task = Realm::get_synchronized_realm(config); | ||
auto pf = util::make_promise_future<std::exception_ptr>(); | ||
task->start([&pf](auto ref, auto error) mutable { | ||
REQUIRE(!ref); | ||
REQUIRE(error); | ||
pf.promise.emplace_value(error); | ||
}); | ||
|
||
auto result = pf.future.get_no_throw(); | ||
REQUIRE(result.is_ok()); | ||
REQUIRE(result.get_value()); | ||
std::lock_guard<std::mutex> lock(mutex); | ||
REQUIRE(location_refresh_called); | ||
if (mode != TestMode::location_fails) { | ||
REQUIRE(token_refresh_called); | ||
} | ||
} | ||
|
||
SECTION("read-only mode sets the schema version") { | ||
{ | ||
SharedRealm realm = Realm::get_shared_realm(config); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't appear to be used anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was previously using this to get the exception thrown in the Async open start callback, but not anymore. Removed this unused code.