diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7b78f4dca2..d9d2d331c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,41 +20,16 @@ jobs:
fail-fast: false
matrix:
php:
- - '7.3'
- - '7.4'
- '8.0'
- '8.1'
laravel:
- - 6.*
- - 7.*
- - 8.*
+ - 9.*
prefer:
- 'prefer-lowest'
- 'prefer-stable'
include:
- - laravel: '6.*'
- testbench: '4.*'
- phpunit: '^8.5.8|^9.3.3'
- - laravel: '7.*'
- testbench: '5.*'
- phpunit: '^8.5.8|^9.3.3'
- - laravel: '8.*'
- testbench: '6.*'
- phpunit: '^9.3.3'
- exclude:
- - php: '8.0'
- laravel: 6.*
- prefer: 'prefer-lowest'
- - php: '8.0'
- laravel: 7.*
- prefer: 'prefer-lowest'
- - php: '8.1'
- laravel: 6.*
- - php: '8.1'
- laravel: 7.*
- - php: '8.1'
- laravel: 8.*
- prefer: 'prefer-lowest'
+ - laravel: '9.*'
+ testbench: '7.*'
name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }}
@@ -68,6 +43,13 @@ jobs:
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv
coverage: pcov
+ - name: Setup MySQL
+ uses: haltuf/mysql-action@master
+ with:
+ mysql version: '8.0'
+ mysql database: 'websockets_test'
+ mysql root password: 'password'
+
- name: Setup Redis
uses: supercharge/redis-github-action@1.1.0
with:
@@ -81,7 +63,7 @@ jobs:
- name: Install dependencies
run: |
- composer require "laravel/framework:${{ matrix.laravel }}" "phpunit/phpunit:${{ matrix.phpunit }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest
- name: Run tests for Local
diff --git a/composer.json b/composer.json
index f68f053bb8..e0760ca84d 100644
--- a/composer.json
+++ b/composer.json
@@ -29,27 +29,33 @@
}
],
"require": {
- "cboden/ratchet": "^0.4.1",
- "clue/redis-react": "^2.5",
+ "php": "^8.0|^8.1",
+ "cboden/ratchet": "^0.4.4",
+ "clue/block-react": "^1.5",
+ "clue/reactphp-sqlite": "^1.0",
+ "clue/redis-react": "^2.6",
+ "doctrine/dbal": "^2.9",
"evenement/evenement": "^2.0|^3.0",
"facade/ignition-contracts": "^1.0",
- "guzzlehttp/psr7": "^1.7|^2.0",
- "illuminate/broadcasting": "^6.3|^7.0|^8.0|^9.0",
- "illuminate/console": "^6.3|^7.0|^8.0|^9.0",
- "illuminate/http": "^6.3|^7.0|^8.0|^9.0",
- "illuminate/queue": "^6.3|^7.0|^8.0|^9.0",
- "illuminate/routing": "^6.3|^7.0|^8.0|^9.0",
- "illuminate/support": "^6.3|^7.0|^8.0|^9.0",
- "pusher/pusher-php-server": "^3.0|^4.0|^5.0|^6.0|^7.0",
+ "guzzlehttp/psr7": "^1.5",
+ "illuminate/broadcasting": "^9.0",
+ "illuminate/console": "^9.0",
+ "illuminate/http": "^9.0",
+ "illuminate/queue": "^9.0",
+ "illuminate/routing": "^9.0",
+ "illuminate/support": "^9.0",
+ "pusher/pusher-php-server": "^6.0|^7.0",
+ "react/mysql": "^0.5",
"react/promise": "^2.8",
- "symfony/http-kernel": "^4.4|^5.4|^6.0",
+ "symfony/http-kernel": "^5.0|^6.0",
"symfony/psr-http-message-bridge": "^1.1|^2.0"
},
"require-dev": {
- "clue/block-react": "^1.4",
+ "clue/buzz-react": "^2.9",
"laravel/legacy-factories": "^1.1",
- "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0",
- "phpunit/phpunit": "^8.5.8|^9.3.3"
+ "orchestra/testbench-browser-kit": "^7.0",
+ "phpunit/phpunit": "^9.0",
+ "ratchet/pawl": "^0.3.5"
},
"suggest": {
"ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown.",
@@ -71,7 +77,8 @@
"config": {
"sort-packages": true
},
- "minimum-stability": "stable",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
"extra": {
"laravel": {
"providers": [
diff --git a/config/websockets.php b/config/websockets.php
index 681bb6bdc1..a321ace98b 100644
--- a/config/websockets.php
+++ b/config/websockets.php
@@ -37,13 +37,40 @@
| the use of the TCP protocol based on, for example, a list of allowed
| applications.
| By default, it uses the defined array in the config file, but you can
- | anytime implement the same interface as the class and add your own
- | custom method to retrieve the apps.
+ | choose to use SQLite or MySQL application managers, or define a
+ | custom application manager.
|
*/
'app' => \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager::class,
+ /*
+ |--------------------------------------------------------------------------
+ | SQLite application manager
+ |--------------------------------------------------------------------------
+ |
+ | The SQLite database to use when using the SQLite application manager.
+ |
+ */
+
+ 'sqlite' => [
+ 'database' => storage_path('laravel-websockets.sqlite'),
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | MySql application manager
+ |--------------------------------------------------------------------------
+ |
+ | The MySQL database connection to use.
+ |
+ */
+
+ 'mysql' => [
+ 'connection' => env('DB_CONNECTION', 'mysql'),
+
+ 'table' => 'websockets_apps',
+ ],
],
/*
diff --git a/database/migrations/0000_00_00_000000_create_websockets_apps_table.php b/database/migrations/0000_00_00_000000_create_websockets_apps_table.php
new file mode 100644
index 0000000000..7c22401fd9
--- /dev/null
+++ b/database/migrations/0000_00_00_000000_create_websockets_apps_table.php
@@ -0,0 +1,40 @@
+string('id')->index();
+ $table->string('key');
+ $table->string('secret');
+ $table->string('name');
+ $table->string('host')->nullable();
+ $table->string('path')->nullable();
+ $table->boolean('enable_client_messages')->default(false);
+ $table->boolean('enable_statistics')->default(true);
+ $table->unsignedInteger('capacity')->nullable();
+ $table->string('allowed_origins');
+ $table->nullableTimestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('websockets_apps');
+ }
+}
diff --git a/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php b/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
index 1b89b4af31..0989f288c5 100644
--- a/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
+++ b/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
@@ -16,9 +16,9 @@ public function up()
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
$table->increments('id');
$table->string('app_id');
- $table->integer('peak_connection_count');
- $table->integer('websocket_message_count');
- $table->integer('api_message_count');
+ $table->integer('peak_connections_count');
+ $table->integer('websocket_messages_count');
+ $table->integer('api_messages_count');
$table->nullableTimestamps();
});
}
diff --git a/database/migrations/0000_00_00_000000_rename_statistics_counters.php b/database/migrations/0000_00_00_000000_rename_statistics_counters.php
deleted file mode 100644
index 95b23f4db7..0000000000
--- a/database/migrations/0000_00_00_000000_rename_statistics_counters.php
+++ /dev/null
@@ -1,44 +0,0 @@
-renameColumn('peak_connection_count', 'peak_connections_count');
- });
- Schema::table('websockets_statistics_entries', function (Blueprint $table) {
- $table->renameColumn('websocket_message_count', 'websocket_messages_count');
- });
- Schema::table('websockets_statistics_entries', function (Blueprint $table) {
- $table->renameColumn('api_message_count', 'api_messages_count');
- });
- }
-
- /**
- * Reverse the migrations.
- *
- * @return void
- */
- public function down()
- {
- Schema::table('websockets_statistics_entries', function (Blueprint $table) {
- $table->renameColumn('peak_connections_count', 'peak_connection_count');
- });
- Schema::table('websockets_statistics_entries', function (Blueprint $table) {
- $table->renameColumn('websocket_messages_count', 'websocket_message_count');
- });
- Schema::table('websockets_statistics_entries', function (Blueprint $table) {
- $table->renameColumn('api_messages_count', 'api_message_count');
- });
- }
-}
diff --git a/database/migrations/sqlite/0000_00_00_000000_create_apps_table.sql b/database/migrations/sqlite/0000_00_00_000000_create_apps_table.sql
new file mode 100644
index 0000000000..c85f01a223
--- /dev/null
+++ b/database/migrations/sqlite/0000_00_00_000000_create_apps_table.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS apps (
+ id STRING NOT NULL,
+ key STRING NOT NULL,
+ secret STRING NOT NULL,
+ name STRING NOT NULL,
+ host STRING NULLABLE,
+ path STRING NULLABLE,
+ enable_client_messages BOOLEAN DEFAULT 0,
+ enable_statistics BOOLEAN DEFAULT 1,
+ capacity INTEGER NULLABLE,
+ allowed_origins STRING NULLABLE
+)
diff --git a/resources/views/apps.blade.php b/resources/views/apps.blade.php
new file mode 100644
index 0000000000..c7fab06310
--- /dev/null
+++ b/resources/views/apps.blade.php
@@ -0,0 +1,183 @@
+@extends('websockets::layout')
+
+@section('title')
+ Apps
+@endsection
+
+@section('content')
+
+
+
+
+
+
+
+
+ Name
+ |
+
+ Allowed origins
+ |
+
+ Statistics
+ |
+
+ Client Messages
+ |
+ |
+
+
+
+
+
+ @{{ app.name }}
+ |
+
+ @{{ app.allowed_origins || '*' }}
+ |
+
+ Yes
+ No
+ |
+
+ Yes
+ No
+ |
+
+ Installation instructions
+ Delete
+ |
+
+
+
+
+
+
+
+
Modify your .env
file:
+
PUSHER_APP_HOST=@{{ app.host === null ? window.location.hostname : app.host }}
+PUSHER_APP_PORT={{ $port }}
+PUSHER_APP_KEY=@{{ app.key }}
+PUSHER_APP_ID=@{{ app.id }}
+PUSHER_APP_SECRET=@{{ app.secret }}
+PUSHER_APP_SCHEME=https
+MIX_PUSHER_APP_HOST="${PUSHER_APP_HOST}"
+MIX_PUSHER_APP_PORT="${PUSHER_APP_PORT}"
+MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+
+
+
+@endsection
+
+@section('scripts')
+
+@endsection
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
index 9343967841..c449a20220 100644
--- a/resources/views/dashboard.blade.php
+++ b/resources/views/dashboard.blade.php
@@ -1,32 +1,13 @@
-
-
-
+@extends('websockets::layout')
- WebSockets Dashboard
+@section('title')
+ Dashboard
+@endsection
-
-
-
-
-
-
-
-
-
-
-
-
-
+@section('content')
@@ -197,256 +178,258 @@ class="flex flex-col my-6"
-
-
- Type
- |
-
- Details
- |
-
- Time
- |
-
+
+
+ Type
+ |
+
+ Details
+ |
+
+ Time
+ |
+
-
-
-
- @{{ log.type }}
-
- |
-
- @{{ log.details }}
- |
-
- @{{ log.time }}
- |
-
+
+
+
+ @{{ log.type }}
+
+ |
+
+ @{{ log.details }}
+ |
+
+ @{{ log.time }}
+ |
+
-
-
-
+ });
+
+@endsection
diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php
new file mode 100644
index 0000000000..4772d3e931
--- /dev/null
+++ b/resources/views/layout.blade.php
@@ -0,0 +1,93 @@
+
+
+
+
+ Laravel WebSockets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @yield('title')
+
+
+
+
+
+ @yield('content')
+
+
+
+
+
+@yield('scripts')
+
+
diff --git a/src/API/Controller.php b/src/API/Controller.php
index c413e9c1bf..8e4513b931 100644
--- a/src/API/Controller.php
+++ b/src/API/Controller.php
@@ -17,6 +17,7 @@
use Pusher\Pusher;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface;
+use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -52,13 +53,6 @@ abstract class Controller implements HttpServerInterface
*/
protected $channelManager;
- /**
- * The app attached with this request.
- *
- * @var \BeyondCode\LaravelWebSockets\Apps\App|null
- */
- protected $app;
-
/**
* Initialize the request.
*
@@ -184,26 +178,43 @@ protected function handleRequest(ConnectionInterface $connection)
$laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
- $this->ensureValidAppId($laravelRequest->get('appId'))
- ->ensureValidSignature($laravelRequest);
+ $this
+ ->ensureValidAppId($laravelRequest->appId)
+ ->then(function ($app) use ($laravelRequest, $connection) {
+ try {
+ $this->ensureValidSignature($app, $laravelRequest);
+ } catch (HttpException $exception) {
+ $this->onError($connection, $exception);
- // Invoke the controller action
- $response = $this($laravelRequest);
+ return;
+ }
- // Allow for async IO in the controller action
- if ($response instanceof PromiseInterface) {
- $response->then(function ($response) use ($connection) {
- $this->sendAndClose($connection, $response);
- });
+ // Invoke the controller action
+ try {
+ $response = $this($laravelRequest);
+ } catch (HttpException $exception) {
+ $this->onError($connection, $exception);
- return;
- }
+ return;
+ }
- if ($response instanceof HttpException) {
- throw $response;
- }
+ // Allow for async IO in the controller action
+ if ($response instanceof PromiseInterface) {
+ $response->then(function ($response) use ($connection) {
+ $this->sendAndClose($connection, $response);
+ });
+
+ return;
+ }
- $this->sendAndClose($connection, $response);
+ if ($response instanceof HttpException) {
+ $this->onError($connection, $response);
+
+ return;
+ }
+
+ $this->sendAndClose($connection, $response);
+ });
}
/**
@@ -222,29 +233,34 @@ protected function sendAndClose(ConnectionInterface $connection, $response)
* Ensure app existence.
*
* @param mixed $appId
- * @return $this
+ * @return PromiseInterface
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function ensureValidAppId($appId)
{
- if (! $appId || ! $this->app = App::findById($appId)) {
- throw new HttpException(401, "Unknown app id `{$appId}` provided.");
- }
+ $deferred = new Deferred();
+
+ App::findById($appId)
+ ->then(function ($app) use ($appId, $deferred) {
+ if (! $app) {
+ throw new HttpException(401, "Unknown app id `{$appId}` provided.");
+ }
+ $deferred->resolve($app);
+ });
- return $this;
+ return $deferred->promise();
}
/**
* Ensure signature integrity coming from an
* authorized application.
*
- * @param \GuzzleHttp\Psr7\ServerRequest $request
+ * @param App $app
+ * @param Request $request
* @return $this
- *
- * @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
- protected function ensureValidSignature(Request $request)
+ protected function ensureValidSignature(App $app, Request $request)
{
// The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
// The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
@@ -261,7 +277,7 @@ protected function ensureValidSignature(Request $request)
$signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
- $authSignature = hash_hmac('sha256', $signature, $this->app->secret);
+ $authSignature = hash_hmac('sha256', $signature, $app->secret);
if ($authSignature !== $request->get('auth_signature')) {
throw new HttpException(401, 'Invalid auth signature provided.');
diff --git a/src/API/TriggerEvent.php b/src/API/TriggerEvent.php
index ec802ae82b..b5120278b1 100644
--- a/src/API/TriggerEvent.php
+++ b/src/API/TriggerEvent.php
@@ -5,6 +5,7 @@
use BeyondCode\LaravelWebSockets\DashboardLogger;
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
use Illuminate\Http\Request;
+use React\Promise\Deferred;
class TriggerEvent extends Controller
{
@@ -16,10 +17,14 @@ class TriggerEvent extends Controller
*/
public function __invoke(Request $request)
{
- $channels = $request->channels ?: [];
+ if ($request->has('channel')) {
+ $channels = [$request->get('channel')];
+ } else {
+ $channels = $request->channels ?: [];
- if (is_string($channels)) {
- $channels = [$channels];
+ if (is_string($channels)) {
+ $channels = [$channels];
+ }
}
foreach ($channels as $channelName) {
@@ -49,17 +54,24 @@ public function __invoke(Request $request)
$request->appId, $request->socket_id, $channelName, (object) $payload
);
- if ($this->app->statisticsEnabled) {
- StatisticsCollector::apiMessage($request->appId);
- }
+ $deferred = new Deferred();
- DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
- 'event' => $request->name,
- 'channel' => $channelName,
- 'payload' => $request->data,
- ]);
+ $this->ensureValidAppId($request->appId)
+ ->then(function ($app) use ($request, $channelName, $deferred) {
+ if ($app->statisticsEnabled) {
+ StatisticsCollector::apiMessage($request->appId);
+ }
+
+ DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
+ 'event' => $request->name,
+ 'channel' => $channelName,
+ 'payload' => $request->data,
+ ]);
+
+ $deferred->resolve((object) []);
+ });
}
- return (object) [];
+ return $deferred->promise();
}
}
diff --git a/src/Apps/App.php b/src/Apps/App.php
index 19d10f6bba..e2f7194c35 100644
--- a/src/Apps/App.php
+++ b/src/Apps/App.php
@@ -3,6 +3,7 @@
namespace BeyondCode\LaravelWebSockets\Apps;
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
+use React\Promise\PromiseInterface;
class App
{
@@ -40,7 +41,7 @@ class App
* Find the app by id.
*
* @param string|int $appId
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
public static function findById($appId)
{
@@ -51,9 +52,9 @@ public static function findById($appId)
* Find the app by app key.
*
* @param string $appKey
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public static function findByKey($appKey): ?self
+ public static function findByKey($appKey): PromiseInterface
{
return app(AppManager::class)->findByKey($appKey);
}
@@ -62,9 +63,9 @@ public static function findByKey($appKey): ?self
* Find the app by app secret.
*
* @param string $appSecret
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public static function findBySecret($appSecret): ?self
+ public static function findBySecret($appSecret): PromiseInterface
{
return app(AppManager::class)->findBySecret($appSecret);
}
diff --git a/src/Apps/ConfigAppManager.php b/src/Apps/ConfigAppManager.php
index eb3d5dbadd..0b1b52f3c0 100644
--- a/src/Apps/ConfigAppManager.php
+++ b/src/Apps/ConfigAppManager.php
@@ -3,6 +3,8 @@
namespace BeyondCode\LaravelWebSockets\Apps;
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
+use React\Promise\PromiseInterface;
+use function React\Promise\resolve as resolvePromise;
class ConfigAppManager implements AppManager
{
@@ -26,54 +28,64 @@ public function __construct()
/**
* Get all apps.
*
- * @return array[\BeyondCode\LaravelWebSockets\Apps\App]
+ * @return PromiseInterface
*/
- public function all(): array
+ public function all(): PromiseInterface
{
- return $this->apps
+ return resolvePromise($this->apps
->map(function (array $appAttributes) {
return $this->convertIntoApp($appAttributes);
})
- ->toArray();
+ ->toArray());
}
/**
* Get app by id.
*
* @param string|int $appId
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public function findById($appId): ?App
+ public function findById($appId): PromiseInterface
{
- return $this->convertIntoApp(
+ return resolvePromise($this->convertIntoApp(
$this->apps->firstWhere('id', $appId)
- );
+ ));
}
/**
* Get app by app key.
*
* @param string $appKey
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public function findByKey($appKey): ?App
+ public function findByKey($appKey): PromiseInterface
{
- return $this->convertIntoApp(
+ return resolvePromise($this->convertIntoApp(
$this->apps->firstWhere('key', $appKey)
- );
+ ));
}
/**
* Get app by secret.
*
* @param string $appSecret
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public function findBySecret($appSecret): ?App
+ public function findBySecret($appSecret): PromiseInterface
{
- return $this->convertIntoApp(
+ return resolvePromise($this->convertIntoApp(
$this->apps->firstWhere('secret', $appSecret)
- );
+ ));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createApp($appData): PromiseInterface
+ {
+ $this->apps->push($appData);
+
+ return resolvePromise();
}
/**
@@ -107,8 +119,8 @@ protected function convertIntoApp(?array $appAttributes): ?App
}
$app
- ->enableClientMessages($appAttributes['enable_client_messages'])
- ->enableStatistics($appAttributes['enable_statistics'])
+ ->enableClientMessages((bool) $appAttributes['enable_client_messages'])
+ ->enableStatistics((bool) $appAttributes['enable_statistics'])
->setCapacity($appAttributes['capacity'] ?? null)
->setAllowedOrigins($appAttributes['allowed_origins'] ?? []);
diff --git a/src/Apps/MysqlAppManager.php b/src/Apps/MysqlAppManager.php
new file mode 100644
index 0000000000..fe82a7ee2d
--- /dev/null
+++ b/src/Apps/MysqlAppManager.php
@@ -0,0 +1,171 @@
+database = $database;
+ }
+
+ protected function getTableName(): string
+ {
+ return config('websockets.managers.mysql.table');
+ }
+
+ /**
+ * Get all apps.
+ *
+ * @return PromiseInterface
+ */
+ public function all(): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * FROM `'.$this->getTableName().'`')
+ ->then(function (QueryResult $result) use ($deferred) {
+ $deferred->resolve($result->resultRows);
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Get app by id.
+ *
+ * @param string|int $appId
+ * @return PromiseInterface
+ */
+ public function findById($appId): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `id` = ?', [$appId])
+ ->then(function (QueryResult $result) use ($deferred) {
+ $deferred->resolve($this->convertIntoApp($result->resultRows[0]));
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Get app by app key.
+ *
+ * @param string $appKey
+ * @return PromiseInterface
+ */
+ public function findByKey($appKey): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `key` = ?', [$appKey])
+ ->then(function (QueryResult $result) use ($deferred) {
+ $deferred->resolve($this->convertIntoApp($result->resultRows[0]));
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Get app by secret.
+ *
+ * @param string $appSecret
+ * @return PromiseInterface
+ */
+ public function findBySecret($appSecret): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `secret` = ?', [$appSecret])
+ ->then(function (QueryResult $result) use ($deferred) {
+ $deferred->resolve($this->convertIntoApp($result->resultRows[0]));
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Map the app into an App instance.
+ *
+ * @param array|null $app
+ * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ */
+ protected function convertIntoApp(?array $appAttributes): ?App
+ {
+ if (! $appAttributes) {
+ return null;
+ }
+
+ $app = new App(
+ $appAttributes['id'],
+ $appAttributes['key'],
+ $appAttributes['secret']
+ );
+
+ if (isset($appAttributes['name'])) {
+ $app->setName($appAttributes['name']);
+ }
+
+ if (isset($appAttributes['host'])) {
+ $app->setHost($appAttributes['host']);
+ }
+
+ if (isset($appAttributes['path'])) {
+ $app->setPath($appAttributes['path']);
+ }
+
+ $app
+ ->enableClientMessages((bool) $appAttributes['enable_client_messages'])
+ ->enableStatistics((bool) $appAttributes['enable_statistics'])
+ ->setCapacity($appAttributes['capacity'] ?? null)
+ ->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
+
+ return $app;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createApp($appData): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query(
+ 'INSERT INTO `'.$this->getTableName().'` (`id`, `key`, `secret`, `name`, `enable_client_messages`, `enable_statistics`, `allowed_origins`, `capacity`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
+ [$appData['id'], $appData['key'], $appData['secret'], $appData['name'], $appData['enable_client_messages'], $appData['enable_statistics'], $appData['allowed_origins'] ?? '', $appData['capacity'] ?? null])
+ ->then(function () use ($deferred) {
+ $deferred->resolve();
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+}
diff --git a/src/Apps/SQLiteAppManager.php b/src/Apps/SQLiteAppManager.php
new file mode 100644
index 0000000000..a265b40f37
--- /dev/null
+++ b/src/Apps/SQLiteAppManager.php
@@ -0,0 +1,167 @@
+database = $database;
+ }
+
+ /**
+ * Get all apps.
+ *
+ * @return PromiseInterface
+ */
+ public function all(): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * FROM `apps`')
+ ->then(function (Result $result) use ($deferred) {
+ $deferred->resolve($result->rows);
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Get app by id.
+ *
+ * @param string|int $appId
+ * @return PromiseInterface
+ */
+ public function findById($appId): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * from apps WHERE `id` = :id', ['id' => $appId])
+ ->then(function (Result $result) use ($deferred) {
+ $deferred->resolve($this->convertIntoApp($result->rows[0]));
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Get app by app key.
+ *
+ * @param string $appKey
+ * @return PromiseInterface
+ */
+ public function findByKey($appKey): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * from apps WHERE `key` = :key', ['key' => $appKey])
+ ->then(function (Result $result) use ($deferred) {
+ $deferred->resolve($this->convertIntoApp($result->rows[0]));
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Get app by secret.
+ *
+ * @param string $appSecret
+ * @return PromiseInterface
+ */
+ public function findBySecret($appSecret): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('SELECT * from apps WHERE `secret` = :secret', ['secret' => $appSecret])
+ ->then(function (Result $result) use ($deferred) {
+ $deferred->resolve($this->convertIntoApp($result->rows[0]));
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+
+ /**
+ * Map the app into an App instance.
+ *
+ * @param array|null $app
+ * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ */
+ protected function convertIntoApp(?array $appAttributes): ?App
+ {
+ if (! $appAttributes) {
+ return null;
+ }
+
+ $app = new App(
+ $appAttributes['id'],
+ $appAttributes['key'],
+ $appAttributes['secret']
+ );
+
+ if (isset($appAttributes['name'])) {
+ $app->setName($appAttributes['name']);
+ }
+
+ if (isset($appAttributes['host'])) {
+ $app->setHost($appAttributes['host']);
+ }
+
+ if (isset($appAttributes['path'])) {
+ $app->setPath($appAttributes['path']);
+ }
+
+ $app
+ ->enableClientMessages((bool) $appAttributes['enable_client_messages'])
+ ->enableStatistics((bool) $appAttributes['enable_statistics'])
+ ->setCapacity($appAttributes['capacity'] ?? null)
+ ->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
+
+ return $app;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createApp($appData): PromiseInterface
+ {
+ $deferred = new Deferred();
+
+ $this->database->query('
+ INSERT INTO apps (id, key, secret, name, host, path, enable_client_messages, enable_statistics, capacity, allowed_origins)
+ VALUES (:id, :key, :secret, :name, :host, :path, :enable_client_messages, :enable_statistics, :capacity, :allowed_origins)
+ ', $appData)
+ ->then(function (Result $result) use ($deferred) {
+ $deferred->resolve();
+ }, function ($error) use ($deferred) {
+ $deferred->reject($error);
+ });
+
+ return $deferred->promise();
+ }
+}
diff --git a/src/Cache/ArrayLock.php b/src/Cache/ArrayLock.php
new file mode 100644
index 0000000000..e16e7faa52
--- /dev/null
+++ b/src/Cache/ArrayLock.php
@@ -0,0 +1,55 @@
+lock = new LaravelLock($store, $name, $seconds, $owner);
+ }
+
+ public function acquire(): PromiseInterface
+ {
+ return Helpers::createFulfilledPromise($this->lock->acquire());
+ }
+
+ public function get($callback = null): PromiseInterface
+ {
+ return $this->lock->get($callback);
+ }
+
+ public function release(): PromiseInterface
+ {
+ return Helpers::createFulfilledPromise($this->lock->release());
+ }
+}
diff --git a/src/Cache/Lock.php b/src/Cache/Lock.php
new file mode 100644
index 0000000000..907e40a40d
--- /dev/null
+++ b/src/Cache/Lock.php
@@ -0,0 +1,46 @@
+name = $name;
+ $this->seconds = $seconds;
+ $this->owner = $owner;
+ }
+
+ abstract public function acquire(): PromiseInterface;
+
+ abstract public function get($callback = null): PromiseInterface;
+
+ abstract public function release(): PromiseInterface;
+}
diff --git a/src/Cache/RedisLock.php b/src/Cache/RedisLock.php
new file mode 100644
index 0000000000..a699d35504
--- /dev/null
+++ b/src/Cache/RedisLock.php
@@ -0,0 +1,69 @@
+redis = $redis;
+ }
+
+ public function acquire(): PromiseInterface
+ {
+ $promise = new Deferred();
+
+ if ($this->seconds > 0) {
+ $this->redis
+ ->set($this->name, $this->owner, 'EX', $this->seconds, 'NX')
+ ->then(function ($result) use ($promise) {
+ $promise->resolve($result === true);
+ });
+ } else {
+ $this->redis
+ ->setnx($this->name, $this->owner)
+ ->then(function ($result) use ($promise) {
+ $promise->resolve($result === 1);
+ });
+ }
+
+ return $promise->promise();
+ }
+
+ public function get($callback = null): PromiseInterface
+ {
+ $promise = new Deferred();
+
+ $this->acquire()
+ ->then(function ($result) use ($callback, $promise) {
+ if ($result) {
+ try {
+ $callback();
+ } finally {
+ $promise->resolve($this->release());
+ }
+ }
+ });
+
+ return $promise->promise();
+ }
+
+ public function release(): PromiseInterface
+ {
+ return $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);
+ }
+}
diff --git a/src/ChannelManagers/LocalChannelManager.php b/src/ChannelManagers/LocalChannelManager.php
index 5144f26ab8..a95376306a 100644
--- a/src/ChannelManagers/LocalChannelManager.php
+++ b/src/ChannelManagers/LocalChannelManager.php
@@ -2,17 +2,18 @@
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
+use BeyondCode\LaravelWebSockets\Cache\ArrayLock;
use BeyondCode\LaravelWebSockets\Channels\Channel;
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
use BeyondCode\LaravelWebSockets\Helpers;
use Carbon\Carbon;
-use Illuminate\Cache\ArrayLock;
use Illuminate\Cache\ArrayStore;
use Illuminate\Support\Str;
use Ratchet\ConnectionInterface;
use React\EventLoop\LoopInterface;
+use function React\Promise\all;
use React\Promise\PromiseInterface;
use stdClass;
@@ -226,9 +227,7 @@ public function unsubscribeFromChannel(ConnectionInterface $connection, string $
{
$channel = $this->findOrCreate($connection->app->id, $channelName);
- return Helpers::createFulfilledPromise(
- $channel->unsubscribe($connection, $payload)
- );
+ return $channel->unsubscribe($connection, $payload);
}
/**
@@ -439,26 +438,24 @@ public function connectionPonged(ConnectionInterface $connection): PromiseInterf
*/
public function removeObsoleteConnections(): PromiseInterface
{
- $lock = $this->lock();
- try {
- if (! $lock->acquire()) {
- return Helpers::createFulfilledPromise(false);
- }
+ return $this->lock()->get(function () {
+ return $this->getLocalConnections()
+ ->then(function ($connections) {
+ $promises = [];
- $this->getLocalConnections()->then(function ($connections) {
- foreach ($connections as $connection) {
- $differenceInSeconds = $connection->lastPongedAt->diffInSeconds(Carbon::now());
+ foreach ($connections as $connection) {
+ $differenceInSeconds = $connection->lastPongedAt->diffInSeconds(Carbon::now());
- if ($differenceInSeconds > 120) {
- $this->unsubscribeFromAllChannels($connection);
+ if ($differenceInSeconds > 120) {
+ $promises[] = $this->unsubscribeFromAllChannels($connection);
+ }
}
- }
- });
- return Helpers::createFulfilledPromise(true);
- } finally {
- optional($lock)->forceRelease();
- }
+ return all($promises);
+ })->then(function () {
+ $this->lock()->release();
+ });
+ });
}
/**
@@ -557,7 +554,7 @@ public function getServerId(): string
/**
* Get a new ArrayLock instance to avoid race conditions.
*
- * @return \Illuminate\Cache\CacheLock
+ * @return ArrayLock
*/
protected function lock()
{
diff --git a/src/ChannelManagers/RedisChannelManager.php b/src/ChannelManagers/RedisChannelManager.php
index 6c87948730..04755f81c0 100644
--- a/src/ChannelManagers/RedisChannelManager.php
+++ b/src/ChannelManagers/RedisChannelManager.php
@@ -2,17 +2,19 @@
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
+use BeyondCode\LaravelWebSockets\Cache\RedisLock;
+use BeyondCode\LaravelWebSockets\Channels\Channel;
use BeyondCode\LaravelWebSockets\DashboardLogger;
use BeyondCode\LaravelWebSockets\Helpers;
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
use Carbon\Carbon;
use Clue\React\Redis\Client;
use Clue\React\Redis\Factory;
-use Illuminate\Cache\RedisLock;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
use Ratchet\ConnectionInterface;
use React\EventLoop\LoopInterface;
+use function React\Promise\all;
use React\Promise\PromiseInterface;
use stdClass;
@@ -100,9 +102,12 @@ public function unsubscribeFromAllChannels(ConnectionInterface $connection): Pro
{
return $this->getGlobalChannels($connection->app->id)
->then(function ($channels) use ($connection) {
+ $promises = [];
foreach ($channels as $channel) {
- $this->unsubscribeFromChannel($connection, $channel, new stdClass);
+ $promises[] = $this->unsubscribeFromChannel($connection, $channel, new stdClass);
}
+
+ return all($promises);
})
->then(function () use ($connection) {
return parent::unsubscribeFromAllChannels($connection);
@@ -144,18 +149,36 @@ public function subscribeToChannel(ConnectionInterface $connection, string $chan
*/
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
{
- return parent::unsubscribeFromChannel($connection, $channelName, $payload)
- ->then(function () use ($connection, $channelName) {
- return $this->decrementSubscriptionsCount($connection->app->id, $channelName);
- })
+ return $this->getGlobalConnectionsCount($connection->app->id, $channelName)
->then(function ($count) use ($connection, $channelName) {
- $this->removeConnectionFromSet($connection);
- // If the total connections count gets to 0 after unsubscribe,
- // try again to check & unsubscribe from the PubSub topic if needed.
- if ($count < 1) {
- $this->removeChannelFromSet($connection->app->id, $channelName);
- $this->unsubscribeFromTopic($connection->app->id, $channelName);
+ if ($count === 0) {
+ // Make sure to not stay subscribed to the PubSub topic
+ // if there are no connections.
+ return $this->unsubscribeFromTopic($connection->app->id, $channelName);
}
+
+ return Helpers::createFulfilledPromise(null);
+ })
+ ->then(function () use ($connection, $channelName) {
+ return $this->decrementSubscriptionsCount($connection->app->id, $channelName)
+ ->then(function ($count) use ($connection, $channelName) {
+ // If the total connections count gets to 0 after unsubscribe,
+ // try again to check & unsubscribe from the PubSub topic if needed.
+ if ($count < 1) {
+ $promises = [];
+
+ $promises[] = $this->unsubscribeFromTopic($connection->app->id, $channelName);
+ $promises[] = $this->removeChannelFromSet($connection->app->id, $channelName);
+
+ return all($promises);
+ }
+ });
+ })
+ ->then(function () use ($connection) {
+ return $this->removeConnectionFromSet($connection);
+ })
+ ->then(function () use ($connection, $channelName, $payload) {
+ return parent::unsubscribeFromChannel($connection, $channelName, $payload);
});
}
@@ -371,23 +394,21 @@ public function connectionPonged(ConnectionInterface $connection): PromiseInterf
*/
public function removeObsoleteConnections(): PromiseInterface
{
- $lock = $this->lock();
- try {
- $lock->get(function () {
- $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
- ->then(function ($connections) {
- foreach ($connections as $socketId => $appId) {
- $connection = $this->fakeConnectionForApp($appId, $socketId);
+ return $this->lock()->get(function () {
+ return $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
+ ->then(function ($connections) {
+ $promises = [];
+ foreach ($connections as $socketId => $appId) {
+ $connection = $this->fakeConnectionForApp($appId, $socketId);
- $this->unsubscribeFromAllChannels($connection);
- }
- });
- });
+ $promises[] = $this->unsubscribeFromAllChannels($connection);
+ }
+ return all($promises);
+ });
+ })->then(function () {
return parent::removeObsoleteConnections();
- } finally {
- optional($lock)->forceRelease();
- }
+ });
}
/**
@@ -846,11 +867,11 @@ public function getRedisTopicName($appId, string $channel = null): string
/**
* Get a new RedisLock instance to avoid race conditions.
*
- * @return \Illuminate\Cache\CacheLock
+ * @return RedisLock
*/
protected function lock()
{
- return new RedisLock($this->redis, static::$lockName, 0);
+ return new RedisLock($this->publishClient, static::$lockName, 0);
}
/**
diff --git a/src/Channels/Channel.php b/src/Channels/Channel.php
index dbd874fd8e..d0c7447b42 100644
--- a/src/Channels/Channel.php
+++ b/src/Channels/Channel.php
@@ -6,9 +6,11 @@
use BeyondCode\LaravelWebSockets\DashboardLogger;
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
+use BeyondCode\LaravelWebSockets\Helpers;
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
use Illuminate\Support\Str;
use Ratchet\ConnectionInterface;
+use React\Promise\PromiseInterface;
use stdClass;
class Channel
@@ -116,12 +118,12 @@ public function subscribe(ConnectionInterface $connection, stdClass $payload): b
* Unsubscribe connection from the channel.
*
* @param \Ratchet\ConnectionInterface $connection
- * @return bool
+ * @return PromiseInterface
*/
- public function unsubscribe(ConnectionInterface $connection): bool
+ public function unsubscribe(ConnectionInterface $connection): PromiseInterface
{
if (! $this->hasConnection($connection)) {
- return false;
+ return Helpers::createFulfilledPromise(false);
}
unset($this->connections[$connection->socketId]);
@@ -132,7 +134,7 @@ public function unsubscribe(ConnectionInterface $connection): bool
$this->getName()
);
- return true;
+ return Helpers::createFulfilledPromise(true);
}
/**
diff --git a/src/Channels/PresenceChannel.php b/src/Channels/PresenceChannel.php
index 1d75b1f05d..51e015e902 100644
--- a/src/Channels/PresenceChannel.php
+++ b/src/Channels/PresenceChannel.php
@@ -5,8 +5,10 @@
use BeyondCode\LaravelWebSockets\DashboardLogger;
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
+use BeyondCode\LaravelWebSockets\Helpers;
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
use Ratchet\ConnectionInterface;
+use React\Promise\PromiseInterface;
use stdClass;
class PresenceChannel extends PrivateChannel
@@ -100,30 +102,30 @@ public function subscribe(ConnectionInterface $connection, stdClass $payload): b
* Unsubscribe connection from the channel.
*
* @param \Ratchet\ConnectionInterface $connection
- * @return bool
+ * @return PromiseInterface
*/
- public function unsubscribe(ConnectionInterface $connection): bool
+ public function unsubscribe(ConnectionInterface $connection): PromiseInterface
{
$truth = parent::unsubscribe($connection);
- $this->channelManager
+ return $this->channelManager
->getChannelMember($connection, $this->getName())
->then(function ($user) {
return @json_decode($user);
})
->then(function ($user) use ($connection) {
if (! $user) {
- return;
+ return Helpers::createFulfilledPromise(true);
}
- $this->channelManager
+ return $this->channelManager
->userLeftPresenceChannel($connection, $user, $this->getName())
->then(function () use ($connection, $user) {
// The `pusher_internal:member_removed` is triggered when a user leaves a channel.
// It's quite possible that a user can have multiple connections to the same channel
// (for example by having multiple browser tabs open)
// and in this case the events will only be triggered when the last one is closed.
- $this->channelManager
+ return $this->channelManager
->getMemberSockets($user->user_id, $connection->app->id, $this->getName())
->then(function ($sockets) use ($connection, $user) {
if (count($sockets) === 0) {
@@ -149,8 +151,9 @@ public function unsubscribe(ConnectionInterface $connection): bool
}
});
});
+ })
+ ->then(function () use ($truth) {
+ return $truth;
});
-
- return $truth;
}
}
diff --git a/src/Console/Commands/StartServer.php b/src/Console/Commands/StartServer.php
index abe1d075a6..69ea1ffe47 100644
--- a/src/Console/Commands/StartServer.php
+++ b/src/Console/Commands/StartServer.php
@@ -12,6 +12,8 @@
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use React\EventLoop\Factory as LoopFactory;
+use React\EventLoop\LoopInterface;
+use function React\Promise\all;
class StartServer extends Command
{
@@ -69,6 +71,10 @@ public function __construct()
*/
public function handle()
{
+ $this->laravel->singleton(LoopInterface::class, function () {
+ return $this->loop;
+ });
+
$this->configureLoggers();
$this->configureManagers();
@@ -311,9 +317,13 @@ protected function triggerSoftShutdown()
// be automatically be unsubscribed from all channels.
$channelManager->getLocalConnections()
->then(function ($connections) {
- foreach ($connections as $connection) {
- $connection->close();
- }
+ return all(collect($connections)->map(function ($connection) {
+ return app('websockets.handler')
+ ->onClose($connection)
+ ->then(function () use ($connection) {
+ $connection->close();
+ });
+ })->toArray());
})
->then(function () {
$this->loop->stop();
diff --git a/src/Contracts/AppManager.php b/src/Contracts/AppManager.php
index 153eda8a8d..f16b691799 100644
--- a/src/Contracts/AppManager.php
+++ b/src/Contracts/AppManager.php
@@ -3,37 +3,46 @@
namespace BeyondCode\LaravelWebSockets\Contracts;
use BeyondCode\LaravelWebSockets\Apps\App;
+use React\Promise\PromiseInterface;
interface AppManager
{
/**
* Get all apps.
*
- * @return array[\BeyondCode\LaravelWebSockets\Apps\App]
+ * @return PromiseInterface
*/
- public function all(): array;
+ public function all(): PromiseInterface;
/**
* Get app by id.
*
* @param string|int $appId
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public function findById($appId): ?App;
+ public function findById($appId): PromiseInterface;
/**
* Get app by app key.
*
* @param string $appKey
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public function findByKey($appKey): ?App;
+ public function findByKey($appKey): PromiseInterface;
/**
* Get app by secret.
*
* @param string $appSecret
- * @return \BeyondCode\LaravelWebSockets\Apps\App|null
+ * @return PromiseInterface
*/
- public function findBySecret($appSecret): ?App;
+ public function findBySecret($appSecret): PromiseInterface;
+
+ /**
+ * Create a new app.
+ *
+ * @param $appData
+ * @return PromiseInterface
+ */
+ public function createApp($appData): PromiseInterface;
}
diff --git a/src/Dashboard/Http/Controllers/AuthenticateDashboard.php b/src/Dashboard/Http/Controllers/AuthenticateDashboard.php
index a25922f734..f1f21fb836 100644
--- a/src/Dashboard/Http/Controllers/AuthenticateDashboard.php
+++ b/src/Dashboard/Http/Controllers/AuthenticateDashboard.php
@@ -4,8 +4,10 @@
use BeyondCode\LaravelWebSockets\Apps\App;
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
+use function Clue\React\Block\await;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
use Illuminate\Http\Request;
+use React\EventLoop\LoopInterface;
class AuthenticateDashboard
{
@@ -21,7 +23,7 @@ class AuthenticateDashboard
*/
public function __invoke(Request $request)
{
- $app = App::findById($request->header('X-App-Id'));
+ $app = await(App::findById($request->header('X-App-Id')), app(LoopInterface::class));
$broadcaster = $this->getPusherBroadcaster([
'key' => $app->key,
diff --git a/src/Dashboard/Http/Controllers/ShowApps.php b/src/Dashboard/Http/Controllers/ShowApps.php
new file mode 100644
index 0000000000..38724d7d40
--- /dev/null
+++ b/src/Dashboard/Http/Controllers/ShowApps.php
@@ -0,0 +1,26 @@
+ await($apps->all(), app(LoopInterface::class), 2.0),
+ 'port' => config('websockets.dashboard.port', 6001),
+ ]);
+ }
+}
diff --git a/src/Dashboard/Http/Controllers/ShowDashboard.php b/src/Dashboard/Http/Controllers/ShowDashboard.php
index eabd22d7c9..b2921bd6d7 100644
--- a/src/Dashboard/Http/Controllers/ShowDashboard.php
+++ b/src/Dashboard/Http/Controllers/ShowDashboard.php
@@ -4,7 +4,9 @@
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
use BeyondCode\LaravelWebSockets\DashboardLogger;
+use function Clue\React\Block\await;
use Illuminate\Http\Request;
+use React\EventLoop\LoopInterface;
class ShowDashboard
{
@@ -18,7 +20,7 @@ class ShowDashboard
public function __invoke(Request $request, AppManager $apps)
{
return view('websockets::dashboard', [
- 'apps' => $apps->all(),
+ 'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
'port' => config('websockets.dashboard.port', 6001),
'channels' => DashboardLogger::$channels,
'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX,
diff --git a/src/Dashboard/Http/Controllers/StoreApp.php b/src/Dashboard/Http/Controllers/StoreApp.php
new file mode 100644
index 0000000000..04717f7668
--- /dev/null
+++ b/src/Dashboard/Http/Controllers/StoreApp.php
@@ -0,0 +1,36 @@
+ (string) Str::uuid(),
+ 'key' => (string) Str::uuid(),
+ 'secret' => (string) Str::uuid(),
+ 'name' => $request->get('name'),
+ 'enable_client_messages' => $request->has('enable_client_messages'),
+ 'enable_statistics' => $request->has('enable_statistics'),
+ 'allowed_origins' => $request->get('allowed_origins'),
+ ];
+
+ await($apps->createApp($appData), app(LoopInterface::class));
+
+ return redirect()->route('laravel-websockets.apps');
+ }
+}
diff --git a/src/Dashboard/Http/Requests/StoreAppRequest.php b/src/Dashboard/Http/Requests/StoreAppRequest.php
new file mode 100644
index 0000000000..1910850080
--- /dev/null
+++ b/src/Dashboard/Http/Requests/StoreAppRequest.php
@@ -0,0 +1,20 @@
+ 'required',
+ ];
+ }
+}
diff --git a/src/Rules/AppId.php b/src/Rules/AppId.php
index ce5ea2eae5..db92052735 100644
--- a/src/Rules/AppId.php
+++ b/src/Rules/AppId.php
@@ -3,7 +3,9 @@
namespace BeyondCode\LaravelWebSockets\Rules;
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
+use function Clue\React\Block\await;
use Illuminate\Contracts\Validation\Rule;
+use React\EventLoop\Factory;
class AppId implements Rule
{
@@ -18,7 +20,7 @@ public function passes($attribute, $value)
{
$manager = app(AppManager::class);
- return $manager->findById($value) ? true : false;
+ return await($manager->findById($value), Factory::create()) ? true : false;
}
/**
diff --git a/src/Server/Router.php b/src/Server/Router.php
index 3092f7cf2e..5be0fe0f2b 100644
--- a/src/Server/Router.php
+++ b/src/Server/Router.php
@@ -70,7 +70,7 @@ public function getCustomRoutes(): array
*/
public function registerRoutes()
{
- $this->get('/app/{appKey}', config('websockets.handlers.websocket'));
+ $this->get('/app/{appKey}', 'websockets.handler');
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
@@ -191,9 +191,10 @@ public function registerCustomRoutes()
*/
protected function getRoute(string $method, string $uri, $action): Route
{
+ $action = app($action);
$action = is_subclass_of($action, MessageComponentInterface::class)
? $this->createWebSocketsServer($action)
- : app($action);
+ : $action;
return new Route($uri, ['_controller' => $action], [], [], null, [], [$method]);
}
@@ -201,13 +202,11 @@ protected function getRoute(string $method, string $uri, $action): Route
/**
* Create a new websockets server to handle the action.
*
- * @param string $action
+ * @param MessageComponentInterface $app
* @return \Ratchet\WebSocket\WsServer
*/
- protected function createWebSocketsServer(string $action): WsServer
+ protected function createWebSocketsServer($app): WsServer
{
- $app = app($action);
-
if (WebsocketsLogger::isEnabled()) {
$app = WebsocketsLogger::decorate($app);
}
diff --git a/src/Server/WebSocketHandler.php b/src/Server/WebSocketHandler.php
index 855532dd3f..a5bff133a5 100644
--- a/src/Server/WebSocketHandler.php
+++ b/src/Server/WebSocketHandler.php
@@ -9,10 +9,14 @@
use BeyondCode\LaravelWebSockets\Events\NewConnection;
use BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived;
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
+use BeyondCode\LaravelWebSockets\Helpers;
+use BeyondCode\LaravelWebSockets\Server\Exceptions\WebSocketException;
use Exception;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\WebSocket\MessageComponentInterface;
+use React\Promise\Deferred;
+use React\Promise\PromiseInterface;
class WebSocketHandler implements MessageComponentInterface
{
@@ -47,30 +51,38 @@ public function onOpen(ConnectionInterface $connection)
}
$this->verifyAppKey($connection)
- ->verifyOrigin($connection)
- ->limitConcurrentConnections($connection)
- ->generateSocketId($connection)
- ->establishConnection($connection);
+ ->then(function () use ($connection) {
+ try {
+ $this->verifyOrigin($connection)
+ ->limitConcurrentConnections($connection)
+ ->generateSocketId($connection)
+ ->establishConnection($connection);
- if (isset($connection->app)) {
- /** @var \GuzzleHttp\Psr7\Request $request */
- $request = $connection->httpRequest;
+ if (isset($connection->app)) {
+ /** @var \GuzzleHttp\Psr7\Request $request */
+ $request = $connection->httpRequest;
- if ($connection->app->statisticsEnabled) {
- StatisticsCollector::connection($connection->app->id);
- }
+ if ($connection->app->statisticsEnabled) {
+ StatisticsCollector::connection($connection->app->id);
+ }
- $this->channelManager->subscribeToApp($connection->app->id);
+ $this->channelManager->subscribeToApp($connection->app->id);
- $this->channelManager->connectionPonged($connection);
+ $this->channelManager->connectionPonged($connection);
- DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [
- 'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
- 'socketId' => $connection->socketId,
- ]);
+ DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [
+ 'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
+ 'socketId' => $connection->socketId,
+ ]);
- NewConnection::dispatch($connection->app->id, $connection->socketId);
- }
+ NewConnection::dispatch($connection->app->id, $connection->socketId);
+ }
+ } catch (WebSocketException $exception) {
+ $this->onError($connection, $exception);
+ }
+ }, function ($exception) use ($connection) {
+ $this->onError($connection, $exception);
+ });
}
/**
@@ -105,11 +117,11 @@ public function onMessage(ConnectionInterface $connection, MessageInterface $mes
* Handle the websocket close.
*
* @param \Ratchet\ConnectionInterface $connection
- * @return void
+ * @return PromiseInterface
*/
public function onClose(ConnectionInterface $connection)
{
- $this->channelManager
+ return $this->channelManager
->unsubscribeFromAllChannels($connection)
->then(function (bool $unsubscribed) use ($connection) {
if (isset($connection->app)) {
@@ -117,8 +129,13 @@ public function onClose(ConnectionInterface $connection)
StatisticsCollector::disconnection($connection->app->id);
}
- $this->channelManager->unsubscribeFromApp($connection->app->id);
+ return $this->channelManager->unsubscribeFromApp($connection->app->id);
+ }
+ return Helpers::createFulfilledPromise(true);
+ })
+ ->then(function () use ($connection) {
+ if (isset($connection->app)) {
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
'socketId' => $connection->socketId,
]);
@@ -160,21 +177,28 @@ protected function connectionCanBeMade(ConnectionInterface $connection): bool
* Verify the app key validity.
*
* @param \Ratchet\ConnectionInterface $connection
- * @return $this
+ * @return PromiseInterface
*/
- protected function verifyAppKey(ConnectionInterface $connection)
+ protected function verifyAppKey(ConnectionInterface $connection): PromiseInterface
{
+ $deferred = new Deferred();
+
$query = QueryParameters::create($connection->httpRequest);
$appKey = $query->get('appKey');
- if (! $app = App::findByKey($appKey)) {
- throw new Exceptions\UnknownAppKey($appKey);
- }
+ App::findByKey($appKey)
+ ->then(function ($app) use ($appKey, $connection, $deferred) {
+ if (! $app) {
+ $deferred->reject(new Exceptions\UnknownAppKey($appKey));
+ }
- $connection->app = $app;
+ $connection->app = $app;
- return $this;
+ $deferred->resolve();
+ });
+
+ return $deferred->promise();
}
/**
diff --git a/src/Statistics/Collectors/MemoryCollector.php b/src/Statistics/Collectors/MemoryCollector.php
index 8de17aa19e..908120c0a0 100644
--- a/src/Statistics/Collectors/MemoryCollector.php
+++ b/src/Statistics/Collectors/MemoryCollector.php
@@ -92,25 +92,27 @@ public function save()
{
$this->getStatistics()->then(function ($statistics) {
foreach ($statistics as $appId => $statistic) {
- if (! $statistic->isEnabled()) {
- continue;
- }
-
- if ($statistic->shouldHaveTracesRemoved()) {
- $this->resetAppTraces($appId);
-
- continue;
- }
-
- $this->createRecord($statistic, $appId);
-
- $this->channelManager
- ->getGlobalConnectionsCount($appId)
- ->then(function ($connections) use ($statistic) {
- $statistic->reset(
- is_null($connections) ? 0 : $connections
- );
- });
+ $statistic->isEnabled()->then(function ($isEnabled) use ($appId, $statistic) {
+ if (! $isEnabled) {
+ return;
+ }
+
+ if ($statistic->shouldHaveTracesRemoved()) {
+ $this->resetAppTraces($appId);
+
+ return;
+ }
+
+ $this->createRecord($statistic, $appId);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount($appId)
+ ->then(function ($connections) use ($statistic) {
+ $statistic->reset(
+ is_null($connections) ? 0 : $connections
+ );
+ });
+ });
}
});
}
diff --git a/src/Statistics/Statistic.php b/src/Statistics/Statistic.php
index 8de67c2a2e..5af761c455 100644
--- a/src/Statistics/Statistic.php
+++ b/src/Statistics/Statistic.php
@@ -3,6 +3,8 @@
namespace BeyondCode\LaravelWebSockets\Statistics;
use BeyondCode\LaravelWebSockets\Apps\App;
+use React\Promise\Deferred;
+use React\Promise\PromiseInterface;
class Statistic
{
@@ -118,11 +120,17 @@ public function setApiMessagesCount(int $apiMessagesCount)
/**
* Check if the app has statistics enabled.
*
- * @return bool
+ * @return PromiseInterface
*/
- public function isEnabled(): bool
+ public function isEnabled(): PromiseInterface
{
- return App::findById($this->appId)->statisticsEnabled;
+ $deferred = new Deferred();
+
+ App::findById($this->appId)->then(function ($app) use ($deferred) {
+ $deferred->resolve($app->statisticsEnabled);
+ });
+
+ return $deferred->promise();
}
/**
diff --git a/src/WebSocketsServiceProvider.php b/src/WebSocketsServiceProvider.php
index 829943e600..06f7a6cc71 100644
--- a/src/WebSocketsServiceProvider.php
+++ b/src/WebSocketsServiceProvider.php
@@ -6,15 +6,25 @@
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
+use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowApps;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
+use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\StoreApp;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
use BeyondCode\LaravelWebSockets\Queue\AsyncRedisConnector;
use BeyondCode\LaravelWebSockets\Server\Router;
+use Clue\React\SQLite\DatabaseInterface;
+use Clue\React\SQLite\Factory as SQLiteFactory;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
+use React\EventLoop\Factory;
+use React\EventLoop\LoopInterface;
+use React\MySQL\ConnectionInterface;
+use React\MySQL\Factory as MySQLFactory;
+use SplFileInfo;
+use Symfony\Component\Finder\Finder;
class WebSocketsServiceProvider extends ServiceProvider
{
@@ -38,8 +48,16 @@ public function boot()
__DIR__.'/../database/migrations/0000_00_00_000000_rename_statistics_counters.php' => database_path('migrations/0000_00_00_000000_rename_statistics_counters.php'),
], 'migrations');
+ $this->registerEventLoop();
+
+ $this->registerSQLiteDatabase();
+
+ $this->registerMySqlDatabase();
+
$this->registerAsyncRedisQueueDriver();
+ $this->registerWebSocketHandler();
+
$this->registerRouter();
$this->registerManagers();
@@ -61,6 +79,13 @@ public function register()
//
}
+ protected function registerEventLoop()
+ {
+ $this->app->singleton(LoopInterface::class, function () {
+ return Factory::create();
+ });
+ }
+
/**
* Register the async, non-blocking Redis queue driver.
*
@@ -73,6 +98,47 @@ protected function registerAsyncRedisQueueDriver()
});
}
+ protected function registerSQLiteDatabase()
+ {
+ $this->app->singleton(DatabaseInterface::class, function () {
+ $factory = new SQLiteFactory($this->app->make(LoopInterface::class));
+
+ $database = $factory->openLazy(
+ config('websockets.managers.sqlite.database', ':memory:')
+ );
+
+ $migrations = (new Finder())
+ ->files()
+ ->ignoreDotFiles(true)
+ ->in(__DIR__.'/../database/migrations/sqlite')
+ ->name('*.sql');
+
+ /** @var SplFileInfo $migration */
+ foreach ($migrations as $migration) {
+ $database->exec($migration->getContents());
+ }
+
+ return $database;
+ });
+ }
+
+ protected function registerMySqlDatabase()
+ {
+ $this->app->singleton(ConnectionInterface::class, function () {
+ $factory = new MySQLFactory($this->app->make(LoopInterface::class));
+
+ $connectionKey = 'database.connections.'.config('websockets.managers.mysql.connection');
+
+ $auth = trim(config($connectionKey.'.username').':'.config($connectionKey.'.password'), ':');
+ $connection = trim(config($connectionKey.'.host').':'.config($connectionKey.'.port'), ':');
+ $database = config($connectionKey.'.database');
+
+ $database = $factory->createLazyConnection(trim("{$auth}@{$connection}/{$database}", '@'));
+
+ return $database;
+ });
+ }
+
/**
* Register the statistics-related contracts.
*
@@ -98,7 +164,7 @@ protected function registerStatistics()
}
/**
- * Regsiter the dashboard components.
+ * Register the dashboard components.
*
* @return void
*/
@@ -165,6 +231,8 @@ protected function registerDashboardRoutes()
'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]),
], function () {
Route::get('/', ShowDashboard::class)->name('dashboard');
+ Route::get('/apps', ShowApps::class)->name('apps');
+ Route::post('/apps', StoreApp::class)->name('apps.store');
Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics');
Route::post('/auth', AuthenticateDashboard::class)->name('auth');
Route::post('/event', SendMessage::class)->name('event');
@@ -182,4 +250,11 @@ protected function registerDashboardGate()
return $this->app->environment('local');
});
}
+
+ protected function registerWebSocketHandler()
+ {
+ $this->app->singleton('websockets.handler', function () {
+ return app(config('websockets.handlers.websocket'));
+ });
+ }
}
diff --git a/tests/Apps/ConfigAppManagerTest.php b/tests/Apps/ConfigAppManagerTest.php
new file mode 100644
index 0000000000..ce0454aa8c
--- /dev/null
+++ b/tests/Apps/ConfigAppManagerTest.php
@@ -0,0 +1,98 @@
+set('websockets.managers.app', ConfigAppManager::class);
+ $app['config']->set('websockets.apps', []);
+ }
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->apps = app()->make(AppManager::class);
+ }
+
+ public function test_can_return_all_apps()
+ {
+ $apps = $this->await($this->apps->all());
+ $this->assertCount(0, $apps);
+
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'test',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $apps = $this->await($this->apps->all());
+ $this->assertCount(1, $apps);
+ }
+
+ public function test_can_find_apps_by_id()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'test',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findById(1));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('test', $app->key);
+ }
+
+ public function test_can_find_apps_by_key()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'key',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findByKey('key'));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('key', $app->key);
+ }
+
+ public function test_can_find_apps_by_secret()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'key',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findBySecret('secret'));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('key', $app->key);
+ }
+}
diff --git a/tests/Apps/MysqlAppManagerTest.php b/tests/Apps/MysqlAppManagerTest.php
new file mode 100644
index 0000000000..3c1b3a470c
--- /dev/null
+++ b/tests/Apps/MysqlAppManagerTest.php
@@ -0,0 +1,110 @@
+set('websockets.managers.app', MysqlAppManager::class);
+ $app['config']->set('database.connections.mysql.database', 'websockets_test');
+ $app['config']->set('database.connections.mysql.username', 'root');
+ $app['config']->set('database.connections.mysql.password', 'password');
+
+ $app['config']->set('websockets.managers.mysql.table', 'websockets_apps');
+ $app['config']->set('websockets.managers.mysql.connection', 'mysql');
+ $app['config']->set('database.connections.default', 'mysql');
+ }
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->artisan('migrate:fresh', [
+ '--database' => 'mysql',
+ '--realpath' => true,
+ '--path' => __DIR__.'/../../database/migrations/',
+ ]);
+
+ $this->apps = app()->make(AppManager::class);
+ }
+
+ public function test_can_return_all_apps()
+ {
+ $apps = $this->await($this->apps->all());
+ $this->assertCount(0, $apps);
+
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'test',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $apps = $this->await($this->apps->all());
+ $this->assertCount(1, $apps);
+ }
+
+ public function test_can_find_apps_by_id()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'test',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findById(1));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('test', $app->key);
+ }
+
+ public function test_can_find_apps_by_key()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'key',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findByKey('key'));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('key', $app->key);
+ }
+
+ public function test_can_find_apps_by_secret()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'key',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findBySecret('secret'));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('key', $app->key);
+ }
+}
diff --git a/tests/Apps/SqliteAppManagerTest.php b/tests/Apps/SqliteAppManagerTest.php
new file mode 100644
index 0000000000..b4250690bc
--- /dev/null
+++ b/tests/Apps/SqliteAppManagerTest.php
@@ -0,0 +1,97 @@
+set('websockets.managers.app', SQLiteAppManager::class);
+ }
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->apps = app()->make(AppManager::class);
+ }
+
+ public function test_can_return_all_apps()
+ {
+ $apps = $this->await($this->apps->all());
+ $this->assertCount(0, $apps);
+
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'test',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $apps = $this->await($this->apps->all());
+ $this->assertCount(1, $apps);
+ }
+
+ public function test_can_find_apps_by_id()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'test',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findById(1));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('test', $app->key);
+ }
+
+ public function test_can_find_apps_by_key()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'key',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findByKey('key'));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('key', $app->key);
+ }
+
+ public function test_can_find_apps_by_secret()
+ {
+ $this->await($this->apps->createApp([
+ 'id' => 1,
+ 'key' => 'key',
+ 'secret' => 'secret',
+ 'name' => 'Test',
+ 'enable_client_messages' => true,
+ 'enable_statistics' => false,
+ ]));
+
+ $app = $this->await($this->apps->findBySecret('secret'));
+
+ $this->assertInstanceOf(App::class, $app);
+ $this->assertSame('key', $app->key);
+ }
+}
diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php
index 2e4f2ed0d2..a871cf4443 100644
--- a/tests/ConnectionTest.php
+++ b/tests/ConnectionTest.php
@@ -2,43 +2,43 @@
namespace BeyondCode\LaravelWebSockets\Test;
-use BeyondCode\LaravelWebSockets\Server\Exceptions\OriginNotAllowed;
use BeyondCode\LaravelWebSockets\Server\Exceptions\UnknownAppKey;
class ConnectionTest extends TestCase
{
public function test_cannot_connect_with_a_wrong_app_key()
{
- $this->expectException(UnknownAppKey::class);
+ $this->startServer();
- $this->newActiveConnection(['public-channel'], 'NonWorkingKey');
+ $response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
+ $this->assertSame('{"event":"pusher:error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
}
public function test_unconnected_app_cannot_store_statistics()
{
- $this->expectException(UnknownAppKey::class);
+ $this->startServer();
- $this->newActiveConnection(['public-channel'], 'NonWorkingKey');
+ $response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
+ $this->assertSame('{"event":"pusher:error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
- $this->assertCount(0, $this->statisticsCollector->getStatistics());
+ $count = $this->await($this->statisticsCollector->getStatistics());
+ $this->assertCount(0, $count);
}
public function test_origin_validation_should_fail_for_no_origin()
{
- $this->expectException(OriginNotAllowed::class);
+ $this->startServer();
- $connection = $this->newConnection('TestOrigin');
-
- $this->pusherServer->onOpen($connection);
+ $response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin'));
+ $this->assertSame('{"event":"pusher:error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
}
public function test_origin_validation_should_fail_for_wrong_origin()
{
- $this->expectException(OriginNotAllowed::class);
+ $this->startServer();
- $connection = $this->newConnection('TestOrigin', ['Origin' => 'https://google.ro']);
-
- $this->pusherServer->onOpen($connection);
+ $response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin', ['Origin' => 'https://google.ro']));
+ $this->assertSame('{"event":"pusher:error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
}
public function test_origin_validation_should_pass_for_the_right_origin()
diff --git a/tests/Dashboard/AppsTest.php b/tests/Dashboard/AppsTest.php
new file mode 100644
index 0000000000..de42be360a
--- /dev/null
+++ b/tests/Dashboard/AppsTest.php
@@ -0,0 +1,38 @@
+set('websockets.managers.app', SQLiteAppManager::class);
+ }
+
+ public function test_can_list_all_apps()
+ {
+ $this->actingAs(factory(User::class)->create())
+ ->get(route('laravel-websockets.apps'))
+ ->assertViewHas('apps', []);
+ }
+
+ public function test_can_create_app()
+ {
+ $this->actingAs(factory(User::class)->create())
+ ->post(route('laravel-websockets.apps.store', [
+ 'name' => 'New App',
+ ]));
+
+ $this->actingAs(factory(User::class)->create())
+ ->get(route('laravel-websockets.apps'))
+ ->assertViewHas('apps', function ($apps) {
+ return count($apps) === 1 && $apps[0]['name'] === 'New App';
+ });
+ }
+}
diff --git a/tests/Dashboard/DashboardTest.php b/tests/Dashboard/DashboardTest.php
index d25d1e0196..f4c7bf5acd 100644
--- a/tests/Dashboard/DashboardTest.php
+++ b/tests/Dashboard/DashboardTest.php
@@ -17,7 +17,6 @@ public function test_can_see_dashboard()
{
$this->actingAs(factory(User::class)->create())
->get(route('laravel-websockets.dashboard'))
- ->assertResponseOk()
- ->see('WebSockets Dashboard');
+ ->assertResponseOk();
}
}
diff --git a/tests/FetchChannelTest.php b/tests/FetchChannelTest.php
index 53300ccd83..e4612bc91f 100644
--- a/tests/FetchChannelTest.php
+++ b/tests/FetchChannelTest.php
@@ -5,33 +5,26 @@
use BeyondCode\LaravelWebSockets\API\FetchChannel;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
-use Symfony\Component\HttpKernel\Exception\HttpException;
+use Pusher\Pusher;
class FetchChannelTest extends TestCase
{
public function test_invalid_signatures_can_not_access_the_api()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid auth signature provided.');
+ $this->startServer();
- $connection = new Mocks\Connection;
-
- $requestPath = '/apps/1234/channel/my-channel';
+ $requestPath = '/apps/1234/channels/my-channel';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
-
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'InvalidSecret', 'GET', $requestPath
- );
+ ));
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ $request = new Request('GET', "{$requestPath}?{$queryString}");
- $controller = app(FetchChannel::class);
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller->onOpen($connection, $request);
+ $this->assertSame(401, $response->getStatusCode());
+ $this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
}
public function test_it_returns_the_channel_information()
@@ -47,7 +40,7 @@ public function test_it_returns_the_channel_information()
'channelName' => 'my-channel',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -78,7 +71,7 @@ public function test_it_returns_presence_channel_information()
'channelName' => 'presence-channel',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -100,26 +93,17 @@ public function test_it_returns_404_for_invalid_channels()
{
$this->skipOnRedisReplication();
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Unknown channel');
+ $this->startServer();
$this->newActiveConnection(['my-channel']);
- $connection = new Mocks\Connection;
-
- $requestPath = '/apps/1234/channel/invalid-channel';
-
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'invalid-channel',
- ];
+ $requestPath = '/apps/1234/channels/invalid-channel';
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller = app(FetchChannel::class);
-
- $controller->onOpen($connection, $request);
+ $this->assertSame(404, $response->getStatusCode());
+ $this->assertSame('{"error":"Unknown channel `invalid-channel`."}', $response->getBody()->getContents());
}
}
diff --git a/tests/FetchChannelsTest.php b/tests/FetchChannelsTest.php
index ff5e3f9d5e..049f27c0aa 100644
--- a/tests/FetchChannelsTest.php
+++ b/tests/FetchChannelsTest.php
@@ -5,32 +5,26 @@
use BeyondCode\LaravelWebSockets\API\FetchChannels;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
-use Symfony\Component\HttpKernel\Exception\HttpException;
+use Pusher\Pusher;
class FetchChannelsTest extends TestCase
{
public function test_invalid_signatures_can_not_access_the_api()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid auth signature provided.');
-
- $connection = new Mocks\Connection;
+ $this->startServer();
$requestPath = '/apps/1234/channels';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'InvalidSecret', 'GET', $requestPath
- );
+ ));
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ $request = new Request('GET', "{$requestPath}?{$queryString}");
- $controller = app(FetchChannels::class);
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller->onOpen($connection, $request);
+ $this->assertSame(401, $response->getStatusCode());
+ $this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
}
public function test_it_returns_the_channel_information()
@@ -45,9 +39,9 @@ public function test_it_returns_the_channel_information()
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'GET', $requestPath
- );
+ ));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -80,9 +74,9 @@ public function test_it_returns_the_channel_information_for_prefix()
'appId' => '1234',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
'filter_by_prefix' => 'presence-global',
- ]);
+ ]));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -116,10 +110,10 @@ public function test_it_returns_the_channel_information_for_prefix_with_user_cou
'appId' => '1234',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
'filter_by_prefix' => 'presence-global',
'info' => 'user_count',
- ]);
+ ]));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -144,29 +138,18 @@ public function test_it_returns_the_channel_information_for_prefix_with_user_cou
public function test_can_not_get_non_presence_channel_user_count()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Request must be limited to presence channels in order to fetch user_count');
-
- $connection = new Mocks\Connection;
+ $this->startServer();
$requestPath = '/apps/1234/channels';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
'info' => 'user_count',
- ]);
+ ]));
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller = app(FetchChannels::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
+ $this->assertSame(400, $response->getStatusCode());
+ $this->assertSame('{"error":"Request must be limited to presence channels in order to fetch user_count"}', $response->getBody()->getContents());
}
public function test_it_returns_empty_object_for_no_channels_found()
@@ -179,7 +162,7 @@ public function test_it_returns_empty_object_for_no_channels_found()
'appId' => '1234',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
diff --git a/tests/FetchUsersTest.php b/tests/FetchUsersTest.php
index a0b664f0bb..f78bad06b4 100644
--- a/tests/FetchUsersTest.php
+++ b/tests/FetchUsersTest.php
@@ -4,87 +4,56 @@
use BeyondCode\LaravelWebSockets\API\FetchUsers;
use GuzzleHttp\Psr7\Request;
-use Symfony\Component\HttpKernel\Exception\HttpException;
+use Pusher\Pusher;
class FetchUsersTest extends TestCase
{
public function test_invalid_signatures_can_not_access_the_api()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid auth signature provided.');
+ $this->startServer();
- $connection = new Mocks\Connection;
-
- $requestPath = '/apps/1234/channel/my-channel';
-
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
+ $requestPath = '/apps/1234/channels/my-channel/users';
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'InvalidSecret', 'GET', $requestPath
- );
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ ));
- $controller = app(FetchUsers::class);
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller->onOpen($connection, $request);
+ $this->assertSame(401, $response->getStatusCode());
+ $this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
}
public function test_it_only_returns_data_for_presence_channels()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid presence channel');
-
- $this->newActiveConnection(['my-channel']);
-
- $connection = new Mocks\Connection;
-
- $requestPath = '/apps/1234/channel/my-channel/users';
+ $this->startServer();
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
+ $requestPath = '/apps/1234/channels/my-channel/users';
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'GET', $requestPath
- );
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ ));
- $controller = app(FetchUsers::class);
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller->onOpen($connection, $request);
+ $this->assertSame(400, $response->getStatusCode());
+ $this->assertSame('{"error":"Invalid presence channel `my-channel`"}', $response->getBody()->getContents());
}
- public function test_it_returns_404_for_invalid_channels()
+ public function test_it_returns_400_for_invalid_channels()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid presence channel');
-
- $this->newActiveConnection(['my-channel']);
-
- $connection = new Mocks\Connection;
+ $this->startServer();
- $requestPath = '/apps/1234/channel/invalid-channel/users';
+ $requestPath = '/apps/1234/channels/invalid-channel/users';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'invalid-channel',
- ];
-
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'GET', $requestPath
- );
+ ));
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchUsers::class);
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller->onOpen($connection, $request);
+ $this->assertSame(400, $response->getStatusCode());
+ $this->assertSame('{"error":"Invalid presence channel `invalid-channel`"}', $response->getBody()->getContents());
}
public function test_it_returns_connected_user_information()
@@ -100,7 +69,7 @@ public function test_it_returns_connected_user_information()
'channelName' => 'presence-channel',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -130,7 +99,7 @@ public function test_multiple_clients_with_same_id_gets_counted_once()
'channelName' => 'presence-channel',
];
- $queryString = self::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
diff --git a/tests/PresenceChannelTest.php b/tests/PresenceChannelTest.php
index 499d319d40..3795d973d1 100644
--- a/tests/PresenceChannelTest.php
+++ b/tests/PresenceChannelTest.php
@@ -6,6 +6,7 @@
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
+use Pusher\Pusher;
use Ratchet\ConnectionInterface;
class PresenceChannelTest extends TestCase
@@ -418,13 +419,13 @@ public function test_it_fires_the_event_to_presence_channel()
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['presence-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -459,13 +460,13 @@ public function test_it_fires_event_across_servers_when_there_are_not_users_loca
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['presence-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -507,13 +508,13 @@ public function test_it_fires_event_across_servers_when_there_are_users_locally_
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['presence-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
diff --git a/tests/PrivateChannelTest.php b/tests/PrivateChannelTest.php
index e2fa3f8c4e..7ae2ba165f 100644
--- a/tests/PrivateChannelTest.php
+++ b/tests/PrivateChannelTest.php
@@ -6,6 +6,7 @@
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
+use Pusher\Pusher;
use Ratchet\ConnectionInterface;
class PrivateChannelTest extends TestCase
@@ -238,13 +239,13 @@ public function test_it_fires_the_event_to_private_channel()
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['private-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -279,13 +280,13 @@ public function test_it_fires_event_across_servers_when_there_are_not_users_loca
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['private-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -327,13 +328,13 @@ public function test_it_fires_event_across_servers_when_there_are_users_locally_
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['private-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
diff --git a/tests/PublicChannelTest.php b/tests/PublicChannelTest.php
index c444d244d7..b37b651696 100644
--- a/tests/PublicChannelTest.php
+++ b/tests/PublicChannelTest.php
@@ -5,6 +5,7 @@
use BeyondCode\LaravelWebSockets\API\TriggerEvent;
use GuzzleHttp\Psr7\Request;
use Illuminate\Http\JsonResponse;
+use Pusher\Pusher;
use Ratchet\ConnectionInterface;
class PublicChannelTest extends TestCase
@@ -209,41 +210,28 @@ public function test_events_get_replicated_across_connections_for_public_channel
public function test_it_fires_the_event_to_public_channel()
{
- $this->newActiveConnection(['public-channel']);
-
- $connection = new Mocks\Connection;
+ $this->startServer();
$requestPath = '/apps/1234/events';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['public-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
- $request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ $response = $this->await($this->browser->post('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller = app(TriggerEvent::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->assertSame([], json_decode($response->getContent(), true));
+ $this->assertSame([], json_decode((string) $response->getBody(), true));
$this->statisticsCollector
->getAppStatistics('1234')
->then(function ($statistic) {
$this->assertEquals([
- 'peak_connections_count' => 1,
- 'websocket_messages_count' => 1,
+ 'peak_connections_count' => 0,
+ 'websocket_messages_count' => 0,
'api_messages_count' => 1,
'app_id' => '1234',
], $statistic->toArray());
@@ -260,13 +248,13 @@ public function test_it_fires_event_across_servers_when_there_are_not_users_loca
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['public-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
@@ -308,13 +296,13 @@ public function test_it_fires_event_across_servers_when_there_are_users_locally_
'appId' => '1234',
];
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'TestSecret', 'POST', $requestPath, [
'name' => 'some-event',
'channels' => ['public-channel'],
'data' => json_encode(['some-data' => 'yes']),
],
- );
+ ));
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 6d4853a042..618759411e 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -7,14 +7,39 @@
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
use BeyondCode\LaravelWebSockets\Facades\WebSocketRouter;
use BeyondCode\LaravelWebSockets\Helpers;
+use BeyondCode\LaravelWebSockets\Server\Loggers\HttpLogger;
+use BeyondCode\LaravelWebSockets\Server\Loggers\WebSocketsLogger;
+use BeyondCode\LaravelWebSockets\ServerFactory;
+use function Clue\React\Block\await;
+use Clue\React\Buzz\Browser;
use GuzzleHttp\Psr7\Request;
use Illuminate\Support\Facades\Redis;
use Orchestra\Testbench\BrowserKit\TestCase as Orchestra;
-use Pusher\Pusher;
+use Ratchet\Server\IoServer;
use React\EventLoop\Factory as LoopFactory;
+use React\EventLoop\LoopInterface;
+use React\Promise\Deferred;
+use React\Promise\PromiseInterface;
+use Symfony\Component\Console\Output\BufferedOutput;
abstract class TestCase extends Orchestra
{
+ const AWAIT_TIMEOUT = 5.0;
+
+ /**
+ * The test Browser.
+ *
+ * @var \Clue\React\Buzz\Browser
+ */
+ protected $browser;
+
+ /**
+ * The test WebSocket server.
+ *
+ * @var IoServer
+ */
+ protected $server;
+
/**
* A test Pusher server.
*
@@ -73,11 +98,31 @@ public function setUp(): void
$this->loop = LoopFactory::create();
+ $this->app->singleton(LoopInterface::class, function () {
+ return $this->loop;
+ });
+
+ $this->browser = (new Browser($this->loop))
+ ->withFollowRedirects(false)
+ ->withRejectErrorResponse(false);
+
+ $this->app->singleton(HttpLogger::class, function () {
+ return (new HttpLogger(new BufferedOutput()))
+ ->enable(false)
+ ->verbose(false);
+ });
+
+ $this->app->singleton(WebSocketsLogger::class, function () {
+ return (new WebSocketsLogger(new BufferedOutput()))
+ ->enable(false)
+ ->verbose(false);
+ });
+
$this->replicationMode = getenv('REPLICATION_MODE') ?: 'local';
$this->resetDatabase();
$this->loadLaravelMigrations(['--database' => 'sqlite']);
- $this->loadMigrationsFrom(__DIR__.'/database/migrations');
+ $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->withFactories(__DIR__.'/database/factories');
$this->registerCustomPath();
@@ -102,6 +147,15 @@ public function setUp(): void
}
}
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ if ($this->server) {
+ $this->server->socket->close();
+ }
+ }
+
/**
* {@inheritdoc}
*/
@@ -270,6 +324,11 @@ protected function registerManagers()
$this->channelManager = $this->app->make(ChannelManager::class);
}
+ protected function await(PromiseInterface $promise, LoopInterface $loop = null, $timeout = null)
+ {
+ return await($promise, $loop ?? $this->loop, $timeout ?? static::AWAIT_TIMEOUT);
+ }
+
/**
* Unregister the managers for testing purposes.
*
@@ -338,6 +397,19 @@ protected function newConnection(string $appKey = 'TestKey', array $headers = []
return $connection;
}
+ protected function joinWebSocketServer(array $channelsToJoin = [], string $appKey = 'TestKey', array $headers = [])
+ {
+ $promise = new Deferred();
+
+ \Ratchet\Client\connect("ws://localhost:4000/app/{$appKey}", [], [], $this->loop)->then(function ($conn) use ($promise) {
+ $conn->on('message', function ($msg) use ($promise) {
+ $promise->resolve($msg);
+ });
+ });
+
+ return $promise->promise();
+ }
+
/**
* Get a connected websocket connection.
*
@@ -485,27 +557,16 @@ protected function skipOnLocalReplication()
}
}
- protected static function build_auth_query_string(
- $auth_key,
- $auth_secret,
- $request_method,
- $request_path,
- $query_params = [],
- $auth_version = '1.0',
- $auth_timestamp = null
- ) {
- $method = method_exists(Pusher::class, 'build_auth_query_params') ? 'build_auth_query_params' : 'build_auth_query_string';
-
- $params = Pusher::$method(
- $auth_key, $auth_secret, $request_method, $request_path, $query_params, $auth_version, $auth_timestamp
- );
-
- if ($method == 'build_auth_query_string') {
- return $params;
- }
+ protected function startServer()
+ {
+ $server = new ServerFactory('0.0.0.0', 4000);
- ksort($params);
+ WebSocketRouter::registerRoutes();
- return http_build_query($params);
+ $this->server = $server
+ ->setLoop($this->loop)
+ ->withRoutes(WebSocketRouter::getRoutes())
+ ->setConsoleOutput(new BufferedOutput())
+ ->createServer();
}
}
diff --git a/tests/TriggerEventTest.php b/tests/TriggerEventTest.php
index 18a487416d..4f97c8d685 100644
--- a/tests/TriggerEventTest.php
+++ b/tests/TriggerEventTest.php
@@ -2,33 +2,25 @@
namespace BeyondCode\LaravelWebSockets\Test;
-use BeyondCode\LaravelWebSockets\API\TriggerEvent;
-use GuzzleHttp\Psr7\Request;
-use Symfony\Component\HttpKernel\Exception\HttpException;
+use Pusher\Pusher;
class TriggerEventTest extends TestCase
{
public function test_invalid_signatures_can_not_fire_the_event()
{
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid auth signature provided.');
+ $this->startServer();
$connection = new Mocks\Connection;
$requestPath = '/apps/1234/events';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = self::build_auth_query_string(
+ $queryString = http_build_query(Pusher::build_auth_query_params(
'TestKey', 'InvalidSecret', 'GET', $requestPath
- );
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+ ));
- $controller = app(TriggerEvent::class);
+ $response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
- $controller->onOpen($connection, $request);
+ $this->assertSame(405, $response->getStatusCode());
+ $this->assertSame('', $response->getBody()->getContents());
}
}
diff --git a/tests/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php b/tests/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
deleted file mode 100644
index 0989f288c5..0000000000
--- a/tests/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
+++ /dev/null
@@ -1,35 +0,0 @@
-increments('id');
- $table->string('app_id');
- $table->integer('peak_connections_count');
- $table->integer('websocket_messages_count');
- $table->integer('api_messages_count');
- $table->nullableTimestamps();
- });
- }
-
- /**
- * Reverse the migrations.
- *
- * @return void
- */
- public function down()
- {
- Schema::dropIfExists('websockets_statistics_entries');
- }
-}