Compare commits

...

3 Commits

Author SHA1 Message Date
funman300 566b112d9e docs(handoff): mark WASM script + 401-retry test complete; update HEAD state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:04:49 -07:00
funman300 198df75f94 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>
2026-05-12 14:04:26 -07:00
funman300 40d07122ba docs(wasm): add build_wasm.sh to document wasm-pack invocation
Captures the exact wasm-pack build command needed to regenerate
solitaire_server/web/pkg/. Removes wasm-pack's package.json and
.gitignore artifacts from the output dir since we manage it directly.
Includes a dependency guard and install instructions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:00:13 -07:00
4 changed files with 125 additions and 8 deletions
+10 -8
View File
@@ -1,6 +1,6 @@
# Solitaire Quest — Session Handoff
**Last updated:** 2026-05-12 — Sync rate limiting + mirror_achievement removal + theme import scan shipped (`6e6f3ef`). HEAD locally: `6e6f3ef`. Push pending.
**Last updated:** 2026-05-12 — WASM build script + push-retry test shipped (`198df75`). HEAD locally: `198df75`. Push pending.
Phase 8 closes the self-hosted-server connection arc end-to-end: login/register
modal, re-auth on token expiry, account deletion flow, server deployment
@@ -12,8 +12,8 @@ and full server integration tests.
## Current state
- **HEAD locally:** `6e6f3ef` (feat: sync rate limiting).
- **HEAD on origin:** `b129664` (pushed — 4 commits ahead).
- **HEAD locally:** `198df75` (test: push retry + build_test_pool).
- **HEAD on origin:** `08f74d1` (pushed — 3 commits ahead).
- **Working tree:** `SESSION_HANDOFF.md` modified, uncommitted.
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
- **Tests:** **1300+ passing / 0 failing** across the workspace.
@@ -83,16 +83,18 @@ Also shipped (pre-Phase 8 but post-v0.22.0, already in CHANGELOG):
- [x] **`mirror_achievement` removed.** Done (`549a817`): method was a no-op
default never overridden and never called; achievements already sync via
`SyncPayload` push. Deleted from trait and blanket impl.
- **WASM build script.** `web/pkg/` contains compiled WASM committed to git.
Need a `build_wasm.sh` or Makefile target documenting the `wasm-pack build`
invocation to regenerate it.
- [x] **WASM build script.** Done (`40d0712`): `build_wasm.sh` at repo root
documents `wasm-pack build --target web`, cleans up pkg metadata files,
includes dependency guard + install instructions.
- **Server password reset.** No admin endpoint or CLI tool for resetting a
user's password. Self-hosters have no recovery path short of direct SQLite
edits.
### 6. Testing gaps
- **Server 401 → refresh → retry path** — the `pull`/`push` retry logic in
`SolitaireServerClient` has no integration test.
- [x] **Server 401 → refresh → retry path.** Done (`198df75`): both
`jwt_refresh_on_401_succeeds` (pull) and
`push_retries_after_401_on_expired_access_token` (push) in
`solitaire_data/tests/sync_round_trip.rs`.
- **WASM winning-replay step-through** — current tests cover 2 stock clicks;
a test stepping through a full winning sequence would catch
`GameState`/`ReplayMove` compatibility regressions.
Executable
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Rebuild the solitaire_wasm crate and install the output into
# solitaire_server/web/pkg/ so the server can serve the replay viewer.
#
# Prerequisites:
# cargo install wasm-pack
# rustup target add wasm32-unknown-unknown
#
# Run from the repo root:
# ./build_wasm.sh
#
# The generated files (solitaire_wasm.js + solitaire_wasm_bg.wasm) are
# committed to git so self-hosters who don't touch the WASM crate can
# skip this step. Regenerate after any change to solitaire_wasm/ or
# solitaire_core/.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="$REPO_ROOT/solitaire_server/web/pkg"
if ! command -v wasm-pack &> /dev/null; then
echo "error: wasm-pack not found." >&2
echo " Install with: cargo install wasm-pack" >&2
exit 1
fi
echo "Building solitaire_wasm (target: web)..."
wasm-pack build \
--target web \
--out-dir "$OUT_DIR" \
--no-typescript \
"$REPO_ROOT/solitaire_wasm"
# wasm-pack writes a package.json and .gitignore into the output dir.
# Remove them — we manage the output directory ourselves.
rm -f "$OUT_DIR/package.json" "$OUT_DIR/.gitignore"
echo "Done. Output:"
ls -lh "$OUT_DIR"
+53
View File
@@ -418,3 +418,56 @@ async fn pull_after_account_deletion_returns_default_or_error() {
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);
}
+22
View File
@@ -97,6 +97,28 @@ pub fn build_router(state: AppState) -> Router {
/// Construct the router without rate limiting.
///
/// 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
/// integration tests do not need to set `JWT_SECRET` in the environment.
#[doc(hidden)]