diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 32ccbf88..9fc43a00 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2114,3 +2114,101 @@ if(PAHO_BUILD_SHARED) PROPERTIES TIMEOUT 540 ) endif() + + +# White-box unit tests for internal modules (Base64, Tree, Proxy parsing, MQTTProperties, utf-8) +# that don't need a broker connection, so link directly against the static library to reach +# internal symbols not exposed by the public MQTTClient/MQTTAsync headers. +if(PAHO_BUILD_STATIC) + add_executable(test_unit_coverage test_unit_coverage.c) + target_link_libraries(test_unit_coverage paho-mqtt3a-static) + + add_test( + NAME test_unit_coverage + COMMAND "test_unit_coverage" + ) +endif() + + +# Black-box tests, using only the public MQTTClient API, that exercise specific +# MQTT 3.1.1/5.0 conformance-statement log lines in the paho.mqtt.testing broker +# that aren't otherwise reached by the rest of the test suite. +if(PAHO_BUILD_STATIC) + add_executable(test_conformance-static test_conformance.c) + target_link_libraries(test_conformance-static paho-mqtt3c-static) + + add_test(NAME test_conformance-1-subscribe_empty-static + COMMAND "test_conformance-static" "--test_no" "1" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-2-keepalive_ping-static + COMMAND "test_conformance-static" "--test_no" "2" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-3-server_keepalive_override-static + COMMAND "test_conformance-static" "--test_no" "3" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-4-zero_length_clientid-static + COMMAND "test_conformance-static" "--test_no" "4" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-5-duplicate_clientid-static + COMMAND "test_conformance-static" "--test_no" "5" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-6-zero_byte_retained_delete-static + COMMAND "test_conformance-static" "--test_no" "6" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-7-unsubscribe_no_match-static + COMMAND "test_conformance-static" "--test_no" "7" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-8-retained_flag_live_publish-static + COMMAND "test_conformance-static" "--test_no" "8" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-9-password_without_username-static + COMMAND "test_conformance-static" "--test_no" "9" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-10-user_property_order-static + COMMAND "test_conformance-static" "--test_no" "10" "--connection" ${MQTT_TEST_BROKER}) + + set_tests_properties( + test_conformance-1-subscribe_empty-static + test_conformance-2-keepalive_ping-static + test_conformance-3-server_keepalive_override-static + test_conformance-4-zero_length_clientid-static + test_conformance-5-duplicate_clientid-static + test_conformance-6-zero_byte_retained_delete-static + test_conformance-7-unsubscribe_no_match-static + test_conformance-8-retained_flag_live_publish-static + test_conformance-9-password_without_username-static + test_conformance-10-user_property_order-static + PROPERTIES TIMEOUT 60 + ) +endif() + +if(PAHO_BUILD_SHARED) + add_executable(test_conformance test_conformance.c) + target_link_libraries(test_conformance paho-mqtt3c) + + add_test(NAME test_conformance-1-subscribe_empty + COMMAND "test_conformance" "--test_no" "1" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-2-keepalive_ping + COMMAND "test_conformance" "--test_no" "2" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-3-server_keepalive_override + COMMAND "test_conformance" "--test_no" "3" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-4-zero_length_clientid + COMMAND "test_conformance" "--test_no" "4" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-5-duplicate_clientid + COMMAND "test_conformance" "--test_no" "5" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-6-zero_byte_retained_delete + COMMAND "test_conformance" "--test_no" "6" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-7-unsubscribe_no_match + COMMAND "test_conformance" "--test_no" "7" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-8-retained_flag_live_publish + COMMAND "test_conformance" "--test_no" "8" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-9-password_without_username + COMMAND "test_conformance" "--test_no" "9" "--connection" ${MQTT_TEST_BROKER}) + add_test(NAME test_conformance-10-user_property_order + COMMAND "test_conformance" "--test_no" "10" "--connection" ${MQTT_TEST_BROKER}) + + set_tests_properties( + test_conformance-1-subscribe_empty + test_conformance-2-keepalive_ping + test_conformance-3-server_keepalive_override + test_conformance-4-zero_length_clientid + test_conformance-5-duplicate_clientid + test_conformance-6-zero_byte_retained_delete + test_conformance-7-unsubscribe_no_match + test_conformance-8-retained_flag_live_publish + test_conformance-9-password_without_username + test_conformance-10-user_property_order + PROPERTIES TIMEOUT 60 + ) +endif() diff --git a/test/test_conformance.c b/test/test_conformance.c new file mode 100644 index 00000000..588523b1 --- /dev/null +++ b/test/test_conformance.c @@ -0,0 +1,792 @@ +/******************************************************************************* + * Copyright (c) 2026 Ian Craggs + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0/ + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * AI Disclosure: This file was partly AI-generated. The AI-generated + * portions are made available under CC0-1.0 and not subject to the + * project's licence. The human contributor has reviewed and verified + * that the code is correct. + * + * SPDX-License-Identifier: EPL-2.0 and CC0-1.0 + * + * Contributors: + * Ian Craggs - initial implementation and documentation + *******************************************************************************/ + +/** + * @file + * Black-box tests, using only the public MQTTClient API, whose purpose is to + * make the paho.mqtt.testing broker (github.com/eclipse-paho/paho.mqtt.testing) + * exercise specific MQTT 3.1.1 / 5.0 conformance-statement log lines in its own + * source that the rest of the C client test suite doesn't otherwise reach: + * + * - MQTT-3.8.3-1 / MQTT5-3.8.3-1 empty subscribe topic list + * - MQTT-3.12.4-1 / MQTT5-3.1.2-20 keepalive PINGREQ/PINGRESP + * - MQTT5-3.1.2-21 server overriding the requested keepalive + * - MQTT5-3.1.3-6 / MQTT5-3.1.3-7 zero-length client id assigned by server + * - MQTT5-3.1.4-3 second client with the same id disconnects the first + * - MQTT-3.3.1-11 zero-byte retained publish deletes a retained message + * - MQTT-3.10.4-5 unsubscribe from a non-subscribed topic still gets an unsuback + * - MQTT-2.1.2-6 / MQTT-2.1.2-10 retained flag on store, cleared flag on live delivery + * - MQTT-3.1.2-22 / MQTT5-3.1.2-22 password set without username + * - MQTT-3.1.3-10 ordering of multiple user properties on a publish + */ + +#include "MQTTClient.h" +#include +#include +#include +#include + +#if !defined(_WINDOWS) + #include + #include +#else + #include + #define setenv(a, b, c) _putenv_s(a, b) +#endif + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) + +void usage(void) +{ + printf("help!!\n"); + exit(EXIT_FAILURE); +} + +struct Options +{ + char* connection; + int verbose; + int test_no; +} options = +{ + "tcp://localhost:1883", + 0, + 0, +}; + +void getopts(int argc, char** argv) +{ + int count = 1; + + while (count < argc) + { + if (strcmp(argv[count], "--test_no") == 0) + { + if (++count < argc) + options.test_no = atoi(argv[count]); + else + usage(); + } + else if (strcmp(argv[count], "--connection") == 0) + { + if (++count < argc) + options.connection = argv[count]; + else + usage(); + } + else if (strcmp(argv[count], "--verbose") == 0) + options.verbose = 1; + count++; + } +} + +#define LOGA_DEBUG 0 +#define LOGA_INFO 1 + +void MyLog(int LOGA_level, char* format, ...) +{ + va_list args; + + if (LOGA_level == LOGA_DEBUG && options.verbose == 0) + return; + + va_start(args, format); + vprintf(format, args); + printf("\n"); + va_end(args); +} + +int tests = 0; +int failures = 0; + +void myassert(char* filename, int lineno, char* description, int value, char* format, ...) +{ + ++tests; + if (!value) + { + va_list args; + + ++failures; + MyLog(LOGA_INFO, "Assertion failed, file %s, line %d, description: %s", filename, lineno, description); + va_start(args, format); + vprintf(format, args); + va_end(args); + } + else + MyLog(LOGA_DEBUG, "Assertion succeeded, file %s, line %d, description: %s", filename, lineno, description); +} + +#define assert(a, b, c, d) myassert(__FILE__, __LINE__, a, b, c, d) + +#if defined(_WIN32) +#define mysleep(ms) Sleep(ms) +#else +#define mysleep(ms) usleep((ms) * 1000L) +#endif + + +/********************************************************************* + +Test1: subscribe with an empty topic list is rejected by the broker + [MQTT-3.8.3-1] / [MQTT5-3.8.3-1] + +*********************************************************************/ +int test_subscribe_empty(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + int MQTTVersions[] = { MQTTVERSION_3_1_1, MQTTVERSION_5 }; + size_t i; + + MyLog(LOGA_INFO, "Starting test - subscribe with empty topic list"); + failures = 0; + + for (i = 0; i < ARRAY_SIZE(MQTTVersions); ++i) + { + createOpts.MQTTVersion = MQTTVersions[i]; + rc = MQTTClient_createWithOptions(&c, options.connection, "conformance_sub_empty", MQTTCLIENT_PERSISTENCE_NONE, + NULL, &createOpts); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 20; + opts.MQTTVersion = MQTTVersions[i]; + opts.cleansession = (MQTTVersions[i] >= MQTTVERSION_5) ? 0 : 1; + opts.cleanstart = (MQTTVersions[i] >= MQTTVERSION_5) ? 1 : 0; + + if (MQTTVersions[i] >= MQTTVERSION_5) + { + MQTTResponse response = MQTTClient_connect5(c, &opts, NULL, NULL); + rc = response.reasonCode; + } + else + rc = MQTTClient_connect(c, &opts); + assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + /* deliberately empty: no topics/qos at all, count == 0 */ + if (MQTTVersions[i] >= MQTTVERSION_5) + { + MQTTResponse response = MQTTClient_subscribeMany5(c, 0, NULL, NULL, NULL, NULL); + rc = response.reasonCode; + } + else + rc = MQTTClient_subscribeMany(c, 0, NULL, NULL); + MyLog(LOGA_INFO, "subscribeMany with 0 topics returned %d for MQTT version %d", rc, MQTTVersions[i]); + + /* the broker considers this a protocol violation and drops the connection without a suback, + * so any subsequent operation on this connection should reflect that */ + mysleep(500); + assert("Connection dropped after empty subscribe", MQTTClient_isConnected(c) == 0, "isConnected was %d\n", + MQTTClient_isConnected(c)); + + MQTTClient_disconnect(c, 0); + MQTTClient_destroy(&c); + } + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test2: an idle connection with a short keepalive causes the client to send + PINGREQ, and the broker to respond with PINGRESP + [MQTT-3.12.4-1] / [MQTT5-3.1.2-20] + +*********************************************************************/ +int test_keepalive_ping(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + int MQTTVersions[] = { MQTTVERSION_3_1_1, MQTTVERSION_5 }; + size_t i; + + MyLog(LOGA_INFO, "Starting test - idle connection triggers keepalive ping"); + failures = 0; + + for (i = 0; i < ARRAY_SIZE(MQTTVersions); ++i) + { + rc = MQTTClient_create(&c, options.connection, "conformance_keepalive", MQTTCLIENT_PERSISTENCE_NONE, NULL); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 2; + opts.MQTTVersion = MQTTVersions[i]; + opts.cleansession = (MQTTVersions[i] >= MQTTVERSION_5) ? 0 : 1; + opts.cleanstart = (MQTTVersions[i] >= MQTTVERSION_5) ? 1 : 0; + + rc = MQTTClient_connect(c, &opts); + assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + /* stay idle for a few keepalive intervals: this is a synchronous-mode client (no + * setCallbacks call), so per the library's documented usage it must call yield() + * or receive() periodically for the internal keepalive ping to actually be sent */ + { + int count; + for (count = 0; count < 60; ++count) + { + MQTTClient_yield(); + mysleep(100); + } + } + + assert("Still connected after idle period covering several keepalive intervals", + MQTTClient_isConnected(c) == 1, "isConnected was %d\n", MQTTClient_isConnected(c)); + + rc = MQTTClient_disconnect(c, 1000); + assert("Good rc from disconnect", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + MQTTClient_destroy(&c); + } + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test3: requesting a keepalive longer than the server's configured maximum + causes the server to return a shorter SERVER_KEEP_ALIVE property + [MQTT5-3.1.2-21] + +*********************************************************************/ +int test_server_keepalive_override(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + MQTTResponse response = MQTTResponse_initializer; + + MyLog(LOGA_INFO, "Starting test - server keepalive override"); + failures = 0; + + createOpts.MQTTVersion = MQTTVERSION_5; + rc = MQTTClient_createWithOptions(&c, options.connection, "conformance_server_ka", MQTTCLIENT_PERSISTENCE_NONE, + NULL, &createOpts); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + /* the test broker's configured serverKeepAlive is 60 seconds; requesting more than that + * should cause it to return SERVER_KEEP_ALIVE in the CONNACK properties */ + opts.keepAliveInterval = 120; + opts.cleanstart = 1; + opts.MQTTVersion = MQTTVERSION_5; + + response = MQTTClient_connect5(c, &opts, NULL, NULL); + assert("Good rc from connect", response.reasonCode == MQTTREASONCODE_SUCCESS, "rc was %d\n", response.reasonCode); + + if (response.properties != NULL) + { + int hasProp = MQTTProperties_hasProperty(response.properties, MQTTPROPERTY_CODE_SERVER_KEEP_ALIVE); + assert("Server returned a keepalive override", hasProp == 1, "hasProperty was %d\n", hasProp); + if (hasProp) + { + int64_t serverKA = MQTTProperties_getNumericValue(response.properties, MQTTPROPERTY_CODE_SERVER_KEEP_ALIVE); + assert("Server keepalive is shorter than requested", serverKA > 0 && serverKA < 120, + "serverKA was %lld\n", (long long)serverKA); + } + } + else + assert("CONNACK properties should be present", 0, "properties was NULL\n", 0); + + MQTTClient_disconnect(c, 1000); + MQTTClient_destroy(&c); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test4: connecting with an empty client id (MQTT 5.0, clean start) causes the + server to assign one and return it in ASSIGNED_CLIENT_IDENTIFIER + [MQTT5-3.1.3-6] / [MQTT5-3.1.3-7] + +*********************************************************************/ +int test_zero_length_clientid(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + MQTTResponse response = MQTTResponse_initializer; + + MyLog(LOGA_INFO, "Starting test - zero length client id"); + failures = 0; + + /* MQTTCLIENT_PERSISTENCE_NONE bypasses the C library's own client-side rejection + * of an empty client id, which only applies to the default (file) persistence */ + createOpts.MQTTVersion = MQTTVERSION_5; + rc = MQTTClient_createWithOptions(&c, options.connection, "", MQTTCLIENT_PERSISTENCE_NONE, NULL, &createOpts); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 20; + opts.cleanstart = 1; + opts.MQTTVersion = MQTTVERSION_5; + + response = MQTTClient_connect5(c, &opts, NULL, NULL); + assert("Good rc from connect", response.reasonCode == MQTTREASONCODE_SUCCESS, "rc was %d\n", response.reasonCode); + + if (response.properties != NULL) + { + MQTTProperty* prop = MQTTProperties_getProperty(response.properties, MQTTPROPERTY_CODE_ASSIGNED_CLIENT_IDENTIFIER); + assert("Server assigned a client identifier", prop != NULL, "prop was %p\n", (void*)prop); + if (prop != NULL) + assert("Assigned client identifier is non-empty", prop->value.data.len > 0, "len was %d\n", prop->value.data.len); + } + else + assert("CONNACK properties should be present", 0, "properties was NULL\n", 0); + + MQTTClient_disconnect(c, 1000); + MQTTClient_destroy(&c); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test5: a second client connecting with the same client id as an existing, + still-connected client causes the server to disconnect the first + [MQTT5-3.1.4-3] + +*********************************************************************/ +static int duplicate_clientid_a_disconnected = 0; + +void duplicate_clientid_disconnected(void* context, MQTTProperties* properties, enum MQTTReasonCodes reasonCode) +{ + MyLog(LOGA_DEBUG, "Callback: disconnected, reason code %d", reasonCode); + duplicate_clientid_a_disconnected = 1; +} + +/* a is put into asynchronous mode (via setCallbacks) so that its background thread + * actually processes the broker's unsolicited DISCONNECT when b takes over its session; + * this messageArrived callback is otherwise unused here */ +int duplicate_clientid_messageArrived(void* context, char* topicName, int topicLen, MQTTClient_message* message) +{ + MQTTClient_free(topicName); + MQTTClient_freeMessage(&message); + return 1; +} + +int test_duplicate_clientid(struct Options options) +{ + int rc; + MQTTClient a, b; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + MQTTResponse response = MQTTResponse_initializer; + const char* clientId = "conformance_dup_clientid"; + + MyLog(LOGA_INFO, "Starting test - duplicate client id disconnects the old client"); + failures = 0; + + createOpts.MQTTVersion = MQTTVERSION_5; + rc = MQTTClient_createWithOptions(&a, options.connection, clientId, MQTTCLIENT_PERSISTENCE_NONE, NULL, &createOpts); + assert("Good rc from create (a)", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + rc = MQTTClient_createWithOptions(&b, options.connection, clientId, MQTTCLIENT_PERSISTENCE_NONE, NULL, &createOpts); + assert("Good rc from create (b)", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 20; + opts.cleanstart = 1; + opts.MQTTVersion = MQTTVERSION_5; + + duplicate_clientid_a_disconnected = 0; + rc = MQTTClient_setCallbacks(a, NULL, NULL, duplicate_clientid_messageArrived, NULL); + assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + rc = MQTTClient_setDisconnected(a, NULL, duplicate_clientid_disconnected); + assert("Good rc from setDisconnected", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + response = MQTTClient_connect5(a, &opts, NULL, NULL); + assert("Good rc from connect (a)", response.reasonCode == MQTTREASONCODE_SUCCESS, "rc was %d\n", response.reasonCode); + assert("a is connected", MQTTClient_isConnected(a) == 1, "isConnected was %d\n", MQTTClient_isConnected(a)); + + response = MQTTClient_connect5(b, &opts, NULL, NULL); + assert("Good rc from connect (b)", response.reasonCode == MQTTREASONCODE_SUCCESS, "rc was %d\n", response.reasonCode); + + { + int count = 0; + while (duplicate_clientid_a_disconnected == 0 && ++count < 30) + mysleep(100); + } + + assert("a's disconnected callback fired when b connected with the same client id", + duplicate_clientid_a_disconnected == 1, "a_disconnected was %d\n", duplicate_clientid_a_disconnected); + assert("b is still connected", MQTTClient_isConnected(b) == 1, "isConnected(b) was %d\n", MQTTClient_isConnected(b)); + + MQTTClient_disconnect(b, 1000); + MQTTClient_destroy(&a); + MQTTClient_destroy(&b); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test6: publishing a zero-byte retained message deletes an existing retained + message on that topic + [MQTT-3.3.1-11] + +*********************************************************************/ +static int zero_byte_retained_messages_arrived = 0; + +int zero_byte_retained_messageArrived(void* context, char* topicName, int topicLen, MQTTClient_message* message) +{ + MyLog(LOGA_DEBUG, "Callback: message received on topic %s, %d bytes, retained %d", + topicName, message->payloadlen, message->retained); + ++zero_byte_retained_messages_arrived; + MQTTClient_free(topicName); + MQTTClient_freeMessage(&message); + return 1; +} + +int test_zero_byte_retained_delete(struct Options options) +{ + int rc; + MQTTClient pub, sub; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_message pubmsg = MQTTClient_message_initializer; + const char* topic = "conformance/zero_byte_retained"; + int count; + + MyLog(LOGA_INFO, "Starting test - zero byte retained message deletes retained message"); + failures = 0; + zero_byte_retained_messages_arrived = 0; + + rc = MQTTClient_create(&pub, options.connection, "conformance_zbr_pub", MQTTCLIENT_PERSISTENCE_NONE, NULL); + assert("Good rc from create (pub)", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + opts.keepAliveInterval = 20; + opts.MQTTVersion = MQTTVERSION_3_1_1; + opts.cleansession = 1; + opts.cleanstart = 0; + rc = MQTTClient_connect(pub, &opts); + assert("Good rc from connect (pub)", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + /* store a real retained message */ + pubmsg.payload = "a retained message"; + pubmsg.payloadlen = (int)strlen(pubmsg.payload); + pubmsg.qos = 1; + pubmsg.retained = 1; + rc = MQTTClient_publishMessage(pub, topic, &pubmsg, NULL); + assert("Good rc from publish of retained message", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + mysleep(500); + + /* now delete it with a zero-byte retained publish */ + pubmsg.payload = NULL; + pubmsg.payloadlen = 0; + rc = MQTTClient_publishMessage(pub, topic, &pubmsg, NULL); + assert("Good rc from publish of zero-byte retained message", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + mysleep(500); + + /* a fresh subscriber should now get nothing retained on this topic */ + rc = MQTTClient_create(&sub, options.connection, "conformance_zbr_sub", MQTTCLIENT_PERSISTENCE_NONE, NULL); + assert("Good rc from create (sub)", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + rc = MQTTClient_setCallbacks(sub, NULL, NULL, zero_byte_retained_messageArrived, NULL); + assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + rc = MQTTClient_connect(sub, &opts); + assert("Good rc from connect (sub)", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + rc = MQTTClient_subscribe(sub, topic, 1); + assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + count = 0; + while (zero_byte_retained_messages_arrived == 0 && ++count < 20) + mysleep(100); + + assert("No retained message should have been delivered after deletion", + zero_byte_retained_messages_arrived == 0, "messages arrived %d\n", zero_byte_retained_messages_arrived); + + MQTTClient_disconnect(pub, 1000); + MQTTClient_disconnect(sub, 1000); + MQTTClient_destroy(&pub); + MQTTClient_destroy(&sub); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test7: unsubscribing from a topic the client was never subscribed to still + results in a normal (non-error) unsuback + [MQTT-3.10.4-5] + +*********************************************************************/ +int test_unsubscribe_no_match(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + MQTTResponse response = MQTTResponse_initializer; + MQTTResponse unsub_response = MQTTResponse_initializer; + + MyLog(LOGA_INFO, "Starting test - unsubscribe with no matching subscription"); + failures = 0; + + createOpts.MQTTVersion = MQTTVERSION_5; + rc = MQTTClient_createWithOptions(&c, options.connection, "conformance_unsub_nomatch", MQTTCLIENT_PERSISTENCE_NONE, + NULL, &createOpts); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 20; + opts.cleanstart = 1; + opts.MQTTVersion = MQTTVERSION_5; + response = MQTTClient_connect5(c, &opts, NULL, NULL); + assert("Good rc from connect", response.reasonCode == MQTTREASONCODE_SUCCESS, "rc was %d\n", response.reasonCode); + + /* [MQTT-3.10.4-5] the server must respond to an unsubscribe of a topic with no matching + * subscription with an UNSUBACK, using reason code 0x11 (No subscription existed) */ + unsub_response = MQTTClient_unsubscribe5(c, "conformance/never_subscribed_to_this", NULL); + assert("Reason code is no subscription existed", unsub_response.reasonCode == MQTTREASONCODE_NO_SUBSCRIPTION_FOUND, + "reasonCode was %d\n", unsub_response.reasonCode); + + MQTTClient_disconnect(c, 1000); + MQTTClient_destroy(&c); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test8: a message published with the retained flag set, while a subscriber is + already subscribed to that topic, is stored as the new retained message + and delivered without the retained flag set on the live forward + [MQTT-2.1.2-6] / [MQTT-2.1.2-10] + +*********************************************************************/ +static int retained_flag_messages_arrived = 0; + +int retained_flag_messageArrived(void* context, char* topicName, int topicLen, MQTTClient_message* message) +{ + MyLog(LOGA_DEBUG, "Callback: message received on topic %s, retained %d", topicName, message->retained); + ++retained_flag_messages_arrived; + MQTTClient_free(topicName); + MQTTClient_freeMessage(&message); + return 1; +} + +int test_retained_flag_live_publish(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_message pubmsg = MQTTClient_message_initializer; + const char* topic = "conformance/retained_flag_live"; + int count; + + MyLog(LOGA_INFO, "Starting test - retain flag on a live publish to an existing subscriber"); + failures = 0; + retained_flag_messages_arrived = 0; + + rc = MQTTClient_create(&c, options.connection, "conformance_retain_live", MQTTCLIENT_PERSISTENCE_NONE, NULL); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + rc = MQTTClient_setCallbacks(c, NULL, NULL, retained_flag_messageArrived, NULL); + assert("Good rc from setCallbacks", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + opts.keepAliveInterval = 20; + opts.MQTTVersion = MQTTVERSION_3_1_1; + opts.cleansession = 1; + opts.cleanstart = 0; + rc = MQTTClient_connect(c, &opts); + assert("Good rc from connect", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + /* subscribe first ... */ + rc = MQTTClient_subscribe(c, topic, 1); + assert("Good rc from subscribe", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + /* ... then publish retained, while still subscribed: a live forward, not a retained replay */ + pubmsg.payload = "live retained publish"; + pubmsg.payloadlen = (int)strlen(pubmsg.payload); + pubmsg.qos = 1; + pubmsg.retained = 1; + rc = MQTTClient_publishMessage(c, topic, &pubmsg, NULL); + assert("Good rc from publish", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + count = 0; + while (retained_flag_messages_arrived == 0 && ++count < 50) + mysleep(100); + + assert("The live retained publish was delivered", retained_flag_messages_arrived == 1, + "messages arrived %d\n", retained_flag_messages_arrived); + + MQTTClient_disconnect(c, 1000); + MQTTClient_destroy(&c); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test9: a CONNECT with a password but no username is not client-side validated + by this library, so it reaches the broker as a non-compliant packet + [MQTT-3.1.2-22] / [MQTT5-3.1.2-22] + +*********************************************************************/ +int test_password_without_username(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + MQTTResponse response = MQTTResponse_initializer; + int MQTTVersions[] = { MQTTVERSION_3_1_1, MQTTVERSION_5 }; + size_t i; + + MyLog(LOGA_INFO, "Starting test - password without username"); + failures = 0; + + for (i = 0; i < ARRAY_SIZE(MQTTVersions); ++i) + { + createOpts.MQTTVersion = MQTTVersions[i]; + rc = MQTTClient_createWithOptions(&c, options.connection, "conformance_pw_no_user", MQTTCLIENT_PERSISTENCE_NONE, + NULL, &createOpts); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 20; + opts.MQTTVersion = MQTTVersions[i]; + opts.cleansession = (MQTTVersions[i] >= MQTTVERSION_5) ? 0 : 1; + opts.cleanstart = (MQTTVersions[i] >= MQTTVERSION_5) ? 1 : 0; + opts.username = NULL; + opts.password = "somepassword"; + + if (MQTTVersions[i] >= MQTTVERSION_5) + response = MQTTClient_connect5(c, &opts, NULL, NULL); + else + { + rc = MQTTClient_connect(c, &opts); + response.reasonCode = (enum MQTTReasonCodes)rc; + } + /* the broker treats this as a malformed CONNECT and drops the connection without + * ever sending a CONNACK, so the client should report the connect as unsuccessful */ + MyLog(LOGA_INFO, "connect with password but no username returned reasonCode %d for MQTT version %d", + response.reasonCode, MQTTVersions[i]); + assert("Connect did not succeed", response.reasonCode != MQTTREASONCODE_SUCCESS, + "reasonCode was %d\n", response.reasonCode); + + MQTTClient_disconnect(c, 0); + MQTTClient_destroy(&c); + } + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +/********************************************************************* + +Test10: a publish with more than one user property causes the broker to + check that it must maintain the order of user properties + [MQTT-3.1.3-10] + +*********************************************************************/ +int test_user_property_order(struct Options options) +{ + int rc; + MQTTClient c; + MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer5; + MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer; + MQTTResponse response = MQTTResponse_initializer; + MQTTProperties props = MQTTProperties_initializer; + MQTTProperty property; + + MyLog(LOGA_INFO, "Starting test - user property order on publish"); + failures = 0; + + createOpts.MQTTVersion = MQTTVERSION_5; + rc = MQTTClient_createWithOptions(&c, options.connection, "conformance_user_props", MQTTCLIENT_PERSISTENCE_NONE, + NULL, &createOpts); + assert("Good rc from create", rc == MQTTCLIENT_SUCCESS, "rc was %d\n", rc); + + opts.keepAliveInterval = 20; + opts.cleanstart = 1; + opts.MQTTVersion = MQTTVERSION_5; + response = MQTTClient_connect5(c, &opts, NULL, NULL); + assert("Good rc from connect", response.reasonCode == MQTTREASONCODE_SUCCESS, "rc was %d\n", response.reasonCode); + + property.identifier = MQTTPROPERTY_CODE_USER_PROPERTY; + property.value.data.data = "key1"; + property.value.data.len = (int)strlen("key1"); + property.value.value.data = "value1"; + property.value.value.len = (int)strlen("value1"); + MQTTProperties_add(&props, &property); + + property.value.data.data = "key2"; + property.value.value.data = "value2"; + MQTTProperties_add(&props, &property); + + response = MQTTClient_publish5(c, "conformance/user_property_order", (int)strlen("payload"), "payload", 0, 0, &props, NULL); + assert("Good rc from publish with multiple user properties", response.reasonCode == MQTTREASONCODE_SUCCESS, + "rc was %d\n", response.reasonCode); + + MQTTProperties_free(&props); + MQTTClient_disconnect(c, 1000); + MQTTClient_destroy(&c); + + MyLog(LOGA_INFO, "TEST: test %s. %d tests run, %d failures.", (failures == 0) ? "passed" : "failed", tests, failures); + return failures; +} + + +int main(int argc, char** argv) +{ + int rc = 0, i; + int (*testfns[])(struct Options) = { NULL, + test_subscribe_empty, + test_keepalive_ping, + test_server_keepalive_override, + test_zero_length_clientid, + test_duplicate_clientid, + test_zero_byte_retained_delete, + test_unsubscribe_no_match, + test_retained_flag_live_publish, + test_password_without_username, + test_user_property_order, + }; + + getopts(argc, argv); + + if (options.test_no == 0) + { + for (options.test_no = 1; options.test_no < (int)ARRAY_SIZE(testfns); ++options.test_no) + rc += testfns[options.test_no](options); + } + else + rc = testfns[options.test_no](options); + + if (rc == 0) + MyLog(LOGA_INFO, "verdict pass"); + else + MyLog(LOGA_INFO, "verdict fail"); + + return rc; +} diff --git a/test/test_unit_coverage.c b/test/test_unit_coverage.c new file mode 100644 index 00000000..9adebf15 --- /dev/null +++ b/test/test_unit_coverage.c @@ -0,0 +1,601 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corp. and Ian Craggs + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * https://www.eclipse.org/legal/epl-2.0/ + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * AI Disclosure: This file was partly AI-generated. The AI-generated + * portions are made available under CC0-1.0 and not subject to the + * project's licence. The human contributor has reviewed and verified + * that the code is correct. + * + * SPDX-License-Identifier: EPL-2.0 and CC0-1.0 + * + * Contributors: + *. Ian Craggs - initial implementation and documentation + *******************************************************************************/ + +/** + * @file + * White-box unit tests for internal modules that don't need a live broker + * connection: Base64, Tree, Proxy (parsing only), MQTTProperties and utf-8. + * These call internal functions directly (not the public MQTTClient/MQTTAsync + * API), targeting error/edge paths that the broker-driven integration tests + * don't reach. + */ + +#include +#include +#include + +#include "MQTTAsync.h" +#include "Base64.h" +#include "Tree.h" +#include "Proxy.h" +#include "MQTTProperties.h" +#include "MQTTPacket.h" +#include "utf-8.h" +#include "Heap.h" /* redefines malloc/free as mymalloc/myfree, matching how Proxy.c allocates */ + +/* internal functions with external linkage that aren't declared in the public headers */ +Node* TreeFindContentIndex(Tree* aTree, void* key, int index); +Node* TreeNextElementIndex(Tree* aTree, Node* curnode, int index); +void* TreeRemoveIndex(Tree* aTree, void* content, int index); +int MQTTProperty_write(char** pptr, MQTTProperty* prop); +int MQTTProperty_read(MQTTProperty* prop, char** pptr, char* enddata); + +static unsigned int fails = 0u; + +#define TEST_EXPECT(name, x) \ + if (!(x)) { fprintf(stderr, "failed test: %s (%s:%d)\n", name, __FILE__, __LINE__); ++fails; } + + +/* ---------------------------------------------------------------------- */ +/* Base64 */ +/* ---------------------------------------------------------------------- */ + +static void test_base64(void) +{ + char out[512]; + b64_size_t r; + + /* round trip, including edge lengths that leave 1 or 2 bytes of padding */ + const char* inputs[] = { "", "p", "pa", "pah", "paho", "paho with websockets." }; + int i; + + for (i = 0; i < (int)(sizeof(inputs) / sizeof(inputs[0])); ++i) + { + char encoded[512]; + char decoded[512]; + b64_size_t inlen = (b64_size_t)strlen(inputs[i]); + b64_size_t enclen; + + enclen = Base64_encode(encoded, sizeof(encoded), (const b64_data_t*)inputs[i], inlen); + TEST_EXPECT("base64 encode length matches Base64_encodeLength", + enclen == Base64_encodeLength((const b64_data_t*)inputs[i], inlen)); + + r = Base64_decode((b64_data_t*)decoded, sizeof(decoded), encoded, enclen); + TEST_EXPECT("base64 decode length matches Base64_decodeLength", + r == Base64_decodeLength(encoded, enclen)); + TEST_EXPECT("base64 round trip content matches", + r == inlen && memcmp(decoded, inputs[i], inlen) == 0); + } + + /* decode: input length not a multiple of 4 - too short to decode anything */ + r = Base64_decode((b64_data_t*)out, sizeof(out), "cGE", 3); + TEST_EXPECT("base64 decode with in_len < 4 returns 0", r == 0); + + /* decode: output buffer too small for the first byte only ("cGE=" -> "pa") */ + r = Base64_decode((b64_data_t*)out, 1, "cGE=", 4); + TEST_EXPECT("base64 decode stops when out_len exhausted after 1st byte", r == 1); + + /* decode: output buffer holds exactly 2 of the 3 decoded bytes ("cGFo" -> "pah") */ + r = Base64_decode((b64_data_t*)out, 2, "cGFo", 4); + TEST_EXPECT("base64 decode stops when out_len exhausted after 2nd byte", r == 2); + + /* decode: invalid character in the 3rd position of a quad */ + r = Base64_decode((b64_data_t*)out, sizeof(out), "AB!D", 4); + TEST_EXPECT("base64 decode with invalid 3rd char stops at 1 byte", r == 1); + + /* decode: invalid character in the 4th position of a quad */ + r = Base64_decode((b64_data_t*)out, sizeof(out), "ABC!", 4); + TEST_EXPECT("base64 decode with invalid 4th char stops at 2 bytes", r == 2); + + /* decodeLength / encodeLength edge cases (NULL / short inputs) */ + TEST_EXPECT("base64 decodeLength NULL input", Base64_decodeLength(NULL, 4) == 3); + TEST_EXPECT("base64 decodeLength zero length", Base64_decodeLength("cGE=", 0) == 0); + TEST_EXPECT("base64 decodeLength length 1, no padding check on 2nd-last byte", + Base64_decodeLength("c", 1) == 0); + TEST_EXPECT("base64 decodeLength one padding char", Base64_decodeLength("cGE=", 4) == 2); + TEST_EXPECT("base64 decodeLength two padding chars", Base64_decodeLength("cA==", 4) == 1); + TEST_EXPECT("base64 decodeLength no padding", Base64_decodeLength("cGFo", 4) == 3); +} + + +/* ---------------------------------------------------------------------- */ +/* Tree */ +/* ---------------------------------------------------------------------- */ + +static void test_tree(void) +{ + Tree* t; + Tree stackTree; + Node* n; + int values[] = { 5, 3, 8, 1, 4, 7, 9, 2, 6, 0 }; + int i, prev, count; + const char* strs[] = { "banana", "apple", "cherry" }; + + /* direct compare function checks (note: these return -1 when the first arg is + * the *greater* one - an inverted convention relative to strcmp, but self-consistent + * for how the tree uses it) */ + { + int a = 1, b = 2; + TEST_EXPECT("TreeIntCompare with a < b", TreeIntCompare(&a, &b, 0) == 1); + TEST_EXPECT("TreeIntCompare with a > b", TreeIntCompare(&b, &a, 0) == -1); + TEST_EXPECT("TreeIntCompare equal", TreeIntCompare(&a, &a, 0) == 0); + } + { + int a = 1, b = 2; + TEST_EXPECT("TreePtrCompare with a > b", TreePtrCompare(&a, &b, 0) == (&a > &b ? -1 : 1)); + TEST_EXPECT("TreePtrCompare equal", TreePtrCompare(&a, &a, 0) == 0); + } + TEST_EXPECT("TreeStringCompare less", TreeStringCompare("apple", "banana", 0) < 0); + TEST_EXPECT("TreeStringCompare equal", TreeStringCompare("apple", "apple", 0) == 0); + TEST_EXPECT("TreeStringCompare greater", TreeStringCompare("banana", "apple", 0) > 0); + + /* dynamically allocated tree, single index, iteration in sorted order */ + t = TreeInitialize(TreeIntCompare); + TEST_EXPECT("TreeInitialize returns non-NULL", t != NULL); + + for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); ++i) + TreeAdd(t, &values[i], sizeof(int)); + TEST_EXPECT("Tree count after adds", t->count == (int)(sizeof(values) / sizeof(values[0]))); + + n = TreeFind(t, &values[0]); + TEST_EXPECT("TreeFind locates an added value", n != NULL && *(int*)(n->content) == values[0]); + + n = NULL; + prev = -1; + count = 0; + while ((n = TreeNextElement(t, n)) != NULL) + { + int v = *(int*)(n->content); + TEST_EXPECT("TreeNextElement returns ascending order", v > prev); + prev = v; + ++count; + } + TEST_EXPECT("TreeNextElement visited every node", count == (int)(sizeof(values) / sizeof(values[0]))); + + { + int key = 4; + void* removed = TreeRemove(t, &key); + TEST_EXPECT("TreeRemove finds and removes an existing key", removed != NULL); + n = TreeFind(t, &key); + TEST_EXPECT("TreeFind no longer finds removed key", n == NULL); + } + + TreeFree(t); + + /* stack-allocated tree (no malloc), second index, both indexes ordering the + * same string content (content type must be consistent across all indexes + * added via TreeAddIndex, since TreeAdd inserts into every index) */ + TreeInitializeNoMalloc(&stackTree, TreeStringCompare); + TreeAddIndex(&stackTree, TreeStringCompare); + TEST_EXPECT("TreeAddIndex increments indexes", stackTree.indexes == 2); + + for (i = 0; i < (int)(sizeof(strs) / sizeof(strs[0])); ++i) + TreeAdd(&stackTree, (void*)strs[i], strlen(strs[i]) + 1); + + n = TreeFindIndex(&stackTree, (void*)"apple", 1); + TEST_EXPECT("TreeFindIndex on second index finds content", + n != NULL && strcmp((char*)n->content, "apple") == 0); + + n = TreeFindContentIndex(&stackTree, (void*)"banana", 1); + TEST_EXPECT("TreeFindContentIndex on second index finds content", + n != NULL && strcmp((char*)n->content, "banana") == 0); + + n = NULL; + count = 0; + while ((n = TreeNextElementIndex(&stackTree, n, 1)) != NULL) + ++count; + TEST_EXPECT("TreeNextElementIndex walks all nodes on second index", + count == (int)(sizeof(strs) / sizeof(strs[0]))); + + { + void* removed = TreeRemoveIndex(&stackTree, (void*)"cherry", 1); + TEST_EXPECT("TreeRemoveIndex removes via second index", removed != NULL); + } + + /* allow_duplicates: when set, adding a key that already compares equal to an + * existing one is silently skipped (unlike the default, which replaces the + * existing node's content in place). Use a fresh, single-index int tree. */ + { + Tree dupTree; + int dupkey1 = 42, dupkey2 = 42; + void* addResult; + Node* found; + + TreeInitializeNoMalloc(&dupTree, TreeIntCompare); + dupTree.allow_duplicates = 1; + + TreeAdd(&dupTree, &dupkey1, sizeof(int)); + addResult = TreeAdd(&dupTree, &dupkey2, sizeof(int)); + + TEST_EXPECT("allow_duplicates skips inserting a second equal key", + dupTree.count == 1 && addResult == NULL); + + found = TreeFind(&dupTree, &dupkey2); + TEST_EXPECT("allow_duplicates leaves the original entry's content in place", + found != NULL && found->content == &dupkey1); + } +} + + +/* ---------------------------------------------------------------------- */ +/* Proxy (pure parsing functions only - no live connection) */ +/* ---------------------------------------------------------------------- */ + +static void test_proxy(void) +{ + char no_proxy_buf[256]; + + strcpy(no_proxy_buf, "example.com"); + TEST_EXPECT("Proxy_noProxy exact match returns 0 (don't use proxy)", + Proxy_noProxy("example.com", no_proxy_buf) == 0); + + strcpy(no_proxy_buf, "example.com"); + TEST_EXPECT("Proxy_noProxy suffix match returns 0", + Proxy_noProxy("sub.example.com", no_proxy_buf) == 0); + + strcpy(no_proxy_buf, "example.com"); + TEST_EXPECT("Proxy_noProxy non-matching host returns 1 (use proxy)", + Proxy_noProxy("notexample.com", no_proxy_buf) == 1); + + strcpy(no_proxy_buf, "*"); + TEST_EXPECT("Proxy_noProxy wildcard matches anything", + Proxy_noProxy("anything.example.org", no_proxy_buf) == 0); + + strcpy(no_proxy_buf, "foo.com,example.com"); + TEST_EXPECT("Proxy_noProxy matches second entry in comma list", + Proxy_noProxy("example.com", no_proxy_buf) == 0); + + strcpy(no_proxy_buf, "foo.com,bar.com"); + TEST_EXPECT("Proxy_noProxy with no match at all returns 1", + Proxy_noProxy("example.com", no_proxy_buf) == 1); + + /* Proxy_setHTTPProxy: no auth */ + { + char source[] = "http://myproxy.example.com:8080/"; + char* dest = NULL; + char* auth_dest = NULL; + int rc = Proxy_setHTTPProxy(NULL, source, &dest, &auth_dest, "http://"); + + TEST_EXPECT("Proxy_setHTTPProxy without auth succeeds", rc == 0); + TEST_EXPECT("Proxy_setHTTPProxy strips the prefix", + dest != NULL && strcmp(dest, "myproxy.example.com:8080/") == 0); + TEST_EXPECT("Proxy_setHTTPProxy leaves auth NULL when none is given", auth_dest == NULL); + } + + /* Proxy_setHTTPProxy: with user:pass auth, verify the encoded auth matches + * an independently computed Base64 encoding of "user:pass" */ + { + char source[] = "http://user:pass@myproxy.example.com:8080/"; + char* dest = NULL; + char* auth_dest = NULL; + char expected[64]; + b64_size_t explen; + int rc; + + explen = Base64_encode(expected, sizeof(expected), (const b64_data_t*)"user:pass", 9); + expected[explen] = '\0'; + + rc = Proxy_setHTTPProxy(NULL, source, &dest, &auth_dest, "http://"); + + TEST_EXPECT("Proxy_setHTTPProxy with auth succeeds", rc == 0); + TEST_EXPECT("Proxy_setHTTPProxy dest starts after the '@'", + dest != NULL && strcmp(dest, "myproxy.example.com:8080/") == 0); + TEST_EXPECT("Proxy_setHTTPProxy encodes auth as expected", + auth_dest != NULL && strcmp(auth_dest, expected) == 0); + free(auth_dest); + } + + /* Proxy_setHTTPProxy: NULL source is a safe no-op */ + { + char* dest = (char*)0x1; /* sentinel: should remain untouched */ + char* auth_dest = NULL; + int rc = Proxy_setHTTPProxy(NULL, NULL, &dest, &auth_dest, "http://"); + + TEST_EXPECT("Proxy_setHTTPProxy with NULL source returns success", rc == 0); + TEST_EXPECT("Proxy_setHTTPProxy with NULL source doesn't touch dest", + dest == (char*)0x1); + } +} + + +/* ---------------------------------------------------------------------- */ +/* utf-8 */ +/* ---------------------------------------------------------------------- */ + +static void test_utf8(void) +{ + /* 1-byte ASCII */ + TEST_EXPECT("utf8 1-byte ascii valid", UTF8_validate(1, "A") == 1); + + /* 2-byte: (c) copyright sign, 0xC2 0xA9 */ + TEST_EXPECT("utf8 2-byte valid", UTF8_validate(2, "\xC2\xA9") == 1); + + /* 3-byte, each of the four sub-ranges in valid_ranges */ + TEST_EXPECT("utf8 3-byte valid (E0 sub-range)", UTF8_validate(3, "\xE0\xA0\x80") == 1); + TEST_EXPECT("utf8 3-byte valid (E1-EC sub-range)", UTF8_validate(3, "\xE1\x80\x80") == 1); + TEST_EXPECT("utf8 3-byte valid (ED sub-range, excludes surrogates)", + UTF8_validate(3, "\xED\x80\x80") == 1); + TEST_EXPECT("utf8 3-byte valid (EE-EF sub-range)", UTF8_validate(3, "\xEE\x80\x80") == 1); + + /* 4-byte, each of the three sub-ranges */ + TEST_EXPECT("utf8 4-byte valid (F0 sub-range)", UTF8_validate(4, "\xF0\x90\x80\x80") == 1); + TEST_EXPECT("utf8 4-byte valid (F1-F3 sub-range)", UTF8_validate(4, "\xF1\x80\x80\x80") == 1); + TEST_EXPECT("utf8 4-byte valid (F4 sub-range)", UTF8_validate(4, "\xF4\x80\x80\x80") == 1); + + /* truncated multi-byte sequences: not enough bytes for the declared length */ + TEST_EXPECT("utf8 truncated 3-byte sequence is invalid", UTF8_validate(2, "\xE0\xA0") == 0); + TEST_EXPECT("utf8 truncated 4-byte sequence is invalid", UTF8_validate(3, "\xF0\x90\x80") == 0); + + /* invalid continuation byte within a valid lead byte's range */ + TEST_EXPECT("utf8 invalid 2-byte continuation is invalid", UTF8_validate(2, "\xC2\x20") == 0); + TEST_EXPECT("utf8 invalid 3-byte continuation is invalid", UTF8_validate(3, "\xE0\x20\x80") == 0); + + /* whole-string validation, embedding a valid multi-byte character */ + TEST_EXPECT("UTF8_validateString accepts a valid multi-byte string", + UTF8_validateString("caf\xC3\xA9") == 1); + TEST_EXPECT("UTF8_validateString rejects an invalid byte sequence", + UTF8_validateString("caf\xFF\xFF") == 0); +} + + +/* ---------------------------------------------------------------------- */ +/* MQTTProperties */ +/* ---------------------------------------------------------------------- */ + +static void test_mqtt_properties(void) +{ + MQTTProperties props = MQTTProperties_initializer; + MQTTProperty prop; + int rc; + + /* invalid property identifier is rejected */ + memset(&prop, 0, sizeof(prop)); + prop.identifier = (enum MQTTPropertyCodes)9999; + rc = MQTTProperties_add(&props, &prop); + TEST_EXPECT("MQTTProperties_add rejects an unknown property id", rc == MQTT_INVALID_PROPERTY_ID); + + /* BYTE-type property: write then read back */ + { + char buf[16]; + char* wptr = buf; + char* rptr = buf; + MQTTProperty byteProp; + MQTTProperty readProp; + + memset(&byteProp, 0, sizeof(byteProp)); + byteProp.identifier = MQTTPROPERTY_CODE_MAXIMUM_QOS; + byteProp.value.byte = 2; + + MQTTProperty_write(&wptr, &byteProp); + rc = MQTTProperty_read(&readProp, &rptr, buf + sizeof(buf)); + TEST_EXPECT("MQTTProperty_read of a BYTE property succeeds", rc > 0); + TEST_EXPECT("MQTTProperty_read of a BYTE property returns the value written", + readProp.identifier == MQTTPROPERTY_CODE_MAXIMUM_QOS && readProp.value.byte == 2); + } + + /* truncated buffers: not enough data for each property type */ + { + char buf[8]; + char* pptr; + MQTTProperty readProp; + + /* BYTE: identifier present, but no value byte */ + pptr = buf; + buf[0] = (char)MQTTPROPERTY_CODE_MAXIMUM_QOS; + rc = MQTTProperty_read(&readProp, &pptr, buf + 1); + TEST_EXPECT("MQTTProperty_read truncated BYTE value fails", rc == -1); + + /* TWO_BYTE_INTEGER: identifier present, only 1 of 2 value bytes */ + pptr = buf; + buf[0] = (char)MQTTPROPERTY_CODE_TOPIC_ALIAS; + buf[1] = 0; + rc = MQTTProperty_read(&readProp, &pptr, buf + 2); + TEST_EXPECT("MQTTProperty_read truncated TWO_BYTE_INTEGER fails", rc == -1); + + /* FOUR_BYTE_INTEGER: identifier present, only 2 of 4 value bytes */ + pptr = buf; + buf[0] = (char)MQTTPROPERTY_CODE_SESSION_EXPIRY_INTERVAL; + buf[1] = 0; + buf[2] = 0; + rc = MQTTProperty_read(&readProp, &pptr, buf + 3); + TEST_EXPECT("MQTTProperty_read truncated FOUR_BYTE_INTEGER fails", rc == -1); + + /* UTF-8 string: identifier present, length prefix says 5 bytes but none follow */ + pptr = buf; + buf[0] = (char)MQTTPROPERTY_CODE_CONTENT_TYPE; + buf[1] = 0; + buf[2] = 5; + rc = MQTTProperty_read(&readProp, &pptr, buf + 3); + TEST_EXPECT("MQTTProperty_read truncated UTF-8 string fails", rc == -1); + + /* just the 1-byte identifier, nothing else in the buffer at all */ + pptr = buf; + buf[0] = (char)MQTTPROPERTY_CODE_MAXIMUM_QOS; + rc = MQTTProperty_read(&readProp, &pptr, buf); + TEST_EXPECT("MQTTProperty_read with zero remaining data fails", rc == -1); + } + + /* MQTTProperties_read: declared remaining length longer than the buffer supplied */ + { + char buf[8]; + char* pptr = buf; + MQTTProperties readProps = MQTTProperties_initializer; + + buf[0] = 100; /* VBI: claims 100 bytes of properties follow, buffer is much shorter */ + rc = MQTTProperties_read(&readProps, &pptr, buf + 1); + TEST_EXPECT("MQTTProperties_read with an over-long declared length fails", + rc == MQTTPACKET_BUFFER_TOO_SHORT); + } + + /* MQTTProperties_read: more than 10 properties, to exercise array growth (realloc) */ + { + char buf[256]; + char* wptr; + char* pptr; + MQTTProperties writeProps = MQTTProperties_initializer; + MQTTProperties readProps = MQTTProperties_initializer; + int i; + int numProps = 15; + + for (i = 0; i < numProps; ++i) + { + MQTTProperty p; + memset(&p, 0, sizeof(p)); + p.identifier = MQTTPROPERTY_CODE_SUBSCRIPTION_IDENTIFIER; + p.value.integer4 = (unsigned int)(i + 1); + rc = MQTTProperties_add(&writeProps, &p); + TEST_EXPECT("MQTTProperties_add succeeds while building the growth test list", rc == 0); + } + + wptr = buf; + rc = MQTTProperties_write(&wptr, &writeProps); + TEST_EXPECT("MQTTProperties_write of >10 properties succeeds", rc > 0); + + /* MQTTProperties_read decodes its own leading VBI length prefix, so pptr + * must point at the very start of the buffer written by MQTTProperties_write */ + pptr = buf; + rc = MQTTProperties_read(&readProps, &pptr, buf + sizeof(buf)); + TEST_EXPECT("MQTTProperties_read of >10 properties succeeds", rc == 1); + TEST_EXPECT("MQTTProperties_read grows the array to hold all properties", + readProps.count == numProps && readProps.max_count >= numProps); + + MQTTProperties_free(&writeProps); + MQTTProperties_free(&readProps); + } + + /* MQTTProperties_read: a corrupt property (unknown identifier) mid-buffer -> MQTTPACKET_BAD */ + { + char buf[8]; + char* pptr = buf; + MQTTProperties readProps = MQTTProperties_initializer; + + buf[0] = 1; /* VBI: 1 byte of properties follows */ + buf[1] = (char)0x7F; /* an identifier that isn't in the known property table */ + rc = MQTTProperties_read(&readProps, &pptr, buf + 2); + TEST_EXPECT("MQTTProperties_read with an unknown property id fails", rc == MQTTPACKET_BAD); + } + + /* MQTTProperties_copy: source list has a manually-injected invalid identifier, + * which should be skipped/logged rather than crashing the copy */ + { + MQTTProperties badProps = MQTTProperties_initializer; + MQTTProperties copied; + MQTTProperty p; + + memset(&p, 0, sizeof(p)); + p.identifier = MQTTPROPERTY_CODE_MAXIMUM_QOS; + p.value.byte = 1; + MQTTProperties_add(&badProps, &p); + + /* bypass validation: hand-corrupt the stored identifier after adding it */ + badProps.array[0].identifier = (enum MQTTPropertyCodes)9999; + + copied = MQTTProperties_copy(&badProps); + TEST_EXPECT("MQTTProperties_copy with a corrupt source entry doesn't crash and copies nothing", + copied.count == 0); + + badProps.array[0].identifier = MQTTPROPERTY_CODE_MAXIMUM_QOS; /* restore so free() is safe */ + MQTTProperties_free(&badProps); + MQTTProperties_free(&copied); + } + + /* MQTTProperties_getNumericValueAt: BYTE type, and the "unsupported type" default */ + { + MQTTProperties numProps = MQTTProperties_initializer; + MQTTProperty p; + int64_t v; + + memset(&p, 0, sizeof(p)); + p.identifier = MQTTPROPERTY_CODE_MAXIMUM_QOS; /* BYTE type */ + p.value.byte = 7; + MQTTProperties_add(&numProps, &p); + + v = MQTTProperties_getNumericValue(&numProps, MQTTPROPERTY_CODE_MAXIMUM_QOS); + TEST_EXPECT("MQTTProperties_getNumericValue reads a BYTE property", v == 7); + + memset(&p, 0, sizeof(p)); + p.identifier = MQTTPROPERTY_CODE_CONTENT_TYPE; /* UTF-8 string: unsupported for numeric read */ + p.value.data.data = (char*)"text/plain"; + p.value.data.len = (int)strlen("text/plain"); + MQTTProperties_add(&numProps, &p); + + v = MQTTProperties_getNumericValue(&numProps, MQTTPROPERTY_CODE_CONTENT_TYPE); + TEST_EXPECT("MQTTProperties_getNumericValue on a non-numeric type returns the default", v == -999999); + + MQTTProperties_free(&numProps); + } + + /* MQTTProperties_getPropertyAt: index > 0 to find the second occurrence */ + { + MQTTProperties userProps = MQTTProperties_initializer; + MQTTProperty p; + MQTTProperty* found; + + memset(&p, 0, sizeof(p)); + p.identifier = MQTTPROPERTY_CODE_USER_PROPERTY; + p.value.data.data = (char*)"k1"; + p.value.data.len = 2; + p.value.value.data = (char*)"v1"; + p.value.value.len = 2; + MQTTProperties_add(&userProps, &p); + + p.value.data.data = (char*)"k2"; + p.value.value.data = (char*)"v2"; + MQTTProperties_add(&userProps, &p); + + found = MQTTProperties_getPropertyAt(&userProps, MQTTPROPERTY_CODE_USER_PROPERTY, 1); + TEST_EXPECT("MQTTProperties_getPropertyAt(index=1) finds the second occurrence", + found != NULL && strncmp(found->value.data.data, "k2", 2) == 0); + + MQTTProperties_free(&userProps); + } + + MQTTProperties_free(&props); +} + + +int main(int argc, char** argv) +{ + (void)argc; + (void)argv; + + /* initialize heap tracking / mutexes used internally by these modules, + * without needing an actual network connection. MQTTAsync_global_init() + * alone isn't enough: Heap_initialize() is normally only called lazily from + * MQTTAsync_createWithOptions, so it's called explicitly here instead. */ + MQTTAsync_global_init(NULL); + Heap_initialize(); + + test_base64(); + test_tree(); + test_proxy(); + test_utf8(); + test_mqtt_properties(); + + if (fails) + printf("%u tests failed\n", fails); + else + printf("all tests passed\n"); + + return (int)fails; +}