From a972c8581c4644273de638111e8c622a30dc15ff Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Thu, 30 Apr 2020 15:17:25 +0200 Subject: [PATCH] wip --- app/Client/Client.php | 19 +++- app/Client/Factory.php | 9 +- .../Http/Controllers/DashboardController.php | 2 +- app/Commands/ShareCommand.php | 3 +- app/Http/Controllers/Concerns/LoadsViews.php | 7 +- .../Concerns/ParsesIncomingRequest.php | 6 +- app/Http/Controllers/Controller.php | 4 +- app/Server/Configuration.php | 11 ++ app/Server/Factory.php | 8 +- .../Controllers/Admin/ListSitesController.php | 2 +- .../Controllers/Admin/ListUsersController.php | 2 +- .../Admin/SaveSettingsController.php | 40 +++++++ .../Admin/ShowSettingsController.php | 40 +++++++ .../Controllers/ControlMessageController.php | 2 +- .../Controllers/TunnelMessageController.php | 2 +- composer.json | 1 + composer.lock | 50 ++++++++- config/expose.php | 6 +- resources/views/server/layouts/app.twig | 14 ++- resources/views/server/settings/index.twig | 61 +++++++++++ tests/Feature/Server/AdminTest.php | 67 +++++++++++- tests/Feature/Server/TunnelTest.php | 103 ++++++++++++++++++ tests/Feature/TestCase.php | 13 +++ tests/TestCase.php | 1 + 24 files changed, 444 insertions(+), 29 deletions(-) create mode 100644 app/Server/Http/Controllers/Admin/SaveSettingsController.php create mode 100644 app/Server/Http/Controllers/Admin/ShowSettingsController.php create mode 100644 resources/views/server/settings/index.twig create mode 100644 tests/Feature/Server/TunnelTest.php diff --git a/app/Client/Client.php b/app/Client/Client.php index d90ff4f..9ff44fa 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -6,6 +6,7 @@ use App\Client\Connections\ControlConnection; use App\Logger\CliRequestLogger; use Ratchet\Client\WebSocket; use React\EventLoop\LoopInterface; +use React\Promise\PromiseInterface; use function Ratchet\Client\connect; class Client @@ -37,8 +38,11 @@ class Client } } - protected function connectToServer(string $sharedUrl, $subdomain) + public function connectToServer(string $sharedUrl, $subdomain): PromiseInterface { + $deferred = new \React\Promise\Deferred(); + $promise = $deferred->promise(); + $token = config('expose.auth_token'); $wsProtocol = $this->configuration->port() === 443 ? "wss" : "ws"; @@ -46,7 +50,7 @@ class Client connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$token}", [], [ 'X-Expose-Control' => 'enabled', ], $this->loop) - ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain) { + ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $deferred) { $connection = ControlConnection::create($clientConnection); $connection->authenticate($sharedUrl, $subdomain); @@ -66,7 +70,7 @@ class Client exit(1); }); - $connection->on('authenticated', function ($data) { + $connection->on('authenticated', function ($data) use ($deferred) { $httpProtocol = $this->configuration->port() === 443 ? "https" : "http"; $host = $this->configuration->host(); @@ -77,12 +81,19 @@ class Client $this->logger->info("Connected to {$httpProtocol}://{$data->subdomain}.{$host}"); static::$subdomains[] = "$data->subdomain.{$this->configuration->host()}:{$this->configuration->port()}"; + + $deferred->resolve(); }); - }, function (\Exception $e) { + }, function (\Exception $e) use ($deferred) { $this->logger->error("Could not connect to the server."); $this->logger->error($e->getMessage()); + + $deferred->reject(); + exit(1); }); + + return $promise; } } diff --git a/app/Client/Factory.php b/app/Client/Factory.php index f2f411c..17a397d 100644 --- a/app/Client/Factory.php +++ b/app/Client/Factory.php @@ -86,12 +86,17 @@ class Factory }); } - public function createClient($sharedUrl, $subdomain = null, $auth = null) + public function createClient() { $this->bindConfiguration(); $this->bindProxyManager(); + return $this; + } + + public function share($sharedUrl, $subdomain = null) + { app(Client::class)->share($sharedUrl, $subdomain); return $this; @@ -131,7 +136,7 @@ class Factory echo("Started Dashboard on port {$dashboardPort}" . PHP_EOL); - echo('If the dashboard does not automatically open, visit: ' . $dashboardUrl . PHP_EOL); + echo('You can visit the dashboard at: ' . $dashboardUrl . PHP_EOL); }); $this->app = new App('127.0.0.1', $dashboardPort, '0.0.0.0', $this->loop); diff --git a/app/Client/Http/Controllers/DashboardController.php b/app/Client/Http/Controllers/DashboardController.php index da2064e..8986fb4 100644 --- a/app/Client/Http/Controllers/DashboardController.php +++ b/app/Client/Http/Controllers/DashboardController.php @@ -15,7 +15,7 @@ class DashboardController extends Controller public function handle(Request $request, ConnectionInterface $httpConnection) { - $httpConnection->send(respond_html($this->getView('client.dashboard', [ + $httpConnection->send(respond_html($this->getView($httpConnection, 'client.dashboard', [ 'subdomains' => Client::$subdomains, ]))); } diff --git a/app/Commands/ShareCommand.php b/app/Commands/ShareCommand.php index 010103c..f59afc7 100644 --- a/app/Commands/ShareCommand.php +++ b/app/Commands/ShareCommand.php @@ -33,7 +33,8 @@ class ShareCommand extends Command ->setHost(config('expose.host', 'localhost')) ->setPort(config('expose.port', 8080)) ->setAuth($this->option('auth')) - ->createClient($this->argument('host'), explode(',', $this->option('subdomain'))) + ->createClient() + ->share($this->argument('host'), explode(',', $this->option('subdomain'))) ->createHttpServer() ->run(); } diff --git a/app/Http/Controllers/Concerns/LoadsViews.php b/app/Http/Controllers/Concerns/LoadsViews.php index 5665708..1906ff4 100644 --- a/app/Http/Controllers/Concerns/LoadsViews.php +++ b/app/Http/Controllers/Concerns/LoadsViews.php @@ -2,13 +2,14 @@ namespace App\Http\Controllers\Concerns; +use Ratchet\ConnectionInterface; use Twig\Environment; use Twig\Loader\ArrayLoader; use function GuzzleHttp\Psr7\stream_for; trait LoadsViews { - protected function getView(string $view, array $data = []) + protected function getView(ConnectionInterface $connection, string $view, array $data = []) { $templatePath = implode(DIRECTORY_SEPARATOR, explode('.', $view)); @@ -19,6 +20,10 @@ trait LoadsViews ]) ); + $data = array_merge($data, [ + 'request' => $connection->laravelRequest ?? null, + ]); + return stream_for($twig->render('template', $data)); } } diff --git a/app/Http/Controllers/Concerns/ParsesIncomingRequest.php b/app/Http/Controllers/Concerns/ParsesIncomingRequest.php index 5342806..6159f72 100644 --- a/app/Http/Controllers/Concerns/ParsesIncomingRequest.php +++ b/app/Http/Controllers/Concerns/ParsesIncomingRequest.php @@ -26,10 +26,10 @@ trait ParsesIncomingRequest protected function checkContentLength(ConnectionInterface $connection) { if (strlen($connection->requestBuffer) === $connection->contentLength) { - $laravelRequest = $this->createLaravelRequest($connection); + $connection->laravelRequest = $this->createLaravelRequest($connection); - if ($this->shouldHandleRequest($laravelRequest, $connection)) { - $this->handle($laravelRequest, $connection); + if ($this->shouldHandleRequest($connection->laravelRequest, $connection)) { + $this->handle($connection->laravelRequest, $connection); } if (!$this->keepConnectionOpen) { diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index d45be23..7e157bb 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -12,7 +12,8 @@ use function GuzzleHttp\Psr7\parse_request; abstract class Controller implements HttpServerInterface { - use LoadsViews, ParsesIncomingRequest; + use LoadsViews; + use ParsesIncomingRequest; protected $keepConnectionOpen = false; @@ -29,6 +30,7 @@ abstract class Controller implements HttpServerInterface public function onClose(ConnectionInterface $connection) { + unset($connection->laravelRequest); unset($connection->requestBuffer); unset($connection->contentLength); unset($connection->request); diff --git a/app/Server/Configuration.php b/app/Server/Configuration.php index e033a6f..6009d30 100644 --- a/app/Server/Configuration.php +++ b/app/Server/Configuration.php @@ -26,4 +26,15 @@ class Configuration { return $this->port; } + + public function __isset($key) + { + return property_exists($this, $key) || ! is_null(config('expose.admin.'.$key)); + } + + public function __get($key) + { + dump(config('expose.admin')); + return $this->$key ?? config('expose.admin.'.$key); + } } diff --git a/app/Server/Factory.php b/app/Server/Factory.php index 81f7a95..b6c63e1 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -11,6 +11,8 @@ use App\Server\Http\Controllers\Admin\ListSitesController; use App\Server\Http\Controllers\Admin\ListUsersController; use App\Server\Http\Controllers\Admin\LoginController; use App\Server\Http\Controllers\Admin\RedirectToUsersController; +use App\Server\Http\Controllers\Admin\SaveSettingsController; +use App\Server\Http\Controllers\Admin\ShowSettingsController; use App\Server\Http\Controllers\Admin\StoreUsersController; use App\Server\Http\Controllers\Admin\VerifyLoginController; use App\Server\Http\Controllers\ControlMessageController; @@ -114,6 +116,8 @@ class Factory $this->router->get('/', RedirectToUsersController::class, $adminCondition); $this->router->get('/users', ListUsersController::class, $adminCondition); + $this->router->get('/settings', ShowSettingsController::class, $adminCondition); + $this->router->post('/settings', SaveSettingsController::class, $adminCondition); $this->router->post('/users', StoreUsersController::class, $adminCondition); $this->router->delete('/users/delete/{id}', DeleteUsersController::class, $adminCondition); $this->router->get('/sites', ListSitesController::class, $adminCondition); @@ -183,7 +187,7 @@ class Factory { app()->singleton(DatabaseInterface::class, function() { $factory = new \Clue\React\SQLite\Factory($this->loop); - return $factory->openLazy(base_path('database/expose.db')); + return $factory->openLazy(config('expose.admin.database', ':memory:')); }); return $this; @@ -210,7 +214,7 @@ class Factory public function validateAuthTokens(bool $validate) { - config()->set('expose.validate_auth_tokens', $validate); + config()->set('expose.admin.validate_auth_tokens', $validate); return $this; } diff --git a/app/Server/Http/Controllers/Admin/ListSitesController.php b/app/Server/Http/Controllers/Admin/ListSitesController.php index 4447266..6cabe7f 100644 --- a/app/Server/Http/Controllers/Admin/ListSitesController.php +++ b/app/Server/Http/Controllers/Admin/ListSitesController.php @@ -31,7 +31,7 @@ class ListSitesController extends AdminController public function handle(Request $request, ConnectionInterface $httpConnection) { try { - $sites = $this->getView('server.sites.index', [ + $sites = $this->getView($httpConnection, 'server.sites.index', [ 'scheme' => $this->configuration->port() === 443 ? 'https' : 'http', 'configuration' => $this->configuration, 'sites' => $this->connectionManager->getConnections() diff --git a/app/Server/Http/Controllers/Admin/ListUsersController.php b/app/Server/Http/Controllers/Admin/ListUsersController.php index c2a490b..b171053 100644 --- a/app/Server/Http/Controllers/Admin/ListUsersController.php +++ b/app/Server/Http/Controllers/Admin/ListUsersController.php @@ -29,7 +29,7 @@ class ListUsersController extends AdminController { $this->database->query('SELECT * FROM users ORDER by created_at DESC')->then(function (Result $result) use ($httpConnection) { $httpConnection->send( - respond_html($this->getView('server.users.index', ['users' => $result->rows])) + respond_html($this->getView($httpConnection, 'server.users.index', ['users' => $result->rows])) ); $httpConnection->close(); diff --git a/app/Server/Http/Controllers/Admin/SaveSettingsController.php b/app/Server/Http/Controllers/Admin/SaveSettingsController.php new file mode 100644 index 0000000..a46dc8f --- /dev/null +++ b/app/Server/Http/Controllers/Admin/SaveSettingsController.php @@ -0,0 +1,40 @@ +connectionManager = $connectionManager; + $this->configuration = $configuration; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + config()->set('expose.admin.validate_auth_tokens', $request->has('validate_auth_tokens')); + + $httpConnection->send(str(new Response(301, [ + 'Location' => '/settings' + ]))); + } +} diff --git a/app/Server/Http/Controllers/Admin/ShowSettingsController.php b/app/Server/Http/Controllers/Admin/ShowSettingsController.php new file mode 100644 index 0000000..6e9741f --- /dev/null +++ b/app/Server/Http/Controllers/Admin/ShowSettingsController.php @@ -0,0 +1,40 @@ +connectionManager = $connectionManager; + $this->configuration = $configuration; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $httpConnection->send( + respond_html($this->getView($httpConnection, 'server.settings.index', [ + 'configuration' => $this->configuration, + ])) + ); + } +} diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 27e41dc..141f02e 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -74,7 +74,7 @@ class ControlMessageController implements MessageComponentInterface protected function authenticate(ConnectionInterface $connection, $data) { - if (config('expose.validate_auth_tokens') === true) { + if (config('expose.admin.validate_auth_tokens') === true) { $this->verifyAuthToken($connection); } diff --git a/app/Server/Http/Controllers/TunnelMessageController.php b/app/Server/Http/Controllers/TunnelMessageController.php index 03fcd35..65c33e0 100644 --- a/app/Server/Http/Controllers/TunnelMessageController.php +++ b/app/Server/Http/Controllers/TunnelMessageController.php @@ -44,7 +44,7 @@ class TunnelMessageController extends Controller if (is_null($controlConnection)) { $httpConnection->send( - respond_html($this->getView('server.errors.404', ['subdomain' => $subdomain])) + respond_html($this->getView($httpConnection, 'server.errors.404', ['subdomain' => $subdomain]), 404) ); $httpConnection->close(); return; diff --git a/composer.json b/composer.json index b2873cd..e71b2fa 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "nyholm/psr7": "^1.2", "phpunit/phpunit": "^8.5", "ratchet/pawl": "^0.3.4", + "react/http": "^0.8.6", "react/socket": "^1.4", "riverline/multipart-parser": "^2.0", "symfony/expression-language": "^5.0", diff --git a/composer.lock b/composer.lock index 0c53ea3..7787032 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "17ed4fb1b80fc6efe2594c3bfbbf3c7f", + "content-hash": "801c0ab8f694ff370f2eefea409a7fcc", "packages": [], "packages-dev": [ { @@ -4470,6 +4470,54 @@ ], "time": "2020-01-01T18:39:52+00:00" }, + { + "name": "react/http", + "version": "v0.8.6", + "source": { + "type": "git", + "url": "https://github.com/reactphp/http.git", + "reference": "248202e57195d06a4375f6d2f5c5b9ff9da3ea9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/http/zipball/248202e57195d06a4375f6d2f5c5b9ff9da3ea9e", + "reference": "248202e57195d06a4375f6d2f5c5b9ff9da3ea9e", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/promise": "^2.3 || ^1.2.1", + "react/promise-stream": "^1.1", + "react/socket": "^1.0 || ^0.8.3", + "react/stream": "^1.0 || ^0.7.1", + "ringcentral/psr7": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.1", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven, streaming plaintext HTTP and secure HTTPS server for ReactPHP", + "keywords": [ + "event-driven", + "http", + "https", + "reactphp", + "server", + "streaming" + ], + "time": "2020-01-12T13:15:06+00:00" + }, { "name": "react/http-client", "version": "v0.5.10", diff --git a/config/expose.php b/config/expose.php index dd39380..fd4a515 100644 --- a/config/expose.php +++ b/config/expose.php @@ -2,11 +2,15 @@ return [ 'host' => 'expose.dev', - 'port' => 8080, + 'port' => 443, 'auth_token' => '', 'admin' => [ + 'database' => base_path('database/expose.db'), + + 'validate_auth_tokens' => false, + /* |-------------------------------------------------------------------------- | Subdomain diff --git a/resources/views/server/layouts/app.twig b/resources/views/server/layouts/app.twig index 3707775..9dfdef8 100644 --- a/resources/views/server/layouts/app.twig +++ b/resources/views/server/layouts/app.twig @@ -14,15 +14,21 @@ diff --git a/resources/views/server/settings/index.twig b/resources/views/server/settings/index.twig new file mode 100644 index 0000000..e75c942 --- /dev/null +++ b/resources/views/server/settings/index.twig @@ -0,0 +1,61 @@ +{% extends "app" %} +{% block title %}Settings{% endblock %} +{% block content %} +
+
+
+
+
+
+
+
+
+
+
+ Authentication +
+
+
+
+
+
+ +
+
+ +

Only allow connection from clients with valid authentication tokens

+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + +
+
+
+
+
+{% endblock %} diff --git a/tests/Feature/Server/AdminTest.php b/tests/Feature/Server/AdminTest.php index 2238aaf..346886e 100644 --- a/tests/Feature/Server/AdminTest.php +++ b/tests/Feature/Server/AdminTest.php @@ -2,11 +2,15 @@ namespace Tests\Feature\Server; +use App\Contracts\ConnectionManager; use App\Http\Server; use App\Server\Factory; use Clue\React\Buzz\Browser; use Clue\React\Buzz\Message\ResponseException; +use GuzzleHttp\Psr7\Response; +use Illuminate\Support\Str; use Psr\Http\Message\ResponseInterface; +use Ratchet\Server\IoConnection; use React\Socket\Connector; use Tests\Feature\TestCase; @@ -49,10 +53,6 @@ class AdminTest extends TestCase /** @test */ public function it_accepts_valid_credentials() { - $this->app['config']['expose.admin.users'] = [ - 'username' => 'secret', - ]; - /** @var ResponseInterface $response */ $response = $this->await($this->browser->get('http://127.0.0.1:8080', [ 'Host' => 'expose.localhost', @@ -61,8 +61,67 @@ class AdminTest extends TestCase $this->assertSame(200, $response->getStatusCode()); } + /** @test */ + public function it_allows_saving_settings() + { + $this->app['config']['expose.admin.validate_auth_tokens'] = false; + + /** @var ResponseInterface $response */ + $this->await($this->browser->post('http://127.0.0.1:8080/settings', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ], json_encode([ + 'validate_auth_tokens' => true, + ]))); + + $this->assertTrue(config('expose.admin.validate_auth_tokens')); + } + + /** @test */ + public function it_can_create_users() + { + $this->await($this->browser->post('http://127.0.0.1:8080/users', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ], json_encode([ + 'name' => 'Marcel', + ]))); + + $this->assertDatabaseHasResults('SELECT * FROM users WHERE name = "Marcel"'); + } + + /** @test */ + public function it_can_list_all_currently_connected_sites() + { + /** @var ConnectionManager $connectionManager */ + $connectionManager = app(ConnectionManager::class); + + $connection = \Mockery::mock(IoConnection::class); + $connectionManager->storeConnection('some-host.text', 'fixed-subdomain', $connection); + + /** @var Response $response */ + $response = $this->await($this->browser->get('http://127.0.0.1:8080/sites', [ + 'Host' => 'expose.localhost', + 'Authorization' => base64_encode("username:secret"), + 'Content-Type' => 'application/json' + ])); + + $body = $response->getBody()->getContents(); + + $this->assertTrue(Str::contains($body, 'some-host.text')); + $this->assertTrue(Str::contains($body, 'fixed-subdomain')); + } + protected function startServer() { + $this->app['config']['expose.admin.database'] = ':memory:'; + + $this->app['config']['expose.admin.users'] = [ + 'username' => 'secret', + ]; + $this->serverFactory = new Factory(); $this->serverFactory->setLoop($this->loop) diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php new file mode 100644 index 0000000..6b83871 --- /dev/null +++ b/tests/Feature/Server/TunnelTest.php @@ -0,0 +1,103 @@ +browser = new Browser($this->loop); + + $this->startServer(); + } + + /** @test */ + public function it_returns_404_for_non_existing_clients() + { + $this->expectException(ResponseException::class); + $this->expectExceptionMessage(404); + + $response = $this->await($this->browser->get('http://127.0.0.1:8080/', [ + 'Host' => 'tunnel.localhost' + ])); + } + + /** @test */ + public function it_sends_incoming_requests_to_the_connected_client() + { + $this->createTestHttpServer(); + + /** + * We create an expose client that connects to our server and shares + * the created test HTTP server + */ + $client = $this->createClient(); + $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel')); + + /** + * Once the client is connected, we perform a GET request on the + * created tunnel. + */ + $response = $this->await($this->browser->get('http://127.0.0.1:8080/', [ + 'Host' => 'tunnel.localhost' + ])); + + $this->assertSame('Hello World!', $response->getBody()->getContents()); + } + + + protected function startServer() + { + $this->app['config']['expose.admin.database'] = ':memory:'; + + $this->serverFactory = new Factory(); + + $this->serverFactory->setLoop($this->loop) + ->setHost('127.0.0.1') + ->setHostname('localhost') + ->createServer(); + } + + protected function createClient() + { + (new \App\Client\Factory()) + ->setLoop($this->loop) + ->setHost('127.0.0.1') + ->setPort(8080) + ->createClient(); + + return app(Client::class); + } + + protected function createTestHttpServer() + { + $server = new Server(function (ServerRequestInterface $request) { + return new Response(200, ['Content-Type' => 'text/plain'], "Hello World!"); + }); + + $socket = new \React\Socket\Server(8085, $this->loop); + $server->listen($socket); + } +} diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 5d8f0eb..10f32e1 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -2,6 +2,10 @@ namespace Tests\Feature; +use Clue\React\SQLite\DatabaseInterface; +use GuzzleHttp\Psr7\Response; +use Illuminate\Support\Str; +use Psr\Http\Message\ResponseInterface; use React\EventLoop\Factory; use React\EventLoop\LoopInterface; use React\EventLoop\StreamSelectLoop; @@ -33,4 +37,13 @@ abstract class TestCase extends \Tests\TestCase { return await($promise, $loop ?? $this->loop, $timeout ?? static::AWAIT_TIMEOUT); } + + protected function assertDatabaseHasResults($query) + { + $database = app(DatabaseInterface::class); + + $result = $this->await($database->query($query)); + + $this->assertGreaterThanOrEqual(1, count($result->rows)); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 50602b9..1ce2411 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Tests; +use Clue\React\SQLite\DatabaseInterface; use LaravelZero\Framework\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase