// Host tests of the bridge logic (src/AlexaBridgeLogic.cpp): what the MQTT callback decides about the fragments a // broker delivers, how directives wait for loop(), which of them are repeats, what may be published, topics and // time stamps. // pio test -e native #include #include #include #include #include #include #include "AlexaBridgeLogic.h" #include "AlexaLimits.h" typedef AlexaDirectiveBuffer::Result Result; #define ASSERT_RESULT(expected, actual) TEST_ASSERT_EQUAL_INT(static_cast(expected), static_cast(actual)) // A directive as Alex2MQTT publishes it on //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"; // The limits the bridge passes to its buffer by default static const size_t LARGEST = 2047; static const size_t MOST_BYTES = 4 * 2047; static const size_t PLACES = AlexaDirectiveBuffer::CAPACITY; // The n-th directive of a series: each has a messageId of its own, as each directive of Alexa has static std::string numbered(unsigned n) { char messageId[37]; snprintf(messageId, sizeof(messageId), "00000000-0000-4000-8000-%012u", n); return directive(n % 2 == 0 ? "TurnOn" : "TurnOff", messageId); } // 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, fragmentSize)); assertWaiting(buffer, turnOn); } AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); // 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()); } // --- directives wait for loop() --- void test_directives_wait_in_the_order_they_arrived() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); size_t bytes = 0; for (unsigned n = 0; n < PLACES; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100)); bytes += numbered(n).size(); TEST_ASSERT_EQUAL_UINT(n + 1, buffer.waitingDirectives()); TEST_ASSERT_EQUAL_UINT(bytes, buffer.waitingBytes()); } for (unsigned n = 0; n < PLACES; n++) { assertWaiting(buffer, numbered(n)); buffer.release(); } TEST_ASSERT_FALSE(buffer.ready()); TEST_ASSERT_EQUAL_UINT(0, buffer.waitingDirectives()); TEST_ASSERT_EQUAL_UINT(0, buffer.waitingBytes()); // The places are a ring: they are used again, in the same order for (unsigned round = 1; round <= 3; round++) { for (unsigned n = 0; n < PLACES - 1; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(100 * round + n), 100)); } for (unsigned n = 0; n < PLACES - 1; n++) { assertWaiting(buffer, numbered(100 * round + n)); buffer.release(); } } TEST_ASSERT_FALSE(buffer.ready()); } void test_directive_beyond_the_places_is_dropped_and_the_others_kept() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); for (unsigned n = 0; n < PLACES; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100)); } // The loss is reported once, by the last fragment ASSERT_RESULT(Result::QUEUE_FULL, deliver(buffer, numbered(PLACES), 100)); TEST_ASSERT_EQUAL_UINT(PLACES, buffer.waitingDirectives()); assertWaiting(buffer, numbered(0)); // A directive that was dropped is not taken for a repeat when it comes again and a place is free buffer.release(); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(PLACES), 100)); for (unsigned n = 1; n <= PLACES; n++) { assertWaiting(buffer, numbered(n)); buffer.release(); } TEST_ASSERT_FALSE(buffer.ready()); } void test_place_that_becomes_free_while_a_directive_arrives_is_used() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); for (unsigned n = 0; n < PLACES; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100)); } std::string late = numbered(PLACES); ASSERT_RESULT(Result::INCOMPLETE, buffer.append(late.data(), 100, 0, late.size())); buffer.release(); // loop() ran between two fragments ASSERT_RESULT(Result::INCOMPLETE, buffer.append(late.data() + 100, 100, 100, late.size())); ASSERT_RESULT(Result::COMPLETE, buffer.append(late.data() + 200, late.size() - 200, 200, late.size())); TEST_ASSERT_EQUAL_UINT(PLACES, buffer.waitingDirectives()); } void test_waiting_directives_are_limited_in_bytes() { AlexaDirectiveBuffer buffer(LARGEST, 3000); std::string first(1500, 'a'); std::string second(1500, 'b'); std::string third(1, 'c'); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 536)); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, second, 536)); // 3000 bytes wait: at the limit ASSERT_RESULT(Result::QUEUE_FULL, deliver(buffer, third, 536)); TEST_ASSERT_EQUAL_UINT(2, buffer.waitingDirectives()); TEST_ASSERT_EQUAL_UINT(3000, buffer.waitingBytes()); buffer.release(); TEST_ASSERT_EQUAL_UINT(1500, buffer.waitingBytes()); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, third, 536)); assertWaiting(buffer, second); } void test_empty_queue_takes_the_largest_directive() { // A limit for the waiting bytes under the limit for one directive is raised to it AlexaDirectiveBuffer buffer(LARGEST, 100); std::string largest(LARGEST, 'x'); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, largest, 536)); assertWaiting(buffer, largest); ASSERT_RESULT(Result::QUEUE_FULL, deliver(buffer, std::string("y"), 536)); } // --- the repeat a mirroring broker delivers --- void test_repeat_of_a_waiting_directive_is_discarded() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); 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)); // in other fragments than the first time TEST_ASSERT_EQUAL_UINT(1, buffer.waitingDirectives()); assertWaiting(buffer, turnOn); } void test_repeat_of_a_directive_that_was_read_does_not_hold_up_the_next() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); std::string turnOn = directive("TurnOn", ID_1); std::string turnOff = directive("TurnOff", ID_2); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100)); buffer.release(); // loop() has handled it // Its repeat and the next directive arrive together ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, turnOn, 100)); TEST_ASSERT_FALSE(buffer.ready()); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100)); assertWaiting(buffer, turnOff); } void test_group_of_five_with_repeats_in_one_burst() { TEST_ASSERT_TRUE(PLACES >= 5); // Every directive followed by its repeat AlexaDirectiveBuffer interleaved(LARGEST, MOST_BYTES); for (unsigned n = 0; n < 5; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(interleaved, numbered(n), 536)); ASSERT_RESULT(Result::DUPLICATE, deliver(interleaved, numbered(n), 536)); } TEST_ASSERT_EQUAL_UINT(5, interleaved.waitingDirectives()); for (unsigned n = 0; n < 5; n++) { assertWaiting(interleaved, numbered(n)); interleaved.release(); } // The five directives, then the five repeats AlexaDirectiveBuffer trailing(LARGEST, MOST_BYTES); for (unsigned n = 0; n < 5; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(trailing, numbered(n), 536)); } for (unsigned n = 0; n < 5; n++) { ASSERT_RESULT(Result::DUPLICATE, deliver(trailing, numbered(n), 536)); } TEST_ASSERT_EQUAL_UINT(5, trailing.waitingDirectives()); } void test_repeat_is_recognised_when_no_place_is_free() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); for (unsigned n = 0; n < PLACES; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100)); } // Not a loss, so not QUEUE_FULL ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, numbered(0), 100)); ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, numbered(PLACES - 1), 100)); TEST_ASSERT_EQUAL_UINT(PLACES, buffer.waitingDirectives()); } void test_directive_that_differs_in_one_byte_is_not_a_repeat() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); std::string first = directive("TurnOn", ID_1); std::string second = first; second[second.size() - 10] = '#'; // the same length, the same up to its last fragment ASSERT_RESULT(Result::COMPLETE, deliver(buffer, first, 100)); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, second, 100)); assertWaiting(buffer, first); buffer.release(); assertWaiting(buffer, second); } void test_repeat_is_forgotten_after_as_many_directives_as_are_remembered() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); const unsigned remembered = AlexaRecentHashes::CAPACITY; ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(0), 100)); buffer.release(); for (unsigned n = 1; n < remembered; n++) { ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(n), 100)); buffer.release(); } ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, numbered(0), 100)); // the oldest that is remembered ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(remembered), 100)); // takes its place buffer.release(); ASSERT_RESULT(Result::COMPLETE, deliver(buffer, numbered(0), 100)); } // --- no memory --- static bool memoryLeft = true; static void *scarceMemory(size_t size) { return memoryLeft ? malloc(size) : nullptr; } void test_directive_without_memory_is_reported_by_its_last_fragment() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES, scarceMemory); std::string turnOn = directive("TurnOn", ID_1); memoryLeft = false; ASSERT_RESULT(Result::NO_MEMORY, deliver(buffer, turnOn, 100)); TEST_ASSERT_FALSE(buffer.ready()); // It was not remembered: when it comes again and there is memory, it is handled memoryLeft = true; ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100)); assertWaiting(buffer, turnOn); } void test_repeat_without_memory_is_still_a_repeat() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES, scarceMemory); std::string turnOn = directive("TurnOn", ID_1); memoryLeft = true; ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100)); memoryLeft = false; ASSERT_RESULT(Result::DUPLICATE, deliver(buffer, turnOn, 100)); memoryLeft = true; assertWaiting(buffer, turnOn); } // --- fragments that do not fit --- void test_fragment_with_a_gap_drops_the_message() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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(LARGEST, MOST_BYTES); 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)); TEST_ASSERT_EQUAL_UINT(1, buffer.waitingDirectives()); assertWaiting(buffer, turnOff); } void test_lost_connection_drops_the_arriving_message_only() { AlexaDirectiveBuffer buffer(LARGEST, MOST_BYTES); 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, another one is arriving ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOff, 100)); ASSERT_RESULT(Result::INCOMPLETE, buffer.append(turnOn.data(), 100, 0, turnOn.size())); buffer.cancelArrival(); TEST_ASSERT_EQUAL_UINT(1, buffer.waitingDirectives()); assertWaiting(buffer, turnOff); // What arrived of the message that was cut off does not make the whole message a repeat ASSERT_RESULT(Result::COMPLETE, deliver(buffer, turnOn, 100)); } // --- 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_ids_are_remembered() { AlexaRecentIds recent; const unsigned remembered = AlexaRecentIds::CAPACITY; char id[16]; for (unsigned n = 0; n <= remembered; n++) { snprintf(id, sizeof(id), "id-%u", n); TEST_ASSERT_FALSE(recent.seenBefore(id)); } // The last one took the place of id-0 for (unsigned n = 1; n <= remembered; n++) { snprintf(id, sizeof(id), "id-%u", n); TEST_ASSERT_TRUE(recent.seenBefore(id)); } TEST_ASSERT_FALSE(recent.seenBefore("id-0")); } void test_recognising_a_repeat_does_not_use_a_place() { AlexaRecentIds recent; const unsigned remembered = AlexaRecentIds::CAPACITY; char id[16]; TEST_ASSERT_FALSE(recent.seenBefore("id-0")); for (int i = 0; i < 100; i++) { TEST_ASSERT_TRUE(recent.seenBefore("id-0")); } for (unsigned n = 1; n < remembered; n++) { snprintf(id, sizeof(id), "id-%u", n); TEST_ASSERT_FALSE(recent.seenBefore(id)); } TEST_ASSERT_TRUE(recent.seenBefore("id-0")); } void test_hash_is_fnv_1a_64() { // Test vectors of the FNV reference code TEST_ASSERT_EQUAL_HEX64(0xcbf29ce484222325ULL, AlexaBridgeLogic::hashBytes("", 0)); TEST_ASSERT_EQUAL_HEX64(0xaf63dc4c8601ec8cULL, AlexaBridgeLogic::hashBytes("a", 1)); TEST_ASSERT_EQUAL_HEX64(0x85944171f73967e8ULL, AlexaBridgeLogic::hashBytes("foobar", 6)); // In pieces, as the fragments of a message are hashed uint64_t hash = AlexaBridgeLogic::hashBytes("foo", 3); TEST_ASSERT_EQUAL_HEX64(0x85944171f73967e8ULL, AlexaBridgeLogic::hashBytes("bar", 3, hash)); } 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(); 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(); 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(); JsonArray properties = answer["context"]["properties"].to(); JsonObject health = properties.add(); 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, ALEX2ESP_MAX_QUEUED_BYTES); 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)); } void test_log_prints_a_topic_without_its_root() { TEST_ASSERT_EQUAL_STRING("ESP-01/alexaResponce", AlexaBridgeLogic::topicForLog("root/ESP-01/alexaResponce", "root")); TEST_ASSERT_EQUAL_STRING("discover", AlexaBridgeLogic::topicForLog("root/discover", "root")); TEST_ASSERT_EQUAL_STRING("+/alexaDirective", AlexaBridgeLogic::topicForLog("a/b/+/alexaDirective", "a/b")); // An empty root topic (begin() with "") still has the separator TEST_ASSERT_EQUAL_STRING("ESP-01/alexaResponce", AlexaBridgeLogic::topicForLog("/ESP-01/alexaResponce", "")); } void test_log_never_prints_the_root_topic() { const char *topics[] = {"root", "root/", "rootless/ESP-01/alexaResponce", "other/root/discover", "", nullptr}; for (const char *topic : topics) { const char *printed = AlexaBridgeLogic::topicForLog(topic, "root"); TEST_ASSERT_NOT_NULL(printed); TEST_ASSERT_NULL(strstr(printed, "root/")); TEST_ASSERT_NULL(strstr(printed, "rootless")); } TEST_ASSERT_EQUAL_STRING("", AlexaBridgeLogic::topicForLog("root/", "root")); TEST_ASSERT_NOT_NULL(AlexaBridgeLogic::topicForLog("root/discover", nullptr)); } // --- reconnect --- void test_wait_doubles_from_a_second_to_a_minute() { AlexaReconnectBackoff backoff; const uint32_t expected[] = {1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000, 60000}; for (uint32_t wait : expected) { TEST_ASSERT_EQUAL_UINT32(wait, backoff.wait()); backoff.attempt(); } } void test_session_that_lasted_a_minute_starts_the_wait_over() { AlexaReconnectBackoff backoff; for (int i = 0; i < 10; i++) { backoff.attempt(); } TEST_ASSERT_EQUAL_UINT32(60000, backoff.wait()); backoff.sessionOpened(100000); TEST_ASSERT_EQUAL_UINT32(60000, backoff.wait()); backoff.sessionEnded(160000); TEST_ASSERT_EQUAL_UINT32(1000, backoff.wait()); backoff.attempt(); TEST_ASSERT_EQUAL_UINT32(2000, backoff.wait()); } // A broker that accepts the session and closes it at once, as it does to one of two boards with the same client id void test_wait_keeps_doubling_across_sessions_that_end_early() { AlexaReconnectBackoff backoff; const uint32_t expected[] = {1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000}; uint32_t now = 5000; for (uint32_t wait : expected) { backoff.sessionEnded(now); TEST_ASSERT_EQUAL_UINT32(wait, backoff.wait()); now += wait; backoff.attempt(); now += 200; // until the broker has accepted the session backoff.sessionOpened(now); now += 59999; } } void test_attempt_that_fails_after_a_long_session_does_not_start_the_wait_over() { AlexaReconnectBackoff backoff; backoff.sessionOpened(1000); backoff.sessionEnded(3600000); TEST_ASSERT_EQUAL_UINT32(1000, backoff.wait()); // The broker is gone: every attempt ends without a session, however long it took backoff.attempt(); backoff.sessionEnded(3700000); TEST_ASSERT_EQUAL_UINT32(2000, backoff.wait()); backoff.attempt(); backoff.sessionEnded(3800000); TEST_ASSERT_EQUAL_UINT32(4000, backoff.wait()); } void test_length_of_a_session_is_counted_across_the_overflow_of_millis() { AlexaReconnectBackoff backoff; backoff.attempt(); backoff.sessionOpened(0xFFFFFF00u); // 256 ms before millis() starts again at 0 backoff.sessionEnded(59743); TEST_ASSERT_EQUAL_UINT32(2000, backoff.wait()); backoff.sessionOpened(0xFFFFFF00u); backoff.sessionEnded(59744); TEST_ASSERT_EQUAL_UINT32(1000, backoff.wait()); } void test_attempt_is_due_when_the_wait_is_over() { AlexaReconnectBackoff backoff; TEST_ASSERT_FALSE(backoff.due(5000, 5000)); TEST_ASSERT_FALSE(backoff.due(5999, 5000)); TEST_ASSERT_TRUE(backoff.due(6000, 5000)); backoff.attempt(); TEST_ASSERT_FALSE(backoff.due(6999, 5000)); TEST_ASSERT_TRUE(backoff.due(7000, 5000)); } void test_wait_is_counted_across_the_overflow_of_millis() { AlexaReconnectBackoff backoff; const uint32_t since = 0xFFFFFF00u; // 256 ms before millis() starts again at 0 TEST_ASSERT_FALSE(backoff.due(0xFFFFFFFFu, since)); TEST_ASSERT_FALSE(backoff.due(743, since)); TEST_ASSERT_TRUE(backoff.due(744, since)); } // --- 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_directives_wait_in_the_order_they_arrived); RUN_TEST(test_directive_beyond_the_places_is_dropped_and_the_others_kept); RUN_TEST(test_place_that_becomes_free_while_a_directive_arrives_is_used); RUN_TEST(test_waiting_directives_are_limited_in_bytes); RUN_TEST(test_empty_queue_takes_the_largest_directive); RUN_TEST(test_repeat_of_a_waiting_directive_is_discarded); RUN_TEST(test_repeat_of_a_directive_that_was_read_does_not_hold_up_the_next); RUN_TEST(test_group_of_five_with_repeats_in_one_burst); RUN_TEST(test_repeat_is_recognised_when_no_place_is_free); RUN_TEST(test_directive_that_differs_in_one_byte_is_not_a_repeat); RUN_TEST(test_repeat_is_forgotten_after_as_many_directives_as_are_remembered); RUN_TEST(test_directive_without_memory_is_reported_by_its_last_fragment); RUN_TEST(test_repeat_without_memory_is_still_a_repeat); 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_ids_are_remembered); RUN_TEST(test_recognising_a_repeat_does_not_use_a_place); RUN_TEST(test_hash_is_fnv_1a_64); 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_log_prints_a_topic_without_its_root); RUN_TEST(test_log_never_prints_the_root_topic); RUN_TEST(test_wait_doubles_from_a_second_to_a_minute); RUN_TEST(test_session_that_lasted_a_minute_starts_the_wait_over); 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_attempt_is_due_when_the_wait_is_over); RUN_TEST(test_wait_is_counted_across_the_overflow_of_millis); 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(); }