Compare commits

..
Author SHA1 Message Date
Hanif Koh 7c71ac0870 Clamp Ironing Line Spacing to a Usable Minimum
An ironing line spacing of 0 reached the fillers from a 3MF, the CLI or
the per-filament override, which has no GUI guard. Concentric ironing
then never finished slicing, because a zero inset never shrinks the
region, and rectilinear ironing was silently dropped. Tiny positive
values produced an unprintable number of lines.

Top surface and support ironing now clamp the spacing to the 0.05 mm
floor the process GUI guard already enforces, so these configurations
iron at that spacing. Spacings at or above the floor, including every
shipped profile, are unchanged. The concentric filler also returns early
on a non-positive step so no other caller can hang it, and the filament
settings page now resets a too-small override the same way the process
page does.
2026-09-28 13:00:25 +08:00
24 changed files with 156 additions and 189 deletions
+2 -2
View File
@@ -1692,9 +1692,9 @@ void Layer::make_ironing()
ironing_params.just_infill = false;
// ORCA: Get filament-specific overrides if configured, otherwise use process values
size_t extruder_idx = ironing_params.extruder - 1;
ironing_params.line_spacing = (!config.filament_ironing_spacing.is_nil(extruder_idx)
ironing_params.line_spacing = std::max(IRONING_SPACING_MIN, !config.filament_ironing_spacing.is_nil(extruder_idx)
? config.filament_ironing_spacing.get_at(extruder_idx)
: config.ironing_spacing);
: config.ironing_spacing.value);
ironing_params.inset = (!config.filament_ironing_inset.is_nil(extruder_idx)
? config.filament_ironing_inset.get_at(extruder_idx)
: config.ironing_inset);
+5
View File
@@ -22,6 +22,9 @@ void FillConcentric::_fill_surface_single(
coord_t min_spacing = scale_(this->spacing) * params.multiline;
coord_t distance = coord_t(min_spacing / params.density);
// A non-positive step never shrinks the region, so the inset loop below would not end.
if (min_spacing <= 0 || distance <= 0)
return;
if (params.density > 0.9999f && !params.dont_adjust) {
distance = this->_adjust_solid_spacing(bounding_box.size()(0), distance);
@@ -108,6 +111,8 @@ void FillConcentric::_fill_surface_single(const FillParams& params,
// no rotation is supported for this infill pattern
Point bbox_size = expolygon.contour.bounding_box().size();
coord_t min_spacing = scaled<coord_t>(this->spacing);
if (min_spacing <= 0)
return;
if (params.density > 0.9999f && !params.dont_adjust) {
coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1;
+4
View File
@@ -178,6 +178,10 @@ enum class IroningType {
Count,
};
// Smallest usable ironing line spacing. Anything tighter yields an unprintable number of lines,
// and zero stops the fillers from making progress.
constexpr double IRONING_SPACING_MIN = 0.05;
//BBS
enum class WallInfillOrder {
InnerOuterInfill,
+1 -1
View File
@@ -64,7 +64,7 @@ struct SupportParameters {
this->ironing = object_config.support_ironing;
this->ironing_flow = support_material_interface_flow.with_height(support_material_interface_flow.height() * 0.01 * object_config.support_ironing_flow.value);
this->ironing_spacing = object_config.support_ironing_spacing;
this->ironing_spacing = std::max(IRONING_SPACING_MIN, object_config.support_ironing_spacing.value);
this->ironing_pattern = object_config.support_ironing_pattern;
// Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um.
+26 -2
View File
@@ -210,6 +210,30 @@ void ConfigManipulation::check_filament_max_volumetric_speed(DynamicPrintConfig
}
void ConfigManipulation::check_filament_ironing_spacing(DynamicPrintConfig *config)
{
const auto *opt = config->option<ConfigOptionFloatsNullable>("filament_ironing_spacing");
if (opt == nullptr)
return;
std::vector<double> values = opt->values;
bool reset = false;
for (size_t i = 0; i < values.size(); ++i)
if (!opt->is_nil(i) && values[i] < IRONING_SPACING_MIN) {
values[i] = 0.1;
reset = true;
}
if (!reset)
return;
const wxString msg_text = _(L("Ironing spacing too small\nIt has been reset to 0.1"));
MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK);
DynamicPrintConfig new_conf = *config;
is_msg_dlg_already_exist = true;
dialog.ShowModal();
new_conf.set_key_value("filament_ironing_spacing", new ConfigOptionFloatsNullable(values));
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
void ConfigManipulation::check_chamber_temperature(DynamicPrintConfig* config)
{
bool support_chamber_temp_control=GUI::wxGetApp().preset_bundle->printers.get_selected_preset().config.opt_bool("support_chamber_temp_control");
@@ -332,7 +356,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
//BBS: ironing_spacing shouldn't be too small or equal to zero
if (config->opt_float("ironing_spacing") < 0.05)
if (config->opt_float("ironing_spacing") < IRONING_SPACING_MIN)
{
const wxString msg_text = _(L("Ironing spacing too small\nIt has been reset to 0.1"));
MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK);
@@ -343,7 +367,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
if (config->opt_float("support_ironing_spacing") < 0.05)
if (config->opt_float("support_ironing_spacing") < IRONING_SPACING_MIN)
{
const wxString msg_text = _(L("Ironing spacing too small\nIt has been reset to 0.1"));
MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK);
+1
View File
@@ -84,6 +84,7 @@ public:
void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config);
void check_adaptive_pressure_advance_model(DynamicPrintConfig* config);
void check_filament_max_volumetric_speed(DynamicPrintConfig *config);
void check_filament_ironing_spacing(DynamicPrintConfig *config);
void check_chamber_temperature(DynamicPrintConfig* config);
void check_chamber_minimal_temperature(DynamicPrintConfig* config);
bool check_layer_height(DynamicPrintConfig* config);
+1
View File
@@ -4905,6 +4905,7 @@ void TabFilament::update()
return; // ys_FIXME
m_config_manipulation.check_filament_max_volumetric_speed(m_config);
m_config_manipulation.check_filament_ironing_spacing(m_config);
m_update_cnt++;
+4 -5
View File
@@ -31,15 +31,14 @@ int UserManager::parse_json(std::string payload)
{
bool restored_json = false;
json j;
json j_pre = json::parse(payload);
if (j_pre.empty()) {
return -1;
}
//bind/unbind
try {
json j_pre = json::parse(payload);
if (j_pre.empty()) {
return -1;
}
if (j_pre.contains("bind")) {
if (j_pre["bind"].contains("command")) {
+9
View File
@@ -274,4 +274,13 @@ bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionTyp
return res;
}
int Duet::get_err_code_from_body(const std::string &body) const
{
pt::ptree root;
std::istringstream iss (body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
}
+1
View File
@@ -40,6 +40,7 @@ private:
ConnectionType connect(wxString &msg) const;
void disconnect(ConnectionType connectionType) const;
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const;
int get_err_code_from_body(const std::string &body) const;
};
}
+9
View File
@@ -146,6 +146,15 @@ bool ESP3D::start_print(wxString& msg, const std::string& filename) const
return ret;
}
int ESP3D::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
std::istringstream iss(body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
// ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour
std::string ESP3D::get_short_name(const std::string& filename) const
{
+1
View File
@@ -33,6 +33,7 @@ private:
std::string m_console_port;
bool start_print(wxString& msg, const std::string& filename) const;
int get_err_code_from_body(const std::string& body) const;
std::string get_short_name(const std::string& filename) const;
std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const;
};
+15 -25
View File
@@ -510,22 +510,12 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
return false;
if (!parse_material_slots(body, slots, supports_material_station)) {
const auto parsed = json::parse(body, nullptr, false, true);
if (parsed.is_discarded()) {
msg = _(L("Flashforge returned an invalid JSON response."));
return false;
}
return true;
}
bool Flashforge::parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station)
{
slots.clear();
const auto parsed = json::parse(body, nullptr, false, true);
if (parsed.is_discarded())
return false;
const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
@@ -552,21 +542,12 @@ bool Flashforge::parse_material_slots(const std::string& body, std::vector<Flash
if (supports_material_station != nullptr)
*supports_material_station = reports_material_station;
// Fields are read leniently: firmware may send numbers as strings or flags as numbers.
for (const auto& slot : slot_infos) {
if (!slot.is_object())
continue;
FlashforgeMaterialSlot info;
info.slot_id = static_cast<int>(slots.size()) + 1;
if (const auto it = slot.find("slotId"); it != slot.end())
try_parse_json_int(*it, info.slot_id);
int has_filament = 0;
if (const auto it = slot.find("hasFilament"); it != slot.end() && try_parse_json_int(*it, has_filament))
info.has_filament = has_filament != 0;
if (const auto it = slot.find("materialName"); it != slot.end() && it->is_string())
info.material_name = it->get<std::string>();
if (const auto it = slot.find("materialColor"); it != slot.end() && it->is_string())
info.material_color = it->get<std::string>();
info.slot_id = slot.value("slotId", static_cast<int>(slots.size()) + 1);
info.has_filament = slot.value("hasFilament", false);
info.material_name = slot.value("materialName", std::string());
info.material_color = slot.value("materialColor", std::string());
slots.emplace_back(std::move(info));
}
@@ -689,4 +670,13 @@ std::string Flashforge::extract_host_name() const
return out;
}
int Flashforge::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
std::istringstream iss(body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
} // namespace Slic3r
+1 -2
View File
@@ -45,8 +45,6 @@ public:
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
std::string get_host() const override { return m_host; }
bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const;
// Parses a local API "detail" reply. Returns false when the body is not valid JSON.
static bool parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station);
static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3);
private:
@@ -70,6 +68,7 @@ private:
bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const;
std::string make_http_url(const std::string& path) const;
std::string extract_host_name() const;
int get_err_code_from_body(const std::string &body) const;
bool connect(wxString& msg) const;
bool start_print(wxString& msg, const std::string& filename) const;
};
+9
View File
@@ -141,4 +141,13 @@ bool MKS::start_print(wxString& msg, const std::string& filename) const
return ret;
}
int MKS::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
std::istringstream iss(body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
} // Slic3r
+1
View File
@@ -34,6 +34,7 @@ private:
std::string get_upload_url(const std::string& filename) const;
bool start_print(wxString& msg, const std::string& filename) const;
int get_err_code_from_body(const std::string& body) const;
};
}
+1 -23
View File
@@ -3,13 +3,10 @@
#include <vector>
#include <thread>
#include <exception>
#include <sstream>
#include <boost/optional.hpp>
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <wx/string.h>
#include <wx/app.h>
@@ -175,20 +172,6 @@ std::string moonraker_error_reason(const std::string &body)
} // namespace
int PrintHost::get_err_code_from_body(const std::string &body)
{
boost::property_tree::ptree root;
std::istringstream iss(body);
try {
boost::property_tree::read_json(iss, root);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "PrintHost: response is not valid JSON: " << ex.what();
return -1;
}
return root.get<int>("err", 0);
}
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
{
if (status != 0) {
@@ -320,12 +303,7 @@ void PrintHostJobQueue::priv::bg_thread_main()
% job.cancelled;
if (! job.cancelled) {
// A failing job must not stop the worker, or later jobs would stay queued forever.
try {
perform_job(std::move(job));
} catch (const std::exception &e) {
emit_error(e.what());
}
perform_job(std::move(job));
}
remove_source();
-2
View File
@@ -87,8 +87,6 @@ public:
static PrintHost* get_print_host(DynamicPrintConfig *config);
static std::string get_print_host_webui(DynamicPrintConfig *config);
// Reads the "err" field of a JSON reply, 0 when absent. Returns -1 when the body is not valid JSON.
static int get_err_code_from_body(const std::string &body);
//Support for cloud webui login
virtual bool is_cloud() const { return false; }
+9
View File
@@ -654,4 +654,13 @@ bool UltiMaker::start_print(wxString &msg, const std::string &filename, Connecti
return res;
}
int UltiMaker::get_err_code_from_body(const std::string &body) const
{
pt::ptree root;
std::istringstream iss (body); // wrap returned json to istringstream
pt::read_json(iss, root);
return root.get<int>("err", 0);
}
}
+1
View File
@@ -64,6 +64,7 @@ private:
void set_auth(Http& http) const;
void disconnect(ConnectionType connectionType) const;
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const;
int get_err_code_from_body(const std::string &body) const;
};
}
+55
View File
@@ -747,6 +747,61 @@ TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
}
// Ironing path count and total length in mm, over the whole object.
static std::pair<size_t, double> ironing_extent(const Print &print)
{
size_t paths = 0;
double length = 0.;
for (const Layer *layer : print.objects().front()->layers())
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
if (ironing_role(entity->role())) {
++paths;
length += unscale<double>(entity->length());
}
return {paths, length};
}
TEST_CASE("Ironing spacing below the minimum irons at the minimum spacing", "[Fill]")
{
const std::string pattern = GENERATE("rectilinear", "concentric");
const bool via_filament = GENERATE(false, true);
const double spacing = GENERATE(0., 0.001);
CAPTURE(pattern, via_filament, spacing);
auto ironing_for = [&pattern, via_filament](double spacing) {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"ironing_type", "top"},
{"ironing_pattern", pattern},
{"layer_height", 0.2}});
// The filament override replaces the process spacing, which stays at a usable value.
if (via_filament)
config.set_deserialize_strict({{"ironing_spacing", 0.1}, {"filament_ironing_spacing", spacing}});
else
config.set_deserialize_strict({{"ironing_spacing", spacing}});
Print print;
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, config);
return ironing_extent(print);
};
const std::pair<size_t, double> clamped = ironing_for(spacing);
const std::pair<size_t, double> minimum = ironing_for(IRONING_SPACING_MIN);
REQUIRE(minimum.first > 0);
CHECK(clamped.first == minimum.first);
CHECK_THAT(clamped.second, Catch::Matchers::WithinRel(minimum.second, 1e-9));
}
TEST_CASE("Concentric fill at zero spacing returns without paths", "[Fill]")
{
std::unique_ptr<Fill> filler(Fill::new_from_type(ipConcentric));
filler->spacing = 0.;
filler->bounding_box = BoundingBox(Point(0, 0), Point::new_scale(10, 10));
FillParams params;
params.density = 1.f;
Surface surface(stTop, ExPolygon({Point(0, 0), Point::new_scale(10, 0), Point::new_scale(10, 10), Point::new_scale(0, 10)}));
CHECK(filler->fill_surface(&surface, params).empty());
}
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
{
auto angles_for = [](int direction) {
-1
View File
@@ -25,7 +25,6 @@ add_executable(${_TEST_NAME}_tests
test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp
test_shortcuts.cpp
test_user_manager.cpp
../fff_print/test_helpers.cpp
)
-103
View File
@@ -1,12 +1,8 @@
#include <catch2/catch_all.hpp>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "slic3r/Utils/PrintHost.hpp"
#include "slic3r/Utils/Flashforge.hpp"
using namespace Slic3r;
@@ -50,14 +46,6 @@ std::string moonraker_error(int code, const std::string& message, const std::str
constexpr const char* k_busy_file_403 =
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
// Replies a print host can send instead of JSON: a proxy or login page, nothing, a cut-off body.
const std::vector<std::string> non_json_replies = {
"<html><body>proxy login required</body></html>",
"",
"{\"err\":",
"{\"detail\":{\"matlStationInfo\":{\"slotInfos\":[{\"slotId\":1,",
};
} // namespace
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
@@ -223,94 +211,3 @@ TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
}
}
TEST_CASE("Print host error code is read from a JSON reply", "[PrintHost]")
{
CHECK(PrintHost::get_err_code_from_body(R"({"err":0})") == 0);
CHECK(PrintHost::get_err_code_from_body(R"({"err":2})") == 2);
CHECK(PrintHost::get_err_code_from_body(R"({"status":"ok"})") == 0);
}
TEST_CASE("Print host error code reports a reply that is not JSON as an error", "[PrintHost]")
{
const std::string body = GENERATE(from_range(non_json_replies));
int err = 0;
REQUIRE_NOTHROW(err = PrintHost::get_err_code_from_body(body));
CHECK(err != 0);
}
TEST_CASE("Print host error code tolerates a wrongly typed err field", "[PrintHost]")
{
const std::string body = GENERATE(as<std::string>{}, R"({"err":"busy"})", R"({"err":{"code":1}})", R"([1,2])");
CHECK_NOTHROW(PrintHost::get_err_code_from_body(body));
}
TEST_CASE("Flashforge material slots are read from a well-formed reply", "[PrintHost][Flashforge]")
{
const std::string body = R"({"code":0,"detail":{"hasMatlStation":true,"matlStationInfo":{"slotCnt":2,"slotInfos":[
{"slotId":1,"hasFilament":true,"materialName":"PLA","materialColor":"#FFFFFF"},
{"slotId":2,"hasFilament":false,"materialName":"","materialColor":""}]}}})";
std::vector<FlashforgeMaterialSlot> slots;
bool supports_station = false;
REQUIRE(Flashforge::parse_material_slots(body, slots, &supports_station));
CHECK(supports_station);
REQUIRE(slots.size() == 2);
CHECK(slots[0].slot_id == 1);
CHECK(slots[0].has_filament);
CHECK(slots[0].material_name == "PLA");
CHECK(slots[0].material_color == "#FFFFFF");
CHECK(slots[1].slot_id == 2);
CHECK_FALSE(slots[1].has_filament);
}
TEST_CASE("Flashforge material slots accept numbers as strings and flags as numbers", "[PrintHost][Flashforge]")
{
const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[
{"slotId":"3","hasFilament":1,"materialName":null,"materialColor":7}]}}})";
std::vector<FlashforgeMaterialSlot> slots;
REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr));
REQUIRE(slots.size() == 1);
CHECK(slots[0].slot_id == 3);
CHECK(slots[0].has_filament);
CHECK(slots[0].material_name.empty());
CHECK(slots[0].material_color.empty());
}
TEST_CASE("Flashforge material slots skip entries that are not objects", "[PrintHost][Flashforge]")
{
const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[5,"slot",null,[],
{"slotId":4,"hasFilament":true,"materialName":"PETG"}]}}})";
std::vector<FlashforgeMaterialSlot> slots;
REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr));
REQUIRE(slots.size() == 1);
CHECK(slots[0].slot_id == 4);
CHECK(slots[0].material_name == "PETG");
}
TEST_CASE("Flashforge material slots tolerate slot info that is not a list", "[PrintHost][Flashforge]")
{
const std::string body = GENERATE(as<std::string>{},
R"({"detail":{"matlStationInfo":{"slotInfos":5}}})",
R"({"detail":{"matlStationInfo":{"slotInfos":"none"}}})",
R"({"detail":{"matlStationInfo":7}})",
R"({"detail":"offline"})");
std::vector<FlashforgeMaterialSlot> slots;
bool ok = false;
REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr));
CHECK(ok);
CHECK(slots.empty());
}
TEST_CASE("Flashforge material slots reject a reply that is not JSON", "[PrintHost][Flashforge]")
{
const std::string body = GENERATE(from_range(non_json_replies));
std::vector<FlashforgeMaterialSlot> slots;
bool ok = true;
REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr));
CHECK_FALSE(ok);
CHECK(slots.empty());
}
-23
View File
@@ -1,23 +0,0 @@
#include <catch2/catch_all.hpp>
#include <string>
#include "slic3r/GUI/UserManager.hpp"
using namespace Slic3r;
TEST_CASE("User message that is not JSON is rejected without throwing", "[UserManager]")
{
const std::string payload = GENERATE(as<std::string>{}, "not json", "", "<html></html>", "{\"bind\":");
UserManager manager;
int result = 0;
REQUIRE_NOTHROW(result = manager.parse_json(payload));
CHECK(result == -1);
}
TEST_CASE("User message without a successful bind is ignored", "[UserManager]")
{
const std::string payload = GENERATE(as<std::string>{}, "{}", R"({"bind":{"command":"unbind"}})", R"({"bind":"bind"})", "[1]");
UserManager manager;
CHECK(manager.parse_json(payload) == -1);
}