Skip to content

Commit b155078

Browse files
authored
Add more tests (#61)
* Add tests for remaining TLS options * Replace test pipeline badge with coverage * Test connecting to TLS endpoint without config * Simplify exit-when-queues-empty condition * Raise notice if socket could not be closed * Test unregistering single message received event handler * Try EMQ X with longer erlang startup time * Switch back to updated v1 of action
1 parent 20802bb commit b155078

File tree

6 files changed

+144
-22
lines changed

6 files changed

+144
-22
lines changed

.ci/tls/.gitignore

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,2 @@
1-
*.crt
2-
*.csr
3-
*.jks
4-
*.key
5-
*.p12
6-
*.srl
1+
*
2+
!.gitignore

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Latest Stable Version](https://poser.pugx.org/php-mqtt/client/v)](https://packagist.org/packages/php-mqtt/client)
44
[![Total Downloads](https://poser.pugx.org/php-mqtt/client/downloads)](https://packagist.org/packages/php-mqtt/client)
5-
[![Tests](https://github.com/php-mqtt/client/workflows/Tests/badge.svg)](https://github.com/php-mqtt/client/actions?query=workflow%3ATests)
5+
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=coverage)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
66
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=alert_status)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
77
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
88
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
@@ -195,6 +195,27 @@ $connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
195195
- Message flows with a QoS level higher than 0 are not persisted as the default implementation uses an in-memory repository for data.
196196
To avoid issues with broken message flows, use the clean session flag to indicate that you don't care about old data.
197197
It will not only instruct the broker to consider the connection new (without previous state), but will also reset the registered repository.
198+
199+
## Developing & Testing
200+
201+
### Certificates (TLS)
202+
203+
To run the tests (especially the TLS tests), you will need to create certificates. A command has been provided for this:
204+
```sh
205+
sh create-certificates.sh
206+
```
207+
This will create all required certificates in the `.ci/tls/` directory. The same script is used for continuous integration as well.
208+
209+
### MQTT Broker for Testing
210+
211+
Running the tests expects an MQTT broker to be running. The easiest way to run an MQTT broker is through Docker:
212+
```sh
213+
docker run --rm -it -p 1883:1883 -p 8883:8883 -p 8884:8884 -v $(pwd)/.ci/tls:/mosquitto-certs -v $(pwd)/.ci/mosquitto.conf:/mosquitto/config/mosquitto.conf eclipse-mosquitto:1.6
214+
```
215+
When run from the project directory, this will spawn a Mosquitto MQTT broker configured with the generated TLS certificates and a custom configuration.
216+
217+
In case you intend to run a different broker or using a different method, or use a public broker instead,
218+
you will need to adjust the environment variables defined in `phpunit.xml` accordingly.
198219

199220
## License
200221

create-certificates.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
#!/bin/sh
22

3+
# Generate a new CA certificate and key.
34
openssl genrsa -out .ci/tls/ca.key 2048
45
openssl req -x509 -new -nodes -key .ci/tls/ca.key -days 1 -out .ci/tls/ca.crt -subj "/C=AT/ST=Vorarlberg/CN=php-mqtt Test CA"
6+
7+
# Copy ca.crt to a file named by the hashed subject of the certificate. This is required for PHP's capath option to find the certificate.
8+
cp .ci/tls/ca.crt .ci/tls/$(openssl x509 -hash -noout -in .ci/tls/ca.crt).0
9+
10+
# Create a Java Trust Store from the CA certificate. This is used by HiveMQ.
511
keytool -import -file .ci/tls/ca.crt -alias ca -keystore .ci/tls/ca.jks -storepass s3cr3t -trustcacerts -noprompt
612

13+
# Generate a new server certificate and key, signed by the created CA.
714
openssl genrsa -out .ci/tls/server.key 2048
815
openssl req -new -key .ci/tls/server.key -out .ci/tls/server.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
916
openssl x509 -req -in .ci/tls/server.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/server.crt -days 1 -sha512
17+
18+
# Generate a Java Key Store from the server certificate. This is used by HiveMQ.
1019
openssl pkcs12 -export -in .ci/tls/server.crt -inkey .ci/tls/server.key -out .ci/tls/server.p12 -passout pass:s3cr3t
1120
keytool -importkeystore -srckeystore .ci/tls/server.p12 -srcstoretype PKCS12 -destkeystore .ci/tls/server.jks -deststoretype JKS -srcstorepass s3cr3t -deststorepass s3cr3t -noprompt
1221

22+
# Generate a client certificate without passphrase, signed by the created CA.
1323
openssl genrsa -out .ci/tls/client.key 2048
1424
openssl req -new -key .ci/tls/client.key -out .ci/tls/client.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
1525
openssl x509 -req -in .ci/tls/client.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client.crt -days 1 -sha256
26+
27+
# Generate a client certificate with passphrase, signed by the created CA.
28+
openssl genrsa -aes128 -passout pass:s3cr3t -out .ci/tls/client2.key 2048
29+
openssl req -new -key .ci/tls/client2.key -passin pass:s3cr3t -out .ci/tls/client2.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
30+
openssl x509 -req -in .ci/tls/client2.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client2.crt -days 1 -sha256

src/MqttClient.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -603,18 +603,16 @@ public function loop(bool $allowSleep = true, bool $exitWhenQueuesEmpty = false,
603603
$this->ping();
604604
}
605605

606-
// This check will ensure, that, if we want to exit as soon as all queues
607-
// are empty and they really are empty, we quit.
608-
if ($exitWhenQueuesEmpty) {
609-
if ($this->allQueuesAreEmpty() && $this->repository->countSubscriptions() === 0) {
606+
// If configured, the loop is exited if all queues are empty or a certain time limit is reached (i.e. retry is aborted).
607+
// In any case, there may not be any active subscriptions though.
608+
if ($exitWhenQueuesEmpty && $this->repository->countSubscriptions() === 0) {
609+
if ($this->allQueuesAreEmpty()) {
610610
break;
611611
}
612612

613-
// We also exit the loop if there are no open topic subscriptions
614-
// and we reached the time limit.
615-
if ($queueWaitLimit !== null &&
616-
(microtime(true) - $loopStartedAt) > $queueWaitLimit &&
617-
$this->repository->countSubscriptions() === 0) {
613+
// The time limit is reached. This most likely means the outgoing queues could not be emptied in time.
614+
// Probably the server did not respond with an acknowledgement.
615+
if ($queueWaitLimit !== null && (microtime(true) - $loopStartedAt) > $queueWaitLimit) {
618616
break;
619617
}
620618
}
@@ -1139,7 +1137,7 @@ protected function closeSocket(): void
11391137
$this->logger->debug('Successfully closed socket connection to the broker.');
11401138
} else {
11411139
$phpError = error_get_last();
1142-
$this->logger->debug('Closing socket connection failed: {error}', [
1140+
$this->logger->notice('Closing socket connection failed: {error}', [
11431141
'error' => $phpError ? $phpError['message'] : 'undefined',
11441142
]);
11451143
}

tests/Feature/ConnectWithTlsSettingsTest.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace Tests\Feature;
88

99
use PhpMqtt\Client\ConnectionSettings;
10+
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
1011
use PhpMqtt\Client\MqttClient;
1112
use Tests\TestCase;
1213

@@ -26,6 +27,19 @@ protected function setUp(): void
2627
}
2728
}
2829

30+
public function test_connecting_with_tls_but_without_further_configuration_throws_for_self_signed_certificate(): void
31+
{
32+
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
33+
34+
$connectionSettings = (new ConnectionSettings)
35+
->setUseTls(true);
36+
37+
$this->expectException(ConnectingToBrokerFailedException::class);
38+
$this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_TLS_ERROR);
39+
40+
$client->connect($connectionSettings, true);
41+
}
42+
2943
public function test_connecting_with_tls_with_ignored_self_signed_certificate_works_as_intended(): void
3044
{
3145
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
@@ -43,7 +57,7 @@ public function test_connecting_with_tls_with_ignored_self_signed_certificate_wo
4357
$client->disconnect();
4458
}
4559

46-
public function test_connecting_with_tls_with_validated_self_signed_certificate_works_as_intended(): void
60+
public function test_connecting_with_tls_with_validated_self_signed_certificate_using_cafile__works_as_intended(): void
4761
{
4862
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
4963

@@ -61,6 +75,24 @@ public function test_connecting_with_tls_with_validated_self_signed_certificate_
6175
$client->disconnect();
6276
}
6377

78+
public function test_connecting_with_tls_with_validated_self_signed_certificate_using_capath_works_as_intended(): void
79+
{
80+
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
81+
82+
$connectionSettings = (new ConnectionSettings)
83+
->setUseTls(true)
84+
->setTlsSelfSignedAllowed(false)
85+
->setTlsVerifyPeer(true)
86+
->setTlsVerifyPeerName(true)
87+
->setTlsCertificateAuthorityPath($this->tlsCertificateDirectory);
88+
89+
$client->connect($connectionSettings, true);
90+
91+
$this->assertTrue($client->isConnected());
92+
93+
$client->disconnect();
94+
}
95+
6496
public function test_connecting_with_tls_and_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void
6597
{
6698
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings');
@@ -80,4 +112,25 @@ public function test_connecting_with_tls_and_client_certificate_with_validated_s
80112

81113
$client->disconnect();
82114
}
115+
116+
public function test_connecting_with_tls_and_passphrase_protected_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void
117+
{
118+
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings');
119+
120+
$connectionSettings = (new ConnectionSettings)
121+
->setUseTls(true)
122+
->setTlsSelfSignedAllowed(false)
123+
->setTlsVerifyPeer(true)
124+
->setTlsVerifyPeerName(true)
125+
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt')
126+
->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client2.crt')
127+
->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client2.key')
128+
->setTlsClientCertificateKeyPassphrase('s3cr3t');
129+
130+
$client->connect($connectionSettings, true);
131+
132+
$this->assertTrue($client->isConnected());
133+
134+
$client->disconnect();
135+
}
83136
}

tests/Feature/MessageReceivedEventHandlerTest.php

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,54 @@ public function test_message_received_event_handlers_are_called_for_each_receive
5353
$subscriber->disconnect();
5454
}
5555

56+
public function test_message_received_event_handler_can_be_unregistered_and_will_not_be_called_anymore(): void
57+
{
58+
// We connect and subscribe to a topic using the first client.
59+
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
60+
$subscriber->connect(null, true);
61+
62+
$callCount = 0;
63+
$handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$handler, &$callCount) {
64+
$callCount++;
65+
66+
$this->assertSame('foo/bar/baz/01', $topic);
67+
$this->assertSame('hello world', $message);
68+
$this->assertSame(0, $qualityOfService);
69+
$this->assertFalse($retained);
70+
71+
$client->unregisterMessageReceivedEventHandler($handler);
72+
$client->interrupt();
73+
};
74+
75+
$subscriber->registerMessageReceivedEventHandler($handler);
76+
$subscriber->subscribe('foo/bar/baz/+');
77+
78+
// We publish a message from a second client on the same topic.
79+
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
80+
$publisher->connect(null, true);
81+
82+
$publisher->publish('foo/bar/baz/01', 'hello world', 0, false);
83+
$publisher->publish('foo/bar/baz/02', 'hello world', 0, false);
84+
85+
// Then we loop on the subscriber to (hopefully) receive the published message.
86+
$subscriber->loop(true);
87+
88+
$this->assertSame(1, $callCount);
89+
90+
// Finally, we disconnect for a graceful shutdown on the broker side.
91+
$publisher->disconnect();
92+
$subscriber->disconnect();
93+
}
94+
5695
public function test_message_received_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void
5796
{
5897
// We connect and subscribe to a topic using the first client.
5998
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
6099
$subscriber->connect(null, true);
61100

62-
$handlerCallCount = 0;
63-
$handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$handlerCallCount) {
64-
$handlerCallCount++;
101+
$callCount = 0;
102+
$handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$callCount) {
103+
$callCount++;
65104

66105
$this->assertSame('foo/bar/baz/01', $topic);
67106
$this->assertSame('hello world', $message);
@@ -85,7 +124,7 @@ public function test_message_received_event_handlers_can_be_unregistered_and_wil
85124
// Then we loop on the subscriber to (hopefully) receive the published message.
86125
$subscriber->loop(true);
87126

88-
$this->assertSame(1, $handlerCallCount);
127+
$this->assertSame(1, $callCount);
89128

90129
// Finally, we disconnect for a graceful shutdown on the broker side.
91130
$publisher->disconnect();

0 commit comments

Comments
 (0)