// The CLI's Client against a real in-process daemon RPC server. #include #include #include #include #include #include "check.hpp" #include "client.hpp" #include "rpc/dispatcher.hpp" #include "rpc/event_loop.hpp" #include "rpc/event_hub.hpp" #include "rpc/uds_server.hpp" #include "store/migrations.hpp" #include "store/settings.hpp" #include "store/sqlite.hpp" namespace rpc = velox::daemon::rpc; static std::string g_allowed_root; using velox::cli::CallError; using velox::cli::Client; void run() { // connect() with no daemon -> a kConnect error, not a crash. { ::setenv("XDG_RUNTIME_DIR", "/tmp/velox-cli-test-nonexistent-xyz", 1); Client c; auto err = c.connect(); CHECK(err.has_value()); if (err) CHECK_EQ(err->code, CallError::kConnect); } // Point XDG_RUNTIME_DIR at a fresh temp dir; the client derives // /velox/velox.sock and the server binds exactly that. char tmpl[] = "/tmp/velox-cli-test-XXXXXX"; const char* xdg = ::mkdtemp(tmpl); CHECK(xdg != nullptr); if (xdg == nullptr) return; ::setenv("XDG_RUNTIME_DIR", xdg, 1); const std::string velox_dir = std::string(xdg) + "/velox"; ::mkdir(velox_dir.c_str(), 0700); const std::string server_sock = velox_dir + "/velox.sock"; rpc::EventLoop loop; auto db = velox::daemon::store::Db::open(":memory:"); CHECK(db.has_value()); if (!db) return; CHECK(velox::daemon::store::migrate_to_head(*db).has_value()); char root_tmpl[] = "/tmp/velox-cli-root-XXXXXX"; g_allowed_root = ::mkdtemp(root_tmpl); CHECK(!g_allowed_root.empty()); { velox::daemon::store::Settings settings(*db); CHECK(settings.set_raw("saveTo.allowedRoots", "[\"" + g_allowed_root + "\"]").has_value()); CHECK(settings.set_raw("saveTo.defaultDir", "\"" + g_allowed_root + "\"").has_value()); } rpc::EventHub hub; rpc::VeloxDispatcher dispatcher(*db, hub); rpc::UdsServer server(loop, dispatcher, hub, server_sock); const auto ec = server.start(); CHECK(!ec); if (ec) return; std::thread th([&loop] { loop.run(); }); { Client c; auto err = c.connect(); CHECK(!err.has_value()); if (err) { loop.stop(); th.join(); return; } CHECK_EQ(c.hello_result().value("transport", ""), std::string("uds")); auto ls = c.call("download.list", nlohmann::json::object()); CHECK(ls.has_value()); if (ls) { CHECK_EQ(ls->value("total", -1), 0); CHECK(ls->at("items").is_array()); } // download.add outside every allowed root -> -32011, original saveDir echoed. auto bad = c.call("download.add", {{"url", "https://example.com/x"}, {"saveDir", "/etc"}}); CHECK(!bad.has_value()); if (!bad) { CHECK_EQ(bad.error().code, -32011); CHECK_EQ(bad.error().data.value("path", ""), std::string("/etc")); } // download.add into an allowed root -> a task id that then shows up in the list. auto ok = c.call( "download.add", {{"url", "https://example.com/movie.mp4"}, {"saveDir", g_allowed_root}}); CHECK(ok.has_value()); std::string task_id; if (ok) { task_id = ok->value("taskId", ""); CHECK(!task_id.empty()); CHECK_EQ(ok->value("state", ""), std::string("queued")); } auto ls2 = c.call("download.list", nlohmann::json::object()); CHECK(ls2.has_value()); if (ls2) { CHECK_EQ(ls2->value("total", -1), 1); CHECK_EQ(ls2->at("items").at(0).value("filename", ""), std::string("movie.mp4")); } auto detail = c.call("download.get", {{"taskId", task_id}}); CHECK(detail.has_value()); if (detail) CHECK_EQ(detail->at("summary").value("taskId", ""), task_id); auto missing = c.call("download.get", {{"taskId", "00000000-0000-4000-8000-000000000000"}}); CHECK(!missing.has_value()); if (!missing) CHECK_EQ(missing.error().code, -32010); } loop.stop(); th.join(); ::unlink(server_sock.c_str()); } TEST_MAIN()