Skip to content

Commit 6d0c6eb

Browse files
add composer.lock rule to .gitignore, add additional decoder and members tests
1 parent 8adb7da commit 6d0c6eb

3 files changed

Lines changed: 234 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
# composer vendor dir
77
/vendor
88

9+
# composer lock file (not committed for libraries; consumers resolve their own)
10+
/composer.lock
11+
912
# IDEA files
1013
/.idea
1114

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace PubNubTests\unit\CryptoModule;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use PubNub\CryptoModule;
7+
use PubNub\Crypto\AesCbcCryptor;
8+
use PubNub\Crypto\LegacyCryptor;
9+
use PubNub\Crypto\Header as CryptoHeader;
10+
11+
/**
12+
* Regression tests for CryptoModule::decodeHeader and the encrypt/decrypt round
13+
* trip (which exercises the padding/unpadding fix in PaddingTrait).
14+
*
15+
* The header length guard previously read `strlen($header < 10)`, which took the
16+
* length of a boolean and never checked the real header length. It is now
17+
* `strlen($header) < 10`. These tests pin the corrected behaviour and confirm
18+
* that both the AES-CBC and legacy round trips (incl. the fallback header path)
19+
* still work end to end.
20+
*/
21+
class DecodeHeaderTest extends TestCase
22+
{
23+
protected string $cipherKey = "myCipherKey";
24+
protected string $message = "Hello world, this is a PubNub crypto regression test payload.";
25+
26+
public function testShortHeaderReturnsFallback(): void
27+
{
28+
// Fewer than 10 bytes: must fall back regardless of the (partial) sentinel.
29+
$module = CryptoModule::aesCbcCryptor($this->cipherKey, true);
30+
$header = $module->decodeHeader('PNED');
31+
32+
$this->assertEquals(CryptoModule::FALLBACK_CRYPTOR_ID, $header->getCryptorId());
33+
$this->assertEquals(0, $header->getLength());
34+
$this->assertEquals('', $header->getSentinel());
35+
}
36+
37+
public function testEmptyHeaderReturnsFallback(): void
38+
{
39+
$module = CryptoModule::aesCbcCryptor($this->cipherKey, true);
40+
$header = $module->decodeHeader('');
41+
42+
$this->assertEquals(CryptoModule::FALLBACK_CRYPTOR_ID, $header->getCryptorId());
43+
$this->assertEquals(0, $header->getLength());
44+
}
45+
46+
public function testLongHeaderWithoutSentinelReturnsFallback(): void
47+
{
48+
// At least 10 bytes but no PNED sentinel: this is legacy-encrypted data.
49+
$module = CryptoModule::aesCbcCryptor($this->cipherKey, true);
50+
$header = $module->decodeHeader(str_repeat('x', 20));
51+
52+
$this->assertEquals(CryptoModule::FALLBACK_CRYPTOR_ID, $header->getCryptorId());
53+
$this->assertEquals(0, $header->getLength());
54+
}
55+
56+
public function testValidHeaderIsDecoded(): void
57+
{
58+
$module = CryptoModule::aesCbcCryptor($this->cipherKey, true);
59+
60+
// A real AES-CBC payload starts with the PNED sentinel header.
61+
$raw = base64_decode($module->encrypt($this->message));
62+
$header = $module->decodeHeader($raw);
63+
64+
$this->assertEquals('PNED', $header->getSentinel());
65+
$this->assertEquals(AesCbcCryptor::CRYPTOR_ID, $header->getCryptorId());
66+
$this->assertGreaterThanOrEqual(10, $header->getLength());
67+
}
68+
69+
public function testAesCbcRoundTrip(): void
70+
{
71+
$module = CryptoModule::aesCbcCryptor($this->cipherKey, true);
72+
73+
$encrypted = $module->encrypt($this->message);
74+
$this->assertEquals($this->message, $module->decrypt($encrypted));
75+
}
76+
77+
public function testLegacyRoundTripUsesFallbackHeaderPath(): void
78+
{
79+
// Legacy-encrypted data carries no PNED header, so decrypt must route
80+
// through the fallback branch of decodeHeader and the legacy cryptor.
81+
$module = CryptoModule::legacyCryptor($this->cipherKey, true);
82+
83+
$encrypted = $module->encrypt($this->message);
84+
$raw = base64_decode($encrypted);
85+
86+
$header = $module->decodeHeader($raw);
87+
$this->assertEquals(CryptoModule::FALLBACK_CRYPTOR_ID, $header->getCryptorId());
88+
89+
$this->assertEquals($this->message, $module->decrypt($encrypted));
90+
}
91+
92+
public function testAesModuleCanDecryptLegacyPayload(): void
93+
{
94+
// Interop: data produced by the legacy cryptor must remain decryptable by
95+
// a module whose default is AES-CBC (both cryptors are registered).
96+
$legacyModule = CryptoModule::legacyCryptor($this->cipherKey, true);
97+
$aesModule = CryptoModule::aesCbcCryptor($this->cipherKey, true);
98+
99+
$encryptedByLegacy = $legacyModule->encrypt($this->message);
100+
$this->assertEquals($this->message, $aesModule->decrypt($encryptedByLegacy));
101+
}
102+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace PubNubTests\unit\objects\member;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use PubNub\PNConfiguration;
7+
use PubNub\PubNub;
8+
use PubNub\Endpoints\Objects\Member\ManageMembers;
9+
use PubNub\Exceptions\PubNubValidationException;
10+
use PubNub\Models\Consumer\Objects\Member\PNChannelMember;
11+
12+
/**
13+
* Regression tests for the members/uuids validation in ManageMembers.
14+
*
15+
* These pin the behaviour of the `||` fix. Previously the endpoint used `or`,
16+
* which binds looser than `=`, so `$members`/`$uuids` were assigned only the
17+
* `set*` operand and the `remove*` operand was silently discarded. As a result
18+
* remove-only operations wrongly failed validation, and mixed members+uuids
19+
* calls wrongly passed. See src/PubNub/Endpoints/Objects/Member/ManageMembers.php.
20+
*/
21+
class ManageMembersValidationTest extends TestCase
22+
{
23+
private PubNub $pubnub;
24+
25+
public function setUp(): void
26+
{
27+
parent::setUp();
28+
$config = new PNConfiguration();
29+
$config->setSubscribeKey('demo');
30+
$config->setPublishKey('demo');
31+
$config->setUuid('validation-test-uuid');
32+
$this->pubnub = new PubNub($config);
33+
}
34+
35+
private function invokeValidateParams(ManageMembers $endpoint): void
36+
{
37+
$method = new \ReflectionMethod($endpoint, 'validateParams');
38+
$method->invoke($endpoint);
39+
}
40+
41+
public function testSetMembersOnlyPassesValidation(): void
42+
{
43+
$endpoint = $this->pubnub->manageMembers()
44+
->channel('ch')
45+
->setMembers([new PNChannelMember('u1')]);
46+
47+
$this->invokeValidateParams($endpoint);
48+
$this->assertTrue(true, 'set-only members must pass validation');
49+
}
50+
51+
public function testRemoveMembersOnlyPassesValidation(): void
52+
{
53+
// Regression: with the old `or` bug this threw "Members or a list of uuids missing".
54+
$endpoint = $this->pubnub->manageMembers()
55+
->channel('ch')
56+
->removeMembers([new PNChannelMember('u1')]);
57+
58+
$this->invokeValidateParams($endpoint);
59+
$this->assertTrue(true, 'remove-only members must pass validation');
60+
}
61+
62+
public function testRemoveUuidsOnlyPassesValidation(): void
63+
{
64+
// Regression: with the old `or` bug this threw "Members or a list of uuids missing".
65+
$endpoint = $this->pubnub->manageMembers()
66+
->channel('ch')
67+
->removeUuids(['u1']);
68+
69+
$this->invokeValidateParams($endpoint);
70+
$this->assertTrue(true, 'remove-only uuids must pass validation');
71+
}
72+
73+
public function testSetAndRemoveMembersTogetherPassesValidation(): void
74+
{
75+
$endpoint = $this->pubnub->manageMembers()
76+
->channel('ch')
77+
->setMembers([new PNChannelMember('u1')])
78+
->removeMembers([new PNChannelMember('u2')]);
79+
80+
$this->invokeValidateParams($endpoint);
81+
$this->assertTrue(true, 'set + remove on the members side must pass validation');
82+
}
83+
84+
public function testMixingRemoveMembersAndSetUuidsThrows(): void
85+
{
86+
// Regression: with the old `or` bug $members was computed as false, so this
87+
// mixed members+uuids call slipped through validation. It must now be rejected.
88+
$endpoint = $this->pubnub->manageMembers()
89+
->channel('ch')
90+
->removeMembers([new PNChannelMember('u1')])
91+
->setUuids(['u2']);
92+
93+
$this->expectException(PubNubValidationException::class);
94+
$this->expectExceptionMessage('Either members or uuids should be provided');
95+
$this->invokeValidateParams($endpoint);
96+
}
97+
98+
public function testMixingSetMembersAndRemoveUuidsThrows(): void
99+
{
100+
$endpoint = $this->pubnub->manageMembers()
101+
->channel('ch')
102+
->setMembers([new PNChannelMember('u1')])
103+
->removeUuids(['u2']);
104+
105+
$this->expectException(PubNubValidationException::class);
106+
$this->expectExceptionMessage('Either members or uuids should be provided');
107+
$this->invokeValidateParams($endpoint);
108+
}
109+
110+
public function testNothingProvidedThrows(): void
111+
{
112+
$endpoint = $this->pubnub->manageMembers()
113+
->channel('ch');
114+
115+
$this->expectException(PubNubValidationException::class);
116+
$this->expectExceptionMessage('Members or a list of uuids missing');
117+
$this->invokeValidateParams($endpoint);
118+
}
119+
120+
public function testMissingChannelThrows(): void
121+
{
122+
$endpoint = $this->pubnub->manageMembers()
123+
->setMembers([new PNChannelMember('u1')]);
124+
125+
$this->expectException(PubNubValidationException::class);
126+
$this->expectExceptionMessage('channel missing');
127+
$this->invokeValidateParams($endpoint);
128+
}
129+
}

0 commit comments

Comments
 (0)