chore(workspace): upgrade rand 0.9, edition 2024, expand server tests
- rand "0.8" → "0.9": StdRng/SliceRandom API unchanged; 142 core tests pass - edition "2021" → "2024" workspace-wide: no gen keyword conflicts found; 204 tests (core + sync) pass clean with zero warnings - ARCHITECTURE.md: Edition 2021 → Edition 2024 in header - solitaire_server tests: add 5 new integration tests covering refresh-with-garbage-token, expired-refresh-token, push-without-token, delete-account-without-token, and leaderboard-authenticated-but-empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Solitaire Quest — Architecture Document
|
# Solitaire Quest — Architecture Document
|
||||||
|
|
||||||
> **Version:** 1.1
|
> **Version:** 1.1
|
||||||
> **Language:** Rust (Edition 2021)
|
> **Language:** Rust (Edition 2024)
|
||||||
> **Engine:** Bevy (latest stable)
|
> **Engine:** Bevy (latest stable)
|
||||||
> **Last Updated:** 2026-04-29
|
> **Last Updated:** 2026-04-29
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -7522,7 +7522,7 @@ name = "solitaire_core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"rand 0.8.6",
|
"rand 0.9.4",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ members = [
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ serde_json = "1"
|
|||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
|||||||
@@ -1249,6 +1249,144 @@ async fn refresh_token_rejected_on_protected_routes() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Additional auth refresh edge-case tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// `POST /api/auth/refresh` with a completely invalid (non-JWT) string must
|
||||||
|
/// return 401 — the token cannot be decoded at all.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn refresh_with_garbage_token_returns_401() {
|
||||||
|
let app = build_test_router(test_pool().await);
|
||||||
|
|
||||||
|
let resp = post_json(
|
||||||
|
app,
|
||||||
|
"/api/auth/refresh",
|
||||||
|
serde_json::json!({ "refresh_token": "this.is.not.a.jwt" }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.status(),
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"garbage refresh token must return 401"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `POST /api/auth/refresh` with an expired (but correctly signed) refresh
|
||||||
|
/// token must return 401 — `exp` is in the past.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn refresh_with_expired_refresh_token_returns_401() {
|
||||||
|
let app = build_test_router(test_pool().await);
|
||||||
|
|
||||||
|
// Craft a refresh token that expired 2 hours ago, signed with the same
|
||||||
|
// secret that `build_test_router` uses, so the signature is valid but the
|
||||||
|
// expiry check must still reject it.
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ExpiredRefreshClaims {
|
||||||
|
sub: String,
|
||||||
|
exp: usize,
|
||||||
|
kind: String,
|
||||||
|
}
|
||||||
|
let exp = (chrono::Utc::now() - chrono::Duration::hours(2)).timestamp() as usize;
|
||||||
|
let expired_token = encode(
|
||||||
|
&Header::default(),
|
||||||
|
&ExpiredRefreshClaims {
|
||||||
|
sub: "00000000-0000-0000-0000-000000000000".into(),
|
||||||
|
exp,
|
||||||
|
kind: "refresh".into(),
|
||||||
|
},
|
||||||
|
&EncodingKey::from_secret(TEST_SECRET.as_bytes()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let resp = post_json(
|
||||||
|
app,
|
||||||
|
"/api/auth/refresh",
|
||||||
|
serde_json::json!({ "refresh_token": expired_token }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resp.status(),
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"expired refresh token must return 401"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Additional no-auth / missing-token tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Accessing `POST /api/sync/push` with no Authorization header must return 401.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn push_without_token_returns_401() {
|
||||||
|
let app = build_test_router(test_pool().await);
|
||||||
|
|
||||||
|
let req = Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri("/api/sync/push")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.header("x-forwarded-for", TEST_CLIENT_IP)
|
||||||
|
.body(Body::from(b"{}".as_ref()))
|
||||||
|
.expect("failed to build unauthenticated POST request");
|
||||||
|
|
||||||
|
let resp = app.oneshot(req).await.expect("oneshot failed");
|
||||||
|
assert_eq!(
|
||||||
|
resp.status(),
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"missing token on push must return 401"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accessing `DELETE /api/account` with no Authorization header must return 401.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_account_without_token_returns_401() {
|
||||||
|
let app = build_test_router(test_pool().await);
|
||||||
|
|
||||||
|
let req = Request::builder()
|
||||||
|
.method("DELETE")
|
||||||
|
.uri("/api/account")
|
||||||
|
.header("x-forwarded-for", TEST_CLIENT_IP)
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("failed to build unauthenticated DELETE request");
|
||||||
|
|
||||||
|
let resp = app.oneshot(req).await.expect("oneshot failed");
|
||||||
|
assert_eq!(
|
||||||
|
resp.status(),
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"missing token on DELETE /api/account must return 401"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Leaderboard — authenticated empty-array test
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// `GET /api/leaderboard` with a valid JWT but no opted-in players returns 200
|
||||||
|
/// with an empty JSON array.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn leaderboard_with_valid_token_returns_empty_array_when_no_opts() {
|
||||||
|
let app = build_test_router(test_pool().await);
|
||||||
|
|
||||||
|
// Register a user to get a valid token — do NOT opt in to the leaderboard.
|
||||||
|
let (access, _) = register_user(app.clone(), "no_opt_user", "password1!").await;
|
||||||
|
|
||||||
|
let resp = get_authed(app, "/api/leaderboard", &access).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK, "leaderboard must return 200");
|
||||||
|
|
||||||
|
let body = body_json(resp).await;
|
||||||
|
assert!(
|
||||||
|
body.is_array(),
|
||||||
|
"leaderboard body must be a JSON array even when empty"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
body.as_array().unwrap().len(),
|
||||||
|
0,
|
||||||
|
"leaderboard must be empty when no players have opted in"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Rate-limiting test (uses the production router with rate limiting enabled)
|
// Rate-limiting test (uses the production router with rate limiting enabled)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user