Ein toter Empfangs-Task war von aussen nicht von "es sendet gerade niemand" zu unterscheiden: beides meldete nur active=false. "multicast" half nicht weiter - es zeigt den gemerkten Erfolg des LETZTEN Beitritts und bleibt true, auch wenn der Socket laengst zu ist. Genau dieser Fall trat am Montana-Black-Shield auf (2026-09-26, direkt nach Migration und Onboarding) und hat eine halbe Stunde Suche gekostet, an deren Ende die Ursache immer noch offen war. nl_sacn_running() gab es da schon, es wurde nur nirgends ausgeliefert. Zwei Tests: der bestehende prueft running=true mit, ein neuer haelt den toten Task fest.
632 lines
29 KiB
C
632 lines
29 KiB
C
#include <string.h>
|
|
#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_STRING("rgb", cJSON_GetStringValue(sacn_field("layout")));
|
|
TEST_ASSERT_EQUAL_INT(3, (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)
|
|
{
|
|
fake_settings.sacn_layout = 0; /* die Vorgabe ist rgb mit 3 Slots - hier geht es um 6 */
|
|
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");
|
|
fake_settings.sacn_layout = 0; /* im dimmer-Layout haengt die Slotzahl am Board */
|
|
/* Ein Board ohne Farbe hat auch mit Dimmer nur 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_info.sacn_running = 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")));
|
|
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("running")));
|
|
}
|
|
|
|
/* Ein toter Empfangs-Task sah von aussen genauso aus wie "es sendet gerade niemand":
|
|
* beides meldete nur active=false. Beim Montana-Black-Shield (2026-09-26) hat das eine
|
|
* halbe Stunde Suche gekostet - "multicast" half nicht weiter, weil es den gemerkten
|
|
* Erfolg des LETZTEN Beitritts zeigt und auch dann true bleibt, wenn der Socket laengst
|
|
* zu ist. */
|
|
static void test_info_reports_a_dead_sacn_task(void)
|
|
{
|
|
fake_info.sacn_running = false;
|
|
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
|
TEST_ASSERT_FALSE(cJSON_IsTrue(sacn_field("running")));
|
|
}
|
|
|
|
/* --- 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());
|
|
}
|
|
|
|
/* --- sacn: Belegung --- */
|
|
|
|
static void test_settings_reports_the_layout_and_matching_slots(void)
|
|
{
|
|
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
|
|
/* Vorgabe: rgb - und darin belegt auch ein Board mit Farbe nur drei Slots. */
|
|
TEST_ASSERT_EQUAL_STRING("rgb", cJSON_GetStringValue(sacn_field("layout")));
|
|
TEST_ASSERT_EQUAL_INT(3, (int)cJSON_GetNumberValue(sacn_field("slots")));
|
|
|
|
fake_settings.sacn_layout = 0;
|
|
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
|
|
TEST_ASSERT_EQUAL_STRING("dimmer", cJSON_GetStringValue(sacn_field("layout")));
|
|
TEST_ASSERT_EQUAL_INT(6, (int)cJSON_GetNumberValue(sacn_field("slots")));
|
|
}
|
|
|
|
static void test_put_sacn_layout(void)
|
|
{
|
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"rgb\"}");
|
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
|
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_layout);
|
|
|
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"dimmer\"}");
|
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
|
TEST_ASSERT_EQUAL_UINT8(0, fake_settings.sacn_layout);
|
|
}
|
|
|
|
static void test_put_sacn_rejects_an_unknown_layout(void)
|
|
{
|
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"pixel\"}");
|
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
|
TEST_ASSERT_EQUAL_STRING("invalid layout", error_text());
|
|
TEST_ASSERT_EQUAL_INT(0, fake_save_calls);
|
|
}
|
|
|
|
/* Die Obergrenze fuer start_slot haengt am Layout. Im rgb-Layout (3 Slots) ist 510 erlaubt,
|
|
* mit Dimmer (6 Slots) nicht mehr - ein Wechsel darf einen unzulaessigen Zustand also nicht
|
|
* still herstellen. */
|
|
static void test_put_sacn_start_slot_limit_follows_the_layout(void)
|
|
{
|
|
fake_settings.sacn_layout = 1;
|
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":510}");
|
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
|
TEST_ASSERT_EQUAL_UINT16(510, fake_settings.sacn_start_slot);
|
|
|
|
/* Zurueck auf dimmer wuerde 510 + 6 - 1 = 515 bedeuten: abgelehnt. */
|
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"dimmer\"}");
|
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
|
TEST_ASSERT_EQUAL_STRING("invalid start_slot", error_text());
|
|
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_layout); /* nichts uebernommen */
|
|
}
|
|
|
|
static void test_put_sacn_layout_and_start_slot_together(void)
|
|
{
|
|
/* Beides in einer Anfrage: geprueft wird gegen das NEUE Layout. */
|
|
fake_settings.sacn_layout = 0;
|
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
|
"{\"type\":\"sacn\",\"layout\":\"rgb\",\"start_slot\":510}");
|
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
|
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_layout);
|
|
TEST_ASSERT_EQUAL_UINT16(510, fake_settings.sacn_start_slot);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_settings_reports_the_layout_and_matching_slots);
|
|
RUN_TEST(test_put_sacn_layout);
|
|
RUN_TEST(test_put_sacn_rejects_an_unknown_layout);
|
|
RUN_TEST(test_put_sacn_start_slot_limit_follows_the_layout);
|
|
RUN_TEST(test_put_sacn_layout_and_start_slot_together);
|
|
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_info_reports_a_dead_sacn_task);
|
|
RUN_TEST(test_commands);
|
|
return UNITY_END();
|
|
}
|