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>
647 lines
26 KiB
C++
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();
|
|
}
|