Alex2ESP/test/test_bridge_logic/test_main.cpp
David 8ea777806a Drop the HTTP fallback: directives arrive on <root>/<id>/alexaDirective and reports leave over MQTT
The library fetched every directive over HTTP (GET /Alex2ESP/<token>, the token taken from <root>/<id>/alexaDirective_e) and posted every report back over HTTP. The detour was built in 2024 because AsyncMqttClient hands a message larger than one TCP segment to onMessage in fragments. Alex2MQTT publishes the whole directive on <root>/<id>/alexaDirective, so the fragments are reassembled instead: by index/total, into one heap block of total + 1 bytes that is freed as soon as loop() has parsed it.

Removed: ESP8266HTTPClient with processHttpGet/processHttpPost and their two HTTPClient and two WiFiClient members; the 5-slot packet, topic and receive queues, combinedData, receivePayload and AlexaStatusMessage::outputString (17,264 bytes of .bss); the {MESSAGE_PAYLOAD_SPLIT} and {REPLACE_WITH_DATETIME} conventions; the subscription to the _e token topic; AlexaUtils.cpp (AlexaUtils::printMemoryInfo stays, in the header).

Receive: the bridge subscribes <root>/discover and <root>/+/alexaDirective. The MQTT callback only collects; loop() parses and dispatches, and answers discovery. A directive over ALEX2ESP_MAX_DIRECTIVE (2047) is refused. One directive is in flight: a second one that arrives before loop() ran is dropped. Both are logged. A byte-identical repeat of the waiting directive and a repeat of one of the last four messageIds (kept as 64-bit hashes, 32 bytes) are ignored, because the broker mirror delivers every message twice. The wildcard subscription also delivers the directives of other boards of the account; they are recognised by their topic before anything is allocated.

Publish: measureJson first. A message is refused when it is over ALEX2ESP_MAX_MESSAGE (2047), when its document overflowed, when there is no session, when the packet does not fit the largest free block, or when AsyncMqttClient returns 0. Every refusal is logged and returned as an AlexaSendResult; AlexaStatusMessage::send() keeps its bool. The limit now also applies to the discovery object of a device.

Time: timeOfSample is an ISO 8601 UTC instant from the clock of the board (gmtime_r + snprintf, no strftime). begin() calls configTime(0, 0, "pool.ntp.org", "time.nist.gov") and loop() holds the first connect until the clock is set or 5 s have passed. setTimeSource(false) leaves the clock to the sketch. AddContextProp() fills timeOfSample in when a hand-built property has none or carries the old placeholder.

Log: AlexaLog, with a level at run time (setLogLevel) and a ceiling at build time (ALEX2ESP_LOG_MAX), replaces the Serial prints and the Alex2ESP_DEBUG define; the new receive and publish paths need a line for every refusal. The log and the time stamp call vsnprintf/snprintf with a PROGMEM format and not the _P variants: newlib keeps those in one object with printf_P and sprintf_P, which links the FILE-based printf. Measured on basicLight: 5,776 bytes of flash.

For a sketch: the 1.x API is unchanged, the five examples compile unmodified and without warnings. loop() no longer blocks for two HTTP round trips per directive. send() publishes at once and returns false without a session, where 1.1.0 queued the report. The MQTT session opens from loop(), up to 5 s after begin(). getState() is CONNECTED once both subscriptions are acknowledged. The 1.2.0 section of readme.md lists every change; its transport section is rewritten.

Measured for d1_mini with empty credentials (PlatformIO 6.2.0, espressif8266 4.2.1, Arduino core 3.1.2), static RAM / flash in bytes, 1.1.0 -> this commit:
  basicLight           52,768 / 350,885 -> 34,116 / 336,757
  lightWithBrightness  52,880 / 354,729 -> 34,232 / 340,517
  lightWithColorTemp   53,028 / 355,389 -> 34,380 / 341,177
  tempSensor           52,676 / 349,441 -> 34,024 / 335,457
  blindControl         52,900 / 353,069 -> 34,256 / 338,925
basicLight with -DALEX2ESP_LOG_MAX=0: 34,108 / 333,289. SNTP and the time stamp are 1,848 bytes of the flash figure.

Tests: platformio.ini with [env:native] and test/test_bridge_logic, 32 host tests of the logic in src/AlexaBridgeLogic.cpp (reassembly at every fragment size, both limits, repeats, publish checks, topics, time stamps). They also pass under -fsanitize=address,undefined. .gitignore no longer hides /test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 14:31:57 +00:00

575 lines
22 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"
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)
{
return std::string("{\"header\":{\"namespace\":\"Alexa.PowerController\",\"name\":\"") + name +
"\",\"payloadVersion\":\"3\",\"messageId\":\"" + messageId +
"\",\"correlationToken\":\"AAAAAAAAAQBnlqNbYnB0dHNmYW5zbGF0ZQ==\"},"
"\"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
}
// --- 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_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();
}