#include #include "csp/db/sqlite_db.h" #include "csp/services/achievement_service.h" #include "csp/services/auth_service.h" #include "csp/services/user_service.h" #include #include #include #include namespace { void CheckSqlite(int rc, sqlite3* db, const char* what) { if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return; throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db)); } int64_t InsertProblem(sqlite3* db, const std::string& slug, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO problems(slug,title,statement_md,difficulty,source,created_at) " "VALUES(?,?,?,?,?,?)"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert problem"); CheckSqlite(sqlite3_bind_text(stmt, 1, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind slug"); CheckSqlite(sqlite3_bind_text(stmt, 2, slug.c_str(), -1, SQLITE_TRANSIENT), db, "bind title"); CheckSqlite(sqlite3_bind_text(stmt, 3, "stub", -1, SQLITE_STATIC), db, "bind statement"); CheckSqlite(sqlite3_bind_int(stmt, 4, 1), db, "bind difficulty"); CheckSqlite(sqlite3_bind_text(stmt, 5, "test", -1, SQLITE_STATIC), db, "bind source"); CheckSqlite(sqlite3_bind_int64(stmt, 6, created_at), db, "bind created_at"); CheckSqlite(sqlite3_step(stmt), db, "insert problem"); sqlite3_finalize(stmt); return sqlite3_last_insert_rowid(db); } void InsertSubmission(sqlite3* db, int64_t user_id, int64_t problem_id, int64_t created_at) { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO submissions(user_id,problem_id,language,code,status,score,time_ms,memory_kb,created_at) " "VALUES(?,?,?,?,?,?,?,?,?)"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare insert submission"); CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id"); CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id"); CheckSqlite(sqlite3_bind_text(stmt, 3, "cpp", -1, SQLITE_STATIC), db, "bind language"); CheckSqlite(sqlite3_bind_text(stmt, 4, "int main(){return 0;}", -1, SQLITE_STATIC), db, "bind code"); CheckSqlite(sqlite3_bind_text(stmt, 5, "AC", -1, SQLITE_STATIC), db, "bind status"); CheckSqlite(sqlite3_bind_int(stmt, 6, 100), db, "bind score"); CheckSqlite(sqlite3_bind_int(stmt, 7, 1), db, "bind time"); CheckSqlite(sqlite3_bind_int(stmt, 8, 1), db, "bind memory"); CheckSqlite(sqlite3_bind_int64(stmt, 9, created_at), db, "bind created_at"); CheckSqlite(sqlite3_step(stmt), db, "insert submission"); sqlite3_finalize(stmt); } } // namespace TEST_CASE("achievement snapshot grants milestone bonus once per milestone") { auto db = csp::db::SqliteDb::OpenMemory(); csp::db::ApplyMigrations(db); csp::services::AuthService auth(db); const auto login = auth.Register("achievement_user", "password123"); csp::services::UserService users(db); csp::services::AchievementService achievements(db); sqlite3* raw = db.raw(); int64_t now = 1700000000; for (int i = 0; i < 40; ++i) { const auto pid = InsertProblem(raw, "ach-test-" + std::to_string(i + 1), now + i); InsertSubmission(raw, login.user_id, pid, now + i); } const auto before = users.GetById(login.user_id); REQUIRE(before.has_value()); REQUIRE(before->rating == 1); // register triggers one login_checkin reward const auto snap1 = achievements.GetSnapshot(login.user_id); REQUIRE(snap1.items.size() == 100); REQUIRE(snap1.summary.total_count == 100); REQUIRE(snap1.summary.completed_count >= 10); REQUIRE(snap1.summary.bonus_milestones_added_now >= 1); REQUIRE(snap1.summary.bonus_rating_added_now == snap1.summary.bonus_milestones_added_now * 20); const auto after_first = users.GetById(login.user_id); REQUIRE(after_first.has_value()); REQUIRE(after_first->rating == before->rating + snap1.summary.bonus_rating_added_now); const auto snap2 = achievements.GetSnapshot(login.user_id); REQUIRE(snap2.summary.bonus_milestones_added_now == 0); REQUIRE(snap2.summary.bonus_rating_added_now == 0); const auto after_second = users.GetById(login.user_id); REQUIRE(after_second.has_value()); REQUIRE(after_second->rating == after_first->rating); }