#include #include "csp/app_state.h" #include "csp/controllers/lark_controller.h" #include "csp/services/crawler_service.h" #include "csp/services/lark_bot_service.h" #include #include #include #include #include namespace { class ScopedEnv { public: ScopedEnv(std::string key, std::optional value) : key_(std::move(key)) { const char* old = std::getenv(key_.c_str()); if (old) old_ = std::string(old); if (value.has_value()) { ::setenv(key_.c_str(), value->c_str(), 1); } else { ::unsetenv(key_.c_str()); } } ~ScopedEnv() { if (old_.has_value()) { ::setenv(key_.c_str(), old_->c_str(), 1); } else { ::unsetenv(key_.c_str()); } } private: std::string key_; std::optional old_; }; drogon::HttpResponsePtr CallEvents(csp::controllers::LarkController& ctl, const Json::Value& body) { auto req = drogon::HttpRequest::newHttpJsonRequest(body); req->setMethod(drogon::Post); std::promise p; ctl.events(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); }); return p.get_future().get(); } Json::Value MakeTextEventBody() { Json::Value body; body["header"]["event_type"] = "im.message.receive_v1"; body["header"]["event_id"] = "evt-1"; body["event"]["sender"]["sender_id"]["open_id"] = "ou_xxx"; body["event"]["message"]["message_type"] = "text"; body["event"]["message"]["message_id"] = "om_xxx"; body["event"]["message"]["chat_id"] = "oc_xxx"; Json::Value content; content["text"] = "你好"; Json::StreamWriterBuilder wb; wb["indentation"] = ""; body["event"]["message"]["content"] = Json::writeString(wb, content); return body; } } // namespace TEST_CASE("lark url verification challenge pass") { ScopedEnv enabled("CSP_LARK_BOT_ENABLED", "1"); ScopedEnv token("CSP_LARK_VERIFICATION_TOKEN", "verify_token"); ScopedEnv app_id("CSP_LARK_APP_ID", "cli_test"); ScopedEnv app_secret("CSP_LARK_APP_SECRET", "secret_test"); csp::services::LarkBotService::Instance().ConfigureFromEnv(); csp::controllers::LarkController ctl; Json::Value body; body["challenge"] = "challenge-abc"; body["token"] = "verify_token"; auto resp = CallEvents(ctl, body); REQUIRE(resp->statusCode() == drogon::k200OK); const auto json = resp->jsonObject(); REQUIRE(json != nullptr); REQUIRE((*json)["challenge"].asString() == "challenge-abc"); } TEST_CASE("lark url verification token mismatch") { ScopedEnv enabled("CSP_LARK_BOT_ENABLED", "1"); ScopedEnv token("CSP_LARK_VERIFICATION_TOKEN", "verify_token"); ScopedEnv app_id("CSP_LARK_APP_ID", "cli_test"); ScopedEnv app_secret("CSP_LARK_APP_SECRET", "secret_test"); csp::services::LarkBotService::Instance().ConfigureFromEnv(); csp::controllers::LarkController ctl; Json::Value body; body["challenge"] = "challenge-abc"; body["token"] = "bad_token"; auto resp = CallEvents(ctl, body); REQUIRE(resp->statusCode() == drogon::k401Unauthorized); } TEST_CASE("lark events ignored when bot disabled") { ScopedEnv enabled("CSP_LARK_BOT_ENABLED", "0"); ScopedEnv token("CSP_LARK_VERIFICATION_TOKEN", std::nullopt); ScopedEnv app_id("CSP_LARK_APP_ID", std::nullopt); ScopedEnv app_secret("CSP_LARK_APP_SECRET", std::nullopt); csp::services::LarkBotService::Instance().ConfigureFromEnv(); csp::controllers::LarkController ctl; auto resp = CallEvents(ctl, MakeTextEventBody()); REQUIRE(resp->statusCode() == drogon::k200OK); const auto json = resp->jsonObject(); REQUIRE(json != nullptr); REQUIRE((*json)["code"].asInt() == 0); } TEST_CASE("lark text url queued into crawler targets") { csp::AppState::Instance().Init(":memory:"); ScopedEnv enabled("CSP_LARK_BOT_ENABLED", "1"); ScopedEnv token("CSP_LARK_VERIFICATION_TOKEN", std::nullopt); ScopedEnv app_id("CSP_LARK_APP_ID", "cli_test"); ScopedEnv app_secret("CSP_LARK_APP_SECRET", "secret_test"); ScopedEnv open_base("CSP_LARK_OPEN_BASE_URL", "invalid-url"); csp::services::LarkBotService::Instance().ConfigureFromEnv(); csp::controllers::LarkController ctl; auto body = MakeTextEventBody(); Json::Value content; content["text"] = "请收录 https://one.hao.work/news/?a=1"; Json::StreamWriterBuilder wb; wb["indentation"] = ""; body["event"]["message"]["content"] = Json::writeString(wb, content); auto resp = CallEvents(ctl, body); REQUIRE(resp->statusCode() == drogon::k200OK); const auto json = resp->jsonObject(); REQUIRE(json != nullptr); REQUIRE((*json)["code"].asInt() == 0); REQUIRE((*json)["msg"].asString() == "crawler targets queued"); csp::services::CrawlerService crawler(csp::AppState::Instance().db()); const auto targets = crawler.ListTargets("", 10); REQUIRE(targets.size() == 1); REQUIRE(targets[0].normalized_url == "https://one.hao.work/news"); }