fix: edit ops second review findings

- A group's sunSensor and a shade's flags are refused by name: the routes
  don't set them.
- setPaired needs paired, and without a shade id it is a bad request.
- groupOptions' payload is parsed in ProtoEdits (parseReadPayload).
- The assembler's mutex is static, so creating it can't fail.
- protoIsReadOp and protoIsShadeOp are gone; the tests use protoOpKind.
- Native tests for missing(), offeredRemoteAddress and the edit ops
  against the TYPE table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 18:29:34 +02:00
parent 8fa087aa89
commit 970256e7f6
13 changed files with 107 additions and 34 deletions

View file

@ -62,6 +62,8 @@ def test_group_options_of_a_missing_group(connect, schema):
("saveRoom", {"zz": 1}, "Unknown field zz"),
("setPaired", {"s": 1, "i": 2}, "paired is not a boolean (1 or 0)"),
("shadeSortOrder", {}, "ids is missing"),
("setPaired", {"s": 1}, "paired is missing"),
("saveGroup", {"g": 1, "ss": 1}, "sunSensor can't be set by this edit"),
])
def test_a_bad_payload_is_refused_before_it_is_queued(connect, schema, name, payload, why):
c, _, _ = synced_model(connect, schema)

View file

@ -31,6 +31,12 @@ static void test_free_remote_address_skips_every_taken_one() {
TEST_ASSERT_EQUAL_UINT32(104, freeRemoteAddress(101, used, 4));
TEST_ASSERT_EQUAL_UINT32(100, freeRemoteAddress(100, used, 4));
}
static void test_a_new_one_is_offered_the_starting_address_plus_its_id() {
TEST_ASSERT_EQUAL_UINT32(1005, offeredRemoteAddress(1000, 5, nullptr, 0));
// Taken by another shade or group, so the next free one.
const uint32_t used[] = { 1006, 1005 };
TEST_ASSERT_EQUAL_UINT32(1007, offeredRemoteAddress(1000, 5, used, 2));
}
/*********************************************************************
* Lists
@ -137,6 +143,23 @@ static void test_255_means_no_pin() {
/*********************************************************************
* What protocol 2 clients are sent after an edit
********************************************************************/
/*********************************************************************
* Results
********************************************************************/
static void test_a_missing_id_is_not_found() {
const edit_result_t r = edit_result_t::missing("Shade Id not found.");
TEST_ASSERT_FALSE(r.ok);
TEST_ASSERT_TRUE(r.notFound);
TEST_ASSERT_EQUAL_STRING("Shade Id not found.", r.desc);
}
static void test_any_other_refusal_is_not_not_found() {
const edit_result_t r = edit_result_t::error("No shade id was supplied.", 3);
TEST_ASSERT_FALSE(r.ok);
TEST_ASSERT_FALSE(r.notFound);
TEST_ASSERT_EQUAL_INT8(3, r.code);
TEST_ASSERT_FALSE(edit_result_t().notFound);
}
static edit_result_t resultFor(uint8_t id, uint8_t shadeId = 0) {
edit_result_t r;
r.id = id;
@ -203,6 +226,9 @@ int main(int argc, char **argv) {
RUN_TEST(test_first_free_id_is_0_when_every_one_is_taken);
RUN_TEST(test_next_sort_order_goes_after_the_last);
RUN_TEST(test_free_remote_address_skips_every_taken_one);
RUN_TEST(test_a_new_one_is_offered_the_starting_address_plus_its_id);
RUN_TEST(test_a_missing_id_is_not_found);
RUN_TEST(test_any_other_refusal_is_not_not_found);
RUN_TEST(test_add_goes_into_the_first_empty_slot);
RUN_TEST(test_adding_one_already_there_changes_nothing);
RUN_TEST(test_adding_to_a_full_list_fails);

View file

@ -47,12 +47,37 @@ static void test_other_types_are_not_edits() {
TEST_ASSERT_FALSE(protoEditType(static_cast<uint8_t>(proto_type_t::getNextShade), t));
TEST_ASSERT_FALSE(protoEditType(static_cast<uint8_t>(proto_type_t::shade), t));
}
static void test_the_read_ops() {
TEST_ASSERT_TRUE(protoIsReadOp(static_cast<uint8_t>(proto_type_t::getNextRoom)));
TEST_ASSERT_TRUE(protoIsReadOp(static_cast<uint8_t>(proto_type_t::getNextShade)));
TEST_ASSERT_TRUE(protoIsReadOp(static_cast<uint8_t>(proto_type_t::getNextGroup)));
TEST_ASSERT_TRUE(protoIsReadOp(static_cast<uint8_t>(proto_type_t::groupOptions)));
TEST_ASSERT_FALSE(protoIsReadOp(static_cast<uint8_t>(proto_type_t::addRoom)));
static void test_the_edit_ops_are_the_schemas_edit_ops() {
// The TYPE table marks the edit ops; each needs its edit here, and only they
// have one.
model_edit_type_t t;
for(int type = 0; type <= 255; type++) {
const bool edit = protoOpKind(static_cast<uint8_t>(type)) == proto_op_t::edit;
TEST_ASSERT_EQUAL_MESSAGE(edit, protoEditType(static_cast<uint8_t>(type), t), protoTypeName(static_cast<uint8_t>(type)));
}
}
/*********************************************************************
* Reads
********************************************************************/
static uint8_t groupId;
static const char *readPayload(proto_type_t type, const char *json) {
payload.clear();
TEST_ASSERT_EQUAL(DeserializationError::Ok, deserializeJson(payload, json).code());
return parseReadPayload(static_cast<uint8_t>(type), payload.as<JsonObjectConst>(), groupId);
}
static void test_group_options_reads_its_group() {
TEST_ASSERT_GOOD(readPayload(proto_type_t::groupOptions, R"({"g":4})"));
TEST_ASSERT_EQUAL_UINT8(4, groupId);
TEST_ASSERT_EQUAL_STRING("groupId is missing", readPayload(proto_type_t::groupOptions, R"({})"));
TEST_ASSERT_EQUAL_STRING("groupId must be 0-255", readPayload(proto_type_t::groupOptions, R"({"g":300})"));
TEST_ASSERT_EQUAL_STRING("groupId must be 0-255", readPayload(proto_type_t::groupOptions, R"({"g":"4"})"));
}
static void test_the_other_reads_need_nothing() {
TEST_ASSERT_GOOD(readPayload(proto_type_t::getNextShade, R"({})"));
TEST_ASSERT_EQUAL_UINT8(0, groupId);
TEST_ASSERT_GOOD(readPayload(proto_type_t::getNextRoom, R"({})"));
TEST_ASSERT_GOOD(readPayload(proto_type_t::getNextGroup, R"({})"));
}
/*********************************************************************
@ -98,6 +123,14 @@ static void test_a_field_the_route_would_ignore_is_refused() {
TEST_ASSERT_EQUAL_STRING("shadeId can't be set by this edit", expand(model_edit_type_t::add_shade, R"({"s":3})"));
TEST_ASSERT_EQUAL_STRING("upTime can't be set by this edit", expand(model_edit_type_t::save_group, R"({"g":1,"U":5})"));
TEST_ASSERT_EQUAL_STRING("name can't be set by this edit", expand(model_edit_type_t::delete_room, R"({"r":1,"n":"x"})"));
// light is its own field, and setSensor sets the sun and wind.
TEST_ASSERT_EQUAL_STRING("flags can't be set by this edit", expand(model_edit_type_t::save_shade, R"({"s":3,"f":1})"));
// Worked out from the group's shades.
TEST_ASSERT_EQUAL_STRING("sunSensor can't be set by this edit", expand(model_edit_type_t::save_group, R"({"g":1,"ss":1})"));
}
static void test_set_paired_needs_paired() {
// Left out is not given, so it doesn't unpair the shade.
TEST_ASSERT_EQUAL_STRING("paired is missing", expand(model_edit_type_t::set_paired, R"({"s":3})"));
}
static void test_link_remote_takes_the_routes_rolling_code() {
TEST_ASSERT_GOOD(expand(model_edit_type_t::link_remote, R"({"s":3,"a":1193046,"c":12})"));
@ -118,7 +151,9 @@ int main(int argc, char **argv) {
UNITY_BEGIN();
RUN_TEST(test_every_edit_op_is_an_edit);
RUN_TEST(test_other_types_are_not_edits);
RUN_TEST(test_the_read_ops);
RUN_TEST(test_the_edit_ops_are_the_schemas_edit_ops);
RUN_TEST(test_group_options_reads_its_group);
RUN_TEST(test_the_other_reads_need_nothing);
RUN_TEST(test_short_keys_become_the_route_names);
RUN_TEST(test_booleans_become_true_and_false);
RUN_TEST(test_only_what_was_sent_is_in_the_request);
@ -126,6 +161,7 @@ int main(int argc, char **argv) {
RUN_TEST(test_values_are_checked_against_their_kind);
RUN_TEST(test_an_unknown_key_is_refused);
RUN_TEST(test_a_field_the_route_would_ignore_is_refused);
RUN_TEST(test_set_paired_needs_paired);
RUN_TEST(test_link_remote_takes_the_routes_rolling_code);
RUN_TEST(test_a_sort_order_takes_only_ids);
RUN_TEST(test_a_shade_save_takes_what_the_form_sends);

View file

@ -28,10 +28,10 @@ static void test_every_command_op_but_the_remote_frame_is_a_shade_request() {
proto_type_t::setPositions, proto_type_t::setSensor, proto_type_t::setMyPosition, proto_type_t::unpairShade,
proto_type_t::setRollingCode
};
for(proto_type_t t : shadeOps) TEST_ASSERT_TRUE(protoIsShadeOp(static_cast<uint8_t>(t)));
TEST_ASSERT_FALSE(protoIsShadeOp(static_cast<uint8_t>(proto_type_t::sendRemoteCommand)));
TEST_ASSERT_FALSE(protoIsShadeOp(static_cast<uint8_t>(proto_type_t::subscribe)));
TEST_ASSERT_FALSE(protoIsShadeOp(static_cast<uint8_t>(proto_type_t::shadeCommanded)));
for(proto_type_t t : shadeOps) TEST_ASSERT_EQUAL(proto_op_t::shadeRequest, protoOpKind(static_cast<uint8_t>(t)));
TEST_ASSERT_EQUAL(proto_op_t::remoteFrame, protoOpKind(static_cast<uint8_t>(proto_type_t::sendRemoteCommand)));
TEST_ASSERT_EQUAL(proto_op_t::none, protoOpKind(static_cast<uint8_t>(proto_type_t::subscribe)));
TEST_ASSERT_EQUAL(proto_op_t::none, protoOpKind(static_cast<uint8_t>(proto_type_t::shadeCommanded)));
}
/*********************************************************************