Skip to content

Commit 942dd40

Browse files
authored
Add support for terminating user connections (#328)
1 parent af3eeae commit 942dd40

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed

src/Pusher.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@ private function validate_socket_id(string $socket_id): void
223223
}
224224
}
225225

226+
/**
227+
* Ensure an user id is valid based on our spec.
228+
*
229+
* @param string $user_id The user id to validate
230+
*
231+
* @throws PusherException If $user_id is invalid
232+
*/
233+
private function validate_user_id(string $user_id): void
234+
{
235+
if ($user_id === null || empty($user_id)) {
236+
throw new PusherException('Invalid user id ' . $user_id);
237+
}
238+
}
239+
226240
/**
227241
* Utility function used to generate signing headers
228242
*
@@ -646,6 +660,40 @@ public function triggerBatchAsync(array $batch = [], bool $already_encoded = fal
646660
return $promise;
647661
}
648662

663+
/**
664+
* Terminates all connections established by the user with the given user id.
665+
*
666+
* @param string $userId
667+
*
668+
* @throws PusherException If $userId is invalid
669+
* @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
670+
*
671+
* @return object response body
672+
*
673+
*/
674+
public function terminateUserConnections(string $userId): object
675+
{
676+
$this->validate_user_id($userId);
677+
return $this->post("/users/" . $userId . "/terminate_connections", "{}");
678+
}
679+
680+
/**
681+
* Asynchronous request to terminates all connections established by the user with the given user id.
682+
*
683+
* @param string $userId
684+
*
685+
* @throws PusherException If $userId is invalid
686+
*
687+
* @return PromiseInterface promise wrapping response body
688+
*
689+
*/
690+
public function terminateUserConnectionsAsync(string $userId): PromiseInterface
691+
{
692+
$this->validate_user_id($userId);
693+
return $this->postAsync("/users/" . $userId . "/terminate_connections", "{}");
694+
}
695+
696+
649697
/**
650698
* Fetch channel information for a specific channel.
651699
*
@@ -768,6 +816,106 @@ public function get(string $path, array $params = [], $associative = false)
768816
return $body;
769817
}
770818

819+
/**
820+
* POST arbitrary REST API resource using a synchronous http client.
821+
* All request signing is handled automatically.
822+
*
823+
* @param string $path Path excluding /apps/APP_ID
824+
* @param mixed $body Request payload (see http://pusher.com/docs/rest_api)
825+
* @param array $params API params (see http://pusher.com/docs/rest_api)
826+
*
827+
* @throws ApiErrorException Throws ApiErrorException if the Channels HTTP API responds with an error
828+
* @throws GuzzleException
829+
* @throws PusherException
830+
*
831+
* @return mixed Post response body
832+
*/
833+
public function post(string $path, $body, array $params = [])
834+
{
835+
$path = $this->settings['base_path'] . $path;
836+
837+
$params['body_md5'] = md5($body);
838+
839+
$params_with_signature = $this->sign($path, 'POST', $params);
840+
841+
$headers = [
842+
'Content-Type' => 'application/json',
843+
'X-Pusher-Library' => 'pusher-http-php ' . self::$VERSION
844+
];
845+
846+
$response = $this->client->request('POST', $path, [
847+
'query' => $params_with_signature,
848+
'body' => $body,
849+
'http_errors' => false,
850+
'headers' => $headers,
851+
'base_uri' => $this->channels_url_prefix()
852+
]);
853+
854+
$status = $response->getStatusCode();
855+
856+
if ($status !== 200) {
857+
$body = (string) $response->getBody();
858+
throw new ApiErrorException($body, $status);
859+
}
860+
861+
try {
862+
$response_body = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR);
863+
} catch (\JsonException $e) {
864+
throw new PusherException('Data decoding error.');
865+
}
866+
867+
return $response_body;
868+
}
869+
870+
/**
871+
* Asynchronously POST arbitrary REST API resource using a synchronous http client.
872+
* All request signing is handled automatically.
873+
*
874+
* @param string $path Path excluding /apps/APP_ID
875+
* @param mixed $body Request payload (see http://pusher.com/docs/rest_api)
876+
* @param array $params API params (see http://pusher.com/docs/rest_api)
877+
*
878+
* @return PromiseInterface Promise wrapping POST response body
879+
*/
880+
public function postAsync(string $path, $body, array $params = []): PromiseInterface
881+
{
882+
$path = $this->settings['base_path'] . $path;
883+
884+
$params['body_md5'] = md5($body);
885+
886+
$params_with_signature = $this->sign($path, 'POST', $params);
887+
888+
$headers = [
889+
'Content-Type' => 'application/json',
890+
'X-Pusher-Library' => 'pusher-http-php ' . self::$VERSION
891+
];
892+
893+
return $this->client->requestAsync('POST', $path, [
894+
'query' => $params_with_signature,
895+
'body' => $body,
896+
'http_errors' => false,
897+
'headers' => $headers,
898+
'base_uri' => $this->channels_url_prefix()
899+
])->then(function ($response) {
900+
$status = $response->getStatusCode();
901+
902+
if ($status !== 200) {
903+
$body = (string) $response->getBody();
904+
throw new ApiErrorException($body, $status);
905+
}
906+
907+
try {
908+
$response_body = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR);
909+
} catch (\JsonException $e) {
910+
throw new PusherException('Data decoding error.');
911+
}
912+
913+
return $response_body;
914+
}, function (ConnectExpcetion $e) {
915+
throw new ApiErrorException($e->getMessage());
916+
});
917+
}
918+
771919
/**
772920
* Creates a socket signature.
773921
*
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace acceptance;
4+
5+
use GuzzleHttp;
6+
use GuzzleHttp\Psr7\Response;
7+
use GuzzleHttp\Exception\RequestException;
8+
use PHPUnit\Framework\TestCase;
9+
use Pusher\ApiErrorException;
10+
use Pusher\Pusher;
11+
use Pusher\PusherException;
12+
use stdClass;
13+
14+
class TerminateUserConnectionsTest extends TestCase
15+
{
16+
private $request_history = [];
17+
18+
/**
19+
* @var Pusher
20+
*/
21+
private $pusher;
22+
23+
protected function setUp(): void
24+
{
25+
if (PUSHERAPP_AUTHKEY === '' || PUSHERAPP_SECRET === '' || PUSHERAPP_APPID === '') {
26+
self::markTestSkipped('Please set the
27+
PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET and
28+
PUSHERAPP_APPID keys.');
29+
} else {
30+
$history = GuzzleHttp\Middleware::history($this->request_history);
31+
$handlerStack = GuzzleHttp\HandlerStack::create();
32+
$handlerStack->push($history);
33+
$httpClient = new GuzzleHttp\Client(['handler' => $handlerStack]);
34+
$this->pusher = new Pusher(PUSHERAPP_AUTHKEY, PUSHERAPP_SECRET, PUSHERAPP_APPID, ['cluster' => PUSHERAPP_CLUSTER], $httpClient);
35+
}
36+
}
37+
38+
public function testTerminateUserConections(): void
39+
{
40+
$result = $this->pusher->terminateUserConnections("123");
41+
self::assertEquals(new stdClass(), $result);
42+
self::assertEquals(1, count($this->request_history));
43+
$request = $this->request_history[0]['request'];
44+
self::assertEquals('api-' . PUSHERAPP_CLUSTER . '.pusher.com', $request->GetUri()->GetHost());
45+
self::assertEquals('POST', $request->GetMethod());
46+
self::assertEquals('/apps/' . PUSHERAPP_APPID . '/users/123/terminate_connections', $request->GetUri()->GetPath());
47+
}
48+
49+
public function testTerminateUserConectionsAsync(): void
50+
{
51+
$result = $this->pusher->terminateUserConnectionsAsync("123")->wait();
52+
self::assertEquals(new stdClass(), $result);
53+
self::assertEquals(1, count($this->request_history));
54+
$request = $this->request_history[0]['request'];
55+
self::assertEquals('api-' . PUSHERAPP_CLUSTER . '.pusher.com', $request->GetUri()->GetHost());
56+
self::assertEquals('POST', $request->GetMethod());
57+
self::assertEquals('/apps/' . PUSHERAPP_APPID . '/users/123/terminate_connections', $request->GetUri()->GetPath());
58+
}
59+
60+
public function testBadUserId(): void
61+
{
62+
$this->expectException(PusherException::class);
63+
$this->pusher->terminateUserConnections("");
64+
}
65+
66+
public function testBadUserIdAsync(): void
67+
{
68+
$this->expectException(PusherException::class);
69+
$this->pusher->terminateUserConnectionsAsync("");
70+
}
71+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace unit;
4+
5+
use GuzzleHttp;
6+
use GuzzleHttp\Psr7\Response;
7+
use GuzzleHttp\Exception\RequestException;
8+
use PHPUnit\Framework\TestCase;
9+
use Pusher\ApiErrorException;
10+
use Pusher\Pusher;
11+
use Pusher\PusherException;
12+
use stdClass;
13+
14+
class TerminateUserConnectionsUnitTest extends TestCase
15+
{
16+
private $request_history = [];
17+
18+
private function mockPusher(array $responses): Pusher
19+
{
20+
$mockHandler = new GuzzleHttp\Handler\MockHandler($responses);
21+
$history = GuzzleHttp\Middleware::history($this->request_history);
22+
$handlerStack = GuzzleHttp\HandlerStack::create($mockHandler);
23+
$handlerStack->push($history);
24+
$httpClient = new GuzzleHttp\Client(['handler' => $handlerStack]);
25+
return new Pusher("auth-key", "secret", "appid", ['cluster' => 'test1'], $httpClient);
26+
}
27+
28+
public function testTerminateUserConections(): void
29+
{
30+
$pusher = $this->mockPusher([new Response(200, [], "{}")]);
31+
$result = $pusher->terminateUserConnections("123");
32+
self::assertEquals(new stdClass(), $result);
33+
self::assertEquals(1, count($this->request_history));
34+
$request = $this->request_history[0]['request'];
35+
self::assertEquals('api-test1.pusher.com', $request->GetUri()->GetHost());
36+
self::assertEquals('POST', $request->GetMethod());
37+
self::assertEquals('/apps/appid/users/123/terminate_connections', $request->GetUri()->GetPath());
38+
}
39+
40+
public function testTerminateUserConectionsAsync(): void
41+
{
42+
$pusher = $this->mockPusher([new Response(200, [], "{}")]);
43+
$result = $pusher->terminateUserConnectionsAsync("123")->wait();
44+
self::assertEquals(new stdClass(), $result);
45+
self::assertEquals(1, count($this->request_history));
46+
$request = $this->request_history[0]['request'];
47+
self::assertEquals('api-test1.pusher.com', $request->GetUri()->GetHost());
48+
self::assertEquals('POST', $request->GetMethod());
49+
self::assertEquals('/apps/appid/users/123/terminate_connections', $request->GetUri()->GetPath());
50+
}
51+
52+
public function testBadUserId(): void
53+
{
54+
$pusher = $this->mockPusher([]);
55+
$this->expectException(PusherException::class);
56+
$pusher->terminateUserConnections("");
57+
}
58+
59+
public function testBadUserIdAsync(): void
60+
{
61+
$pusher = $this->mockPusher([]);
62+
$this->expectException(PusherException::class);
63+
$pusher->terminateUserConnectionsAsync("");
64+
}
65+
66+
public function testTerminateUserConectionsError(): void
67+
{
68+
$pusher = $this->mockPusher([new Response(500, [], "{}")]);
69+
$this->expectException(ApiErrorException::class);
70+
$pusher->terminateUserConnections("123");
71+
}
72+
73+
public function testTerminateUserConectionsAsyncError(): void
74+
{
75+
$pusher = $this->mockPusher([new Response(500, [], "{}")]);
76+
$this->expectException(ApiErrorException::class);
77+
$pusher->terminateUserConnectionsAsync("123")->wait();
78+
}
79+
}

0 commit comments

Comments
 (0)