#include #include "unity.h" #include "cJSON.h" #include "fake_api_ctx.h" #define TOKEN "aabbccddeeff0011223344" static nl_api_response_t resp; static cJSON *doc; static void call(nl_http_method_t m, const char *path, const char *auth, const char *body) { nl_http_request_t r; memset(&r, 0, sizeof r); r.method = m; strcpy(r.path, path); strcpy(r.auth, auth ? auth : ""); r.body = body; r.body_len = body ? strlen(body) : 0; nl_api_handle(&fake_ctx, &r, &resp); cJSON_Delete(doc); doc = cJSON_Parse(resp.body); } static const cJSON *field(const char *name) { return cJSON_GetObjectItemCaseSensitive(doc, name); } static const char *error_text(void) { return cJSON_GetStringValue(field("error")); } void setUp(void) { fake_api_reset("evk"); doc = NULL; } void tearDown(void) { cJSON_Delete(doc); doc = NULL; } /* --- routing --- */ static void test_unknown_path_and_method(void) { call(NL_HTTP_GET, "/api/v1/nope", NULL, NULL); TEST_ASSERT_EQUAL_INT(404, resp.status); TEST_ASSERT_EQUAL_STRING("not found", error_text()); call(NL_HTTP_PUT, "/api/v1/device/state", NULL, "{}"); TEST_ASSERT_EQUAL_INT(405, resp.status); call(NL_HTTP_POST, "/api/v1/system/info", NULL, "{}"); TEST_ASSERT_EQUAL_INT(405, resp.status); } /* --- device/state --- */ static void test_get_state(void) { const cJSON *cap; fake_settings.fade_in_ms = 700; call(NL_HTTP_GET, "/api/v1/device/state", NULL, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_TRUE(cJSON_IsTrue(field("on"))); TEST_ASSERT_EQUAL_INT(100, field("brightness")->valueint); TEST_ASSERT_EQUAL_STRING("white", cJSON_GetStringValue(field("mode"))); TEST_ASSERT_EQUAL_INT(255, cJSON_GetObjectItem(field("color"), "g")->valueint); TEST_ASSERT_EQUAL_INT(2700, field("color_temp")->valueint); TEST_ASSERT_EQUAL_STRING("last", cJSON_GetStringValue(field("power_on_behaviour"))); TEST_ASSERT_EQUAL_INT(700, field("fade_in")->valueint); TEST_ASSERT_EQUAL_INT(500, field("fade_out")->valueint); TEST_ASSERT_EQUAL_INT(300, field("transition_default")->valueint); TEST_ASSERT_TRUE(cJSON_IsFalse(field("wake_on_change"))); cap = field("capabilities"); TEST_ASSERT_TRUE(cJSON_IsTrue(cJSON_GetObjectItem(cap, "color"))); TEST_ASSERT_EQUAL_INT(2700, cJSON_GetArrayItem(cJSON_GetObjectItem(cap, "color_temp"), 0)->valueint); TEST_ASSERT_EQUAL_INT(6500, cJSON_GetArrayItem(cJSON_GetObjectItem(cap, "color_temp"), 1)->valueint); } static void test_white_board_capabilities(void) { fake_api_reset("e27_cw"); call(NL_HTTP_GET, "/api/v1/device/state", NULL, NULL); TEST_ASSERT_TRUE(cJSON_IsFalse(cJSON_GetObjectItem(field("capabilities"), "color"))); } static void test_post_state_applies_change(void) { call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"on\":false,\"brightness\":50,\"transition\":800}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(1, fake_apply_calls); TEST_ASSERT_TRUE(fake_last_change.has_on); TEST_ASSERT_FALSE(fake_last_change.on); TEST_ASSERT_EQUAL_UINT8(50, fake_last_change.brightness); TEST_ASSERT_TRUE(fake_last_change.has_transition); TEST_ASSERT_EQUAL_UINT32(800, fake_last_change.transition_ms); TEST_ASSERT_TRUE(cJSON_IsFalse(field("on"))); TEST_ASSERT_EQUAL_INT(50, field("brightness")->valueint); TEST_ASSERT_EQUAL_INT(0, fake_save_calls); } static void test_post_color_and_ct(void) { call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"color\":{\"r\":255,\"g\":120,\"b\":0}}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_TRUE(fake_last_change.has_color); TEST_ASSERT_EQUAL_UINT8(120, fake_last_change.g); TEST_ASSERT_EQUAL_STRING("color", cJSON_GetStringValue(field("mode"))); call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"color_temp\":4000}"); TEST_ASSERT_TRUE(fake_last_change.has_ct); TEST_ASSERT_EQUAL_UINT16(4000, fake_last_change.ct); } static void test_post_validation(void) { static const struct { const char *body; const char *err; } CASES[] = { {"not json", "invalid json"}, {"[1,2]", "invalid json"}, {"{\"a\":[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]}", "invalid json"}, /* C1: 20 levels > CJSON_NESTING_LIMIT */ {"{\"on\":1}", "invalid on"}, {"{\"brightness\":0}", "invalid brightness"}, {"{\"brightness\":101}", "invalid brightness"}, {"{\"brightness\":1.5}", "invalid brightness"}, {"{\"brightness\":\"50\"}", "invalid brightness"}, {"{\"brightness\":1e300}", "invalid brightness"}, /* M5: range check before the (long) cast */ {"{\"color\":{\"r\":1,\"g\":2}}", "invalid color"}, {"{\"color\":{\"r\":1,\"g\":2,\"b\":256}}", "invalid color"}, {"{\"color_temp\":999}", "invalid color_temp"}, {"{\"transition\":60001}", "invalid transition"}, {"{\"power_on_behaviour\":\"maybe\"}", "invalid power_on_behaviour"}, {"{\"fade_in\":10001}", "invalid fade_in"}, {"{\"fade_out\":-1}", "invalid fade_out"}, {"{\"transition_default\":60001}", "invalid transition_default"}, {"{\"wake_on_change\":\"ja\"}", "invalid wake_on_change"}, }; for (size_t i = 0; i < sizeof CASES / sizeof CASES[0]; i++) { call(NL_HTTP_POST, "/api/v1/device/state", NULL, CASES[i].body); TEST_ASSERT_EQUAL_INT_MESSAGE(400, resp.status, CASES[i].body); TEST_ASSERT_EQUAL_STRING_MESSAGE(CASES[i].err, error_text(), CASES[i].body); } call(NL_HTTP_POST, "/api/v1/device/state", NULL, NULL); TEST_ASSERT_EQUAL_STRING("invalid json", error_text()); TEST_ASSERT_EQUAL_INT(0, fake_apply_calls); TEST_ASSERT_EQUAL_INT(0, fake_save_calls); } static void test_post_control_errors(void) { fake_apply_result = NL_CTRL_ERR_NO_RGB; call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"color\":{\"r\":1,\"g\":2,\"b\":3},\"fade_in\":100}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("color not supported", error_text()); TEST_ASSERT_EQUAL_INT(0, fake_save_calls); /* settings not saved after a failed change */ fake_apply_result = NL_CTRL_ERR_COLOR_AND_CT; call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"color\":{\"r\":1,\"g\":2,\"b\":3},\"color_temp\":3000}"); TEST_ASSERT_EQUAL_STRING("color and color_temp are exclusive", error_text()); } static void test_post_settings_fields(void) { call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"power_on_behaviour\":\"on\",\"fade_in\":1000,\"fade_out\":0,\"transition\":300}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(0, fake_apply_calls); /* transition alone is ignored */ TEST_ASSERT_EQUAL_INT(1, fake_save_calls); TEST_ASSERT_EQUAL_UINT8(NL_POB_ON, fake_settings.power_on_behaviour); TEST_ASSERT_EQUAL_UINT16(1000, fake_settings.fade_in_ms); TEST_ASSERT_EQUAL_UINT16(0, fake_settings.fade_out_ms); TEST_ASSERT_EQUAL_STRING("on", cJSON_GetStringValue(field("power_on_behaviour"))); } static void test_post_transition_default_and_wake(void) { call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"transition_default\":1200,\"wake_on_change\":true}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(1, fake_save_calls); TEST_ASSERT_EQUAL_UINT16(1200, fake_settings.transition_ms); TEST_ASSERT_EQUAL_UINT8(1, fake_settings.wake_on_change); TEST_ASSERT_EQUAL_INT(1200, field("transition_default")->valueint); TEST_ASSERT_TRUE(cJSON_IsTrue(field("wake_on_change"))); } /* A colour and a per-call transition travel in the same request - the light change is applied, and nothing is treated as a settings write. */ static void test_post_color_with_transition(void) { call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"color\":{\"r\":255,\"g\":0,\"b\":0},\"transition\":2000}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(1, fake_apply_calls); TEST_ASSERT_EQUAL_INT(0, fake_save_calls); TEST_ASSERT_TRUE(fake_last_change.has_transition); TEST_ASSERT_EQUAL_UINT32(2000, fake_last_change.transition_ms); TEST_ASSERT_TRUE(fake_last_change.has_color); TEST_ASSERT_EQUAL_UINT8(255, fake_last_change.r); } static void test_post_save_failure(void) { fake_save_result = -1; call(NL_HTTP_POST, "/api/v1/device/state", NULL, "{\"fade_in\":100}"); TEST_ASSERT_EQUAL_INT(500, resp.status); TEST_ASSERT_EQUAL_STRING("save failed", error_text()); } static void test_auth_all_protects_device_state(void) { fake_settings.auth_all = 1; call(NL_HTTP_GET, "/api/v1/device/state", NULL, NULL); TEST_ASSERT_EQUAL_INT(401, resp.status); TEST_ASSERT_EQUAL_STRING("unauthorized", error_text()); call(NL_HTTP_GET, "/api/v1/device/state", TOKEN, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); fake_open = true; call(NL_HTTP_GET, "/api/v1/device/state", NULL, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); } static void test_auth_too_long_is_unauthenticated(void) { /* M4: even a token that starts with the real one but was truncated at NL_TOKEN_LEN - 1 * (i.e. the header carried more than that) must not authenticate. */ nl_http_request_t r; memset(&r, 0, sizeof r); r.method = NL_HTTP_GET; strcpy(r.path, "/api/v1/system/settings"); strcpy(r.auth, TOKEN); /* would match on its own */ r.auth_too_long = true; nl_api_handle(&fake_ctx, &r, &resp); TEST_ASSERT_EQUAL_INT(401, resp.status); } /* --- system/info --- */ static void test_info_is_open(void) { const cJSON *w; call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_STRING("evk", cJSON_GetStringValue(field("device_type"))); TEST_ASSERT_EQUAL_STRING("W600 EVK v1.2", cJSON_GetStringValue(field("product_name"))); TEST_ASSERT_EQUAL_STRING("1.2.3", cJSON_GetStringValue(field("fw_version"))); TEST_ASSERT_EQUAL_STRING("2026-09-11", cJSON_GetStringValue(field("build_date"))); TEST_ASSERT_EQUAL_STRING("28:6d:cd:5c:68:1f", cJSON_GetStringValue(field("mac"))); TEST_ASSERT_EQUAL_STRING("Wohnzimmer", cJSON_GetStringValue(field("name"))); w = field("wifi"); TEST_ASSERT_EQUAL_STRING("sta", cJSON_GetStringValue(cJSON_GetObjectItem(w, "mode"))); TEST_ASSERT_EQUAL_STRING("ninHome", cJSON_GetStringValue(cJSON_GetObjectItem(w, "ssid"))); TEST_ASSERT_EQUAL_INT(-58, cJSON_GetObjectItem(w, "rssi")->valueint); TEST_ASSERT_EQUAL_STRING("192.168.1.50", cJSON_GetStringValue(cJSON_GetObjectItem(w, "ip"))); TEST_ASSERT_TRUE(cJSON_IsNull(cJSON_GetObjectItem(w, "sta_ip"))); TEST_ASSERT_TRUE(cJSON_IsNull(cJSON_GetObjectItem(w, "error"))); TEST_ASSERT_EQUAL_INT(123, field("uptime")->valueint); TEST_ASSERT_EQUAL_INT(40000, field("free_heap")->valueint); TEST_ASSERT_NULL(cJSON_GetObjectItem(doc, "reset_reason")); TEST_ASSERT_TRUE(cJSON_IsNull(field("last_ota_error"))); } static void test_info_with_sta_ip_and_error(void) { strcpy(fake_info.sta_ip, "192.168.1.50"); fake_info.wifi_error = "auth"; call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL); TEST_ASSERT_EQUAL_STRING("192.168.1.50", cJSON_GetStringValue(cJSON_GetObjectItem(field("wifi"), "sta_ip"))); TEST_ASSERT_EQUAL_STRING("auth", cJSON_GetStringValue(cJSON_GetObjectItem(field("wifi"), "error"))); } /* --- system/settings --- */ static void test_settings_auth(void) { call(NL_HTTP_GET, "/api/v1/system/settings", NULL, NULL); TEST_ASSERT_EQUAL_INT(401, resp.status); call(NL_HTTP_GET, "/api/v1/system/settings", "aabb", NULL); TEST_ASSERT_EQUAL_INT(401, resp.status); call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN "x", NULL); TEST_ASSERT_EQUAL_INT(401, resp.status); call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_STRING("Wohnzimmer", cJSON_GetStringValue(field("name"))); TEST_ASSERT_EQUAL_STRING("ninHome", cJSON_GetStringValue(cJSON_GetObjectItem(field("wifi"), "ssid"))); TEST_ASSERT_EQUAL_STRING("stored", cJSON_GetStringValue(cJSON_GetObjectItem(field("wifi"), "source"))); TEST_ASSERT_TRUE(cJSON_IsFalse(field("auth_all"))); TEST_ASSERT_NULL(cJSON_GetObjectItem(field("wifi"), "password")); /* never exposed */ fake_open = true; call(NL_HTTP_GET, "/api/v1/system/settings", NULL, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); } /* F1: nie onboardet -> gespeicherte ssid ist leer, die Lampe laeuft auf den * einkompilierten Zugangsdaten (siehe app_main.c). "source" muss das additiv * sichtbar machen, ohne "ssid" umzudeuten - sonst widerspricht die Antwort * scheinbar system/info, das die tatsaechlich verbundene SSID zeigt. */ static void test_settings_wifi_source_reflects_compiled_fallback(void) { fake_settings.wifi_ssid[0] = '\0'; call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_STRING("", cJSON_GetStringValue(cJSON_GetObjectItem(field("wifi"), "ssid"))); TEST_ASSERT_EQUAL_STRING("compiled", cJSON_GetStringValue(cJSON_GetObjectItem(field("wifi"), "source"))); } static void test_settings_wifi(void) { call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"ninIOT\",\"password\":\"secret12\"}"); TEST_ASSERT_EQUAL_INT(202, resp.status); TEST_ASSERT_EQUAL_STRING("connecting", cJSON_GetStringValue(field("status"))); TEST_ASSERT_EQUAL_STRING("ninIOT", fake_wifi_ssid); TEST_ASSERT_EQUAL_STRING("secret12", fake_wifi_pass); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"open\"}"); TEST_ASSERT_EQUAL_INT(202, resp.status); TEST_ASSERT_EQUAL_STRING("", fake_wifi_pass); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"123456789012345678901234567890123\"}"); TEST_ASSERT_EQUAL_STRING("invalid ssid", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"x\",\"password\":5}"); TEST_ASSERT_EQUAL_STRING("invalid password", error_text()); /* M2: password must be empty (open network) or 8..64 chars, never 1..7 */ call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"x\",\"password\":\"short\"}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("invalid password", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"x\",\"password\":\"\"}"); TEST_ASSERT_EQUAL_INT(202, resp.status); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"x\",\"password\":\"12345678\"}"); TEST_ASSERT_EQUAL_INT(202, resp.status); fake_wifi_result = -1; call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"wifi\",\"ssid\":\"x\"}"); TEST_ASSERT_EQUAL_INT(503, resp.status); TEST_ASSERT_EQUAL_STRING("wifi busy", error_text()); } static void test_settings_name_token_auth(void) { call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"device_name\",\"name\":\"Küche\"}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_TRUE(cJSON_IsTrue(field("ok"))); TEST_ASSERT_EQUAL_STRING("Küche", fake_settings.name); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"device_name\",\"name\":\"\"}"); TEST_ASSERT_EQUAL_STRING("invalid name", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"device_name\",\"name\":\"123456789012345678901234567890123\"}"); TEST_ASSERT_EQUAL_STRING("invalid name", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"token\",\"token\":\"1234567\"}"); TEST_ASSERT_EQUAL_STRING("invalid token", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"token\",\"token\":\"12345678\"}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_STRING("12345678", fake_settings.token); call(NL_HTTP_PUT, "/api/v1/system/settings", "12345678", "{\"type\":\"auth\",\"all\":true}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_UINT8(1, fake_settings.auth_all); call(NL_HTTP_PUT, "/api/v1/system/settings", "12345678", "{\"type\":\"auth\",\"all\":\"yes\"}"); TEST_ASSERT_EQUAL_STRING("invalid all", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", "12345678", "{\"type\":\"colour\"}"); TEST_ASSERT_EQUAL_STRING("invalid type", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", "12345678", "{}"); TEST_ASSERT_EQUAL_STRING("invalid type", error_text()); fake_save_result = -1; call(NL_HTTP_PUT, "/api/v1/system/settings", "12345678", "{\"type\":\"device_name\",\"name\":\"x\"}"); TEST_ASSERT_EQUAL_INT(500, resp.status); } /* --- system/settings: sacn --- */ static const cJSON *sacn_field(const char *name) { const cJSON *s = cJSON_GetObjectItemCaseSensitive(doc, "sacn"); return cJSON_GetObjectItemCaseSensitive(s, name); } static void test_settings_exposes_sacn_block(void) { call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("enabled"))); TEST_ASSERT_EQUAL_INT(1, (int)cJSON_GetNumberValue(sacn_field("universe"))); TEST_ASSERT_EQUAL_INT(1, (int)cJSON_GetNumberValue(sacn_field("start_slot"))); TEST_ASSERT_EQUAL_STRING("release", cJSON_GetStringValue(sacn_field("on_timeout"))); TEST_ASSERT_EQUAL_INT(6, (int)cJSON_GetNumberValue(sacn_field("slots"))); } static void test_settings_sacn_block_reflects_stored_values(void) { fake_settings.sacn_enabled = 0; fake_settings.sacn_universe = 12; fake_settings.sacn_start_slot = 7; fake_settings.sacn_hold = 1; call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL); TEST_ASSERT_FALSE(cJSON_IsTrue(sacn_field("enabled"))); TEST_ASSERT_EQUAL_INT(12, (int)cJSON_GetNumberValue(sacn_field("universe"))); TEST_ASSERT_EQUAL_INT(7, (int)cJSON_GetNumberValue(sacn_field("start_slot"))); TEST_ASSERT_EQUAL_STRING("hold", cJSON_GetStringValue(sacn_field("on_timeout"))); } static void test_put_sacn_all_fields(void) { call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"enabled\":false,\"universe\":300," "\"start_slot\":100,\"on_timeout\":\"hold\"}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(1, fake_save_calls); TEST_ASSERT_EQUAL_UINT8(0, fake_settings.sacn_enabled); TEST_ASSERT_EQUAL_UINT16(300, fake_settings.sacn_universe); TEST_ASSERT_EQUAL_UINT16(100, fake_settings.sacn_start_slot); TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_hold); } static void test_put_sacn_single_field_leaves_rest(void) { fake_settings.sacn_universe = 9; fake_settings.sacn_hold = 1; call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":50}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_UINT16(50, fake_settings.sacn_start_slot); TEST_ASSERT_EQUAL_UINT16(9, fake_settings.sacn_universe); TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_hold); } static void test_put_sacn_rejects_bad_values(void) { call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"enabled\":\"ja\"}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("invalid enabled", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"universe\":0}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("invalid universe", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"universe\":64000}"); TEST_ASSERT_EQUAL_STRING("invalid universe", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":0}"); TEST_ASSERT_EQUAL_STRING("invalid start_slot", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"on_timeout\":\"halten\"}"); TEST_ASSERT_EQUAL_STRING("invalid on_timeout", error_text()); TEST_ASSERT_EQUAL_INT(0, fake_save_calls); /* nichts davon darf gespeichert werden */ } /* universe/start_slot muessen ueber get_int() laufen wie jedes andere Zahlenfeld in dieser * Datei - ein Bruchwert darf nicht klammheimlich auf einen Integer abgeschnitten werden. */ static void test_put_sacn_rejects_fractional_numbers(void) { call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"universe\":12.5}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("invalid universe", error_text()); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":2.5}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("invalid start_slot", error_text()); TEST_ASSERT_EQUAL_INT(0, fake_save_calls); } /* Ein Board mit RGB braucht 6 Slots: ab 508 passt der Satz nicht mehr ins Universe. */ static void test_put_sacn_start_slot_must_leave_room_for_all_slots(void) { call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":507}"); TEST_ASSERT_EQUAL_INT(200, resp.status); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":508}"); TEST_ASSERT_EQUAL_INT(400, resp.status); TEST_ASSERT_EQUAL_STRING("invalid start_slot", error_text()); } static void test_put_sacn_start_slot_limit_follows_the_board(void) { fake_api_reset("g95_cw"); /* drei Slots: 510 passt noch */ call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":510}"); TEST_ASSERT_EQUAL_INT(200, resp.status); call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":511}"); TEST_ASSERT_EQUAL_INT(400, resp.status); } static void test_info_exposes_sacn_state(void) { fake_info.sacn_active = true; fake_info.sacn_joined = true; fake_settings.sacn_universe = 5; call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("active"))); TEST_ASSERT_EQUAL_INT(5, (int)cJSON_GetNumberValue(sacn_field("universe"))); TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("multicast"))); } /* --- system/command --- */ static void test_commands(void) { call(NL_HTTP_GET, "/api/v1/system/command", TOKEN, NULL); TEST_ASSERT_EQUAL_INT(405, resp.status); call(NL_HTTP_PUT, "/api/v1/system/command", NULL, "{\"command\":{\"type\":\"ping\"}}"); TEST_ASSERT_EQUAL_INT(401, resp.status); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"ping\"}}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(1, fake_ping_calls); TEST_ASSERT_EQUAL(NL_API_AFTER_NONE, resp.after); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"reboot\"}}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL(NL_API_AFTER_REBOOT, resp.after); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"factory_reset\"}}"); TEST_ASSERT_EQUAL_INT(200, resp.status); TEST_ASSERT_EQUAL_INT(1, fake_factory_calls); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"fw_update\",\"url\":\"http://x/y.img\"}}"); TEST_ASSERT_EQUAL_INT(409, resp.status); TEST_ASSERT_EQUAL_STRING("an update is already running", error_text()); TEST_ASSERT_EQUAL_STRING("http://x/y.img", fake_fw_url); fake_fw_result = 0; call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"fw_update\",\"url\":\"http://x/y.img\"}}"); TEST_ASSERT_EQUAL_INT(202, resp.status); TEST_ASSERT_EQUAL_STRING("updating", cJSON_GetStringValue(field("status"))); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"fw_update\",\"url\":\"ftp://x\"}}"); TEST_ASSERT_EQUAL_STRING("invalid url", error_text()); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"command\":{\"type\":\"dance\"}}"); TEST_ASSERT_EQUAL_STRING("invalid command", error_text()); call(NL_HTTP_PUT, "/api/v1/system/command", TOKEN, "{\"type\":\"reboot\"}"); TEST_ASSERT_EQUAL_STRING("invalid command", error_text()); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_unknown_path_and_method); RUN_TEST(test_get_state); RUN_TEST(test_white_board_capabilities); RUN_TEST(test_post_state_applies_change); RUN_TEST(test_post_color_and_ct); RUN_TEST(test_post_validation); RUN_TEST(test_post_control_errors); RUN_TEST(test_post_settings_fields); RUN_TEST(test_post_transition_default_and_wake); RUN_TEST(test_post_color_with_transition); RUN_TEST(test_post_save_failure); RUN_TEST(test_auth_all_protects_device_state); RUN_TEST(test_auth_too_long_is_unauthenticated); RUN_TEST(test_info_is_open); RUN_TEST(test_info_with_sta_ip_and_error); RUN_TEST(test_settings_auth); RUN_TEST(test_settings_wifi_source_reflects_compiled_fallback); RUN_TEST(test_settings_wifi); RUN_TEST(test_settings_name_token_auth); RUN_TEST(test_settings_exposes_sacn_block); RUN_TEST(test_settings_sacn_block_reflects_stored_values); RUN_TEST(test_put_sacn_all_fields); RUN_TEST(test_put_sacn_single_field_leaves_rest); RUN_TEST(test_put_sacn_rejects_bad_values); RUN_TEST(test_put_sacn_rejects_fractional_numbers); RUN_TEST(test_put_sacn_start_slot_must_leave_room_for_all_slots); RUN_TEST(test_put_sacn_start_slot_limit_follows_the_board); RUN_TEST(test_info_exposes_sacn_state); RUN_TEST(test_commands); return UNITY_END(); }