New notifications table (migration 0009) stores event-driven alerts alongside the existing dynamic state notifications (unclaimed objectives, expiring loans, season ending soon). Persistent notifications are created automatically during match processing: one per level gained, one per expired loan card, one per objective newly completed, and one when a season ends (with promotion/relegation result and rewards in the body). GET /notifications now returns a merged list — persistent entries (newest-first, limit 50) followed by dynamic entries — plus an unread_count for the badge. Each item carries type, title, body, is_read, and (for persistent) id and created_at. PATCH /notifications/:id/read marks a single persistent notification read. POST /notifications/read-all marks all persistent ones read. Four new tests: unread_count field, level-up notification creation, mark-all-read, single-read PATCH. Core now at 77 tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1513,3 +1513,83 @@ async fn test_level_up_grants_milestone_pack_at_level_5() {
|
||||
assert_eq!(pack_for_level(10), Some("silver_pack"));
|
||||
assert_eq!(pack_for_level(7), None);
|
||||
}
|
||||
|
||||
// ── Phase 19: Persistent notifications ───────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_notifications_have_unread_count() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "NotifCountPlayer").await;
|
||||
|
||||
let (status, json) = json_get(&app, "/notifications").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert!(json["unread_count"].is_number(), "unread_count missing");
|
||||
assert!(json["notifications"].is_array(), "notifications not an array");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_level_up_creates_persistent_notification() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "LevelUpNotifPlayer").await;
|
||||
|
||||
// Play several wins to guarantee crossing the 500 XP threshold (level 2)
|
||||
for _ in 0..5 {
|
||||
json_post(&app, "/matches/result", serde_json::json!({
|
||||
"squad_id": "dummy", "opponent_name": "Bot",
|
||||
"goals_for": 3, "goals_against": 0, "mode": "squad_battles"
|
||||
})).await;
|
||||
}
|
||||
|
||||
let (status, json) = json_get(&app, "/notifications").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
let notifs = json["notifications"].as_array().unwrap();
|
||||
let has_level_up = notifs.iter().any(|n| n["type"] == "level_up");
|
||||
assert!(has_level_up, "level_up notification not found after gaining levels");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_all_notifications_read() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "MarkReadPlayer").await;
|
||||
|
||||
// Generate a notification via level-up
|
||||
for _ in 0..5 {
|
||||
json_post(&app, "/matches/result", serde_json::json!({
|
||||
"squad_id": "dummy", "opponent_name": "Bot",
|
||||
"goals_for": 3, "goals_against": 0, "mode": "squad_battles"
|
||||
})).await;
|
||||
}
|
||||
|
||||
// Mark all persistent notifications read
|
||||
let (status, json) = json_post(&app, "/notifications/read-all", serde_json::json!({})).await;
|
||||
assert_eq!(status, StatusCode::OK, "{json}");
|
||||
assert!(json["marked_read"].as_i64().unwrap_or(0) >= 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_single_notification_read() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "SingleReadPlayer").await;
|
||||
|
||||
// Generate level-up notifications
|
||||
for _ in 0..5 {
|
||||
json_post(&app, "/matches/result", serde_json::json!({
|
||||
"squad_id": "dummy", "opponent_name": "Bot",
|
||||
"goals_for": 3, "goals_against": 0, "mode": "squad_battles"
|
||||
})).await;
|
||||
}
|
||||
|
||||
let (_, json) = json_get(&app, "/notifications").await;
|
||||
let notifs = json["notifications"].as_array().unwrap();
|
||||
// Find one with an id (persistent) and PATCH it
|
||||
let persistent = notifs.iter().find(|n| n["id"].is_string());
|
||||
if let Some(n) = persistent {
|
||||
let id = n["id"].as_str().unwrap();
|
||||
let uri = format!("/notifications/{id}/read");
|
||||
let resp = app.clone().oneshot(
|
||||
Request::builder().method("PATCH").uri(&uri)
|
||||
.body(Body::empty()).unwrap()
|
||||
).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user