AlexaStatusMessage: DeferredResponse, ChangeReport, ErrorResponse, scene and doorbell events, UUID-shaped messageId
A message has a kind, and the kind decides its topic: answers on <root>/<id>/alexaResponce, the answer after a DeferredResponse on <root>/<id>/deferredResponse (sendAsync()), a ChangeReport on <root>/changeReport with the changed properties apart from the others (context()), DoorbellPress on <root>/event. AlexaDirective gains deferred(), error(type, message), sceneStarted() and sceneStopped(); thermostat error types go out in the namespace of the thermostat. The helpers are add*Prop, the Add*Prop names stay; a temperature keeps its scale (69 FAHRENHEIT, was 20.56 CELSIUS). messageId is a version 4 UUID from the hardware random source, where rand() seeded per second gave two messages one id. basicLight: static RAM 30,540 B (-76), flash 334,981 B (+1,280); 112 host tests (bridge logic 52, messages 19, new). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
42af413a6e
commit
3215457f91
12 changed files with 1093 additions and 90 deletions
|
|
@ -882,6 +882,33 @@ void test_wait_is_counted_across_the_overflow_of_millis()
|
|||
TEST_ASSERT_TRUE(backoff.due(744, since));
|
||||
}
|
||||
|
||||
// --- message id ---
|
||||
|
||||
void test_message_id_is_a_uuid_of_version_4()
|
||||
{
|
||||
const uint32_t random[4] = {0x01234567u, 0x89abcdefu, 0xfedcba98u, 0x76543210u};
|
||||
char id[ALEXA_MESSAGE_ID_SIZE];
|
||||
|
||||
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatMessageId(random, id, sizeof(id)));
|
||||
// The digits of the numbers in their order; the 13th is the version, the 17th has the variant in its two
|
||||
// highest bits (f, 1111, becomes b, 1011)
|
||||
TEST_ASSERT_EQUAL_STRING("01234567-89ab-4def-bedc-ba9876543210", id);
|
||||
|
||||
const uint32_t zeros[4] = {0, 0, 0, 0};
|
||||
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatMessageId(zeros, id, sizeof(id)));
|
||||
TEST_ASSERT_EQUAL_STRING("00000000-0000-4000-8000-000000000000", id);
|
||||
}
|
||||
|
||||
void test_message_id_needs_a_buffer_of_its_size()
|
||||
{
|
||||
const uint32_t random[4] = {1, 2, 3, 4};
|
||||
char id[ALEXA_MESSAGE_ID_SIZE] = "untouched";
|
||||
|
||||
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatMessageId(random, id, ALEXA_MESSAGE_ID_SIZE - 1));
|
||||
TEST_ASSERT_EQUAL_STRING("", id);
|
||||
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatMessageId(random, nullptr, ALEXA_MESSAGE_ID_SIZE));
|
||||
}
|
||||
|
||||
// --- time ---
|
||||
|
||||
void test_timestamp_is_iso_8601_in_utc()
|
||||
|
|
@ -984,6 +1011,8 @@ int main(int, char **)
|
|||
RUN_TEST(test_wait_keeps_doubling_across_sessions_that_end_early);
|
||||
RUN_TEST(test_attempt_that_fails_after_a_long_session_does_not_start_the_wait_over);
|
||||
RUN_TEST(test_length_of_a_session_is_counted_across_the_overflow_of_millis);
|
||||
RUN_TEST(test_message_id_is_a_uuid_of_version_4);
|
||||
RUN_TEST(test_message_id_needs_a_buffer_of_its_size);
|
||||
RUN_TEST(test_attempt_is_due_when_the_wait_is_over);
|
||||
RUN_TEST(test_wait_is_counted_across_the_overflow_of_millis);
|
||||
|
||||
|
|
|
|||
519
test/test_messages/test_main.cpp
Normal file
519
test/test_messages/test_main.cpp
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
// Host tests of the messages: what every kind of AlexaStatusMessage carries, byte for byte, and the topic it is
|
||||
// published on (src/AlexaStatusMessage.cpp, the messages of src/AlexaDevice.cpp). The transport of the devices is
|
||||
// a fake that keeps what would have been published.
|
||||
// pio test -e native
|
||||
#include <unity.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "AlexaDevice.h"
|
||||
#include "AlexaLog.h"
|
||||
|
||||
class FakeTransport : public AlexaTransport
|
||||
{
|
||||
public:
|
||||
struct Message
|
||||
{
|
||||
std::string topic;
|
||||
std::string json;
|
||||
};
|
||||
std::vector<Message> sent;
|
||||
|
||||
AlexaSendResult publish(const char *topic, JsonDocument &doc) override
|
||||
{
|
||||
Message message;
|
||||
message.topic = topic;
|
||||
serializeJson(doc, message.json);
|
||||
sent.push_back(message);
|
||||
return AlexaSendResult::OK;
|
||||
}
|
||||
|
||||
void timestamp(char *buffer, size_t size) override { snprintf(buffer, size, "2026-09-28T13:05:09Z"); }
|
||||
};
|
||||
|
||||
class CapturedLog : public Print
|
||||
{
|
||||
public:
|
||||
std::string text;
|
||||
|
||||
size_t write(const uint8_t *buffer, size_t size) override
|
||||
{
|
||||
text.append(reinterpret_cast<const char *>(buffer), size);
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
static void assertLogged(const CapturedLog &log, const char *expected)
|
||||
{
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(log.text.c_str(), expected), log.text.c_str());
|
||||
}
|
||||
|
||||
static void assertIsUuid(const char *id)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_MESSAGE(36, strlen(id), id);
|
||||
for (int i = 0; i < 36; i++)
|
||||
{
|
||||
if (i == 8 || i == 13 || i == 18 || i == 23)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_MESSAGE('-', id[i], id);
|
||||
}
|
||||
else
|
||||
{
|
||||
TEST_ASSERT_TRUE_MESSAGE(isdigit(id[i]) || (id[i] >= 'a' && id[i] <= 'f'), id);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_EQUAL_MESSAGE('4', id[14], id);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strchr("89ab", id[19]), id);
|
||||
}
|
||||
|
||||
// The message as it was published. Its messageId is random: it is checked for its shape and replaced by "id".
|
||||
static std::string published(const FakeTransport::Message &message)
|
||||
{
|
||||
JsonDocument doc;
|
||||
TEST_ASSERT_TRUE(deserializeJson(doc, message.json) == DeserializationError::Ok);
|
||||
assertIsUuid(doc["event"]["header"]["messageId"] | "");
|
||||
doc["event"]["header"]["messageId"] = "id";
|
||||
std::string text;
|
||||
serializeJson(doc, text);
|
||||
return text;
|
||||
}
|
||||
|
||||
#define HEADER_OF(ns, name) "\"header\":{\"namespace\":\"" ns "\",\"name\":\"" name "\",\"payloadVersion\":\"3\",\"messageId\":\"id\""
|
||||
#define TOKEN ",\"correlationToken\":\"token-of-the-directive\"}"
|
||||
#define ENDPOINT ",\"endpoint\":{\"endpointId\":\"ESP-01\"}"
|
||||
#define SAMPLED ",\"timeOfSample\":\"2026-09-28T13:05:09Z\",\"uncertaintyInMilliseconds\":0}"
|
||||
#define HEALTH_OK "{\"namespace\":\"Alexa.EndpointHealth\",\"name\":\"connectivity\",\"value\":{\"value\":\"OK\"}" SAMPLED
|
||||
#define POWER_ON "{\"namespace\":\"Alexa.PowerController\",\"name\":\"powerState\",\"value\":\"ON\"" SAMPLED
|
||||
|
||||
static const char TOKEN_OF_THE_DIRECTIVE[] = "token-of-the-directive";
|
||||
|
||||
// What a handler is given, for the answers a directive builds
|
||||
static AlexaDirective directiveFor(AlexaDevice &device, const char *ns, const char *name)
|
||||
{
|
||||
AlexaDirective directive;
|
||||
directive.device = &device;
|
||||
directive.capability = nullptr;
|
||||
directive.type = AlexaInterfaceType::UNKNOWN;
|
||||
directive.ns = ns;
|
||||
directive.name = name;
|
||||
directive.instance = "";
|
||||
directive.correlationToken = TOKEN_OF_THE_DIRECTIVE;
|
||||
return directive;
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
AlexaLog::setOutput(nullptr);
|
||||
}
|
||||
|
||||
void tearDown(void) {}
|
||||
|
||||
void test_response_carries_the_token_and_the_properties(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
AlexaDirective directive = directiveFor(lamp, "Alexa.PowerController", "TurnOn");
|
||||
|
||||
AlexaStatusMessage message = directive.response();
|
||||
TEST_ASSERT_TRUE(AlexaMessageKind::RESPONSE == message.getKind());
|
||||
TEST_ASSERT_TRUE(message.addHealthProp(EndpointHealth::OK).addPowerControllerProp(PowerController::ON).send());
|
||||
|
||||
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/alexaResponce", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "Response") TOKEN ENDPOINT ",\"payload\":{}},"
|
||||
"\"context\":{\"properties\":[" HEALTH_OK "," POWER_ON "]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_state_report_carries_the_token_and_the_properties(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
AlexaDirective directive = directiveFor(lamp, "Alexa", "ReportState");
|
||||
|
||||
TEST_ASSERT_TRUE(directive.stateReport().addHealthProp(EndpointHealth::OK).addPowerControllerProp(PowerController::ON).send());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/alexaResponce", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "StateReport") TOKEN ENDPOINT ",\"payload\":{}},"
|
||||
"\"context\":{\"properties\":[" HEALTH_OK "," POWER_ON "]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_names_of_1x_build_the_same_properties(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice blind("Blind", "root", "ESP-01", &transport);
|
||||
|
||||
blind.buildStatusMessage(TOKEN_OF_THE_DIRECTIVE, true)
|
||||
.AddHealthProp(EndpointHealth::OK)
|
||||
.AddPowerControllerProp(PowerController::ON)
|
||||
.AddBrightnessControllerProp(40)
|
||||
.AddColorTemperatureControllerProp(2700)
|
||||
.AddToggleControllerProp(PowerController::OFF, "ESP-01.Toggle")
|
||||
.send();
|
||||
blind.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addHealthProp(EndpointHealth::OK)
|
||||
.addPowerControllerProp(PowerController::ON)
|
||||
.addBrightnessControllerProp(40)
|
||||
.addColorTemperatureControllerProp(2700)
|
||||
.addToggleControllerProp("ESP-01.Toggle", PowerController::OFF)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL(2, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "Response") TOKEN ENDPOINT ",\"payload\":{}},"
|
||||
"\"context\":{\"properties\":[" HEALTH_OK "," POWER_ON ","
|
||||
"{\"namespace\":\"Alexa.BrightnessController\",\"name\":\"brightness\",\"value\":40" SAMPLED ","
|
||||
"{\"namespace\":\"Alexa.ColorTemperatureController\",\"name\":\"colorTemperatureInKelvin\",\"value\":2700" SAMPLED ","
|
||||
"{\"namespace\":\"Alexa.ToggleController\",\"name\":\"toggleState\",\"instance\":\"ESP-01.Toggle\",\"value\":\"OFF\"" SAMPLED
|
||||
"]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(published(transport.sent[0]).c_str(), published(transport.sent[1]).c_str());
|
||||
}
|
||||
|
||||
void test_temperature_is_reported_in_the_scale_it_is_given_in(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice sensor("Sensor", "root", "ESP-01", &transport);
|
||||
|
||||
sensor.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)
|
||||
.addTemperatureSensorProp(20.5f)
|
||||
.addTemperatureSensorProp(293.5f, TemperatureSensorScale::KELVIN, 500)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "Response") TOKEN ENDPOINT ",\"payload\":{}},"
|
||||
"\"context\":{\"properties\":["
|
||||
"{\"namespace\":\"Alexa.TemperatureSensor\",\"name\":\"temperature\",\"value\":{\"value\":69,\"scale\":\"FAHRENHEIT\"}" SAMPLED ","
|
||||
"{\"namespace\":\"Alexa.TemperatureSensor\",\"name\":\"temperature\",\"value\":{\"value\":20.5,\"scale\":\"CELSIUS\"}" SAMPLED ","
|
||||
"{\"namespace\":\"Alexa.TemperatureSensor\",\"name\":\"temperature\",\"value\":{\"value\":293.5,\"scale\":\"KELVIN\"},"
|
||||
"\"timeOfSample\":\"2026-09-28T13:05:09Z\",\"uncertaintyInMilliseconds\":500}"
|
||||
"]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_property_of_the_sketch_gets_the_time_in_place_of_the_placeholder(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lock("Lock", "root", "ESP-01", &transport);
|
||||
JsonDocument property;
|
||||
property["namespace"] = "Alexa.LockController";
|
||||
property["name"] = "lockState";
|
||||
property["value"] = "LOCKED";
|
||||
property["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
|
||||
property["uncertaintyInMilliseconds"] = 0;
|
||||
|
||||
lock.response(TOKEN_OF_THE_DIRECTIVE).addContextProp(property.as<JsonObject>()).send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "Response") TOKEN ENDPOINT ",\"payload\":{}},"
|
||||
"\"context\":{\"properties\":[{\"namespace\":\"Alexa.LockController\",\"name\":\"lockState\",\"value\":\"LOCKED\"" SAMPLED "]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_deferred_response_has_the_estimate_and_no_context(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lock("Lock", "root", "ESP-01", &transport);
|
||||
AlexaDirective directive = directiveFor(lock, "Alexa.LockController", "Lock");
|
||||
|
||||
AlexaStatusMessage message = directive.deferred(7);
|
||||
TEST_ASSERT_TRUE(AlexaMessageKind::DEFERRED_RESPONSE == message.getKind());
|
||||
TEST_ASSERT_TRUE(message.addHealthProp(EndpointHealth::OK).send());
|
||||
TEST_ASSERT_TRUE(directive.deferred().send());
|
||||
|
||||
TEST_ASSERT_EQUAL(2, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/alexaResponce", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "DeferredResponse") TOKEN ENDPOINT ",\"payload\":{\"estimatedDeferralInSeconds\":7}}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "DeferredResponse") TOKEN ENDPOINT ",\"payload\":{}}}",
|
||||
published(transport.sent[1]).c_str());
|
||||
}
|
||||
|
||||
void test_answer_after_a_deferred_response_goes_to_the_deferred_topic(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lock("Lock", "root", "ESP-01", &transport);
|
||||
|
||||
TEST_ASSERT_TRUE(lock.response(TOKEN_OF_THE_DIRECTIVE).addHealthProp(EndpointHealth::OK).sendAsync());
|
||||
TEST_ASSERT_TRUE(lock.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.asErrorResponse(AlexaErrorType::DEVICE_STUCK, "The bolt does not move")
|
||||
.sendAsync());
|
||||
|
||||
TEST_ASSERT_EQUAL(2, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/deferredResponse", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "Response") TOKEN ENDPOINT ",\"payload\":{}},"
|
||||
"\"context\":{\"properties\":[" HEALTH_OK "]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/deferredResponse", transport.sent[1].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "ErrorResponse") TOKEN ENDPOINT
|
||||
",\"payload\":{\"type\":\"DEVICE_STUCK\",\"message\":\"The bolt does not move\"}}}",
|
||||
published(transport.sent[1]).c_str());
|
||||
}
|
||||
|
||||
void test_send_async_refuses_what_is_no_answer_to_a_directive(void)
|
||||
{
|
||||
CapturedLog log;
|
||||
AlexaLog::setOutput(&log);
|
||||
FakeTransport transport;
|
||||
AlexaDevice lock("Lock", "root", "ESP-01", &transport);
|
||||
AlexaDirective directive = directiveFor(lock, "Alexa.LockController", "Lock");
|
||||
|
||||
TEST_ASSERT_FALSE(directive.deferred(7).sendAsync());
|
||||
TEST_ASSERT_FALSE(lock.changeReport(AlexaCause::PHYSICAL_INTERACTION).addHealthProp(EndpointHealth::OK).sendAsync());
|
||||
|
||||
TEST_ASSERT_EQUAL(0, transport.sent.size());
|
||||
assertLogged(log, "error: message for ESP-01 not sent: sendAsync() is for the Response, StateReport or "
|
||||
"ErrorResponse that follows a DeferredResponse, use send()");
|
||||
}
|
||||
|
||||
void test_change_report_keeps_the_changed_properties_apart_from_the_others(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
AlexaStatusMessage message = lamp.changeReport(AlexaCause::PHYSICAL_INTERACTION);
|
||||
TEST_ASSERT_TRUE(AlexaMessageKind::CHANGE_REPORT == message.getKind());
|
||||
TEST_ASSERT_TRUE(message.addPowerControllerProp(PowerController::ON).context().addHealthProp(EndpointHealth::OK).send());
|
||||
|
||||
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/changeReport", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "ChangeReport") "}" ENDPOINT
|
||||
",\"payload\":{\"change\":{\"cause\":{\"type\":\"PHYSICAL_INTERACTION\"},\"properties\":[" POWER_ON "]}}},"
|
||||
"\"context\":{\"properties\":[" HEALTH_OK "]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_change_report_names_its_cause(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
const AlexaCause causes[] = {AlexaCause::APP_INTERACTION, AlexaCause::PERIODIC_POLL,
|
||||
AlexaCause::PHYSICAL_INTERACTION, AlexaCause::VOICE_INTERACTION};
|
||||
const char *const names[] = {"APP_INTERACTION", "PERIODIC_POLL", "PHYSICAL_INTERACTION", "VOICE_INTERACTION"};
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
lamp.changeReport(causes[i]).addPowerControllerProp(PowerController::ON).send();
|
||||
JsonDocument report;
|
||||
TEST_ASSERT_TRUE(deserializeJson(report, transport.sent[i].json) == DeserializationError::Ok);
|
||||
TEST_ASSERT_EQUAL_STRING(names[i], report["event"]["payload"]["change"]["cause"]["type"] | "");
|
||||
TEST_ASSERT_EQUAL(0, report["context"]["properties"].size());
|
||||
}
|
||||
}
|
||||
|
||||
void test_change_report_without_a_changed_property_is_refused_with_an_error(void)
|
||||
{
|
||||
CapturedLog log;
|
||||
AlexaLog::setOutput(&log);
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
TEST_ASSERT_FALSE(lamp.changeReport(AlexaCause::PERIODIC_POLL).context().addHealthProp(EndpointHealth::OK).send());
|
||||
|
||||
TEST_ASSERT_EQUAL(0, transport.sent.size());
|
||||
assertLogged(log, "error: change report for ESP-01 not sent: it has no property that changed, add one before context()");
|
||||
}
|
||||
|
||||
void test_error_response_has_the_type_the_message_and_what_the_sketch_adds(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
AlexaDirective directive = directiveFor(lamp, "Alexa.BrightnessController", "SetBrightness");
|
||||
|
||||
AlexaStatusMessage message = directive.error(AlexaErrorType::VALUE_OUT_OF_RANGE, "The brightness is 0 to 100");
|
||||
TEST_ASSERT_TRUE(AlexaMessageKind::ERROR_RESPONSE == message.getKind());
|
||||
message.payload()["validRange"]["minimumValue"] = 0;
|
||||
message.payload()["validRange"]["maximumValue"] = 100;
|
||||
TEST_ASSERT_TRUE(message.addHealthProp(EndpointHealth::OK).send());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/alexaResponce", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "ErrorResponse") TOKEN ENDPOINT
|
||||
",\"payload\":{\"type\":\"VALUE_OUT_OF_RANGE\",\"message\":\"The brightness is 0 to 100\","
|
||||
"\"validRange\":{\"minimumValue\":0,\"maximumValue\":100}}}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_error_of_a_thermostat_is_sent_in_the_namespace_of_the_thermostat(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||
AlexaDirective directive = directiveFor(thermostat, "Alexa.ThermostatController", "SetTargetTemperature");
|
||||
|
||||
directive.error(AlexaErrorType::REQUESTED_SETPOINTS_TOO_CLOSE, "The setpoints are 2 degrees apart or more").send();
|
||||
directive.error(AlexaErrorType::UNWILLING_TO_SET_VALUE, "The window is open").send();
|
||||
directive.error(AlexaErrorType::VALUE_OUT_OF_RANGE, "10 to 30").send();
|
||||
directive.error(AlexaErrorType::ALREADY_IN_OPERATION, "Heating").send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa.ThermostatController", "ErrorResponse") TOKEN ENDPOINT
|
||||
",\"payload\":{\"type\":\"REQUESTED_SETPOINTS_TOO_CLOSE\",\"message\":\"The setpoints are 2 degrees apart or more\"}}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa.ThermostatController", "ErrorResponse") TOKEN ENDPOINT
|
||||
",\"payload\":{\"type\":\"UNWILLING_TO_SET_VALUE\",\"message\":\"The window is open\"}}}",
|
||||
published(transport.sent[1]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "ErrorResponse") TOKEN ENDPOINT
|
||||
",\"payload\":{\"type\":\"VALUE_OUT_OF_RANGE\",\"message\":\"10 to 30\"}}}",
|
||||
published(transport.sent[2]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "ErrorResponse") TOKEN ENDPOINT
|
||||
",\"payload\":{\"type\":\"ALREADY_IN_OPERATION\",\"message\":\"Heating\"}}}",
|
||||
published(transport.sent[3]).c_str());
|
||||
}
|
||||
|
||||
void test_scene_answers_with_its_own_events(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice scene("Movie scene", "root", "ESP-01", &transport);
|
||||
scene.addCapability(AlexaInterfaces::SceneController);
|
||||
AlexaDirective directive = directiveFor(scene, "Alexa.SceneController", "Activate");
|
||||
|
||||
AlexaStatusMessage message = directive.sceneStarted();
|
||||
TEST_ASSERT_TRUE(AlexaMessageKind::SCENE_EVENT == message.getKind());
|
||||
TEST_ASSERT_TRUE(message.send());
|
||||
TEST_ASSERT_TRUE(directive.sceneStopped().send());
|
||||
|
||||
TEST_ASSERT_EQUAL(2, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/alexaResponce", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa.SceneController", "ActivationStarted") TOKEN ENDPOINT
|
||||
",\"payload\":{\"cause\":{\"type\":\"VOICE_INTERACTION\"},\"timestamp\":\"2026-09-28T13:05:09Z\"}},\"context\":{}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("root/ESP-01/alexaResponce", transport.sent[1].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa.SceneController", "DeactivationStarted") TOKEN ENDPOINT
|
||||
",\"payload\":{\"cause\":{\"type\":\"VOICE_INTERACTION\"},\"timestamp\":\"2026-09-28T13:05:09Z\"}},\"context\":{}}",
|
||||
published(transport.sent[1]).c_str());
|
||||
}
|
||||
|
||||
void test_doorbell_press_goes_to_the_event_topic(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice doorbell("Doorbell", "root", "ESP-01", &transport);
|
||||
doorbell.addCapability(AlexaInterfaces::DoorbellEventSource);
|
||||
|
||||
AlexaStatusMessage message = doorbell.doorbellPress();
|
||||
TEST_ASSERT_TRUE(AlexaMessageKind::EVENT == message.getKind());
|
||||
TEST_ASSERT_TRUE(message.send());
|
||||
|
||||
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/event", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa.DoorbellEventSource", "DoorbellPress") "}" ENDPOINT
|
||||
",\"payload\":{\"cause\":{\"type\":\"PHYSICAL_INTERACTION\"},\"timestamp\":\"2026-09-28T13:05:09Z\"}},\"context\":{}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_event_of_an_interface_the_device_lacks_is_reported(void)
|
||||
{
|
||||
CapturedLog log;
|
||||
AlexaLog::setOutput(&log);
|
||||
FakeTransport transport;
|
||||
AlexaDevice button("Button", "root", "ESP-01", &transport);
|
||||
button.addCapability(AlexaInterfaces::PowerController);
|
||||
|
||||
AlexaStatusMessage message = button.event(AlexaInterfaces::WakeOnLANController, "WakeUp");
|
||||
assertLogged(log, "error: ESP-01: event of an interface the device does not have, Alexa will drop it: "
|
||||
"add the capability with addCapability()");
|
||||
TEST_ASSERT_TRUE(message.send());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("root/event", transport.sent[0].topic.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa.WakeOnLANController", "WakeUp") "}" ENDPOINT ",\"payload\":{}},\"context\":{}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_every_message_has_an_id_of_its_own(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
lamp.response(TOKEN_OF_THE_DIRECTIVE).send();
|
||||
lamp.response(TOKEN_OF_THE_DIRECTIVE).send();
|
||||
|
||||
JsonDocument first;
|
||||
JsonDocument second;
|
||||
TEST_ASSERT_TRUE(deserializeJson(first, transport.sent[0].json) == DeserializationError::Ok);
|
||||
TEST_ASSERT_TRUE(deserializeJson(second, transport.sent[1].json) == DeserializationError::Ok);
|
||||
assertIsUuid(first["event"]["header"]["messageId"] | "");
|
||||
TEST_ASSERT_TRUE(strcmp(first["event"]["header"]["messageId"] | "", second["event"]["header"]["messageId"] | "") != 0);
|
||||
}
|
||||
|
||||
static void sendsAChangeReportAndNoAnswer(AlexaDirective &directive)
|
||||
{
|
||||
directive.device->changeReport(AlexaCause::VOICE_INTERACTION).addPowerControllerProp(PowerController::ON).send();
|
||||
}
|
||||
|
||||
void test_change_report_of_a_handler_is_not_its_answer(void)
|
||||
{
|
||||
CapturedLog log;
|
||||
AlexaLog::setOutput(&log);
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||
lamp.onDirective(sendsAChangeReportAndNoAnswer);
|
||||
|
||||
JsonDocument message;
|
||||
message["header"]["namespace"] = "Alexa.PowerController";
|
||||
message["header"]["name"] = "TurnOn";
|
||||
message["header"]["correlationToken"] = TOKEN_OF_THE_DIRECTIVE;
|
||||
message["endpoint"]["endpointId"] = "ESP-01";
|
||||
message["payload"].to<JsonObject>();
|
||||
lamp.handleDirective(message);
|
||||
|
||||
TEST_ASSERT_EQUAL(1, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("root/changeReport", transport.sent[0].topic.c_str());
|
||||
assertLogged(log, "error: ESP-01: the handler sent no answer to Alexa.PowerController.TurnOn");
|
||||
}
|
||||
|
||||
void test_message_is_sent_once(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
AlexaStatusMessage message = lamp.response(TOKEN_OF_THE_DIRECTIVE);
|
||||
TEST_ASSERT_TRUE(message.addHealthProp(EndpointHealth::OK).send());
|
||||
message.send();
|
||||
|
||||
TEST_ASSERT_EQUAL(2, transport.sent.size());
|
||||
TEST_ASSERT_EQUAL_STRING("null", transport.sent[1].json.c_str()); // the bridge refuses it as empty
|
||||
}
|
||||
|
||||
int main(int, char **)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_response_carries_the_token_and_the_properties);
|
||||
RUN_TEST(test_state_report_carries_the_token_and_the_properties);
|
||||
RUN_TEST(test_names_of_1x_build_the_same_properties);
|
||||
RUN_TEST(test_temperature_is_reported_in_the_scale_it_is_given_in);
|
||||
RUN_TEST(test_property_of_the_sketch_gets_the_time_in_place_of_the_placeholder);
|
||||
RUN_TEST(test_deferred_response_has_the_estimate_and_no_context);
|
||||
RUN_TEST(test_answer_after_a_deferred_response_goes_to_the_deferred_topic);
|
||||
RUN_TEST(test_send_async_refuses_what_is_no_answer_to_a_directive);
|
||||
RUN_TEST(test_change_report_keeps_the_changed_properties_apart_from_the_others);
|
||||
RUN_TEST(test_change_report_names_its_cause);
|
||||
RUN_TEST(test_change_report_without_a_changed_property_is_refused_with_an_error);
|
||||
RUN_TEST(test_error_response_has_the_type_the_message_and_what_the_sketch_adds);
|
||||
RUN_TEST(test_error_of_a_thermostat_is_sent_in_the_namespace_of_the_thermostat);
|
||||
RUN_TEST(test_scene_answers_with_its_own_events);
|
||||
RUN_TEST(test_doorbell_press_goes_to_the_event_topic);
|
||||
RUN_TEST(test_event_of_an_interface_the_device_lacks_is_reported);
|
||||
RUN_TEST(test_every_message_has_an_id_of_its_own);
|
||||
RUN_TEST(test_change_report_of_a_handler_is_not_its_answer);
|
||||
RUN_TEST(test_message_is_sent_once);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue