Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 282
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 3
ChannelMessagesTest
0.00% covered (danger)
0.00%
0 / 272
0.00% covered (danger)
0.00%
0 / 21
1722
0.00% covered (danger)
0.00%
0 / 1
 setUpBeforeClass
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 tearDownAfterClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 executePublishTestOnChannel
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 testPublishMessagesVariousTypesUnencrypted
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 testPublishMessagesVariousTypesAES128
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 testPublishMessagesVariousTypesAES256
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 testPublishSingleMessageUnencrypted
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 testPublishSingleMessageUnencryptedParams
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 testMessageArraySingleRequest
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 testInvalidTypes
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 testTooLargeMessage
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 testPublishConnectionKey
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 testEncryptedMessageUnencryptedHistory
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 testUnencryptedMessageEncryptedHistory
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 testEncryptionKeyMismatch
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 testChannelCaching
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getMessageEncoding
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 testMessageEncodings
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 testEncodingInteroperabilityRawToAbly
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 testEncodingInteroperabilityAblyToRaw
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 testNullData
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
HttpMockMsgCounter
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 request
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
HttpSaveWrapper
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 request
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace tests;
3use Ably\AblyRest;
4use Ably\Channel;
5use Ably\Http;
6use Ably\Log;
7use Ably\Exceptions\AblyException;
8use Ably\Models\Message;
9use Ably\Utils\Crypto;
10
11require_once __DIR__ . '/factories/TestApp.php';
12
13class ChannelMessagesTest extends \PHPUnit_Framework_TestCase {
14    protected static $testApp;
15    protected static $defaultOptions;
16    protected static $ably;
17
18    public static function setUpBeforeClass() {
19        self::$testApp = new TestApp();
20        self::$defaultOptions = self::$testApp->getOptions();
21        self::$ably = new AblyRest( array_merge( self::$defaultOptions, [
22            'key' => self::$testApp->getAppKeyDefault()->string,
23        ] ) );
24    }
25
26    public static function tearDownAfterClass() {
27        self::$testApp->release();
28    }
29
30    /**
31     * Partial test reused by actual exposed tests.
32     * Publishes messages to a provided channel, checks for encryption, retrieves history,
33     * compares decoded payloads with original messages
34     */
35    private function executePublishTestOnChannel(Channel $channel) {
36       
37        // first publish some messages
38        $data = [
39            'utf' => 'This is a UTF-8 string message payload. äôč ビール',
40            'binary' => hex2bin('00102030405060708090a0b0c0d0e0f0ff'),
41            'object' => (object)[ 'test' => 'This is a JSONObject message payload' ],
42            'array' => [ 'This is a JSONarray message payload', 'Test' ],
43        ];
44
45        $messages = [];
46
47        foreach ($data as $type => $payload) {
48            $msg = new Message();
49            $msg->name = $type;
50            $msg->data = $payload;
51            
52            $messages[] = $msg;
53        }
54
55        $channel->publish( $messages ); // publish all messages at once
56
57        foreach ($messages as $msg) {
58            if ( $channel->getCipherParams() ) {
59                // check if the messages are encrypted
60                $msgJSON = json_decode( $msg->toJSON() );
61                
62                $this->assertTrue(
63                    strpos( $msgJSON->encoding, $channel->getCipherParams()->getAlgorithmString() ) !== false,
64                    'Expected message encoding to contain a cipher algorithm'
65                );
66                $this->assertFalse( $msgJSON->data === $payload, 'Expected encrypted message payload not to match original data' );
67            } else {
68                // check if the messages are unencrypted
69                $msgJSON = json_decode( $msg->toJSON() );
70                
71                $this->assertTrue(
72                    strpos( $msgJSON->encoding, 'cipher' ) === false,
73                    'Expected message encoding not to contain a cipher algorithm'
74                );
75            }
76        }
77
78        // get the history for this channel
79        $messages = $channel->history();
80        $this->assertNotNull( $messages, 'Expected non-null messages' );
81        $this->assertEquals( 4, count($messages->items), 'Expected 4 messages' );
82
83        $actual_message_order = [];
84
85        // verify message contents
86        foreach ($messages->items as $message) {
87            $actual_message_order[] = $message->name;
88            
89            // payload must exactly match the one that was sent and must be decrypted automatically
90            $originalPayload = $data[$message->name];
91            $this->assertEquals( $originalPayload, $message->data, 'Expected retrieved message\'s data to match the original data (' . $message->name . ')' );
92        }
93
94        // verify message order
95        $this->assertEquals( array_reverse( array_keys( $data ) ), $actual_message_order, 'Expected messages in reverse order' );
96    }
97
98    /**
99     * Publish events with data of various datatypes to an unencrypted channel
100     */
101    public function testPublishMessagesVariousTypesUnencrypted() {
102        $unencrypted = self::$ably->channels->get( 'persisted:unencrypted' );
103
104        $this->executePublishTestOnChannel( $unencrypted );
105    }
106
107    /**
108     * Publish events with data of various datatypes to an aes-128-cbc encrypted channel
109     */
110    public function testPublishMessagesVariousTypesAES128() {
111        $options = [ 'cipher' => [
112            'key' => Crypto::generateRandomKey( 128 ),
113        ] ];
114        $encrypted1 = self::$ably->channels->get( 'persisted:encrypted1', $options );
115        
116        $this->assertNotNull( $encrypted1->getCipherParams(), 'Expected channel to be encrypted' );
117
118        $this->executePublishTestOnChannel( $encrypted1 );
119    }
120
121    /**
122     * Publish events with data of various datatypes to an aes-256-cbc encrypted channel
123     */
124    public function testPublishMessagesVariousTypesAES256() {
125        $options = [ 'cipher' => [
126            'key' => Crypto::generateRandomKey( 256 ),
127        ] ];
128        $encrypted2 = self::$ably->channels->get( 'persisted:encrypted2', $options );
129        
130        $this->assertNotNull( $encrypted2->getCipherParams(), 'Expected channel to be encrypted' );
131
132        $this->executePublishTestOnChannel( $encrypted2 );
133    }
134
135    /**
136     * Publish a single Message
137     */
138    public function testPublishSingleMessageUnencrypted() {
139        $channel = self::$ably->channel( 'persisted:unencryptedSingle' );
140        $data = (object)[ 'test' => 'This is a JSONObject message payload' ];
141
142        $msg = new Message();
143        $msg->name = 'single';
144        $msg->data = $data;
145
146        $channel->publish( $msg );
147
148        // get the history for this channel
149        $messages = $channel->history();
150        $this->assertNotNull( $messages, 'Expected non-null messages' );
151        $this->assertEquals( 1, count($messages->items), 'Expected 1 message' );
152        $this->assertEquals( $data, $messages->items[0]->data, 'Expected message contents to match' );
153    }
154
155    /**
156     * Publish a single message by passing data to Channel::publish() directly
157     */
158    public function testPublishSingleMessageUnencryptedParams() {
159        $channel = self::$ably->channel( 'persisted:unencryptedSingleParams' );
160
161        $channel->publish( 'testEvent', 'testPayload', 'testClientId' );
162
163        $messages = $channel->history();
164        $this->assertNotNull( $messages, 'Expected non-null messages' );
165        $this->assertEquals( 1, count($messages->items), 'Expected 1 message' );
166        $this->assertEquals( 'testEvent',    $messages->items[0]->name,     'Expected message event name to match' );
167        $this->assertEquals( 'testPayload',  $messages->items[0]->data,     'Expected message payload to match' );
168        $this->assertEquals( 'testClientId', $messages->items[0]->clientId, 'Expected message clientId to match' );
169    }
170
171    /**
172     * Verify that batch sending messages actually makes just one request
173     */
174    public function testMessageArraySingleRequest() {
175        $messages = [];
176        
177        for ( $i = 0; $i < 10; $i++ ) {
178            $msg = new Message();
179            $msg->name = 'msg'.$i;
180            $msg->data = 'test string'.$i;
181            $messages[] = $msg;
182        }
183
184        $ably = new AblyRest( array_merge( self::$defaultOptions, [
185            'key' => self::$testApp->getAppKeyDefault()->string,
186            'httpClass' => 'tests\HttpMockMsgCounter',
187        ] ) );
188
189        $channel = $ably->channel( 'singleReq' );
190        $channel->publish( $messages );
191
192        $this->assertEquals( 1, $ably->http->requestCount, 'Expected 1 request to be made' );
193    }
194
195    /**
196     * Verify that publishing invalid types fails
197     */
198    public function testInvalidTypes() {
199        $channel = self::$ably->channel( 'invalidTypes' );
200        
201        $msg = new Message();
202        $msg->name = 'int';
203        $msg->data = 81403;
204
205        try {
206            $channel->publish( $msg );
207            $this->fail( 'Expected an exception' );
208        } catch (AblyException $e) {
209            if ( $e->getCode() != 40003 ) $this->fail('Expected exception error code 40003');
210        }
211
212        $msg = new Message();
213        $msg->name = 'bool';
214        $msg->data = true;
215
216        try {
217            $channel->publish( $msg );
218            $this->fail( 'Expected an exception' );
219        } catch (AblyException $e) {
220            if ( $e->getCode() != 40003 ) $this->fail('Expected exception error code 40003');
221        }
222
223        $msg = new Message();
224        $msg->name = 'float';
225        $msg->data = 42.23;
226
227        try {
228            $channel->publish( $msg );
229            $this->fail( 'Expected an exception' );
230        } catch (AblyException $e) {
231            if ( $e->getCode() != 40003 ) $this->fail('Expected exception error code 40003');
232        }
233
234        $msg = new Message();
235        $msg->name = 'function';
236        $msg->data = function($param) {
237            return "mock function";
238        };
239
240        try {
241            $channel->publish( $msg );
242            $this->fail( 'Expected an exception' );
243        } catch (AblyException $e) {
244            if ( $e->getCode() != 40003 ) $this->fail('Expected exception error code 40003');
245        }
246    }
247
248    /**
249     * Verify that publishing too large message (>128KB) fails
250     */
251    public function testTooLargeMessage() {
252        $channel = self::$ably->channel( 'huge' );
253        
254        $msg = new Message();
255        $msg->name = 'huge';
256        $msg->data = str_repeat("~", 128 * 1024); // 128 kilobytes + message JSON
257
258        $this->setExpectedException( 'Ably\Exceptions\AblyException', '', 40009 );
259        $channel->publish( $msg );
260    }
261
262    /**
263     * Verify that publishing on behalf of realtime clients works
264     */
265    public function testPublishConnectionKey() {
266        $channel = self::$ably->channel( 'connKey' );
267
268
269        $msg = new Message();
270        $msg->name = 'delegatedMsg';
271        $msg->data = 'test payload';
272        $msg->connectionKey = 'fake.realtime_key';
273
274        // publishing the message with an invalid key must fail
275        $this->setExpectedException( 'Ably\Exceptions\AblyException', '', 40006 );
276        $channel->publish( $msg );
277    }
278
279    /**
280     * Encryption mismatch - publish message over encrypted channel, retrieve history over unencrypted channel
281     */
282    public function testEncryptedMessageUnencryptedHistory() {
283        $errorLogged = false;
284
285        $ably = new AblyRest( array_merge( self::$defaultOptions, [
286            'key' => self::$testApp->getAppKeyDefault()->string,
287            'logHandler' => function( $level, $args ) use ( &$errorLogged ) {
288                if ( $level == Log::ERROR ) {
289                    $errorLogged = true;
290                }
291            },
292        ] ) );
293
294        $payload = 'This is a test message';
295
296        $options = [ 'cipher' => [ 'key' => Crypto::generateRandomKey( 128 ) ] ];
297        $encrypted1 = $ably->channel( 'persisted:mismatch1', $options );
298        $encrypted1->publish( 'test', $payload );
299
300        $options2 = [];
301        $encrypted2 = $ably->channel( 'persisted:mismatch1', $options2 );
302        $messages = $encrypted2->history();
303        $msg = $messages->items[0];
304
305        $this->assertTrue( $errorLogged, 'Expected an error to be logged' );
306        $this->assertEquals( 'utf-8/cipher+aes-128-cbc/base64', $msg->originalEncoding, 'Expected the original message to be encrypted + base64 encoded' );
307        $this->assertEquals( 'utf-8/cipher+aes-128-cbc', $msg->encoding, 'Expected to receive the message still encrypted, but base64 decoded' );
308    }
309
310    /**
311     * Publish message over unencrypted channel, retrieve history over encrypted channel, there should be no decryption attempts
312     */
313    public function testUnencryptedMessageEncryptedHistory() {
314        $payload = 'This is a test message';
315
316        $encrypted = self::$ably->channel( 'persisted:mismatch2' );
317        $encrypted->publish( 'test', $payload );
318
319        $options = [ 'cipher' => [ 'key' => Crypto::generateRandomKey( 128 ) ] ];
320        $unencrypted = self::$ably->channel( 'persisted:mismatch2', $options );
321        $messages = $unencrypted->history();
322        $this->assertNotNull( $messages, 'Expected non-null messages' );
323        $this->assertEquals( 1, count($messages->items), 'Expected 1 message' );
324        $this->assertEquals( $messages->items[0]->originalData, $messages->items[0]->data, 'Expected to have message data untouched' );
325    }
326
327    /**
328     * Encryption key mismatch - publish message with key1, retrieve history with key2
329     */
330    public function testEncryptionKeyMismatch() {
331        $errorLogged = false;
332
333        $ably = new AblyRest( array_merge( self::$defaultOptions, [
334            'key' => self::$testApp->getAppKeyDefault()->string,
335            'logHandler' => function( $level, $args ) use ( &$errorLogged ) {
336                if ( $level == Log::ERROR ) {
337                    $errorLogged = true;
338                }
339            },
340        ] ) );
341
342        $payload = 'This is a test message';
343
344        $options = [ 'cipher' => [ 'key' => 'fake key 1xxxxxx' ] ];
345        $encrypted1 = $ably->channel( 'persisted:mismatch3', $options );
346        $encrypted1->publish( 'test', $payload );
347
348        $options2 = [ 'cipher' => [ 'key' => 'fake key 2xxxxxx' ] ];
349        $encrypted2 = $ably->channel( 'persisted:mismatch3', $options2 );
350        $messages = $encrypted2->history();
351        $msg = $messages->items[0];
352
353        $this->assertTrue( $errorLogged, 'Expected an error to be logged' );
354        $this->assertEquals( 'utf-8/cipher+aes-128-cbc/base64', $msg->originalEncoding, 'Expected the original message to be encrypted + base64 encoded' );
355        $this->assertEquals( 'utf-8/cipher+aes-128-cbc', $msg->encoding, 'Expected to receive the message still encrypted, but base64 decoded' );
356    }
357
358    /**
359     * Verify if channel caching and releasing works
360     */
361    public function testChannelCaching() {
362        $channel1 = self::$ably->channel( 'cache_test' );
363        $channel2 = self::$ably->channels->get( 'cache_test' );
364
365        $this->assertTrue( $channel1 === $channel2, 'Expected to get the same instance of the channel' );
366
367        self::$ably->channels->release( 'cache_test' );
368
369        $channel3 = self::$ably->channels->get( 'cache_test' );
370        $this->assertTrue( $channel1 !== $channel3, 'Expected to get a new instance of the channel' );
371
372        $this->assertNull( $channel3->getCipherParams(), 'Expected the channel to not have CipherParams' );
373
374        self::$ably->channel( 'cache_test', [ 'cipher' => [ 'key' => Crypto::generateRandomKey( 128 ) ] ] );
375
376        $this->assertNotNull( $channel3->getCipherParams(), 'Expected the channel to have CipherParams even when specified for a new instance' );
377    }
378
379    private function getMessageEncoding( $msg ) {
380        $msgEnc = json_decode( $msg->toJSON() );
381        return $msgEnc->encoding;
382    }
383
384    /**
385     * Check if message encodings are correct, including the default encryption
386     */
387    public function testMessageEncodings() {
388        $msg = new Message();
389
390        $msg->data = 'This is a UTF-8 string message payload. äôč ビール';
391        $this->assertEquals( '', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
392
393        $msg->data = (object)[ 'test' => 'This is a JSONObject message payload' ];
394        $this->assertEquals( 'json', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
395
396        $msg->data = hex2bin( '00102030405060708090a0b0c0d0e0f0ff' );
397        $this->assertEquals( 'base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
398
399        $msg->setCipherParams( Crypto::getDefaultParams( [ 'key' => Crypto::generateRandomKey( 128 ) ] ) );
400
401        $msg->data = 'This is a UTF-8 string message payload. äôč ビール';
402        $this->assertEquals( 'utf-8/cipher+aes-128-cbc/base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
403
404        $msg->data = (object)[ 'test' => 'This is a JSONObject message payload' ];
405        $this->assertEquals( 'json/utf-8/cipher+aes-128-cbc/base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
406
407        $msg->data = hex2bin( '00102030405060708090a0b0c0d0e0f0ff' );
408        $this->assertEquals( 'cipher+aes-128-cbc/base64', $this->getMessageEncoding( $msg ), 'Expected empty message encoding' );
409    }
410
411    /**
412     * Test library interoperability by sending messages from messages-encoding.json
413     * via raw HTTP and decoding them using the library
414     */
415    public function testEncodingInteroperabilityRawToAbly() {
416        $fixture = json_decode( file_get_contents( __DIR__ . '/../ably-common/test-resources/messages-encoding.json' ) );
417
418        $defaultOpts = new \Ably\Models\ClientOptions( self::$defaultOptions );
419        $http = new \Ably\Http( $defaultOpts ); // initialize http class for raw requests with default timeouts
420        $server = 'https://' . $defaultOpts->restHost;
421
422        $messages = [];
423        foreach ($fixture->messages as $i => $testMsgData) {
424            $messages[] = (object) [
425                'data' => $testMsgData->data,
426                'encoding' => $testMsgData->encoding,
427            ];
428        }
429
430        $http->request(
431            'POST',
432            $server . '/channels/interopTest1/messages',
433            [
434                'Content-Type: application/json',
435                'Accept: application/json',
436                'Authorization: Basic ' . base64_encode( self::$testApp->getAppKeyDefault()->string ),
437            ],
438            json_encode( $messages )
439        );
440
441        $history = self::$ably->channel("interopTest1")->history([ 'direction' => 'forwards' ]);
442
443        foreach ($fixture->messages as $i => $testMsgData) {
444            $msg = $history->items[$i];
445
446            if ( isset( $testMsgData->expectedHexValue ) ) {
447                $expected = pack( 'H*', $testMsgData->expectedHexValue );
448            } else {
449                $expected = $testMsgData->expectedValue;
450            }
451
452            $this->assertEquals($expected, $msg->data);
453        }
454
455    }
456
457    /**
458     * Test library interoperability by sending messages from messages-encoding.json
459     * via the library, fetching them using raw HTTP and comparing
460     */
461    public function testEncodingInteroperabilityAblyToRaw() {
462        $fixture = json_decode( file_get_contents( __DIR__ . '/../ably-common/test-resources/messages-encoding.json' ) );
463
464        $defaultOpts = new \Ably\Models\ClientOptions( self::$defaultOptions );
465        $http = new \Ably\Http( $defaultOpts ); // initialize http class for raw requests with default timeouts
466        $server = 'https://' . $defaultOpts->restHost;
467
468        $messages = [];
469        foreach ($fixture->messages as $i => $testMsgData) {
470            $msg = new Message();
471            $msg->data = $testMsgData->data;
472            $msg->encoding = $testMsgData->encoding;
473            $messages[] = $msg;
474        }
475
476        self::$ably->channel("interopTest2")->publish($messages);
477
478        $res = $http->request(
479            'GET',
480            $server . '/channels/interopTest2/messages?direction=forwards',
481            [
482                'Accept: application/json',
483                'Authorization: Basic ' . base64_encode( self::$testApp->getAppKeyDefault()->string ),
484            ]
485        );
486
487        $history = $res['body'];
488
489        foreach ($fixture->messages as $i => $testMsgData) {
490            $msg = $history[$i];
491
492            $this->assertEquals($testMsgData->data, $msg->data);
493            $this->assertEquals($testMsgData->encoding, $msg->encoding);
494        }
495
496    }
497
498    /**
499     * Test if null name and data elements are allowed when publishing messages
500     */
501    public function testNullData() {
502        $ably = new AblyRest( array_merge( self::$defaultOptions, [
503            'key' => self::$testApp->getAppKeyDefault()->string,
504            'httpClass' => 'tests\HttpSaveWrapper',
505        ] ) );
506
507        $channel = $ably->channels->get( 'testChannel' );
508
509        $msg = new Message();
510        $msg->name = 'onlyName';
511        $msg->data = null;
512
513        $channel->publish( $msg );
514
515        $publishedMsg = json_decode( $ably->http->lastParams );
516
517        $this->assertEquals( $msg->name, $publishedMsg->name );
518        $this->assertFalse( isset( $publishedMsg->data ) );
519
520        $msg = new Message();
521        $msg->name = null;
522        $msg->data = 'onlyData';
523
524        $channel->publish( $msg );
525
526        $publishedMsg = json_decode( $ably->http->lastParams );
527
528        $this->assertEquals( $msg->data, $publishedMsg->data );
529        $this->assertFalse( isset( $publishedMsg->name ) );
530    }
531}
532
533
534class HttpMockMsgCounter extends Http {
535    public $requestCount = 0;
536    
537    public function request( $method, $url, $headers = [], $params = [] ) {
538
539        $this->requestCount++;
540
541        return [
542            'headers' => 'HTTP/1.1 200 OK'."\n",
543            'body' => [],
544        ];
545    }
546}
547
548
549class HttpSaveWrapper extends Http {
550    public $lastResponse;
551    public $lastHeaders;
552    public $lastParams;
553    
554    public function request( $method, $url, $headers = [], $params = [] ) {
555        $this->lastHeaders = $headers;
556        $this->lastParams = $params;
557        $this->lastResponse = parent::request( $method, $url, $headers, $params );
558        return $this->lastResponse;
559    }
560}