// 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 #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"; // 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(); 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); 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(); }