test(data): add push retry-on-401 integration test + server test pool helper
Adds push_retries_after_401_on_expired_access_token to sync_round_trip.rs, closing the push-side coverage gap alongside the existing pull test (jwt_refresh_on_401_succeeds). Both tests use an expired-but-validly-signed access token to trigger the 401 → refresh → retry path in SolitaireServerClient. Also exposes build_test_pool() from solitaire_server so downstream crates can boot a test server without duplicating the migration boilerplate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -418,3 +418,56 @@ async fn pull_after_account_deletion_returns_default_or_error() {
|
|||||||
|
|
||||||
let _ = delete_tokens(username);
|
let _ = delete_tokens(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **Push retry on 401.**
|
||||||
|
///
|
||||||
|
/// Mirrors `jwt_refresh_on_401_succeeds` but for the `push()` path.
|
||||||
|
/// We install an expired access token so the first push attempt returns 401,
|
||||||
|
/// the client refreshes, and the retry push succeeds.
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn push_retries_after_401_on_expired_access_token() {
|
||||||
|
ensure_mock_keyring();
|
||||||
|
|
||||||
|
let base = spawn_test_server().await;
|
||||||
|
let username = "rt_push_expiring";
|
||||||
|
|
||||||
|
let (_real_access, real_refresh) =
|
||||||
|
register_user_raw(&base, username, "pushexpirepass1!").await;
|
||||||
|
let user_id = decode_sub(&_real_access);
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct Claims {
|
||||||
|
sub: String,
|
||||||
|
exp: usize,
|
||||||
|
kind: String,
|
||||||
|
}
|
||||||
|
let exp = (Utc::now() - chrono::Duration::hours(2)).timestamp() as usize;
|
||||||
|
let expired_access = encode(
|
||||||
|
&Header::default(),
|
||||||
|
&Claims {
|
||||||
|
sub: user_id.clone(),
|
||||||
|
exp,
|
||||||
|
kind: "access".into(),
|
||||||
|
},
|
||||||
|
&EncodingKey::from_secret(TEST_SECRET.as_bytes()),
|
||||||
|
)
|
||||||
|
.expect("failed to encode expired access token");
|
||||||
|
|
||||||
|
store_tokens(username, &expired_access, &real_refresh)
|
||||||
|
.expect("storing tokens in mock keyring must succeed");
|
||||||
|
|
||||||
|
let client = SolitaireServerClient::new(&base, username);
|
||||||
|
let payload = make_payload(&user_id, 17);
|
||||||
|
|
||||||
|
// Push: server returns 401, client refreshes, retries, succeeds.
|
||||||
|
let push_resp = client
|
||||||
|
.push(&payload)
|
||||||
|
.await
|
||||||
|
.expect("push must succeed after the client transparently refreshes the access token");
|
||||||
|
assert_eq!(
|
||||||
|
push_resp.merged.stats.games_played, 17,
|
||||||
|
"merged games_played must reflect what was pushed after auto-refresh"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = delete_tokens(username);
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,6 +97,28 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
/// Construct the router without rate limiting.
|
/// Construct the router without rate limiting.
|
||||||
///
|
///
|
||||||
/// Intended for integration tests only — do not use in production.
|
/// Intended for integration tests only — do not use in production.
|
||||||
|
/// Create an in-memory SQLite pool and run all pending migrations.
|
||||||
|
///
|
||||||
|
/// `max_connections(1)` is required for SQLite in-memory databases: every
|
||||||
|
/// additional connection sees an empty schema.
|
||||||
|
///
|
||||||
|
/// Exposed so integration tests in other crates (e.g. `solitaire_data`) can
|
||||||
|
/// boot a real server without duplicating the migration boilerplate.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub async fn build_test_pool() -> SqlitePool {
|
||||||
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("failed to connect to in-memory SQLite database");
|
||||||
|
sqlx::migrate!("./migrations")
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.expect("failed to run database migrations");
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
|
||||||
/// Uses a fixed test JWT secret (`"test_secret_32_chars_minimum_ok!"`) so
|
/// Uses a fixed test JWT secret (`"test_secret_32_chars_minimum_ok!"`) so
|
||||||
/// integration tests do not need to set `JWT_SECRET` in the environment.
|
/// integration tests do not need to set `JWT_SECRET` in the environment.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|||||||
Reference in New Issue
Block a user