mirror of https://github.com/eclipse/paho.mqtt.c
793 lines
28 KiB
C
793 lines
28 KiB
C
/*******************************************************************************
|
|
* 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 <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
|
|
#if !defined(_WINDOWS)
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
#else
|
|
#include <windows.h>
|
|
#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;
|
|
}
|