#include #include "csp/app_state.h" #include "csp/controllers/me_controller.h" #include "csp/services/auth_service.h" #include "csp/services/daily_task_service.h" #include "csp/services/problem_service.h" #include "csp/services/user_service.h" #include #include #include namespace { drogon::HttpResponsePtr CallProfile(csp::controllers::MeController& ctl, const std::string& token) { auto req = drogon::HttpRequest::newHttpRequest(); req->setMethod(drogon::Get); req->addHeader("Authorization", "Bearer " + token); std::promise p; ctl.profile(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }); return p.get_future().get(); } drogon::HttpResponsePtr CallProfileWithAuthHeader( csp::controllers::MeController& ctl, const std::string& auth_header) { auto req = drogon::HttpRequest::newHttpRequest(); req->setMethod(drogon::Get); req->addHeader("Authorization", auth_header); std::promise p; ctl.profile(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }); return p.get_future().get(); } drogon::HttpResponsePtr CallListWrongBook(csp::controllers::MeController& ctl, const std::string& token) { auto req = drogon::HttpRequest::newHttpRequest(); req->setMethod(drogon::Get); req->addHeader("Authorization", "Bearer " + token); std::promise p; ctl.listWrongBook(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }); return p.get_future().get(); } drogon::HttpResponsePtr CallPatchWrongBook(csp::controllers::MeController& ctl, const std::string& token, int64_t problem_id, const std::string& note) { Json::Value body; body["note"] = note; auto req = drogon::HttpRequest::newHttpJsonRequest(body); req->setMethod(drogon::Patch); req->addHeader("Authorization", "Bearer " + token); std::promise p; ctl.upsertWrongBookNote(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }, problem_id); return p.get_future().get(); } drogon::HttpResponsePtr CallDeleteWrongBook(csp::controllers::MeController& ctl, const std::string& token, int64_t problem_id) { auto req = drogon::HttpRequest::newHttpRequest(); req->setMethod(drogon::Delete); req->addHeader("Authorization", "Bearer " + token); std::promise p; ctl.deleteWrongBookItem(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }, problem_id); return p.get_future().get(); } drogon::HttpResponsePtr CallScoreWrongBook(csp::controllers::MeController& ctl, const std::string& token, int64_t problem_id, const std::string& note) { Json::Value body; body["note"] = note; auto req = drogon::HttpRequest::newHttpJsonRequest(body); req->setMethod(drogon::Post); req->addHeader("Authorization", "Bearer " + token); std::promise p; ctl.scoreWrongBookNote(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }, problem_id); return p.get_future().get(); } int QueryDailyTaskCount(csp::db::SqliteDb& db, int64_t user_id, const std::string& task_code, const std::string& day_key) { sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT COUNT(1) FROM daily_task_logs WHERE user_id=? AND task_code=? AND day_key=?"; REQUIRE(sqlite3_prepare_v2(db.raw(), sql, -1, &stmt, nullptr) == SQLITE_OK); REQUIRE(sqlite3_bind_int64(stmt, 1, user_id) == SQLITE_OK); REQUIRE(sqlite3_bind_text(stmt, 2, task_code.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK); REQUIRE(sqlite3_bind_text(stmt, 3, day_key.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK); REQUIRE(sqlite3_step(stmt) == SQLITE_ROW); const int count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return count; } } // namespace TEST_CASE("me controller profile and wrong-book") { csp::AppState::Instance().Init(":memory:"); csp::services::AuthService auth(csp::AppState::Instance().db()); const auto login = auth.Register("me_http_user", "password123"); csp::services::ProblemService problems(csp::AppState::Instance().db()); const auto list = problems.List(csp::services::ProblemQuery{}); REQUIRE_FALSE(list.items.empty()); const int64_t problem_id = list.items.front().id; csp::controllers::MeController ctl; auto profile = CallProfile(ctl, login.token); REQUIRE(profile->statusCode() == drogon::k200OK); auto profile_json = profile->jsonObject(); REQUIRE(profile_json != nullptr); REQUIRE((*profile_json)["ok"].asBool()); auto profile_basic = CallProfileWithAuthHeader(ctl, "Basic bWVfaHR0cF91c2VyOnBhc3N3b3JkMTIz"); REQUIRE(profile_basic->statusCode() == drogon::k200OK); auto patch = CallPatchWrongBook(ctl, login.token, problem_id, "复盘记录"); REQUIRE(patch->statusCode() == drogon::k200OK); auto score = CallScoreWrongBook(ctl, login.token, problem_id, "题意+思路+踩坑总结"); REQUIRE(score->statusCode() == drogon::k200OK); const auto score_json = score->jsonObject(); REQUIRE(score_json != nullptr); REQUIRE((*score_json)["ok"].asBool()); REQUIRE((*score_json)["data"].isObject()); REQUIRE((*score_json)["data"]["note_score"].asInt() >= 0); REQUIRE((*score_json)["data"]["note_score"].asInt() <= 60); REQUIRE((*score_json)["data"]["note_rating"].asInt() >= 0); REQUIRE((*score_json)["data"]["note_rating"].asInt() <= 6); auto list_resp = CallListWrongBook(ctl, login.token); REQUIRE(list_resp->statusCode() == drogon::k200OK); auto list_json = list_resp->jsonObject(); REQUIRE(list_json != nullptr); REQUIRE((*list_json)["data"].isArray()); REQUIRE((*list_json)["data"].size() >= 1); auto del = CallDeleteWrongBook(ctl, login.token, problem_id); REQUIRE(del->statusCode() == drogon::k200OK); } TEST_CASE("me profile auto recovers daily login checkin for stale session") { csp::AppState::Instance().Init(":memory:"); csp::services::AuthService auth(csp::AppState::Instance().db()); const auto login = auth.Register("me_daily_user", "password123"); csp::controllers::MeController ctl; csp::services::DailyTaskService daily(csp::AppState::Instance().db()); csp::services::UserService users(csp::AppState::Instance().db()); const auto day_key = daily.CurrentDayKey(); // Simulate a stale session user whose today's login_checkin wasn't recorded. { sqlite3* db = csp::AppState::Instance().db().raw(); sqlite3_stmt* del = nullptr; const char* del_sql = "DELETE FROM daily_task_logs WHERE user_id=? AND task_code=? AND day_key=?"; REQUIRE(sqlite3_prepare_v2(db, del_sql, -1, &del, nullptr) == SQLITE_OK); REQUIRE(sqlite3_bind_int64(del, 1, login.user_id) == SQLITE_OK); REQUIRE(sqlite3_bind_text( del, 2, csp::services::DailyTaskService::kTaskLoginCheckin, -1, SQLITE_STATIC) == SQLITE_OK); REQUIRE(sqlite3_bind_text(del, 3, day_key.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK); REQUIRE(sqlite3_step(del) == SQLITE_DONE); sqlite3_finalize(del); // Keep rating consistent with removed daily task log. csp::AppState::Instance().db().Exec("UPDATE users SET rating=rating-1 WHERE id=" + std::to_string(login.user_id)); } REQUIRE(QueryDailyTaskCount(csp::AppState::Instance().db(), login.user_id, csp::services::DailyTaskService::kTaskLoginCheckin, day_key) == 0); auto profile = CallProfile(ctl, login.token); REQUIRE(profile->statusCode() == drogon::k200OK); const auto user = users.GetById(login.user_id); REQUIRE(user.has_value()); REQUIRE(user->rating == 1); REQUIRE(QueryDailyTaskCount(csp::AppState::Instance().db(), login.user_id, csp::services::DailyTaskService::kTaskLoginCheckin, day_key) == 1); }