From c04d3bb5517e1319ad75d660ec4925ebaffee4ee Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 11:01:32 +0200 Subject: [PATCH 01/15] Tests for auth HTTP headers and for empty forced token auth --- src/Auth.php | 4 ++-- tests/AuthTest.php | 56 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 4b63cbf..36d7f40 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -88,10 +88,10 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc public function getAuthHeaders() { $header = array(); if ( $this->isUsingBasicAuth() ) { - $header = array( 'authorization: Basic ' . base64_encode( $this->authOptions->key ) ); + $header = array( 'Authorization: Basic ' . base64_encode( $this->authOptions->key ) ); } else if ( !empty( $this->tokenDetails ) ) { $this->authorise(); - $header = array( 'authorization: Bearer '. base64_encode( $this->tokenDetails->token ) ); + $header = array( 'Authorization: Bearer '. base64_encode( $this->tokenDetails->token ) ); } else { throw new AblyException( 'Unable to provide auth headers. No auth parameters defined.', 40101, 401 ); } diff --git a/tests/AuthTest.php b/tests/AuthTest.php index 6899830..d4a893f 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -214,13 +214,24 @@ public function testAuthWithToken() { */ public function testAuthWithKeyForceToken() { $ably = new AblyRest( array( - 'key' => 'fakeKey', + 'key' => 'fake.key:totallyFake', 'useTokenAuth' => true, ) ); $this->assertFalse( $ably->auth->isUsingBasicAuth(), 'Expected token auth to be used' ); } + /** + * Init library without providing a key or a token, force use of token with useTokenAuth + */ + public function testAuthEmptyForceToken() { + $this->setExpectedException( 'Ably\Exceptions\AblyException', '', 40103 ); + + $ably = new AblyRest( array( + 'useTokenAuth' => true, + ) ); + } + /** * Verify than token auth works without TLS */ @@ -295,6 +306,36 @@ public function testAuthorise() { $ably->auth->authorise(array(), array(), $force = true); $this->assertFalse( $tokenOriginal->token == $ably->auth->getTokenDetails()->token, 'Expected token to renew' ); } + + /** + * When using Basic Auth, the API key is sent in the `Authorization: Basic` header with a Base64 encoded value + */ + public function testHTTPHeadersKey() { + $fakeKey = 'fake.key:totallyFake'; + $ably = new AblyRest( array( + 'key' => $fakeKey, + 'httpClass' => 'tests\HttpMockAuthTest', + ) ); + + $ably->get("/dummy_test"); + + $this->assertRegExp('/Authorization\s*:\s*Basic\s+'.base64_encode($fakeKey).'/i', $ably->http->headers[0]); + } + + /** + * Verify that the token string is Base64 encoded and used in the `Authorization: Bearer` header + */ + public function testHTTPHeadersToken() { + $fakeToken = 'fakeToken'; + $ably = new AblyRest( array( + 'token' => $fakeToken, + 'httpClass' => 'tests\HttpMockAuthTest', + ) ); + + $ably->get("/dummy_test"); + + $this->assertRegExp('/Authorization\s*:\s*Bearer\s+'.base64_encode($fakeToken).'/i', $ably->http->headers[0]); + } } class HttpMockAuthTest extends Http { @@ -359,10 +400,15 @@ public function request($method, $url, $headers = array(), $params = array()) { 'headers' => 'HTTP/1.1 200 OK'."\n", 'body' => json_decode ( $response ), ); + } else { + $this->method = $method; + $this->headers = $headers; + $this->params = $params; + + return array( + 'headers' => 'HTTP/1.1 200 OK'."\n", + 'body' => (object) array('defaultRoute' => true), + ); } - - echo $url."\n"; - - return '?'; } } \ No newline at end of file From c0a3c3ab63a421ee4038a2501f9fb9fdb2b7b7e7 Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 11:02:20 +0200 Subject: [PATCH 02/15] Removed isFirst() from paginated results and from tests --- src/Models/PaginatedResult.php | 20 +------------------- tests/ChannelHistoryTest.php | 4 ---- tests/PresenceTest.php | 7 ------- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/Models/PaginatedResult.php b/src/Models/PaginatedResult.php index 054e775..11df7e2 100644 --- a/src/Models/PaginatedResult.php +++ b/src/Models/PaginatedResult.php @@ -66,9 +66,7 @@ public function __construct( \Ably\AblyRest $ably, $model, $cipherParams, $path, * @return PaginatedResult Returns self if the current page is the first */ public function first() { - if ($this->isFirst()) { - return this; - } else if (isset($this->paginationHeaders['first'])) { + if (isset($this->paginationHeaders['first'])) { return new PaginatedResult( $this->ably, $this->model, $this->cipherParams, $this->paginationHeaders['first'] ); } else { return null; @@ -101,22 +99,6 @@ public function hasNext() { return $this->isPaginated() && isset($this->paginationHeaders['next']); } - /** - * @return boolean Whether the current page is the first, always true for single-page results - */ - public function isFirst() { - if (!$this->isPaginated() ) { - return true; - } - - if ( isset($this->paginationHeaders['first']) && isset($this->paginationHeaders['current']) - && $this->paginationHeaders['first'] == $this->paginationHeaders['current'] ) { - return true; - } - - return false; - } - /** * @return boolean Whether the current page is the last, always true for single-page results */ diff --git a/tests/ChannelHistoryTest.php b/tests/ChannelHistoryTest.php index 9d2140b..2c895b9 100644 --- a/tests/ChannelHistoryTest.php +++ b/tests/ChannelHistoryTest.php @@ -139,7 +139,6 @@ public function testPublishEventsGetLimitedHistoryAndCheckOrderForwards() { $this->assertTrue( $messages->isPaginated(), 'Expected messages to be paginated' ); $this->assertTrue( $messages->hasFirst(), 'Expected to have first page' ); $this->assertTrue( $messages->hasNext(), 'Expected to have next page' ); - $this->assertTrue( $messages->isFirst(), 'Expected to be the first page' ); $this->assertFalse( $messages->isLast(), 'Expected not to be the last page' ); // next page @@ -156,7 +155,6 @@ public function testPublishEventsGetLimitedHistoryAndCheckOrderForwards() { $this->assertTrue( $messages2->isPaginated(), 'Expected messages to be paginated' ); $this->assertTrue( $messages2->hasFirst(), 'Expected to have first page' ); $this->assertFalse( $messages2->hasNext(), 'Expected not to have next page' ); - $this->assertFalse( $messages2->isFirst(), 'Expected not to be the first page' ); $this->assertTrue( $messages2->isLast(), 'Expected to be the last page' ); $this->assertNull( $messages2->next(), 'Expected the 3rd page to be null' ); @@ -203,7 +201,6 @@ public function testPublishEventsGetLimitedHistoryAndCheckOrderBackwards() { $this->assertTrue( $messages->isPaginated(), 'Expected messages to be paginated' ); $this->assertTrue( $messages->hasFirst(), 'Expected to have first page' ); $this->assertTrue( $messages->hasNext(), 'Expected to have next page' ); - $this->assertTrue( $messages->isFirst(), 'Expected to be the first page' ); $this->assertFalse( $messages->isLast(), 'Expected not to be the last page' ); // next page @@ -220,7 +217,6 @@ public function testPublishEventsGetLimitedHistoryAndCheckOrderBackwards() { $this->assertTrue( $messages2->isPaginated(), 'Expected messages to be paginated' ); $this->assertTrue( $messages2->hasFirst(), 'Expected to have first page' ); $this->assertFalse( $messages2->hasNext(), 'Expected not to have next page' ); - $this->assertFalse( $messages2->isFirst(), 'Expected not to be the first page' ); $this->assertTrue( $messages2->isLast(), 'Expected to be the last page' ); $this->assertNull( $messages2->next(), 'Expected the 3rd page to be null' ); diff --git a/tests/PresenceTest.php b/tests/PresenceTest.php index 7539dd3..98037a7 100644 --- a/tests/PresenceTest.php +++ b/tests/PresenceTest.php @@ -73,11 +73,6 @@ public function testComparePresenceDataWithFixture() { $nextPage = $firstPage->next(); $this->assertEquals( 3, count($nextPage->items), 'Expected 3 presence entries on the 2nd page' ); $this->assertTrue( $nextPage->isLast(), 'Expected last page' ); - - $this->markTestIncomplete( - 'Ignore `isFirst` for presence pagination as we have no proper way of determining this yet' - ); - $this->assertTrue( $firstPage->isFirst(), 'Expected the page to be first' ); } /** @@ -107,7 +102,6 @@ public function testComparePresenceHistoryWithFixture() { // verify limit / pagination - forwards $firstPage = self::$channel->presence->history( array( 'limit' => 3, 'direction' => 'forwards' ) ); - $this->assertTrue( $firstPage->isFirst(), 'Expected the page to be first' ); $this->assertEquals( 3, count($firstPage->items), 'Expected 3 presence entries' ); $nextPage = $firstPage->next(); @@ -118,7 +112,6 @@ public function testComparePresenceHistoryWithFixture() { // verify limit / pagination - backwards (default) $firstPage = self::$channel->presence->history( array( 'limit' => 3 ) ); - $this->assertTrue( $firstPage->isFirst(), 'Expected the page to be first' ); $this->assertEquals( 3, count($firstPage->items), 'Expected 3 presence entries' ); $nextPage = $firstPage->next(); From 1abd87d3efb865fb2bc380975e3191cc8ad3a2b0 Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 11:13:30 +0200 Subject: [PATCH 03/15] Test for clientId and connectionId filters on Presence GET --- tests/PresenceTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PresenceTest.php b/tests/PresenceTest.php index 98037a7..b29f67e 100644 --- a/tests/PresenceTest.php +++ b/tests/PresenceTest.php @@ -182,4 +182,20 @@ public function testComparePresenceDataWithFixtureEncrypted() { $this->assertEquals( $messageMap['client_decoded'], $messageMap['client_encoded'], 'Expected decrypted and sample data to match' ); } + + /** + * Ensure clientId and connectionId filters on Presence GET works + */ + public function testFilters() { + $presenceClientFilter = self::$channel->presence->get( array( 'clientId' => 'client_string' ) ); + $this->assertEquals( 1, count($presenceClientFilter->items), 'Expected the clientId filter to return 1 user' ); + + $connId = $presenceClientFilter->items[0]->connectionId; + + $presenceConnFilter1 = self::$channel->presence->get( array( 'connectionId' => $connId ) ); + $this->assertEquals( 6, count($presenceConnFilter1->items), 'Expected the connectionId filter to return 6 users' ); + + $presenceConnFilter2 = self::$channel->presence->get( array( 'connectionId' => '*FAKE CONNECTION ID*' ) ); + $this->assertEquals( 0, count($presenceConnFilter2->items), 'Expected the connectionId filter to return no users' ); + } } From fca16b5bbfa19c669ee78f67cadbb61edd7507b8 Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 13:20:25 +0200 Subject: [PATCH 04/15] Fixed some tests failing with ABLY_ENV environment variable set Better time synchronization assertion error message --- src/Models/ClientOptions.php | 4 ---- tests/AblyRestTest.php | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Models/ClientOptions.php b/src/Models/ClientOptions.php index 1da186a..ef58613 100644 --- a/src/Models/ClientOptions.php +++ b/src/Models/ClientOptions.php @@ -70,10 +70,6 @@ class ClientOptions extends AuthOptions { public function __construct( $options = array() ) { parent::__construct( $options ); - if ( empty( $this->environment ) && getenv( 'ABLY_ENV' ) ) { - $this->environment = getenv( 'ABLY_ENV' ); - } - if ( empty( $this->host ) ) { $this->host = 'rest.ably.io'; diff --git a/tests/AblyRestTest.php b/tests/AblyRestTest.php index 8aeeeef..6470b3a 100644 --- a/tests/AblyRestTest.php +++ b/tests/AblyRestTest.php @@ -233,7 +233,8 @@ public function testTimeAndAccuracy() { $reportedTime = intval($ably->time()); $actualTime = intval(microtime(true)*1000); - $this->assertTrue( abs($reportedTime - $actualTime) < 2000 ); + $this->assertTrue( abs($reportedTime - $actualTime) < 2000, + 'The time difference was larger than 2000ms: ' . ($reportedTime - $actualTime) .'. Please check your system clock.' ); } /** From 977a881a75dd45462d8ad460f4304e5adcfbd838 Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 13:21:24 +0200 Subject: [PATCH 05/15] Allow messages to have null payload/data + test --- src/Models/BaseMessage.php | 9 ++++-- src/Models/Message.php | 4 ++- tests/ChannelMessagesTest.php | 54 ++++++++++++++++++++++++++++------- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/Models/BaseMessage.php b/src/Models/BaseMessage.php index ec01437..40eb166 100644 --- a/src/Models/BaseMessage.php +++ b/src/Models/BaseMessage.php @@ -117,8 +117,10 @@ protected function encode() { $msg->data = $this->data; $isBinary = true; } + } else if ( !isset( $this->data ) || $this->data === null ) { + return $msg; } else { - throw new AblyException( 'Message data must be either, string, string with binary data, or JSON-encodable array or object.', 40003, 400 ); + throw new AblyException( 'Message data must be either, string, string with binary data, JSON-encodable array or object, or null.', 40003, 400 ); } if ( $this->cipherParams ) { @@ -136,7 +138,10 @@ protected function encode() { } } - $msg->encoding = implode( '/', $encodings ); + if ( count( $encodings ) ) { + $msg->encoding = implode( '/', $encodings ); + } + return $msg; } diff --git a/src/Models/Message.php b/src/Models/Message.php index 848c87e..f1a57b2 100644 --- a/src/Models/Message.php +++ b/src/Models/Message.php @@ -11,7 +11,9 @@ class Message extends BaseMessage { protected function encode() { $msg = parent::encode(); - $msg->name = $this->name; + if ( isset( $this->name ) && $this->name ) { + $msg->name = $this->name; + } return $msg; } diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 9b485fe..2cc8d2b 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -224,16 +224,6 @@ public function testInvalidTypes() { } catch (AblyException $e) { if ( $e->getCode() != 40003 ) $this->fail('Expected exception error code 40003'); } - - $msg = new Message(); - $msg->name = 'null'; - - try { - $channel->publish( $msg ); - $this->fail( 'Expected an exception' ); - } catch (AblyException $e) { - if ( $e->getCode() != 40003 ) $this->fail('Expected exception error code 40003'); - } } /** @@ -384,13 +374,41 @@ public function testMessageEncodings() { $msg->data = hex2bin( '00102030405060708090a0b0c0d0e0f0ff' ); $this->assertEquals( 'cipher+aes-128-cbc/base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' ); } + + /** + * Test if null name and data elements are allowed when publishing messages + */ + public function testNullData() { + $ably = new AblyRest( array_merge( self::$defaultOptions, array( + 'key' => self::$testApp->getAppKeyDefault()->string, + 'httpClass' => 'tests\HttpSaveWrapper', + ) ) ); + + $channel = $ably->channels->get( 'testChannel' ); + + $msg = new Message(); + $msg->name = 'onlyName'; + $msg->data = null; + + $channel->publish( $msg ); + + $this->assertEquals( (object) array( 'name' => 'onlyName' ), json_decode( $ably->http->lastParams ) ); + + $msg = new Message(); + $msg->name = null; + $msg->data = 'onlyData'; + + $channel->publish( $msg ); + + $this->assertEquals( (object) array( 'data' => 'onlyData' ), json_decode( $ably->http->lastParams ) ); + } } class HttpMockMsgCounter extends Http { public $requestCount = 0; - public function request($method, $url, $headers = array(), $params = array()) { + public function request( $method, $url, $headers = array(), $params = array() ) { $this->requestCount++; @@ -400,3 +418,17 @@ public function request($method, $url, $headers = array(), $params = array()) { ); } } + + +class HttpSaveWrapper extends Http { + public $lastResponse; + public $lastHeaders; + public $lastParams; + + public function request( $method, $url, $headers = array(), $params = array() ) { + $this->lastHeaders = $headers; + $this->lastParams = $params; + $this->lastResponse = parent::request( $method, $url, $headers, $params ); + return $lastResponse; + } +} From 471ef26e5517f1018eed6ba8a90da7c1dcf91237 Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 15:20:47 +0200 Subject: [PATCH 06/15] Refactored CipherParams to comform with the spec --- src/Models/BaseMessage.php | 4 ++-- src/Models/CipherParams.php | 27 ++++++++++++++++++++------- src/Utils/Crypto.php | 6 +++--- tests/ChannelHistoryTest.php | 1 - tests/ChannelMessagesTest.php | 16 ++++++++-------- tests/CryptoTest.php | 18 ++++++++++++------ tests/PresenceTest.php | 9 +++++++-- 7 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/Models/BaseMessage.php b/src/Models/BaseMessage.php index 40eb166..5f67262 100644 --- a/src/Models/BaseMessage.php +++ b/src/Models/BaseMessage.php @@ -129,7 +129,7 @@ protected function encode() { } $msg->data = base64_encode( Crypto::encrypt( $msg->data, $this->cipherParams ) ); - $encodings[] = 'cipher+' . $this->cipherParams->algorithm; + $encodings[] = 'cipher+' . $this->cipherParams->getAlgorithmString(); $encodings[] = 'base64'; } else { if ( $isBinary ) { @@ -141,7 +141,7 @@ protected function encode() { if ( count( $encodings ) ) { $msg->encoding = implode( '/', $encodings ); } - + return $msg; } diff --git a/src/Models/CipherParams.php b/src/Models/CipherParams.php index 461b412..5d4374c 100644 --- a/src/Models/CipherParams.php +++ b/src/Models/CipherParams.php @@ -8,20 +8,33 @@ class CipherParams { /** @var string Key used for encryption, may be a binary string. */ public $key; - /** @var string Algorithm to be used for encryption. Valid values are: 'aes-128-cbc' (default) and 'aes-256-cbc'. */ + /** @var string Algorithm to be used for encryption. The only supported algorithm is currently 'aes'. */ public $algorithm; + /** @var string Key length of the algorithm. Valid values for 'aes' are 128 or 256. */ + public $keyLength; + /** @var string Algorithm mode. The only supported mode is currenty 'cbc'. */ + public $mode; /** @var string Initialization vector for encryption, may be a binary string. */ public $iv; /** - * Constructor + * Constructor. The encryption algorithm defaults to the only supported algorithm - AES CBC with + * a default key length of 128. A random IV is generated. * @param string|null $key Encryption key, if not provided a random key is generated. - * @param string|null $algorithm Algorithm to be used for encryption. Valid values are: 'aes-128-cbc' (default) and 'aes-256-cbc'. - * @param string|null $iv Initialization vector for encryption, if not provided, random IV is generated. + * @param string|null $keyLength Cipher key length, defaults to 128. */ - public function __construct( $key = null, $algorithm = null, $iv = null ) { + public function __construct( $key = null, $keyLength = 128 ) { $this->key = $key ? $key : openssl_random_pseudo_bytes( 16 ); - $this->algorithm = $algorithm ? $algorithm : 'aes-128-cbc'; - $this->iv = $iv ? $iv : openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->algorithm ) ); + $this->algorithm = 'aes'; + $this->keyLength = $keyLength; + $this->mode = 'cbc'; + $this->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->getAlgorithmString() ) ); + } + + /** + * @return string Algorithm string as required by openssl - for instance `aes-128-cbc` + */ + public function getAlgorithmString() { + return $this->algorithm . '-' . $this->keyLength . '-' . $this->mode; } } \ No newline at end of file diff --git a/src/Utils/Crypto.php b/src/Utils/Crypto.php index 8d85f9d..1365512 100644 --- a/src/Utils/Crypto.php +++ b/src/Utils/Crypto.php @@ -14,7 +14,7 @@ class Crypto { public static function encrypt( $plaintext, $cipherParams ) { $raw = defined( 'OPENSSL_RAW_DATA' ) ? OPENSSL_RAW_DATA : true; - $ciphertext = openssl_encrypt( $plaintext, $cipherParams->algorithm, $cipherParams->key, $raw, $cipherParams->iv ); + $ciphertext = openssl_encrypt( $plaintext, $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $cipherParams->iv ); if ($ciphertext === false) { return false; @@ -36,7 +36,7 @@ public static function decrypt( $payload, $cipherParams ) { $iv = substr( $payload, 0, 16 ); $ciphertext = substr( $payload, 16 ); - return openssl_decrypt( $ciphertext, $cipherParams->algorithm, $cipherParams->key, $raw, $iv ); + return openssl_decrypt( $ciphertext, $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $iv ); } /** @@ -56,7 +56,7 @@ protected static function updateIV( CipherParams $cipherParams ) { $ivLength = strlen( $cipherParams->iv ); - $cipherParams->iv = openssl_encrypt( str_repeat( ' ', $ivLength ), $cipherParams->algorithm, $cipherParams->key, $raw, $cipherParams->iv ); + $cipherParams->iv = openssl_encrypt( str_repeat( ' ', $ivLength ), $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $cipherParams->iv ); $cipherParams->iv = substr( $cipherParams->iv, 0, $ivLength); } } diff --git a/tests/ChannelHistoryTest.php b/tests/ChannelHistoryTest.php index 2c895b9..9e8c54a 100644 --- a/tests/ChannelHistoryTest.php +++ b/tests/ChannelHistoryTest.php @@ -2,7 +2,6 @@ namespace tests; use Ably\AblyRest; use Ably\Channel; -use Ably\Models\CipherParams; use Ably\Models\Message; require_once __DIR__ . '/factories/TestApp.php'; diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 2cc8d2b..8aca518 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -61,7 +61,7 @@ private function executePublishTestOnChannel(Channel $channel) { $msgJSON = json_decode( $msg->toJSON() ); $this->assertTrue( - strpos( $msgJSON->encoding, $channel->getCipherParams()->algorithm ) !== false, + strpos( $msgJSON->encoding, $channel->getCipherParams()->getAlgorithmString() ) !== false, 'Expected message encoding to contain a cipher algorithm' ); $this->assertFalse( $msgJSON->data === $payload, 'Expected encrypted message payload not to match original data' ); @@ -109,7 +109,7 @@ public function testPublishMessagesVariousTypesUnencrypted() { * Publish events with data of various datatypes to an aes-128-cbc encrypted channel */ public function testPublishMessagesVariousTypesAES128() { - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes-128-cbc' )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); $encrypted1 = self::$ably->channels->get( 'persisted:encrypted1', $options ); $this->assertNotNull( $encrypted1->getCipherParams(), 'Expected channel to be encrypted' ); @@ -121,7 +121,7 @@ public function testPublishMessagesVariousTypesAES128() { * Publish events with data of various datatypes to an aes-256-cbc encrypted channel */ public function testPublishMessagesVariousTypesAES256() { - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes-256-cbc' )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 256 )); $encrypted2 = self::$ably->channels->get( 'persisted:encrypted2', $options ); $this->assertNotNull( $encrypted2->getCipherParams(), 'Expected channel to be encrypted' ); @@ -257,7 +257,7 @@ public function testEncryptedMessageUnencryptedHistory() { $payload = 'This is a test message'; - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes-128-cbc' )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); $encrypted1 = $ably->channel( 'persisted:mismatch1', $options ); $encrypted1->publish( 'test', $payload ); @@ -280,7 +280,7 @@ public function testUnencryptedMessageEncryptedHistory() { $encrypted = self::$ably->channel( 'persisted:mismatch2' ); $encrypted->publish( 'test', $payload ); - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes-128-cbc' )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); $unencrypted = self::$ably->channel( 'persisted:mismatch2', $options ); $messages = $unencrypted->history(); $this->assertNotNull( $messages, 'Expected non-null messages' ); @@ -305,11 +305,11 @@ public function testEncryptionKeyMismatch() { $payload = 'This is a test message'; - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes-128-cbc' )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); $encrypted1 = $ably->channel( 'persisted:mismatch3', $options ); $encrypted1->publish( 'test', $payload ); - $options2 = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'DIFFERENT PASSWORD', 'aes-128-cbc' )); + $options2 = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'DIFFERENT PASSWORD', 128 )); $encrypted2 = $ably->channel( 'persisted:mismatch3', $options2 ); $messages = $encrypted2->history(); $msg = $messages->items[0]; @@ -337,7 +337,7 @@ public function testChannelCaching() { self::$ably->channel( 'cache_test', array( 'encrypted' => true, - 'cipherParams' => new CipherParams( 'password', 'aes-128-cbc' ) + 'cipherParams' => new CipherParams( 'password', 128 ) ) ); $this->assertNotNull( $channel3->getCipherParams(), 'Expected the channel to have CipherParams even when specified for a new instance' ); diff --git a/tests/CryptoTest.php b/tests/CryptoTest.php index 63a316f..099b8cc 100644 --- a/tests/CryptoTest.php +++ b/tests/CryptoTest.php @@ -20,9 +20,12 @@ public function testMessageEncryptionAgainstFixture( $filename ) { foreach ($fixture->items as $example) { $key = base64_decode( $fixture->key ); - $algorithm = $fixture->algorithm . '-' . $fixture->keylength . '-' . $fixture->mode; - $iv = base64_decode( $fixture->iv ); - $cipherParams = new CipherParams( $key, $algorithm, $iv ); + + $cipherParams = new CipherParams( $key ); + $cipherParams->algorithm = $fixture->algorithm; + $cipherParams->keyLength = $fixture->keylength; + $cipherParams->mode = $fixture->mode; + $cipherParams->iv = base64_decode( $fixture->iv ); $decodedExample = new Message(); $decodedExample->fromJSON( $example->encoded ); @@ -53,9 +56,12 @@ public function testPrenenceMessageEncryptionAgainstFixture( $filename ) { unset ($example->encrypted->name); $key = base64_decode( $fixture->key ); - $algorithm = $fixture->algorithm . '-' . $fixture->keylength . '-' . $fixture->mode; - $iv = base64_decode( $fixture->iv ); - $cipherParams = new CipherParams( $key, $algorithm, $iv ); + + $cipherParams = new CipherParams( $key ); + $cipherParams->algorithm = $fixture->algorithm; + $cipherParams->keyLength = $fixture->keylength; + $cipherParams->mode = $fixture->mode; + $cipherParams->iv = base64_decode( $fixture->iv ); $decodedExample = new PresenceMessage(); $decodedExample->fromJSON( $example->encoded ); diff --git a/tests/PresenceTest.php b/tests/PresenceTest.php index b29f67e..6d12726 100644 --- a/tests/PresenceTest.php +++ b/tests/PresenceTest.php @@ -27,11 +27,16 @@ public static function setUpBeforeClass() { self::$presenceFixture = $fixture->post_apps->channels[0]->presence; $key = base64_decode( $fixture->cipher->key ); - $algorithm = $fixture->cipher->algorithm . '-' . $fixture->cipher->keylength . '-' . $fixture->cipher->mode; + + $cipherParams = new CipherParams( $key ); + $cipherParams->algorithm = $fixture->cipher->algorithm; + $cipherParams->keyLength = $fixture->cipher->keylength; + $cipherParams->mode = $fixture->cipher->mode; + $cipherParams->iv = base64_decode( $fixture->cipher->iv ); $options = array( 'encrypted' => true, - 'cipherParams' => new CipherParams( $key, $algorithm ) + 'cipherParams' => $cipherParams, ); self::$channel = self::$ably->channel('persisted:presence_fixtures', $options); From 168feb08c11406302bbf2010d2ed46d837aed77f Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 29 Sep 2015 15:43:38 +0200 Subject: [PATCH 07/15] Added support for `port` and `tlsPort` parameters in ClientOptions + test --- src/AblyRest.php | 8 ++++++++ src/Models/ClientOptions.php | 14 +++++++++++++- tests/AblyRestTest.php | 28 +++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/AblyRest.php b/src/AblyRest.php index 828c9c7..dfcee9c 100644 --- a/src/AblyRest.php +++ b/src/AblyRest.php @@ -131,6 +131,14 @@ public function request( $method, $path, $headers = array(), $params = array(), $res = $this->requestWithFallback( $method, $path, $mergedHeaders, $params ); } else { $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->host; + + if ( $this->options->tls && !empty( $this->options->tlsPort ) ) { + $server .= ':' . $this->options->tlsPort; + } + if ( !$this->options->tls && !empty( $this->options->port ) ) { + $server .= ':' . $this->options->port; + } + $res = $this->http->request( $method, $server . $path, $mergedHeaders, $params ); } } catch (AblyRequestException $e) { diff --git a/src/Models/ClientOptions.php b/src/Models/ClientOptions.php index ef58613..3f8b1a9 100644 --- a/src/Models/ClientOptions.php +++ b/src/Models/ClientOptions.php @@ -41,10 +41,22 @@ class ClientOptions extends AuthOptions { /** * @var string alternate server domain - * For use in development environments only. + * For development environments only. */ public $host; + /** + * @var integer Allows a non-default Ably non-TLS port to be used. + * For development environments only. + */ + public $port; + + /** + * @var integer Allows a non-default Ably TLS port to be used. + * For development environments only. + */ + public $tlsPort; + /** * @var string optional prefix to be prepended to $host * Example: 'sandbox' -> 'sandbox-rest.ably.io' diff --git a/tests/AblyRestTest.php b/tests/AblyRestTest.php index 6470b3a..b54f573 100644 --- a/tests/AblyRestTest.php +++ b/tests/AblyRestTest.php @@ -61,7 +61,7 @@ public function testInitLibWithTokenDetailsOption() { } /** - * Init library with specified host + * Init library with a specified host */ public function testInitLibWithSpecifiedHost() { $opts = array( @@ -74,6 +74,32 @@ public function testInitLibWithSpecifiedHost() { $this->assertRegExp( '/^https?:\/\/some\.other\.host/', $ably->http->lastUrl, 'Unexpected host mismatch' ); } + /** + * Init library with a specified port + */ + public function testInitLibWithSpecifiedPort() { + $opts = array( + 'key' => 'fake.key:veryFake', + 'host' => 'some.other.host', + 'tlsPort' => 999, + 'httpClass' => 'tests\HttpMockInitTest', + ); + $ably = new AblyRest( $opts ); + $ably->time(); // make a request + $this->assertContains( 'https://' . $opts['host'] . ':' . $opts['tlsPort'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); + + $opts = array( + 'token' => 'fakeToken', + 'host' => 'some.other.host', + 'port' => 999, + 'tls' => false, + 'httpClass' => 'tests\HttpMockInitTest', + ); + $ably = new AblyRest( $opts ); + $ably->time(); // make a request + $this->assertContains( 'http://' . $opts['host'] . ':' . $opts['port'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); + } + /** * Init library with specified environment */ From 5a31d3f070ba7784022a3f104cd38ef9577944b8 Mon Sep 17 00:00:00 2001 From: blade Date: Wed, 30 Sep 2015 10:20:54 +0200 Subject: [PATCH 08/15] Avoid race conditions with tokens with a 15 second safe buffer Added missing docs --- src/Auth.php | 15 ++++++++++++--- tests/TokenTest.php | 7 ++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 36d7f40..c110561 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -18,6 +18,7 @@ class Auth { private $basicAuth; private $tokenDetails; private $ably; + const TOKEN_EXPIRY_MARGIN = 15000; // a token is considered expired a bit earlier to prevent race conditions public function __construct( AblyRest $ably, ClientOptions $options ) { $this->authOptions = new AuthOptions($options); @@ -57,9 +58,14 @@ public function isUsingBasicAuth() { } /** - * Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. + * Ensures that a valid token is present for the library instance. This may rely on an already-known and valid token, + * and will obtain a new token if necessary. * In the event that a new token request is made, the specified options are used. * If not already using token based auth, this will enable it. + * @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones + * @param array|null $tokenParams Requested token parameters + * @param boolean|null $force Forces generation of a fresh token + * @return \Ably\Models\TokenDetails The new token */ public function authorise( $authOptions = array(), $tokenParams = array(), $force = false ) { if ( !$force && !empty( $this->tokenDetails ) ) { @@ -67,7 +73,7 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc // using cached token Log::d( 'Auth::authorise: using cached token, unknown expiration time' ); return $this; - } else if ( $this->tokenDetails->expires > $this->ably->systemTime() ) { + } else if ( $this->tokenDetails->expires - self::TOKEN_EXPIRY_MARGIN > $this->ably->systemTime() ) { // using cached token Log::d( 'Auth::authorise: using cached token, expires on ' . date( 'Y-m-d H:i:s', $this->tokenDetails->expires / 1000 ) ); return $this; @@ -84,6 +90,7 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc /** * Get HTTP headers with authentication data * Automatically attempts to authorise token requests + * @return Array Array of HTTP headers containing an `Authorization` header */ public function getAuthHeaders() { $header = array(); @@ -106,11 +113,12 @@ public function getTokenDetails() { } /** - * Request a new Token + * Request a new token. * @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones * @param array|null $tokenParams Requested token parameters * @param \Ably\Models\ClientOptions|array $options * @throws \Ably\Exceptions\AblyException + * @return \Ably\Models\TokenDetails The new token */ public function requestToken( $authOptions = array(), $tokenParams = array() ) { @@ -205,6 +213,7 @@ public function requestToken( $authOptions = array(), $tokenParams = array() ) { * signed requests for submission by another client. * @param \Ably\Models\AuthOptions $authOptions * @param \Ably\Models\TokenParams $tokenParams + * @return \Ably\Models\TokenRequest A signed token request */ public function createTokenRequest( $authOptions = array(), $tokenParams = array() ) { $authOptions = new AuthOptions( array_merge( $this->authOptions->toArray(), $authOptions ) ); diff --git a/tests/TokenTest.php b/tests/TokenTest.php index df2f781..da81e07 100644 --- a/tests/TokenTest.php +++ b/tests/TokenTest.php @@ -1,6 +1,7 @@ function( $tokenParams ) use( &$ablyKeyAuth ) { $capability = array( 'testchannel' => array('publish') ); $tokenParams = array( - 'ttl' => 2 * 1000, // 2 seconds + 'ttl' => 2 * 1000 + Auth::TOKEN_EXPIRY_MARGIN, // 2 seconds + expiry margin 'capability' => $capability, ); return $ablyKeyAuth->auth->requestToken( array(), $tokenParams ); @@ -227,7 +228,7 @@ public function testFailingTokenRenewalKnownExpiration() { $ablyKeyAuth = self::$ably; $tokenParams = array( - 'ttl' => 2 * 1000, // 2 seconds + 'ttl' => 2 * 1000 + Auth::TOKEN_EXPIRY_MARGIN, // 2 seconds + expiry margin ); $tokenDetails = $ablyKeyAuth->auth->requestToken( array(), $tokenParams ); @@ -259,7 +260,7 @@ public function testTokenRenewalUnknownExpiration() { 'capability' => $capability, ); $tokenDetails = $ablyKeyAuth->auth->requestToken( array(), $tokenParams ); - return $tokenDetails->token; + return $tokenDetails->token; // returning just the token string, not TokenDetails => expiry time is unknown } ) ); From d10e5e4899a6e7aa62f010838b449c4afdc44249 Mon Sep 17 00:00:00 2001 From: blade Date: Wed, 30 Sep 2015 11:30:05 +0200 Subject: [PATCH 09/15] Updated TokenParams, TokenRequest, and AuthOptions parameters to match the spec --- src/Auth.php | 2 +- src/Models/AuthOptions.php | 8 ++++++++ src/Models/TokenParams.php | 9 --------- src/Models/TokenRequest.php | 5 +++++ 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index c110561..251a8a2 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -236,7 +236,7 @@ public function createTokenRequest( $authOptions = array(), $tokenParams = array $tokenRequest->keyName = $keyName; } - if ( $tokenRequest->queryTime ) { + if ( $authOptions->queryTime ) { $tokenRequest->timestamp = $this->ably->time(); } else if ( empty( $tokenRequest->timestamp ) ) { $tokenRequest->timestamp = $this->ably->systemTime(); diff --git a/src/Models/AuthOptions.php b/src/Models/AuthOptions.php index 8839b2e..9b6888c 100644 --- a/src/Models/AuthOptions.php +++ b/src/Models/AuthOptions.php @@ -67,6 +67,14 @@ class AuthOptions extends BaseOptions { */ public $authMethod = 'GET'; + /** + * @var boolean This may be set in instances that the library is to sign + * token requests based on a given key. If true, the library + * will query the Ably system for the current time instead of + * relying on a locally-available time of day. + */ + public $queryTime; + public function __construct( $options = array() ) { parent::__construct( $options ); diff --git a/src/Models/TokenParams.php b/src/Models/TokenParams.php index b1a91dd..ba9bc6f 100644 --- a/src/Models/TokenParams.php +++ b/src/Models/TokenParams.php @@ -5,10 +5,6 @@ * Provides parameters of a token request. */ class TokenParams extends BaseOptions { - /** - * @var string The keyName of the key against which this request is made. - */ - public $keyName; /** * @var integer Requested time to live for the token in milliseconds. If the token request @@ -37,11 +33,6 @@ class TokenParams extends BaseOptions { */ public $timestamp; - /** - * @var boolean whether timestamp should be obtained from server (true) or local time should be used (false) - */ - public $queryTime; - /** * Constructor. Automatically canonicalizes capability, if provided as array or object. * If capability is a string, it is assumed that it's already a canonicalized json_encoded string. diff --git a/src/Models/TokenRequest.php b/src/Models/TokenRequest.php index 78f7e27..c634f62 100644 --- a/src/Models/TokenRequest.php +++ b/src/Models/TokenRequest.php @@ -6,6 +6,11 @@ */ class TokenRequest extends TokenParams { + /** + * @var string The keyName of the key against which this request is made. + */ + public $keyName; + /** * @var string An opaque nonce string of at least 16 characters to ensure * uniqueness of this request. Any subsequent request using the From 7656395e0abe884bdf9bd88be42adb1dc12125e3 Mon Sep 17 00:00:00 2001 From: blade Date: Wed, 30 Sep 2015 12:17:45 +0200 Subject: [PATCH 10/15] Added a method for easily retrieving the library instance's clientId --- src/Auth.php | 11 +++++++++++ tests/AuthTest.php | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Auth.php b/src/Auth.php index 251a8a2..014db84 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -112,6 +112,17 @@ public function getTokenDetails() { return $this->tokenDetails; } + /** + * @return string|null Library instance's clientId, if instanced with a clientId + */ + public function getClientId() { + if ( !empty( $this->tokenDetails ) && !empty( $this->tokenDetails->clientId ) ) { + return $this->tokenDetails->clientId; + } + + return null; + } + /** * Request a new token. * @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones diff --git a/tests/AuthTest.php b/tests/AuthTest.php index d4a893f..856a328 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -178,7 +178,7 @@ public function testAuthWithAuthUrlTokenString() { } /** - * Init library with a key and clientId; expect token auth to be chosen + * Init library with a key and clientId; expect token auth to be chosen; expect Auth::getClientId to return the id */ public function testAuthWithKeyAndClientId() { $ably = new AblyRest( array_merge( self::$defaultOptions, array( @@ -187,6 +187,9 @@ public function testAuthWithKeyAndClientId() { ) ) ); $this->assertFalse( $ably->auth->isUsingBasicAuth(), 'Expected token auth to be used' ); + + $ably->auth->authorise(); + $this->assertEquals( 'testClientId', $ably->auth->getClientId(), 'Expected clientId to match the provided id' ); } /** From 660b161fbd8fbb59933f4eeff4693297a0abb1fe Mon Sep 17 00:00:00 2001 From: blade Date: Fri, 2 Oct 2015 10:05:17 +0200 Subject: [PATCH 11/15] Added getClientId to auth, clientId checks when publishing messages Messages can explicitly set clientId Comments in AblyException Removed bad test TokenTest::testFailingTokenRenewalKnownExpiration --- src/Auth.php | 11 +++--- src/Channel.php | 12 +++++-- src/Exceptions/AblyException.php | 7 ++++ src/Models/BaseMessage.php | 4 +++ tests/ChannelMessagesTest.php | 61 ++++++++++++++++++++++++++++++++ tests/TokenTest.php | 25 ------------- 6 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index 014db84..c49aaf2 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -72,11 +72,11 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc if ( empty( $this->tokenDetails->expires ) ) { // using cached token Log::d( 'Auth::authorise: using cached token, unknown expiration time' ); - return $this; + return $this->tokenDetails; } else if ( $this->tokenDetails->expires - self::TOKEN_EXPIRY_MARGIN > $this->ably->systemTime() ) { // using cached token Log::d( 'Auth::authorise: using cached token, expires on ' . date( 'Y-m-d H:i:s', $this->tokenDetails->expires / 1000 ) ); - return $this; + return $this->tokenDetails; } } Log::d( 'Auth::authorise: requesting new token' ); @@ -96,12 +96,11 @@ public function getAuthHeaders() { $header = array(); if ( $this->isUsingBasicAuth() ) { $header = array( 'Authorization: Basic ' . base64_encode( $this->authOptions->key ) ); - } else if ( !empty( $this->tokenDetails ) ) { + } else { $this->authorise(); $header = array( 'Authorization: Bearer '. base64_encode( $this->tokenDetails->token ) ); - } else { - throw new AblyException( 'Unable to provide auth headers. No auth parameters defined.', 40101, 401 ); } + return $header; } @@ -119,7 +118,7 @@ public function getClientId() { if ( !empty( $this->tokenDetails ) && !empty( $this->tokenDetails->clientId ) ) { return $this->tokenDetails->clientId; } - + return null; } diff --git a/src/Channel.php b/src/Channel.php index 182de98..7b06f5f 100644 --- a/src/Channel.php +++ b/src/Channel.php @@ -60,20 +60,28 @@ public function publish() { $args = func_get_args(); $json = ''; + $authClientId = $this->ably->auth->getClientId(); if (count($args) == 1 && is_a( $args[0], 'Ably\Models\Message' )) { // single Message $msg = $args[0]; + if ( !empty( $msg->clientId ) && !empty( $authClientId ) && $msg->clientId != $authClientId) { + throw new AblyException( 'Message\'s clientId does not match the clientId of the authorisation token.', 40102, 401 ); + } + if ($this->options->encrypted) { $msg->setCipherParams( $this->options->cipherParams ); } $json = $msg->toJSON(); } else if (count($args) == 1 && is_array( $args[0] )) { // array of Messages - $msg = $args[0]; $jsonArray = array(); foreach ($args[0] as $msg) { + if ( !empty( $msg->clientId ) && !empty( $authClientId ) && $msg->clientId != $authClientId) { + throw new AblyException( 'Message\'s clientId does not match the clientId of the authorisation token.', 40102, 401 ); + } + if ($this->options->encrypted) { $msg->setCipherParams( $this->options->cipherParams ); } @@ -93,7 +101,7 @@ public function publish() { $json = $msg->toJSON(); } else { - throw new AblyException( 'Wrong parameters provided, use either Message, array of Messages, or name and data' ); + throw new AblyException( 'Wrong parameters provided, use either Message, array of Messages, or name and data', 40003, 400 ); } $this->ably->post( $this->channelPath . '/messages', $headers = array(), $json ); diff --git a/src/Exceptions/AblyException.php b/src/Exceptions/AblyException.php index d9ce4fc..3ab5d2f 100644 --- a/src/Exceptions/AblyException.php +++ b/src/Exceptions/AblyException.php @@ -14,6 +14,13 @@ class AblyException extends Exception { */ public $errorInfo; + /** + * @param string $message Exception's error message text + * @param integer|null $code 5-digit Ably error code + * also used as a PHP exception code + * @see https://github.com/ably/ably-common/blob/master/protocol/errors.json + * @param integer|null $statusCode HTTP error code + */ public function __construct( $message, $code = null, $statusCode = null ) { parent::__construct( $message, $code ); $this->errorInfo = new ErrorInfo(); diff --git a/src/Models/BaseMessage.php b/src/Models/BaseMessage.php index 5f67262..d47ce61 100644 --- a/src/Models/BaseMessage.php +++ b/src/Models/BaseMessage.php @@ -103,6 +103,10 @@ protected function encode() { return $msg; } + + if ($this->clientId) { + $msg->clientId = $this->clientId; + } $isBinary = false; $encodings = array(); diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 8aca518..530f3ad 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -402,6 +402,67 @@ public function testNullData() { $this->assertEquals( (object) array( 'data' => 'onlyData' ), json_decode( $ably->http->lastParams ) ); } + + /** + * Check if messages can be assigned a clientId with an anonymous lib instance + */ + public function testClientIdMsg() { + $ablyKey = self::$ably; + $ablyToken = $ably = new AblyRest( array_merge( self::$defaultOptions, array( + 'key' => self::$testApp->getAppKeyDefault()->string, + 'useTokenAuth' => true, + ) ) ); + + $clientId = 'testClientId'; + $msg = new Message(); + $msg->data = 'test'; + $msg->clientId = $clientId; + + $keyChan = $ablyKey->channels->get( 'persisted:clientIdTestKey' ); + $keyChan->publish( $msg ); + $retrievedMsg = $keyChan->history()->items[0]; + + $this->assertEquals( $clientId, $retrievedMsg->clientId, 'Expected clientIds to match'); + + $tokenChan = $ablyToken->channels->get( 'persisted:clientIdTestToken' ); + $tokenChan->publish( $msg ); + $retrievedMsg = $tokenChan->history()->items[0]; + + $this->assertEquals( $clientId, $retrievedMsg->clientId, 'Expected clientIds to match'); + } + + /** + * Check if messages are assigned a clientID automatically with a non-anonymous lib instance + * Verify that clientID mismatch produces an exception within the library + */ + public function testClientIdLib() { + $clientId = 'testClientId'; + + $ablyCId = new AblyRest( array_merge( self::$defaultOptions, array( + 'key' => self::$testApp->getAppKeyDefault()->string, + 'useTokenAuth' => true, + 'clientId' => 'testClientId', + ) ) ); + + $ablyCId->auth->authorise(); // obtain a token + $this->assertEquals( $clientId, $ablyCId->auth->getClientId(), 'Expected a token with specified clientId to be used' ); + + $msg = new Message(); + $msg->data = 'test'; + + $channel = $ablyCId->channels->get( 'persisted:clientIdTestLib' ); + $channel->publish( $msg ); + $retrievedMsg = $channel->history()->items[0]; + + $this->assertEquals( $clientId, $retrievedMsg->clientId, 'Expected clientIds to match'); + + $msg = new Message(); + $msg->data = 'test'; + $msg->clientId = 'DIFFERENT_clientId'; + + $this->setExpectedException( 'Ably\Exceptions\AblyException', '', 40102 ); + $channel->publish( $msg ); + } } diff --git a/tests/TokenTest.php b/tests/TokenTest.php index da81e07..aea0b37 100644 --- a/tests/TokenTest.php +++ b/tests/TokenTest.php @@ -221,31 +221,6 @@ public function testTokenRenewalKnownExpiration() { $this->assertFalse( $tokenReq1 == $tokenReq2, 'Expected token to change after expiration' ); } - /** - * Automatic token renewal on expiration (known time) should fail with no means of renewal - */ - public function testFailingTokenRenewalKnownExpiration() { - $ablyKeyAuth = self::$ably; - - $tokenParams = array( - 'ttl' => 2 * 1000 + Auth::TOKEN_EXPIRY_MARGIN, // 2 seconds + expiry margin - ); - $tokenDetails = $ablyKeyAuth->auth->requestToken( array(), $tokenParams ); - - $options = array_merge( self::$defaultOptions, array( - 'tokenDetails' => $tokenDetails, - ) ); - - $ablyTokenAuth = new AblyRest( $options ); - $channel = $ablyTokenAuth->channel( 'testchannel' ); - $channel->publish( 'test', 'test' ); // this should work - - sleep(2); - - $this->setExpectedException( 'Ably\Exceptions\AblyException', '', 40101 ); - $channel->publish( 'test', 'test' ); // this should fail - } - /** * Automatic token renewal on expiration (unknown time) */ From de5bae7fb6922112420401f91e7a572ddfb0b7cf Mon Sep 17 00:00:00 2001 From: blade Date: Fri, 2 Oct 2015 16:05:07 +0200 Subject: [PATCH 12/15] Updated types to exactly match the spec TypesTest --- src/AblyRest.php | 4 +- src/Models/BaseMessage.php | 6 +- src/Models/ClientOptions.php | 10 +-- src/Models/Stats.php | 7 +- tests/AblyRestTest.php | 18 ++-- tests/TypesTest.php | 165 +++++++++++++++++++++++++++++++++++ tests/factories/TestApp.php | 2 +- 7 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 tests/TypesTest.php diff --git a/src/AblyRest.php b/src/AblyRest.php index dfcee9c..28ece7d 100644 --- a/src/AblyRest.php +++ b/src/AblyRest.php @@ -130,7 +130,7 @@ public function request( $method, $path, $headers = array(), $params = array(), if ( !empty( $this->options->fallbackHosts ) ) { $res = $this->requestWithFallback( $method, $path, $mergedHeaders, $params ); } else { - $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->host; + $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->restHost; if ( $this->options->tls && !empty( $this->options->tlsPort ) ) { $server .= ':' . $this->options->tlsPort; @@ -173,7 +173,7 @@ public function request( $method, $path, $headers = array(), $params = array(), protected function requestWithFallback( $method, $path, $headers = array(), $params = array(), $attempt = 0 ) { try { if ( $attempt == 0 ) { // using default host - $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->host; + $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->restHost; } else { // using a fallback host Log::d( 'Connection failed, attempting with fallback server #' . $attempt ); // attempt 1 uses fallback host with index 0 diff --git a/src/Models/BaseMessage.php b/src/Models/BaseMessage.php index d47ce61..f160efe 100644 --- a/src/Models/BaseMessage.php +++ b/src/Models/BaseMessage.php @@ -12,7 +12,11 @@ abstract class BaseMessage { /** - * @var mixed The message payload. + * @var string Unique ID for this message. Populated by the system. + */ + public $id; + /** + * @var mixed|null The message payload. */ public $data; /** diff --git a/src/Models/ClientOptions.php b/src/Models/ClientOptions.php index 3f8b1a9..fb9a24e 100644 --- a/src/Models/ClientOptions.php +++ b/src/Models/ClientOptions.php @@ -43,7 +43,7 @@ class ClientOptions extends AuthOptions { * @var string alternate server domain * For development environments only. */ - public $host; + public $restHost; /** * @var integer Allows a non-default Ably non-TLS port to be used. @@ -58,7 +58,7 @@ class ClientOptions extends AuthOptions { public $tlsPort; /** - * @var string optional prefix to be prepended to $host + * @var string optional prefix to be prepended to $restHost * Example: 'sandbox' -> 'sandbox-rest.ably.io' */ public $environment; @@ -82,8 +82,8 @@ class ClientOptions extends AuthOptions { public function __construct( $options = array() ) { parent::__construct( $options ); - if ( empty( $this->host ) ) { - $this->host = 'rest.ably.io'; + if ( empty( $this->restHost ) ) { + $this->restHost = 'rest.ably.io'; if ( empty( $this->environment ) ) { $this->fallbackHosts = array( @@ -99,7 +99,7 @@ public function __construct( $options = array() ) { } if ( !empty( $this->environment ) ) { - $this->host = $this->environment . '-' . $this->host; + $this->restHost = $this->environment . '-' . $this->restHost; } } } \ No newline at end of file diff --git a/src/Models/Stats.php b/src/Models/Stats.php index 981ea0e..01b44fc 100644 --- a/src/Models/Stats.php +++ b/src/Models/Stats.php @@ -15,6 +15,9 @@ class Stats { * @var stdClass $channels ResourceCount representing the number of channels activated and used; * @var stdClass $apiRequests RequestCount representing the number of requests made to the REST API; * @var stdClass $tokenRequests RequestCount representing the number of requests made to issue access tokens. + * @var stdClass $intervalId The interval that this statistic applies to. + * @var stdClass $intervalGranularity The granularity of the interval for the stat. May be one of: minute, hour, day, month + * @var stdClass $intervalTime A timestamp representing the start of the interval. */ public $all; public $inbound; @@ -24,9 +27,9 @@ class Stats { public $channels; public $apiRequests; public $tokenRequests; - public $count; - public $unit; public $intervalId; + public $intervalGranularity; + public $intervalTime; /** * Populates stats from JSON diff --git a/tests/AblyRestTest.php b/tests/AblyRestTest.php index b54f573..d950c5a 100644 --- a/tests/AblyRestTest.php +++ b/tests/AblyRestTest.php @@ -66,7 +66,7 @@ public function testInitLibWithTokenDetailsOption() { public function testInitLibWithSpecifiedHost() { $opts = array( 'key' => 'fake.key:veryFake', - 'host' => 'some.other.host', + 'restHost' => 'some.other.host', 'httpClass' => 'tests\HttpMockInitTest', ); $ably = new AblyRest( $opts ); @@ -80,24 +80,24 @@ public function testInitLibWithSpecifiedHost() { public function testInitLibWithSpecifiedPort() { $opts = array( 'key' => 'fake.key:veryFake', - 'host' => 'some.other.host', + 'restHost' => 'some.other.host', 'tlsPort' => 999, 'httpClass' => 'tests\HttpMockInitTest', ); $ably = new AblyRest( $opts ); $ably->time(); // make a request - $this->assertContains( 'https://' . $opts['host'] . ':' . $opts['tlsPort'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); + $this->assertContains( 'https://' . $opts['restHost'] . ':' . $opts['tlsPort'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); $opts = array( 'token' => 'fakeToken', - 'host' => 'some.other.host', + 'restHost' => 'some.other.host', 'port' => 999, 'tls' => false, 'httpClass' => 'tests\HttpMockInitTest', ); $ably = new AblyRest( $opts ); $ably->time(); // make a request - $this->assertContains( 'http://' . $opts['host'] . ':' . $opts['port'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); + $this->assertContains( 'http://' . $opts['restHost'] . ':' . $opts['port'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); } /** @@ -119,7 +119,7 @@ public function testInitLibWithSpecifiedEnv() { public function testInitLibWithSpecifiedEnvHost() { $ably = new AblyRest( array( 'key' => 'fake.key:veryFake', - 'host' => 'some.other.host', + 'restHost' => 'some.other.host', 'environment' => 'sandbox', 'httpClass' => 'tests\HttpMockInitTest', ) ); @@ -173,7 +173,7 @@ public function testTLSExplicitTrue() { */ public function testFallbackHosts() { $defaultOpts = new ClientOptions(); - $hostWithFallbacks = array_merge( array( $defaultOpts->host ), $defaultOpts->fallbackHosts ); + $hostWithFallbacks = array_merge( array( $defaultOpts->restHost ), $defaultOpts->fallbackHosts ); // reuse default options so that fallback host order is not randomized again $opts = array_merge ( $defaultOpts->toArray(), array( @@ -220,7 +220,7 @@ public function testNoFallbackOnCustomHost() { $opts = array( 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTestTimeout', - 'host' => 'custom.host.com', + 'restHost' => 'custom.host.com', ); $ably = new AblyRest( $opts ); try { @@ -269,7 +269,7 @@ public function testTimeAndAccuracy() { public function testTimeFailsWithInvalidHost() { $ablyInvalidHost = new AblyRest( array( 'key' => 'fake.key:veryFake', - 'host' => 'this.host.does.not.exist', + 'restHost' => 'this.host.does.not.exist', )); $this->setExpectedException('Ably\Exceptions\AblyRequestException'); diff --git a/tests/TypesTest.php b/tests/TypesTest.php new file mode 100644 index 0000000..ff838f5 --- /dev/null +++ b/tests/TypesTest.php @@ -0,0 +1,165 @@ +assertTrue( $valid, "Expected class `$class` to contain a field named `$member`." ); + } + + protected function verifyClassConstants( $class, $expectedMembers ) { + $valid = true; + foreach( $expectedMembers as $member => $value ) { + if ( constant( "$class::$member" ) != $value ) { + $valid = false; + break; + } + } + + $this->assertTrue( $valid, "Expected class `$class` to have a constant `$member` with a value of `$value`." ); + } + + public function testMessageType() { + $this->verifyClassMembers( '\Ably\Models\Message', array( + 'id', + 'clientId', + 'connectionId', + 'data', + 'encoding', + 'timestamp', + ) ); + } + + public function testPresenceMessageType() { + $this->verifyClassMembers( '\Ably\Models\PresenceMessage', array( + 'id', + 'action', + 'clientId', + 'connectionId', + 'data', + 'encoding', + 'timestamp', + 'memberKey' + ) ); + + $this->verifyClassConstants( '\Ably\Models\PresenceMessage', array( + 'ABSENT' => 0, + 'PRESENT' => 1, + 'ENTER' => 2, + 'LEAVE' => 3, + 'UPDATE' => 4 + ) ); + } + + public function testTokenRequestType() { + $this->verifyClassMembers( '\Ably\Models\TokenRequest', array( + 'keyName', + 'clientId', + 'nonce', + 'mac', + 'capability', + 'ttl', + ) ); + } + + public function testTokenDetailsType() { + $this->verifyClassMembers( '\Ably\Models\TokenDetails', array( + 'token', + 'expires', + 'issued', + 'capability', + 'clientId', + ) ); + } + + public function testStatsType() { + $this->verifyClassMembers( '\Ably\Models\Stats', array( + 'all', + 'apiRequests', + 'channels', + 'connections', + 'inbound', + 'intervalGranularity', + 'intervalId', + 'intervalTime', + 'outbound', + 'persisted', + 'tokenRequests' + ) ); + } + + public function testErrorInfoType() { + $this->verifyClassMembers( '\Ably\Models\ErrorInfo', array( + 'code', + 'statusCode', + 'message', + ) ); + } + + public function testClientOptionsType() { + $this->verifyClassMembers( '\Ably\Models\ClientOptions', array( + 'clientId', + 'logLevel', + 'logHandler', + 'tls', + 'useBinaryProtocol', + 'key', + 'token', + 'tokenDetails', + 'useTokenAuth', + 'authCallback', + 'authUrl', + 'authMethod', + 'authHeaders', + 'authParams', + 'queryTime', + 'environment', + 'restHost', + 'port', + 'tlsPort' + ) ); + } + + public function testTokenParamsType() { + $this->verifyClassMembers( '\Ably\Models\TokenParams', array( + 'ttl', + 'capability', + 'clientId', + 'timestamp', + ) ); + } + + public function testChannelOptionsType() { + $this->verifyClassMembers( '\Ably\Models\ChannelOptions', array( + 'encrypted', + 'cipherParams', + ) ); + } + + public function testCipherParamsType() { + $this->verifyClassMembers( '\Ably\Models\CipherParams', array( + 'algorithm', + 'keyLength', + 'mode' + ) ); + } +} \ No newline at end of file diff --git a/tests/factories/TestApp.php b/tests/factories/TestApp.php index fafe2a0..aed214e 100644 --- a/tests/factories/TestApp.php +++ b/tests/factories/TestApp.php @@ -33,7 +33,7 @@ public function __construct() { $this->options = $settings; $scheme = 'http' . ($clientOpts->tls ? 's' : ''); - $this->server = $scheme .'://'. $clientOpts->host; + $this->server = $scheme .'://'. $clientOpts->restHost; $this->init(); return $this; From a590358fe55ba76be26ab4f0a9e6259828c2f25d Mon Sep 17 00:00:00 2001 From: blade Date: Mon, 5 Oct 2015 12:38:43 +0200 Subject: [PATCH 13/15] Created sub-types for Stats, Stats are now pre-populated with zeroes --- src/Models/Stats.php | 68 ++++++++++++++------- src/Models/Stats/ConnectionTypes.php | 23 +++++++ src/Models/Stats/MessageCount.php | 19 ++++++ src/Models/Stats/MessageTraffic.php | 26 ++++++++ src/Models/Stats/MessageTypes.php | 23 +++++++ src/Models/Stats/RequestCount.php | 22 +++++++ src/Models/Stats/ResourceCount.php | 29 +++++++++ tests/AppStatsTest.php | 22 +++++++ tests/TypesTest.php | 90 ++++++++++++++++++++++++---- 9 files changed, 287 insertions(+), 35 deletions(-) create mode 100644 src/Models/Stats/ConnectionTypes.php create mode 100644 src/Models/Stats/MessageCount.php create mode 100644 src/Models/Stats/MessageTraffic.php create mode 100644 src/Models/Stats/MessageTypes.php create mode 100644 src/Models/Stats/RequestCount.php create mode 100644 src/Models/Stats/ResourceCount.php diff --git a/src/Models/Stats.php b/src/Models/Stats.php index 01b44fc..04504e8 100644 --- a/src/Models/Stats.php +++ b/src/Models/Stats.php @@ -6,18 +6,27 @@ */ class Stats { /** - * @var stdClass $all MessageTypes representing the total of all inbound and outbound message traffic. - * This is the aggregate number that is considered in applying account message limits. - * @var stdClass $inbound MessageTraffic representing inbound messages (ie published by clients and sent inbound to the Ably service) by all transport types; - * @var stdClass $outbound MessageTraffic representing outbound messages (ie delivered by the Ably service to connected and subscribed clients); - * @var stdClass $persisted MessageTypes representing the aggregate volume of messages persisted; - * @var stdClass $connections ConnectionTypes representing the usage of connections; - * @var stdClass $channels ResourceCount representing the number of channels activated and used; - * @var stdClass $apiRequests RequestCount representing the number of requests made to the REST API; - * @var stdClass $tokenRequests RequestCount representing the number of requests made to issue access tokens. - * @var stdClass $intervalId The interval that this statistic applies to. - * @var stdClass $intervalGranularity The granularity of the interval for the stat. May be one of: minute, hour, day, month - * @var stdClass $intervalTime A timestamp representing the start of the interval. + * @var \Ably\Models\Stats\MessageTypes $all MessageTypes representing the total of all inbound and + * outbound message traffic. This is the aggregate number that is considered in applying account + * message limits. + * @var \Ably\Models\Stats\MessageTraffic $inbound MessageTraffic representing inbound messages + * (ie published by clients and sent inbound to the Ably service) by all transport types. + * @var \Ably\Models\Stats\MessageTraffic $outbound MessageTraffic representing outbound messages + * (ie delivered by the Ably service to connected and subscribed clients). + * @var \Ably\Models\Stats\MessageTypes $persisted MessageTypes representing the aggregate volume + * of messages persisted. + * @var \Ably\Models\Stats\ConnectionTypes $connections ConnectionTypes representing the usage + * of connections. + * @var \Ably\Models\Stats\ResourceCount $channels ResourceCount representing the number of channels + * activated and used. + * @var \Ably\Models\Stats\RequestCount $apiRequests RequestCount representing the number of requests + * made to the REST API. + * @var \Ably\Models\Stats\RequestCount $tokenRequests RequestCount representing the number of requests + * made to issue access tokens. + * @var string $intervalId The interval that this statistic applies to. + * @var string $intervalGranularity The granularity of the interval for the stat. May be one of values: + * minute, hour, day, month + * @var int $intervalTime A timestamp representing the start of the interval. */ public $all; public $inbound; @@ -31,6 +40,9 @@ class Stats { public $intervalGranularity; public $intervalTime; + public function __construct() { + $this->clearFields(); + } /** * Populates stats from JSON * @param string|stdClass $json JSON string or an already decoded object. @@ -53,10 +65,15 @@ public function fromJSON( $json ) { $obj = $obj[0]; } - $class = get_class( $this ); - foreach ($obj as $key => $value) { - if (property_exists( $class, $key )) { - $this->$key = $value; + self::deepCopy( $obj, $this ); + } + + protected static function deepCopy( $target, $dst ) { + foreach ( $target as $key => $value ) { + if ( is_object( $value )) { + self::deepCopy( $value, $dst->$key ); + } else { + $dst->$key = $value; } } } @@ -64,12 +81,17 @@ public function fromJSON( $json ) { /** * Sets all the public fields to null */ - protected function clearFields() { - $fields = get_object_vars( $this ); - unset( $fields['cipherParams'] ); - - foreach ($fields as $key => $value) { - $this->$key = null; - } + public function clearFields() { + $this->all = new Stats\MessageTypes(); + $this->inbound = new Stats\MessageTraffic(); + $this->outbound = new Stats\MessageTraffic(); + $this->persisted = new Stats\MessageTypes(); + $this->connections = new Stats\ConnectionTypes(); + $this->channels = new Stats\ResourceCount(); + $this->apiRequests = new Stats\RequestCount(); + $this->tokenRequests = new Stats\RequestCount(); + $this->intervalId = ''; + $this->intervalGranularity = ''; + $this->intervalTime = 0; } } \ No newline at end of file diff --git a/src/Models/Stats/ConnectionTypes.php b/src/Models/Stats/ConnectionTypes.php new file mode 100644 index 0000000..329894b --- /dev/null +++ b/src/Models/Stats/ConnectionTypes.php @@ -0,0 +1,23 @@ +all = new ResourceCount(); + $this->plain = new ResourceCount(); + $this->tls = new ResourceCount(); + } +} \ No newline at end of file diff --git a/src/Models/Stats/MessageCount.php b/src/Models/Stats/MessageCount.php new file mode 100644 index 0000000..24fbf47 --- /dev/null +++ b/src/Models/Stats/MessageCount.php @@ -0,0 +1,19 @@ +count = 0; + $this->data = 0; + } +} \ No newline at end of file diff --git a/src/Models/Stats/MessageTraffic.php b/src/Models/Stats/MessageTraffic.php new file mode 100644 index 0000000..b0c4a80 --- /dev/null +++ b/src/Models/Stats/MessageTraffic.php @@ -0,0 +1,26 @@ +all = new MessageTypes(); + $this->realtime = new MessageTypes(); + $this->rest = new MessageTypes(); + $this->webhook = new MessageTypes(); + } +} \ No newline at end of file diff --git a/src/Models/Stats/MessageTypes.php b/src/Models/Stats/MessageTypes.php new file mode 100644 index 0000000..d7843d6 --- /dev/null +++ b/src/Models/Stats/MessageTypes.php @@ -0,0 +1,23 @@ +all = new MessageCount(); + $this->messages = new MessageCount(); + $this->presence = new MessageCount(); + } +} \ No newline at end of file diff --git a/src/Models/Stats/RequestCount.php b/src/Models/Stats/RequestCount.php new file mode 100644 index 0000000..17848ef --- /dev/null +++ b/src/Models/Stats/RequestCount.php @@ -0,0 +1,22 @@ +failed = 0; + $this->refused = 0; + $this->succeeded = 0; + } +} \ No newline at end of file diff --git a/src/Models/Stats/ResourceCount.php b/src/Models/Stats/ResourceCount.php new file mode 100644 index 0000000..bd04712 --- /dev/null +++ b/src/Models/Stats/ResourceCount.php @@ -0,0 +1,29 @@ +mean = 0; + $this->min = 0; + $this->opened = 0; + $this->peak = 0; + $this->refused = 0; + } +} \ No newline at end of file diff --git a/tests/AppStatsTest.php b/tests/AppStatsTest.php index b2a1ade..1021351 100644 --- a/tests/AppStatsTest.php +++ b/tests/AppStatsTest.php @@ -66,6 +66,28 @@ public static function tearDownAfterClass() { self::$testApp->release(); } + /** + * Check if stats are automatically populated by zeroes + */ + public function testStatsDefaultValues() { + $stats = new \Ably\Models\Stats(); + $this->assertTrue( $this->iterateObjectCheck0( $stats ), 'Expected newly created Stats to have zero values.' ); + } + + protected function iterateObjectCheck0($obj, $level = 0) { + $valid = true; + + foreach ($obj as $key => $value) { + if (is_object($value)) { + if (!$this->iterateObjectCheck0($value, $level + 1)) $valid = false; + } else { + if ($level > 0 && $value !== 0) $valid = false; + } + } + + return $valid; + } + /** * Check minute-level stats exist (forwards) */ diff --git a/tests/TypesTest.php b/tests/TypesTest.php index ff838f5..3b548f3 100644 --- a/tests/TypesTest.php +++ b/tests/TypesTest.php @@ -17,25 +17,30 @@ public static function tearDownAfterClass() { protected function verifyClassMembers( $class, $expectedMembers ) { $valid = true; foreach( $expectedMembers as $member ) { - if ( !property_exists( $class, $member ) ) { - $valid = false; - break; - } + $this->assertTrue( property_exists( $class, $member ), "Expected class `$class` to contain a field named `$member`." ); } - - $this->assertTrue( $valid, "Expected class `$class` to contain a field named `$member`." ); } protected function verifyClassConstants( $class, $expectedMembers ) { - $valid = true; foreach( $expectedMembers as $member => $value ) { - if ( constant( "$class::$member" ) != $value ) { - $valid = false; - break; - } + $this->assertEquals( $value, constant( "$class::$member" ), + "Expected class `$class` to have a constant `$member` with a value of `$value`." + ); } + } - $this->assertTrue( $valid, "Expected class `$class` to have a constant `$member` with a value of `$value`." ); + protected function verifyObjectTypes( $obj, $expectedTypes ) { + foreach( $obj as $key => $value ) { + if ( gettype( $value ) == 'object' ) { + $this->assertEquals( $expectedTypes[$key], get_class( $value ), + "Expected object (".get_class($obj).") to contain a member `$key` of type `".$expectedTypes[$key]."`." + ); + } else { + $this->assertEquals( $expectedTypes[$key], gettype( $value ), + "Expected object (".get_class($obj).") to contain a member `$key` of type `".$expectedTypes[$key]."`." + ); + } + } } public function testMessageType() { @@ -162,4 +167,65 @@ public function testCipherParamsType() { 'mode' ) ); } + + public function testStatsTypes() { + $stats = new \Ably\Models\Stats(); + $this->verifyObjectTypes( $stats, array( + 'all' => 'Ably\Models\Stats\MessageTypes', + 'inbound' => 'Ably\Models\Stats\MessageTraffic', + 'outbound' => 'Ably\Models\Stats\MessageTraffic', + 'persisted' => 'Ably\Models\Stats\MessageTypes', + 'connections' => 'Ably\Models\Stats\ConnectionTypes', + 'channels' => 'Ably\Models\Stats\ResourceCount', + 'apiRequests' => 'Ably\Models\Stats\RequestCount', + 'tokenRequests' => 'Ably\Models\Stats\RequestCount', + 'intervalId' => 'string', + 'intervalGranularity' => 'string', + 'intervalTime' => 'integer', + ) ); + + // verify MessageTypes + $this->verifyObjectTypes( $stats->all, array( + 'all' => 'Ably\Models\Stats\MessageCount', + 'messages' => 'Ably\Models\Stats\MessageCount', + 'presence' => 'Ably\Models\Stats\MessageCount', + ) ); + + // verify MessageCount + $this->verifyObjectTypes( $stats->all->all, array( + 'count' => 'integer', + 'data' => 'integer', + ) ); + + // verify MessageTraffic + $this->verifyObjectTypes( $stats->inbound, array( + 'all' => 'Ably\Models\Stats\MessageTypes', + 'realtime' => 'Ably\Models\Stats\MessageTypes', + 'rest' => 'Ably\Models\Stats\MessageTypes', + 'webhook' => 'Ably\Models\Stats\MessageTypes', + ) ); + + // verify ConnectionTypes + $this->verifyObjectTypes( $stats->connections, array( + 'all' => 'Ably\Models\Stats\ResourceCount', + 'plain' => 'Ably\Models\Stats\ResourceCount', + 'tls' => 'Ably\Models\Stats\ResourceCount', + ) ); + + // verify ResourceCount + $this->verifyObjectTypes( $stats->connections->all, array( + 'mean' => 'integer', + 'min' => 'integer', + 'opened' => 'integer', + 'peak' => 'integer', + 'refused' => 'integer', + ) ); + + // verify RequestCount + $this->verifyObjectTypes( $stats->apiRequests, array( + 'failed' => 'integer', + 'refused' => 'integer', + 'succeeded' => 'integer', + ) ); + } } \ No newline at end of file From 93d6cec4d788135e2a82f9cf17f7aef4f25b948b Mon Sep 17 00:00:00 2001 From: blade Date: Wed, 7 Oct 2015 12:49:24 +0200 Subject: [PATCH 14/15] Fixed failing tests on strict PHP error reporting (E_STRICT & E_NOTICE) --- src/Models/BaseMessage.php | 2 ++ tests/ChannelMessagesTest.php | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Models/BaseMessage.php b/src/Models/BaseMessage.php index f160efe..bf903ed 100644 --- a/src/Models/BaseMessage.php +++ b/src/Models/BaseMessage.php @@ -148,6 +148,8 @@ protected function encode() { if ( count( $encodings ) ) { $msg->encoding = implode( '/', $encodings ); + } else { + $msg->encoding = ''; } return $msg; diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 530f3ad..67112f4 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -392,7 +392,10 @@ public function testNullData() { $channel->publish( $msg ); - $this->assertEquals( (object) array( 'name' => 'onlyName' ), json_decode( $ably->http->lastParams ) ); + $publishedMsg = json_decode( $ably->http->lastParams ); + + $this->assertEquals( $msg->name, $publishedMsg->name ); + $this->assertFalse( isset( $publishedMsg->data ) ); $msg = new Message(); $msg->name = null; @@ -400,7 +403,10 @@ public function testNullData() { $channel->publish( $msg ); - $this->assertEquals( (object) array( 'data' => 'onlyData' ), json_decode( $ably->http->lastParams ) ); + $publishedMsg = json_decode( $ably->http->lastParams ); + + $this->assertEquals( $msg->data, $publishedMsg->data ); + $this->assertFalse( isset( $publishedMsg->name ) ); } /** @@ -490,6 +496,6 @@ public function request( $method, $url, $headers = array(), $params = array() ) $this->lastHeaders = $headers; $this->lastParams = $params; $this->lastResponse = parent::request( $method, $url, $headers, $params ); - return $lastResponse; + return $this->lastResponse; } } From ef52bd0b708b95ed631283ca66a3a3af3b4e24a2 Mon Sep 17 00:00:00 2001 From: blade Date: Tue, 13 Oct 2015 10:08:09 +0200 Subject: [PATCH 15/15] Updated CipherParams constructor to support more parameters, updated tests using CipherParams --- src/Models/CipherParams.php | 12 +++++++----- tests/ChannelMessagesTest.php | 14 +++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Models/CipherParams.php b/src/Models/CipherParams.php index 5d4374c..f572011 100644 --- a/src/Models/CipherParams.php +++ b/src/Models/CipherParams.php @@ -20,14 +20,16 @@ class CipherParams { /** * Constructor. The encryption algorithm defaults to the only supported algorithm - AES CBC with * a default key length of 128. A random IV is generated. - * @param string|null $key Encryption key, if not provided a random key is generated. - * @param string|null $keyLength Cipher key length, defaults to 128. + * @param string|null $key Encryption key, if not provided a random key is generated. + * @param string|null $algorithm Encryption algorithm, defaults to 'aes'. + * @param Integer|null $keyLength Cipher key length, defaults to 128. + * @param string|null $mode Algorithm mode, defaults to 'cbc'. */ - public function __construct( $key = null, $keyLength = 128 ) { + public function __construct( $key = null, $algorithm = 'aes', $keyLength = 128, $mode = 'cbc' ) { $this->key = $key ? $key : openssl_random_pseudo_bytes( 16 ); - $this->algorithm = 'aes'; + $this->algorithm = $algorithm; $this->keyLength = $keyLength; - $this->mode = 'cbc'; + $this->mode = $mode; $this->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->getAlgorithmString() ) ); } diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 67112f4..a08a2ad 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -109,7 +109,7 @@ public function testPublishMessagesVariousTypesUnencrypted() { * Publish events with data of various datatypes to an aes-128-cbc encrypted channel */ public function testPublishMessagesVariousTypesAES128() { - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 )); $encrypted1 = self::$ably->channels->get( 'persisted:encrypted1', $options ); $this->assertNotNull( $encrypted1->getCipherParams(), 'Expected channel to be encrypted' ); @@ -121,7 +121,7 @@ public function testPublishMessagesVariousTypesAES128() { * Publish events with data of various datatypes to an aes-256-cbc encrypted channel */ public function testPublishMessagesVariousTypesAES256() { - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 256 )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 256 )); $encrypted2 = self::$ably->channels->get( 'persisted:encrypted2', $options ); $this->assertNotNull( $encrypted2->getCipherParams(), 'Expected channel to be encrypted' ); @@ -257,7 +257,7 @@ public function testEncryptedMessageUnencryptedHistory() { $payload = 'This is a test message'; - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 )); $encrypted1 = $ably->channel( 'persisted:mismatch1', $options ); $encrypted1->publish( 'test', $payload ); @@ -280,7 +280,7 @@ public function testUnencryptedMessageEncryptedHistory() { $encrypted = self::$ably->channel( 'persisted:mismatch2' ); $encrypted->publish( 'test', $payload ); - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 )); $unencrypted = self::$ably->channel( 'persisted:mismatch2', $options ); $messages = $unencrypted->history(); $this->assertNotNull( $messages, 'Expected non-null messages' ); @@ -305,11 +305,11 @@ public function testEncryptionKeyMismatch() { $payload = 'This is a test message'; - $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 128 )); + $options = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'password', 'aes', 128 )); $encrypted1 = $ably->channel( 'persisted:mismatch3', $options ); $encrypted1->publish( 'test', $payload ); - $options2 = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'DIFFERENT PASSWORD', 128 )); + $options2 = array( 'encrypted' => true, 'cipherParams' => new CipherParams( 'DIFFERENT PASSWORD', 'aes', 128 )); $encrypted2 = $ably->channel( 'persisted:mismatch3', $options2 ); $messages = $encrypted2->history(); $msg = $messages->items[0]; @@ -337,7 +337,7 @@ public function testChannelCaching() { self::$ably->channel( 'cache_test', array( 'encrypted' => true, - 'cipherParams' => new CipherParams( 'password', 128 ) + 'cipherParams' => new CipherParams( 'password', 'aes', 128 ) ) ); $this->assertNotNull( $channel3->getCipherParams(), 'Expected the channel to have CipherParams even when specified for a new instance' );