LedFx und Verwandte denken in RGB-Pixeln: Ihr e131-Geraet schreibt je Pixel genau drei Kanaele. Unsere Belegung beginnt mit einem Master-Dimmer, also landet deren erstes Pixel auf Dimmer, R, G - ein rotes Effektbild (255,0,0) hiesse bei uns "Dimmer 255, Rot 0, Gruen 0" und damit Dunkelheit. Von Hand eintragen liess sich eine Lampe schon, sie zeigte aber Unsinn. Der Dimmer bleibt fuer DMX-Pulte richtig, deshalb eine zweite Belegung statt einer anderen: layout = "dimmer" (Vorgabe, unveraendert) oder "rgb" (R,G,B, drei Slots auf jedem Board). Im rgb-Layout ist eine Lampe genau ein Pixel. Ein Board ohne Farbkanaele bekommt trotzdem ein Farbtripel. Genommen wird der groesste der drei Werte auf beide Weisskanaele: die Lampe folgt sichtbar der Helligkeit des Effekts, ohne dass ein Farbmodell in den DMX-Pfad kommt - genau das wird dort sonst bewusst vermieden. Die Obergrenze fuer start_slot haengt jetzt am Layout (512 - slots + 1). Ein Wechsel, der einen bisher zulaessigen start_slot unzulaessig macht, wird abgelehnt statt still verschoben: eine Lampe, die nach einer Einstellung woanders im Universe liegt als angegeben, findet niemand wieder. Das neue Settings-Feld beginnt bei Offset 216, dem sizeof davor, und ist durch eine eigene Pruefung zur Uebersetzungszeit und einen Migrationstest abgesichert - derselbe Fallstrick, der beim Schlussreview fuer sacn_enabled gefunden wurde.
612 lines
28 KiB
C
612 lines
28 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_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());
|
|
}
|
|
|
|
/* --- sacn: Belegung --- */
|
|
|
|
static void test_settings_reports_the_layout_and_matching_slots(void)
|
|
{
|
|
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")));
|
|
|
|
fake_settings.sacn_layout = 1;
|
|
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
|
|
TEST_ASSERT_EQUAL_STRING("rgb", cJSON_GetStringValue(sacn_field("layout")));
|
|
/* Im rgb-Layout belegt auch ein Board mit Farbe nur drei Slots. */
|
|
TEST_ASSERT_EQUAL_INT(3, (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_commands);
|
|
return UNITY_END();
|
|
}
|