feat: Phase 5 — events system complete
CI / Build, lint & test (push) Successful in 2m5s

Adds a data-driven event system (#47 schema, #48 activation, #49 TOTW):

- EventDefinition JSON schema (event_type, effects, date range, is_manual)
- data/events/totw_week1.json — TOTW event active by date range (2024–2030),
  injects 5 TOTW cards into the market as [EVENT] listings
- data/events/seasonal_events.json — Spring Festival and Icon Weekend,
  manual-activation-only example events
- migrations/0004_events.sql — events override table (NULL/1/0 per event)
- services/event — load_event_definitions, compute_is_active (date range +
  manual override), get_active_events, get_all_with_status, activate/deactivate
- routes/events — GET /events, GET /events/:id, POST .../activate, .../deactivate
- services/market::refresh_npc_listings now accepts event_defs, injects bonus
  market cards from active events at 2× premium price
- 5 new integration tests (events list, single, activate/deactivate cycle,
  404 on unknown, market injection verification); 19/19 passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 16:02:45 -07:00
parent f6dd41fd41
commit 1ef2c436ab
13 changed files with 522 additions and 21 deletions
+100
View File
@@ -382,3 +382,103 @@ async fn test_win_streak_tracking() {
assert_eq!(stats["win_streak"], 3);
assert_eq!(stats["best_win_streak"], 3);
}
// ── Phase 5: Events ──────────────────────────────────────────────────────────
#[tokio::test]
async fn test_events_list() {
let app = build_test_app().await;
let (status, json) = json_get(&app, "/events").await;
assert_eq!(status, StatusCode::OK);
assert!(json["events"].is_array());
// TOTW Week 1 is date-range active (20242030)
let active = json["active_count"].as_u64().unwrap_or(0);
assert!(active >= 1, "expected at least 1 active event (totw_week1)");
}
#[tokio::test]
async fn test_event_get_single() {
let app = build_test_app().await;
let (status, json) = json_get(&app, "/events/totw_week1").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["event"]["id"], "totw_week1");
assert_eq!(json["is_active"], true);
}
#[tokio::test]
async fn test_event_activate_deactivate() {
let app = build_test_app().await;
// spring_festival is manual-only — should start inactive
let (s, json) = json_get(&app, "/events/spring_festival").await;
assert_eq!(s, StatusCode::OK);
assert_eq!(json["is_active"], false);
// Activate it
let (s, json) = json_post(
&app,
"/events/spring_festival/activate",
serde_json::json!({}),
)
.await;
assert_eq!(s, StatusCode::OK);
assert_eq!(json["is_active"], true);
// Confirm it's now active
let (s, json) = json_get(&app, "/events/spring_festival").await;
assert_eq!(s, StatusCode::OK);
assert_eq!(json["is_active"], true);
// Deactivate it
let (s, json) = json_post(
&app,
"/events/spring_festival/deactivate",
serde_json::json!({}),
)
.await;
assert_eq!(s, StatusCode::OK);
assert_eq!(json["is_active"], false);
// Confirm inactive again
let (s, json) = json_get(&app, "/events/spring_festival").await;
assert_eq!(s, StatusCode::OK);
assert_eq!(json["is_active"], false);
}
#[tokio::test]
async fn test_event_unknown_returns_404() {
let app = build_test_app().await;
let (status, _) = json_get(&app, "/events/no_such_event").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_event_market_refresh_injects_totw_cards() {
let app = build_test_app().await;
auth(&app, "EventMarketPlayer").await;
// Trigger a market refresh (TOTW event is active by date range)
let (s, json) = json_post(&app, "/market/refresh", serde_json::json!({})).await;
assert_eq!(s, StatusCode::OK);
// totw_week1 has 5 bonus cards + up to 20 NPC listings
let count = json["listings_generated"].as_u64().unwrap_or(0);
assert!(
count >= 5,
"expected at least 5 listings (event bonus cards)"
);
// Check market contains at least one [EVENT] listing
let (s, market) = json_get(&app, "/market").await;
assert_eq!(s, StatusCode::OK);
let listings = market["listings"].as_array().expect("listings array");
let event_listing = listings.iter().any(|l| {
l["seller_name"]
.as_str()
.map(|n| n.starts_with("[EVENT]"))
.unwrap_or(false)
});
assert!(
event_listing,
"expected at least one [EVENT] market listing"
);
}