diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 2e14ee403f..6da7d3e2ec 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -193,20 +193,10 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, return false; } - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - error = "Invalid JSON response"; + nlohmann::json status; + nlohmann::json variables; + if (!parse_slot_response(response_body, status, variables, error)) return false; - } - - if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") || - !json["result"]["status"]["save_variables"].contains("variables")) { - error = "Unexpected JSON structure"; - return false; - } - - auto& variables = json["result"]["status"]["save_variables"]["variables"]; - auto& status = json["result"]["status"]; box_count = variables.value("box_count", 1); if (box_count < 0) { @@ -281,6 +271,31 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, return true; } +bool QidiPrinterAgent::parse_slot_response(const std::string& response_body, + nlohmann::json& status, + nlohmann::json& variables, + std::string& error) +{ + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + error = "Invalid JSON response"; + return false; + } + + if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") || + !json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") || + !json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") || + !json["result"]["status"]["save_variables"]["variables"].is_object()) { + // why: Qidi firmware may send null here, but json::value() throws for it. + error = "Unexpected JSON structure: save_variables.variables must be an object"; + return false; + } + + status = json["result"]["status"]; + variables = status["save_variables"]["variables"]; + return true; +} + bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, diff --git a/src/slic3r/Utils/QidiPrinterAgent.hpp b/src/slic3r/Utils/QidiPrinterAgent.hpp index 1eba2c35d1..4064c9e7b5 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.hpp +++ b/src/slic3r/Utils/QidiPrinterAgent.hpp @@ -2,6 +2,7 @@ #define __QIDI_PRINTER_AGENT_HPP__ #include "MoonrakerPrinterAgent.hpp" +#include "nlohmann/json_fwd.hpp" #include #include @@ -21,6 +22,11 @@ public: // Override filament sync (Qidi-specific implementation) bool fetch_filament_info(std::string dev_id) override; + static bool parse_slot_response(const std::string& response_body, + nlohmann::json& status, + nlohmann::json& variables, + std::string& error); + // Print operations — emit QiDi multi-color box config, then delegate to base. int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override; int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override; diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index da312fc483..d04be0273a 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_capabilities_in_use.cpp test_plugin_status.cpp test_printer_agent.cpp + test_qidi_printer_agent.cpp test_plugin_install.cpp test_plugin_lifecycle.cpp test_slicing_pipeline_bindings.cpp diff --git a/tests/slic3rutils/test_qidi_printer_agent.cpp b/tests/slic3rutils/test_qidi_printer_agent.cpp new file mode 100644 index 0000000000..2d3307381a --- /dev/null +++ b/tests/slic3rutils/test_qidi_printer_agent.cpp @@ -0,0 +1,131 @@ +#include + +#include + +#include + +#include + +using namespace Slic3r; + +TEST_CASE("Qidi slot response rejects null variables without throwing", "[QidiPrinterAgent]") +{ + const std::string response = R"({ + "result": { + "status": { + "save_variables": { + "variables": null + } + } + } + })"; + nlohmann::json status; + nlohmann::json variables; + std::string error; + bool parsed = true; + + REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error)); + CHECK_FALSE(parsed); + CHECK_THAT(error, Catch::Matchers::ContainsSubstring("variables")); + CHECK_THAT(error, Catch::Matchers::ContainsSubstring("object")); +} + +TEST_CASE("Qidi slot response rejects missing and non-object fields without throwing", "[QidiPrinterAgent]") +{ + std::string response; + + SECTION("missing result") + { + response = R"({})"; + } + + SECTION("non-object result") + { + response = R"({"result":null})"; + } + + SECTION("missing status") + { + response = R"({"result":{}})"; + } + + SECTION("non-object status") + { + response = R"({"result":{"status":null}})"; + } + + SECTION("missing save_variables") + { + response = R"({"result":{"status":{}}})"; + } + + SECTION("non-object save_variables") + { + response = R"({"result":{"status":{"save_variables":null}}})"; + } + + SECTION("missing variables") + { + response = R"({"result":{"status":{"save_variables":{}}}})"; + } + + SECTION("scalar") + { + response = R"({"result":{"status":{"save_variables":{"variables":42}}}})"; + } + + SECTION("array") + { + response = R"({"result":{"status":{"save_variables":{"variables":[]}}}})"; + } + + nlohmann::json status; + nlohmann::json variables; + std::string error; + bool parsed = true; + + REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error)); + CHECK_FALSE(parsed); +} + +TEST_CASE("Qidi slot response exposes valid status and variables", "[QidiPrinterAgent]") +{ + const std::string response = R"({ + "result": { + "status": { + "save_variables": { + "variables": { + "box_count": 2, + "color_slot0": 3 + } + }, + "box_stepper slot0": { + "runout_button": 0 + } + } + } + })"; + nlohmann::json status; + nlohmann::json variables; + std::string error; + bool parsed = false; + + REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error)); + REQUIRE(parsed); + CHECK(status.is_object()); + CHECK(variables.is_object()); + CHECK(variables.at("box_count") == 2); + CHECK(status.contains("box_stepper slot0")); +} + +TEST_CASE("Qidi slot response rejects invalid JSON", "[QidiPrinterAgent]") +{ + nlohmann::json status; + nlohmann::json variables; + std::string error; + bool parsed = true; + + REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response("{not json", status, variables, error)); + CHECK_FALSE(parsed); + CHECK(error == "Invalid JSON response"); +}