feat(leaderboard): opt-out support — server endpoint, client method, UI button

- Server: DELETE /api/leaderboard/opt-in sets leaderboard_opt_in=0,
  hiding the player without deleting their row (scores preserved for re-opt-in)
- SyncProvider trait: opt_out_leaderboard() default no-op method + blanket impl
- SolitaireServerClient: implements opt_out_leaderboard via DELETE request with JWT refresh
- Leaderboard UI: "Opt Out" button (dark red) alongside existing "Opt In" button
- Server integration test: opt-out hides, opt-in restores (round-trip verified)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-27 02:01:20 +00:00
parent 15b9b5477b
commit 648c5c18d9
6 changed files with 205 additions and 21 deletions
+55
View File
@@ -776,3 +776,58 @@ async fn push_lower_score_does_not_overwrite_leaderboard_best() {
assert_eq!(entry["best_score"], 5_000, "best_score must not regress");
assert_eq!(entry["best_time_secs"], 120, "best_time_secs must stay at fastest");
}
/// Opting out hides the player from the leaderboard; opting back in restores them.
#[tokio::test]
async fn opt_out_hides_then_opt_in_restores() {
set_jwt_secret();
let pool = test_pool().await;
let app = build_test_router(pool);
let (access, _) = register_user(app.clone(), "visible", "pass1234").await;
// Opt in.
let resp = post_authed(
app.clone(),
"/api/leaderboard/opt-in",
&access,
serde_json::json!({ "display_name": "Visible" }),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
// Verify they appear.
let lb = get_authed(app.clone(), "/api/leaderboard", &access).await;
let entries = body_json(lb).await;
assert!(
entries.as_array().unwrap().iter().any(|e| e["display_name"] == "Visible"),
"opted-in user must appear"
);
// Opt out.
let resp = delete_authed(app.clone(), "/api/leaderboard/opt-in", &access).await;
assert_eq!(resp.status(), StatusCode::OK);
// Verify they are hidden.
let lb = get_authed(app.clone(), "/api/leaderboard", &access).await;
let entries = body_json(lb).await;
assert!(
!entries.as_array().unwrap().iter().any(|e| e["display_name"] == "Visible"),
"opted-out user must be hidden"
);
// Opt back in — should restore without losing display name.
post_authed(
app.clone(),
"/api/leaderboard/opt-in",
&access,
serde_json::json!({ "display_name": "Visible" }),
)
.await;
let lb = get_authed(app.clone(), "/api/leaderboard", &access).await;
let entries = body_json(lb).await;
assert!(
entries.as_array().unwrap().iter().any(|e| e["display_name"] == "Visible"),
"re-opted-in user must appear again"
);
}