Alex2ESP/test/test_bridge_logic/test_main.cpp
David e48f8580f9 Leave room for the answer: ALEX2ESP_MAX_MESSAGE defaults to ALEX2ESP_MAX_DIRECTIVE + 1024
Both limits were 2047 bytes. An answer repeats the correlationToken of its directive, which is most of a large directive, and adds 140 to 170 bytes for every property. So a directive between about 1,750 and 2,047 bytes was accepted, the sketch acted on it, and the answer was refused: the lamp switched and Alexa reported a device that does not respond.

The limit for messages now follows the limit for directives unless it is set: 2047 + 1024 = 3071 bytes, room for six properties on top of the largest directive. Both defines moved to src/AlexaLimits.h, next to each other, with the relation in the comment. The limit also applies to the discovery object of a device, which may now be 3071 bytes.

For a sketch: a report between 2,048 and 3,071 bytes is sent, where it was refused with an error. A project that sets ALEX2ESP_MAX_MESSAGE keeps its value.

Measured with the bridge built for the host against a fake MQTT client (not in the repository), a TurnOn of 2,047 bytes answered with two properties:
  8ea7778      handler ran, send() false, "2374 bytes not sent, the limit is 2047"
  this commit  handler ran, send() true, 2,374 bytes published

Tests: test_largest_directive_can_be_answered puts a directive of ALEX2ESP_MAX_DIRECTIVE bytes through the receive buffer, builds the Response of a lamp to it and checks it against ALEX2ESP_MAX_MESSAGE, also with six properties. 33 host tests pass.

examples/basicLight.cpp for d1_mini, static RAM / flash in bytes: 34,116 / 336,757 -> 34,116 / 336,757, no warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 15:38:46 +00:00

647 lines
26 KiB
C++

// Host tests of the bridge logic (src/AlexaBridgeLogic.cpp): what the MQTT callback decides about the fragments a
// broker delivers, which directives are repeats, what may be published, topics and time stamps.
// pio test -e native
#include <unity.h>
#include <ArduinoJson.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include "AlexaBridgeLogic.h"
#include "AlexaLimits.h"
typedef AlexaDirectiveBuffer::Result Result;
#define ASSERT_RESULT(expected, actual) TEST_ASSERT_EQUAL_INT(static_cast<int>(expected), static_cast<int>(actual))
// A directive as Alex2MQTT publishes it on <root>/<endpointId>/alexaDirective
static std::string directive(const char *name, const char *messageId,
const std::string &correlationToken = "AAAAAAAAAQBnlqNbYnB0dHNmYW5zbGF0ZQ==")
{
return std::string("{\"header\":{\"namespace\":\"Alexa.PowerController\",\"name\":\"") + name +
"\",\"payloadVersion\":\"3\",\"messageId\":\"" + messageId +
"\",\"correlationToken\":\"" + correlationToken + "\"},"
"\"endpoint\":{\"endpointId\":\"ESP-01\",\"cookie\":{}},\"payload\":{}}";
}
static const char ID_1[] = "1bd5d003-31b9-476f-ad03-71d471922820";
static const char ID_2[] = "7c0e1f6a-52d4-4b8e-9a3c-0d9f4e2b6a11";
// Hands the message over as the MQTT client does: pieces of fragmentSize bytes with their offset and the total.
// Returns the result of the last piece; every piece before it has to be INCOMPLETE.
static Result deliver(AlexaDirectiveBuffer &buffer, const std::string &message, size_t fragmentSize)
{
Result result = Result::INCOMPLETE;
for (size_t index = 0; index < message.size(); index += fragmentSize)
{
ASSERT_RESULT(Result::INCOMPLETE, result);
size_t length = message.size() - index < fragmentSize ? message.size() - index : fragmentSize;
result = buffer.append(message.data() + index, length, index, message.size());
}
return result;
}
static void assertWaiting(const AlexaDirectiveBuffer &buffer, const std::string &message)
{
TEST_ASSERT_TRUE(buffer.ready());
TEST_ASSERT_EQUAL_UINT(message.size(), buffer.length());
TEST_ASSERT_EQUAL_STRING(message.c_str(), buffer.data()); // also proves the terminating NUL
}
void setUp() {}
void tearDown() {}
// --- reassembly ---
void test_directive_in_one_piece_is_complete()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::COMPLETE, buffer.append(turnOn.data(), turnOn.size(), 0, turnOn.size()));
assertWaiting(buffer, turnOn);
buffer.release();
TEST_ASSERT_FALSE(buffer.ready());
TEST_ASSERT_NULL(buffer.data());
TEST_ASSERT_EQUAL_UINT(0, buffer.length());
}
void test_directive_in_fragments_is_reassembled_and_parses()
{
std::string turnOn = directive("TurnOn", ID_1);
// Every fragment size from one byte per fragment to the message in two halves
for (size_t fragmentSize = 1; fragmentSize < turnOn.size(); fragmentSize++)
{
AlexaDirectiveBuffer buffer(2047);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, fragmentSize));
assertWaiting(buffer, turnOn);
}
AlexaDirectiveBuffer buffer(2047);
deliver(buffer, turnOn, 100);
JsonDocument parsed;
TEST_ASSERT_TRUE(deserializeJson(parsed, buffer.data(), buffer.length()) == DeserializationError::Ok);
TEST_ASSERT_EQUAL_STRING("TurnOn", parsed["header"]["name"]);
TEST_ASSERT_EQUAL_STRING("ESP-01", parsed["endpoint"]["endpointId"]);
}
void test_nothing_waits_before_the_last_fragment()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 50, 0, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
TEST_ASSERT_NULL(buffer.data());
}
// --- limits ---
void test_directive_at_the_limit_is_accepted()
{
AlexaDirectiveBuffer buffer(2047);
std::string atLimit(2047, 'x');
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, atLimit, 536));
assertWaiting(buffer, atLimit);
}
void test_directive_over_the_limit_is_refused_once()
{
AlexaDirectiveBuffer buffer(2047);
std::string tooLarge(2048, 'x');
ASSERT_RESULT(Result::TOO_LARGE, buffer.append(tooLarge.data(), 536, 0, tooLarge.size()));
// Its other fragments still arrive: dropped without another report
ASSERT_RESULT(Result::IGNORED, buffer.append(tooLarge.data() + 536, 536, 536, tooLarge.size()));
ASSERT_RESULT(Result::IGNORED, buffer.append(tooLarge.data() + 1072, 976, 1072, tooLarge.size()));
TEST_ASSERT_FALSE(buffer.ready());
}
void test_refused_directive_does_not_block_the_next_one()
{
AlexaDirectiveBuffer buffer(2047);
std::string tooLarge(5000, 'x');
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::TOO_LARGE, buffer.append(tooLarge.data(), 1460, 0, tooLarge.size()));
ASSERT_RESULT(Result::IGNORED, buffer.append(tooLarge.data() + 1460, 1460, 1460, tooLarge.size()));
// The connection drops before the rest of it arrives; the next message is a normal directive
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
assertWaiting(buffer, turnOn);
}
void test_empty_message_is_reported()
{
AlexaDirectiveBuffer buffer(2047);
// AsyncMqttClient hands a message without a payload over as (nullptr, 0, 0, 0)
ASSERT_RESULT(Result::EMPTY, buffer.append(nullptr, 0, 0, 0));
TEST_ASSERT_FALSE(buffer.ready());
}
// --- one directive at a time ---
void test_second_directive_before_loop_is_dropped_and_the_first_kept()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
std::string turnOff = directive("TurnOff", ID_2);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
ASSERT_RESULT(Result::BUSY, buffer.append(turnOff.data(), 100, 0, turnOff.size()));
ASSERT_RESULT(Result::IGNORED, buffer.append(turnOff.data() + 100, turnOff.size() - 100, 100, turnOff.size()));
assertWaiting(buffer, turnOn);
// Once loop() has read the first, the buffer takes directives again
buffer.release();
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
assertWaiting(buffer, turnOff);
}
void test_second_directive_of_the_same_size_is_dropped()
{
AlexaDirectiveBuffer buffer(2047);
std::string first = directive("TurnOn", ID_1);
std::string second = directive("TurnOn", ID_2); // same length, another messageId
TEST_ASSERT_EQUAL_UINT(first.size(), second.size());
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100));
ASSERT_RESULT(Result::BUSY, buffer.append(second.data(), second.size(), 0, second.size()));
assertWaiting(buffer, first);
}
// --- the repeat a mirroring broker delivers ---
void test_repeat_of_the_waiting_directive_is_a_duplicate()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
ASSERT_RESULT(Result::DUPLICATE, buffer.append(turnOn.data(), turnOn.size(), 0, turnOn.size()));
ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, turnOn, 64));
assertWaiting(buffer, turnOn);
}
void test_repeat_that_differs_in_a_later_fragment_is_dropped()
{
AlexaDirectiveBuffer buffer(2047);
std::string first = directive("TurnOn", ID_1);
std::string second = first;
second[second.size() - 10] = '#'; // the same up to its last fragment
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data(), 100, 0, second.size()));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data() + 100, 100, 100, second.size()));
ASSERT_RESULT(Result::BUSY, buffer.append(second.data() + 200, second.size() - 200, 200, second.size()));
assertWaiting(buffer, first);
}
void test_repeat_still_arriving_when_the_first_is_read()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
buffer.release(); // loop() ran between two fragments of the repeat
TEST_ASSERT_FALSE(buffer.ready());
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
ASSERT_RESULT(Result::DUPLICATE, buffer.append(turnOn.data() + 200, turnOn.size() - 200, 200, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
// and the buffer is free for the next directive
std::string turnOff = directive("TurnOff", ID_2);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
assertWaiting(buffer, turnOff);
}
void test_directive_that_starts_like_the_one_just_read_is_not_lost()
{
AlexaDirectiveBuffer buffer(2047);
std::string first = directive("TurnOn", ID_1);
std::string second = first;
second[second.size() - 10] = '#';
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data(), 100, 0, second.size()));
buffer.release(); // the first has been read: nothing waits, so the second has a place to go
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(second.data() + 100, 100, 100, second.size()));
ASSERT_RESULT(Result::COMPLETE, buffer.append(second.data() + 200, second.size() - 200, 200, second.size()));
assertWaiting(buffer, second);
}
// --- fragments that do not fit ---
void test_fragment_with_a_gap_drops_the_message()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 150, 50, 150, turnOn.size()));
ASSERT_RESULT(Result::IGNORED, buffer.append(turnOn.data() + 200, turnOn.size() - 200, 200, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100));
assertWaiting(buffer, turnOn);
}
void test_fragment_beyond_the_total_is_refused()
{
AlexaDirectiveBuffer buffer(2047);
std::string message(300, 'x');
// The block holds 200 + 1 bytes; a fragment that would end at 250 must not be copied
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(message.data(), 100, 0, 200));
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(message.data() + 100, 150, 100, 200));
TEST_ASSERT_FALSE(buffer.ready());
// A first fragment longer than its own total
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(message.data(), 300, 0, 200));
TEST_ASSERT_FALSE(buffer.ready());
}
void test_fragment_with_another_total_is_refused()
{
AlexaDirectiveBuffer buffer(2047);
std::string message(300, 'x');
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(message.data(), 100, 0, 200));
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(message.data() + 100, 100, 100, 300));
TEST_ASSERT_FALSE(buffer.ready());
}
void test_fragment_without_a_start_is_refused_once()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
ASSERT_RESULT(Result::IGNORED, buffer.append(turnOn.data() + 200, turnOn.size() - 200, 200, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
}
void test_new_message_replaces_one_that_never_completed()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
std::string turnOff = directive("TurnOff", ID_2);
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
assertWaiting(buffer, turnOff);
}
void test_lost_connection_drops_the_arriving_message_only()
{
AlexaDirectiveBuffer buffer(2047);
std::string turnOn = directive("TurnOn", ID_1);
std::string turnOff = directive("TurnOff", ID_2);
// Nothing waits, a message is arriving
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size()));
buffer.cancelArrival();
ASSERT_RESULT(Result::OUT_OF_ORDER, buffer.append(turnOn.data() + 100, 100, 100, turnOn.size()));
TEST_ASSERT_FALSE(buffer.ready());
// A directive waits, its repeat is arriving
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100));
ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOff.data(), 100, 0, turnOff.size()));
buffer.cancelArrival();
assertWaiting(buffer, turnOff);
}
// --- repeated messageIds ---
void test_repeated_message_id_is_recognised()
{
AlexaRecentIds recent;
TEST_ASSERT_FALSE(recent.seenBefore(ID_1));
TEST_ASSERT_TRUE(recent.seenBefore(ID_1));
TEST_ASSERT_FALSE(recent.seenBefore(ID_2));
TEST_ASSERT_TRUE(recent.seenBefore(ID_1));
TEST_ASSERT_TRUE(recent.seenBefore(ID_2));
}
void test_only_the_last_four_ids_are_remembered()
{
AlexaRecentIds recent;
const char *ids[] = {"id-1", "id-2", "id-3", "id-4", "id-5"};
for (const char *id : ids)
{
TEST_ASSERT_FALSE(recent.seenBefore(id));
}
// id-5 took the place of id-1
TEST_ASSERT_TRUE(recent.seenBefore("id-2"));
TEST_ASSERT_TRUE(recent.seenBefore("id-3"));
TEST_ASSERT_TRUE(recent.seenBefore("id-4"));
TEST_ASSERT_TRUE(recent.seenBefore("id-5"));
TEST_ASSERT_FALSE(recent.seenBefore("id-1"));
}
void test_recognising_a_repeat_does_not_use_a_place()
{
AlexaRecentIds recent;
TEST_ASSERT_FALSE(recent.seenBefore("id-1"));
for (int i = 0; i < 10; i++)
{
TEST_ASSERT_TRUE(recent.seenBefore("id-1"));
}
TEST_ASSERT_FALSE(recent.seenBefore("id-2"));
TEST_ASSERT_FALSE(recent.seenBefore("id-3"));
TEST_ASSERT_FALSE(recent.seenBefore("id-4"));
TEST_ASSERT_TRUE(recent.seenBefore("id-1"));
}
void test_directive_without_a_message_id_is_never_a_repeat()
{
AlexaRecentIds recent;
TEST_ASSERT_FALSE(recent.seenBefore(""));
TEST_ASSERT_FALSE(recent.seenBefore(""));
TEST_ASSERT_FALSE(recent.seenBefore(nullptr));
TEST_ASSERT_FALSE(recent.seenBefore(nullptr));
}
// --- what may be published ---
void test_message_within_the_limit_is_measured()
{
JsonDocument doc;
doc["event"]["header"]["name"] = "Response";
size_t length = 0;
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
TEST_ASSERT_EQUAL_UINT(strlen("{\"event\":{\"header\":{\"name\":\"Response\"}}}"), length);
}
void test_message_at_the_limit_is_accepted_and_one_byte_more_is_refused()
{
// {"v":"xxx...x"} is 8 bytes and the text
JsonDocument doc;
size_t length = 0;
doc["v"] = std::string(2047 - 8, 'x');
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
TEST_ASSERT_EQUAL_UINT(2047, length);
doc["v"] = std::string(2048 - 8, 'x');
ASSERT_RESULT(AlexaSendResult::TOO_LARGE, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
TEST_ASSERT_EQUAL_UINT(2048, length);
}
void test_empty_document_is_not_published()
{
JsonDocument doc;
size_t length = 99;
ASSERT_RESULT(AlexaSendResult::EMPTY, AlexaBridgeLogic::checkMessage(doc, 2047, &length));
TEST_ASSERT_EQUAL_UINT(0, length);
}
// An allocator that runs dry, as the heap of the board does
struct ScarceAllocator : ArduinoJson::Allocator
{
size_t left;
explicit ScarceAllocator(size_t bytes) : left(bytes) {}
void *allocate(size_t size) override
{
if (size > left)
{
return nullptr;
}
left -= size;
return malloc(size);
}
void deallocate(void *pointer) override { free(pointer); }
void *reallocate(void *pointer, size_t size) override
{
if (size > left)
{
return nullptr;
}
left -= size;
return realloc(pointer, size);
}
};
void test_document_that_ran_out_of_memory_is_not_published()
{
ScarceAllocator allocator(8192);
JsonDocument doc(&allocator);
for (int i = 0; i < 200 && !doc.overflowed(); i++)
{
doc["context"]["properties"][i]["value"] = std::string(100, 'x');
}
TEST_ASSERT_TRUE(doc.overflowed());
size_t length = 99;
ASSERT_RESULT(AlexaSendResult::TOO_LARGE, AlexaBridgeLogic::checkMessage(doc, 1000000, &length));
TEST_ASSERT_EQUAL_UINT(0, length); // 0 tells the two reasons for TOO_LARGE apart
}
// --- the limit of a directive and the limit of its answer ---
// One property of a report as AlexaStatusMessage::AddProperty writes it
static void addProperty(JsonArray properties, const char *interfaceName, const char *name, const char *value)
{
JsonObject property = properties.add<JsonObject>();
property["namespace"] = interfaceName;
property["name"] = name;
property["value"] = value;
property["timeOfSample"] = "2026-09-28T13:05:09Z";
property["uncertaintyInMilliseconds"] = 0;
}
// The Response of a lamp as AlexaStatusMessage builds it (src/AlexaStatusMessage.cpp): the correlationToken of the
// directive, the health of the endpoint and the power state
static void buildResponse(JsonDocument &answer, const JsonDocument &received)
{
JsonObject header = answer["event"]["header"].to<JsonObject>();
header["namespace"] = "Alexa";
header["name"] = "Response";
header["payloadVersion"] = "3";
header["messageId"] = "OQpZQ2l8Pr2f9kkS8g6ffwpx7bJgJARngGUEQ"; // 37 characters, as generateMessageId() returns
header["correlationToken"] = received["header"]["correlationToken"];
answer["event"]["endpoint"]["endpointId"] = received["endpoint"]["endpointId"];
answer["event"]["payload"].to<JsonObject>();
JsonArray properties = answer["context"]["properties"].to<JsonArray>();
JsonObject health = properties.add<JsonObject>();
health["namespace"] = "Alexa.EndpointHealth";
health["name"] = "connectivity";
health["value"]["value"] = "OK";
health["timeOfSample"] = "2026-09-28T13:05:09Z";
health["uncertaintyInMilliseconds"] = 0;
addProperty(properties, "Alexa.PowerController", "powerState", "ON");
}
void test_largest_directive_can_be_answered()
{
// The correlationToken is what makes a directive large, and the answer repeats it
std::string withoutToken = directive("TurnOn", ID_1, "");
std::string largest = directive("TurnOn", ID_1, std::string(ALEX2ESP_MAX_DIRECTIVE - withoutToken.size(), 'T'));
TEST_ASSERT_EQUAL_UINT(ALEX2ESP_MAX_DIRECTIVE, largest.size());
AlexaDirectiveBuffer buffer(ALEX2ESP_MAX_DIRECTIVE);
ASSERT_RESULT(Result::COMPLETE, deliver(buffer, largest, 536));
JsonDocument received;
TEST_ASSERT_TRUE(deserializeJson(received, buffer.data(), buffer.length()) == DeserializationError::Ok);
buffer.release();
JsonDocument answer;
buildResponse(answer, received);
size_t length = 0;
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(answer, ALEX2ESP_MAX_MESSAGE, &length));
// The answer is the larger of the two: a limit for messages as low as the limit for directives refuses it,
// after the sketch has switched the lamp
TEST_ASSERT_GREATER_THAN_UINT(ALEX2ESP_MAX_DIRECTIVE, length);
ASSERT_RESULT(AlexaSendResult::TOO_LARGE, AlexaBridgeLogic::checkMessage(answer, ALEX2ESP_MAX_DIRECTIVE, &length));
// The default leaves room for six properties
JsonArray properties = answer["context"]["properties"];
addProperty(properties, "Alexa.BrightnessController", "brightness", "100");
addProperty(properties, "Alexa.ColorTemperatureController", "colorTemperatureInKelvin", "2700");
addProperty(properties, "Alexa.ToggleController", "toggleState", "ON");
addProperty(properties, "Alexa.ToggleController", "toggleState", "OFF");
TEST_ASSERT_EQUAL_UINT(6, properties.size());
ASSERT_RESULT(AlexaSendResult::OK, AlexaBridgeLogic::checkMessage(answer, ALEX2ESP_MAX_MESSAGE, &length));
}
// --- topics ---
void test_endpoint_is_taken_from_the_directive_topic()
{
size_t length = 0;
const char *topic = "AEXAMPLEROOT/ESP-01/alexaDirective";
const char *endpointId = AlexaBridgeLogic::directiveEndpoint(topic, "AEXAMPLEROOT", &length);
TEST_ASSERT_NOT_NULL(endpointId);
TEST_ASSERT_EQUAL_UINT(6, length);
TEST_ASSERT_EQUAL_STRING_LEN("ESP-01", endpointId, length);
TEST_ASSERT_EQUAL_PTR(topic + strlen("AEXAMPLEROOT/"), endpointId);
// An empty root topic (begin() with "") still has the separator
TEST_ASSERT_NOT_NULL(AlexaBridgeLogic::directiveEndpoint("/ESP-01/alexaDirective", "", &length));
TEST_ASSERT_EQUAL_UINT(6, length);
}
void test_other_topics_are_not_directive_topics()
{
size_t length = 0;
// the token topic of the 1.x HTTP fallback, published next to every directive
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective_e", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/discover", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaResponce", "root", &length));
// another root, also one that only starts like ours
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("other/ESP-01/alexaDirective", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("rootless/ESP-01/alexaDirective", "root", &length));
// no endpoint id, more than one level
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root//alexaDirective", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/alexaDirective", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/a/b/alexaDirective", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("", "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint(nullptr, "root", &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective", nullptr, &length));
TEST_ASSERT_NULL(AlexaBridgeLogic::directiveEndpoint("root/ESP-01/alexaDirective", "root", nullptr));
}
// --- time ---
void test_timestamp_is_iso_8601_in_utc()
{
char stamp[ALEXA_TIMESTAMP_SIZE];
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(0, stamp, sizeof(stamp)));
TEST_ASSERT_EQUAL_STRING("1970-01-01T00:00:00Z", stamp);
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(1709210096, stamp, sizeof(stamp)));
TEST_ASSERT_EQUAL_STRING("2024-02-29T12:34:56Z", stamp); // a leap day
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(1790600709, stamp, sizeof(stamp)));
TEST_ASSERT_EQUAL_STRING("2026-09-28T13:05:09Z", stamp);
TEST_ASSERT_TRUE(AlexaBridgeLogic::formatTimestamp(1798761599, stamp, sizeof(stamp)));
TEST_ASSERT_EQUAL_STRING("2026-12-31T23:59:59Z", stamp);
}
void test_timestamp_needs_a_buffer_of_its_size()
{
char small[ALEXA_TIMESTAMP_SIZE - 1];
memset(small, 'x', sizeof(small));
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatTimestamp(1790600709, small, sizeof(small)));
TEST_ASSERT_EQUAL_STRING("", small);
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatTimestamp(1790600709, nullptr, ALEXA_TIMESTAMP_SIZE));
// The year 10000 does not fit the format
char stamp[ALEXA_TIMESTAMP_SIZE + 4];
TEST_ASSERT_FALSE(AlexaBridgeLogic::formatTimestamp(253402300800LL, stamp, sizeof(stamp)));
TEST_ASSERT_EQUAL_STRING("", stamp);
}
void test_clock_counts_as_set_from_2024()
{
TEST_ASSERT_FALSE(AlexaBridgeLogic::clockIsSet(0));
TEST_ASSERT_FALSE(AlexaBridgeLogic::clockIsSet(5)); // seconds since boot, before SNTP has answered
TEST_ASSERT_FALSE(AlexaBridgeLogic::clockIsSet(1704067199)); // 2023-12-31T23:59:59Z
TEST_ASSERT_TRUE(AlexaBridgeLogic::clockIsSet(1704067200)); // 2024-01-01T00:00:00Z
TEST_ASSERT_TRUE(AlexaBridgeLogic::clockIsSet(1790600709));
}
int main(int, char **)
{
UNITY_BEGIN();
RUN_TEST(test_directive_in_one_piece_is_complete);
RUN_TEST(test_directive_in_fragments_is_reassembled_and_parses);
RUN_TEST(test_nothing_waits_before_the_last_fragment);
RUN_TEST(test_directive_at_the_limit_is_accepted);
RUN_TEST(test_directive_over_the_limit_is_refused_once);
RUN_TEST(test_refused_directive_does_not_block_the_next_one);
RUN_TEST(test_empty_message_is_reported);
RUN_TEST(test_second_directive_before_loop_is_dropped_and_the_first_kept);
RUN_TEST(test_second_directive_of_the_same_size_is_dropped);
RUN_TEST(test_repeat_of_the_waiting_directive_is_a_duplicate);
RUN_TEST(test_repeat_that_differs_in_a_later_fragment_is_dropped);
RUN_TEST(test_repeat_still_arriving_when_the_first_is_read);
RUN_TEST(test_directive_that_starts_like_the_one_just_read_is_not_lost);
RUN_TEST(test_fragment_with_a_gap_drops_the_message);
RUN_TEST(test_fragment_beyond_the_total_is_refused);
RUN_TEST(test_fragment_with_another_total_is_refused);
RUN_TEST(test_fragment_without_a_start_is_refused_once);
RUN_TEST(test_new_message_replaces_one_that_never_completed);
RUN_TEST(test_lost_connection_drops_the_arriving_message_only);
RUN_TEST(test_repeated_message_id_is_recognised);
RUN_TEST(test_only_the_last_four_ids_are_remembered);
RUN_TEST(test_recognising_a_repeat_does_not_use_a_place);
RUN_TEST(test_directive_without_a_message_id_is_never_a_repeat);
RUN_TEST(test_message_within_the_limit_is_measured);
RUN_TEST(test_message_at_the_limit_is_accepted_and_one_byte_more_is_refused);
RUN_TEST(test_empty_document_is_not_published);
RUN_TEST(test_document_that_ran_out_of_memory_is_not_published);
RUN_TEST(test_largest_directive_can_be_answered);
RUN_TEST(test_endpoint_is_taken_from_the_directive_topic);
RUN_TEST(test_other_topics_are_not_directive_topics);
RUN_TEST(test_timestamp_is_iso_8601_in_utc);
RUN_TEST(test_timestamp_needs_a_buffer_of_its_size);
RUN_TEST(test_clock_counts_as_set_from_2024);
return UNITY_END();
}