#include #include "csp/db/sqlite_db.h" #include "csp/services/auth_service.h" #include "csp/services/contest_service.h" #include "csp/services/season_service.h" #include "csp/services/user_service.h" #include TEST_CASE("season reward claim is idempotent and writes loot log") { auto db = csp::db::SqliteDb::OpenMemory(); csp::db::ApplyMigrations(db); csp::db::SeedDemoData(db); csp::services::AuthService auth(db); const auto login = auth.Register("season_user_1", "password123"); csp::services::SeasonService seasons(db); const auto season = seasons.GetCurrentSeason(); REQUIRE(season.has_value()); const auto tracks = seasons.ListRewardTracks(season->id); REQUIRE_FALSE(tracks.empty()); const auto target_track = tracks.back(); db.Exec("UPDATE users SET rating=200 WHERE id=" + std::to_string(login.user_id)); const auto before_progress = seasons.GetOrSyncUserProgress(season->id, login.user_id); REQUIRE(before_progress.xp >= target_track.required_xp); const auto first_claim = seasons.ClaimReward( season->id, login.user_id, target_track.tier_no, target_track.reward_type); REQUIRE(first_claim.claimed); REQUIRE(first_claim.claim.has_value()); REQUIRE(first_claim.rating_after >= 200 + target_track.reward_value); const auto second_claim = seasons.ClaimReward( season->id, login.user_id, target_track.tier_no, target_track.reward_type); REQUIRE_FALSE(second_claim.claimed); REQUIRE(second_claim.claim.has_value()); REQUIRE(second_claim.rating_after == first_claim.rating_after); const auto loot = seasons.ListLootDropsByUser(login.user_id, 20); REQUIRE_FALSE(loot.empty()); REQUIRE(loot.front().source_type == "season"); REQUIRE(loot.front().source_id == season->id); csp::services::UserService users(db); const auto user = users.GetById(login.user_id); REQUIRE(user.has_value()); REQUIRE(user->rating == first_claim.rating_after); } TEST_CASE("contest modifiers create update and filtered list") { auto db = csp::db::SqliteDb::OpenMemory(); csp::db::ApplyMigrations(db); csp::db::SeedDemoData(db); csp::services::ContestService contests(db); const auto contest_list = contests.ListContests(); REQUIRE_FALSE(contest_list.empty()); const int64_t contest_id = contest_list.front().id; csp::services::SeasonService seasons(db); csp::services::ContestModifierWrite create; create.code = "no_recursion"; create.title = "禁用递归"; create.description = "仅允许循环写法。"; create.rule_json = R"({"forbid":["recursion"]})"; create.is_active = true; const auto created = seasons.CreateContestModifier(contest_id, create); REQUIRE(created.id > 0); REQUIRE(created.contest_id == contest_id); REQUIRE(created.is_active); const auto active_list = seasons.ListContestModifiers(contest_id, false); bool found_created = false; for (const auto& one : active_list) { if (one.id == created.id) { found_created = true; break; } } REQUIRE(found_created); csp::services::ContestModifierPatch patch; patch.title = "禁用递归(更新)"; patch.is_active = false; const auto updated = seasons.UpdateContestModifier(contest_id, created.id, patch); REQUIRE(updated.title == "禁用递归(更新)"); REQUIRE_FALSE(updated.is_active); const auto active_after = seasons.ListContestModifiers(contest_id, false); bool still_active = false; for (const auto& one : active_after) { if (one.id == created.id) { still_active = true; break; } } REQUIRE_FALSE(still_active); const auto all_after = seasons.ListContestModifiers(contest_id, true); bool found_updated = false; for (const auto& one : all_after) { if (one.id == created.id && one.title == "禁用递归(更新)" && !one.is_active) { found_updated = true; break; } } REQUIRE(found_updated); }