├── tests ├── bootstrap.php └── FoundationTestResponseTest.php ├── .gitignore ├── src ├── RefreshDatabaseState.php ├── Concerns │ ├── InteractsWithConsole.php │ ├── WithFaker.php │ ├── InteractsWithContainer.php │ ├── InteractsWithDatabase.php │ ├── InteractsWithRedis.php │ ├── RefreshDatabase.php │ ├── InteractsWithAuthentication.php │ ├── InteractsWithExceptionHandling.php │ ├── MocksApplicationServices.php │ └── MakesHttpRequests.php ├── TestCase.php ├── Constraints │ ├── SeeInOrder.php │ └── SoftDeletedInDatabase.php ├── PendingCommand.php └── TestResponse.php ├── composer.json ├── phpunit.xml ├── .travis.yml ├── LICENSE └── README.md /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithConsole.php: -------------------------------------------------------------------------------- 1 | app[Kernel::class]->call($command, $parameters); 19 | } 20 | 21 | /** 22 | * Disable mocking the console output. 23 | * 24 | * @return $this 25 | */ 26 | protected function withoutMockingConsoleOutput() 27 | { 28 | $this->mockConsoleOutput = false; 29 | $this->app->offsetUnset(OutputStyle::class); 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: bionic 3 | sudo: false 4 | 5 | matrix: 6 | include: 7 | - php: 7.1 8 | env: FRAMEWORK_VERSION=laravel/lumen-framework:5.3.* 9 | - php: 7.1 10 | env: FRAMEWORK_VERSION=laravel/lumen-framework:5.4.* 11 | - php: 7.1 12 | env: FRAMEWORK_VERSION=laravel/lumen-framework:5.5.* 13 | - php: 7.1 14 | env: FRAMEWORK_VERSION=laravel/lumen-framework:5.6.* 15 | - php: 7.1 16 | env: FRAMEWORK_VERSION=laravel/lumen-framework:5.7.* 17 | - php: 7.1 18 | env: FRAMEWORK_VERSION=laravel/lumen-framework:5.8.* 19 | - php: 7.2 20 | env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* 21 | - php: 7.3 22 | env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* 23 | - php: 7.4 24 | env: FRAMEWORK_VERSION=laravel/lumen-framework:6.* 25 | 26 | install: 27 | - composer require "${FRAMEWORK_VERSION}" --no-update -n 28 | - travis_retry composer install --no-suggest --prefer-dist -n -o 29 | 30 | script: vendor/bin/phpunit 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Albert Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/Concerns/WithFaker.php: -------------------------------------------------------------------------------- 1 | faker = $this->makeFaker(); 24 | } 25 | 26 | /** 27 | * Get the default Faker instance for a given locale. 28 | * 29 | * @param string $locale 30 | * @return \Faker\Generator 31 | */ 32 | protected function faker($locale = null) 33 | { 34 | return is_null($locale) ? $this->faker : $this->makeFaker($locale); 35 | } 36 | 37 | /** 38 | * Create a Faker instance for the given locale. 39 | * 40 | * @param string $locale 41 | * @return \Faker\Generator 42 | */ 43 | protected function makeFaker($locale = null) 44 | { 45 | return Factory::create($locale ?? Factory::DEFAULT_LOCALE); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithContainer.php: -------------------------------------------------------------------------------- 1 | instance($abstract, $instance); 20 | } 21 | /** 22 | * Register an instance of an object in the container. 23 | * 24 | * @param string $abstract 25 | * @param object $instance 26 | * @return object 27 | */ 28 | protected function instance($abstract, $instance) 29 | { 30 | $this->app->instance($abstract, $instance); 31 | return $instance; 32 | } 33 | 34 | /** 35 | * Mock an instance of an object in the container. 36 | * 37 | * @param string $abstract 38 | * @param \Closure|null $mock 39 | * @return object 40 | */ 41 | protected function mock($abstract, Closure $mock = null) 42 | { 43 | return $this->instance($abstract, Mockery::mock(...array_filter(func_get_args()))); 44 | } 45 | /** 46 | * Spy an instance of an object in the container. 47 | * 48 | * @param string $abstract 49 | * @param \Closure|null $mock 50 | * @return object 51 | */ 52 | protected function spy($abstract, Closure $mock = null) 53 | { 54 | return $this->instance($abstract, Mockery::spy(...array_filter(func_get_args()))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/TestCase.php: -------------------------------------------------------------------------------- 1 | appendTraits(); 30 | } 31 | 32 | /** 33 | * Creates the application. 34 | * 35 | * @return \Laravel\Lumen\Application 36 | */ 37 | public function createApplication() 38 | { 39 | return require $this->getAppPath(); 40 | } 41 | 42 | /** 43 | * Append the testing helper traits. 44 | * 45 | * @return void 46 | */ 47 | protected function appendTraits() 48 | { 49 | $uses = array_flip(class_uses_recursive(get_class($this))); 50 | 51 | if (isset($uses[RefreshDatabase::class])) { 52 | $this->refreshDatabase(); 53 | } 54 | } 55 | 56 | /** 57 | * Get the path of lumen application. 58 | * 59 | * @return string 60 | */ 61 | protected function getAppPath() 62 | { 63 | if (is_null(static::$appPath)) { 64 | return static::$appPath = base_path('bootstrap/app.php'); 65 | } 66 | 67 | return static::$appPath; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Constraints/SeeInOrder.php: -------------------------------------------------------------------------------- 1 | content = $content; 33 | } 34 | 35 | /** 36 | * Determine if the rule passes validation. 37 | * 38 | * @param array $values 39 | * @return bool 40 | */ 41 | public function matches($values): bool 42 | { 43 | $position = 0; 44 | 45 | foreach ($values as $value) { 46 | if (empty($value)) { 47 | continue; 48 | } 49 | 50 | $valuePosition = mb_strpos($this->content, $value, $position); 51 | 52 | if ($valuePosition === false || $valuePosition < $position) { 53 | $this->failedValue = $value; 54 | 55 | return false; 56 | } 57 | 58 | $position = $valuePosition + mb_strlen($value); 59 | } 60 | 61 | return true; 62 | } 63 | 64 | /** 65 | * Get the description of the failure. 66 | * 67 | * @param array $values 68 | * @return string 69 | */ 70 | public function failureDescription($values): string 71 | { 72 | return sprintf( 73 | 'Failed asserting that \'%s\' contains "%s" in specified order.', 74 | $this->content, 75 | $this->failedValue 76 | ); 77 | } 78 | 79 | /** 80 | * Get a string representation of the object. 81 | * 82 | * @return string 83 | */ 84 | public function toString(): string 85 | { 86 | return (new ReflectionClass($this))->name; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithDatabase.php: -------------------------------------------------------------------------------- 1 | seeInDatabase($table, $data, $onConnection); 20 | } 21 | 22 | /** 23 | * Assert that a given where condition does not exist in the database. 24 | * 25 | * @param string $table 26 | * @param array $data 27 | * @param string $connection 28 | * @return $this 29 | */ 30 | protected function assertDatabaseMissing($table, array $data, $connection = null) 31 | { 32 | return $this->notSeeInDatabase($table, $data, $connection); 33 | } 34 | 35 | /** 36 | * Assert the given record has been deleted. 37 | * 38 | * @param string $table 39 | * @param array $data 40 | * @param string $connection 41 | * @return $this 42 | */ 43 | protected function assertSoftDeleted($table, array $data, $connection = null) 44 | { 45 | $this->assertThat( 46 | $table, new SoftDeletedInDatabase($this->getConnection($connection), $data) 47 | ); 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Get the database connection. 54 | * 55 | * @param string|null $connection 56 | * @return \Illuminate\Database\Connection 57 | */ 58 | protected function getConnection($connection = null) 59 | { 60 | $database = $this->app->make('db'); 61 | 62 | $connection = $connection ?: $database->getDefaultConnection(); 63 | 64 | return $database->connection($connection); 65 | } 66 | 67 | /** 68 | * Seed a given database connection. 69 | * 70 | * @param string $class 71 | * @return $this 72 | */ 73 | public function seed($class = 'DatabaseSeeder') 74 | { 75 | $this->artisan('db:seed', ['--class' => $class]); 76 | 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Constraints/SoftDeletedInDatabase.php: -------------------------------------------------------------------------------- 1 | data = $data; 41 | 42 | $this->database = $database; 43 | } 44 | 45 | /** 46 | * Check if the data is found in the given table. 47 | * 48 | * @param string $table 49 | * @return bool 50 | */ 51 | public function matches($table): bool 52 | { 53 | return $this->database->table($table) 54 | ->where($this->data)->whereNotNull('deleted_at')->count() > 0; 55 | } 56 | 57 | /** 58 | * Get the description of the failure. 59 | * 60 | * @param string $table 61 | * @return string 62 | */ 63 | public function failureDescription($table): string 64 | { 65 | return sprintf( 66 | "any soft deleted row in the table [%s] matches the attributes %s.\n\n%s", 67 | $table, $this->toString(), $this->getAdditionalInfo($table) 68 | ); 69 | } 70 | 71 | /** 72 | * Get additional info about the records found in the database table. 73 | * 74 | * @param string $table 75 | * @return string 76 | */ 77 | protected function getAdditionalInfo($table): string 78 | { 79 | $results = $this->database->table($table)->get(); 80 | 81 | if ($results->isEmpty()) { 82 | return 'The table is empty'; 83 | } 84 | 85 | $description = 'Found: '.json_encode($results->take($this->show), JSON_PRETTY_PRINT); 86 | 87 | if ($results->count() > $this->show) { 88 | $description .= sprintf(' and %s others', $results->count() - $this->show); 89 | } 90 | 91 | return $description; 92 | } 93 | 94 | /** 95 | * Get a string representation of the object. 96 | * 97 | * @return string 98 | */ 99 | public function toString(): string 100 | { 101 | return json_encode($this->data); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithRedis.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Trying default host/port failed, please set environment variable REDIS_HOST & REDIS_PORT to enable '.__CLASS__); 35 | 36 | return; 37 | } 38 | 39 | foreach ($this->redisDriverProvider() as $driver) { 40 | $this->redis[$driver[0]] = new RedisManager($driver[0], [ 41 | 'cluster' => false, 42 | 'default' => [ 43 | 'host' => $host, 44 | 'port' => $port, 45 | 'database' => 5, 46 | 'timeout' => 0.5, 47 | ], 48 | ]); 49 | } 50 | 51 | try { 52 | $this->redis['predis']->connection()->flushdb(); 53 | } catch (\Exception $e) { 54 | if ($host === '127.0.0.1' && $port === 6379 && getenv('REDIS_HOST') === false) { 55 | $this->markTestSkipped('Trying default host/port failed, please set environment variable REDIS_HOST & REDIS_PORT to enable '.__CLASS__); 56 | static::$connectionFailedOnceWithDefaultsSkip = true; 57 | 58 | return; 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Teardown redis connection. 65 | * 66 | * @return void 67 | */ 68 | public function tearDownRedis() 69 | { 70 | $this->redis['predis']->connection()->flushdb(); 71 | 72 | foreach ($this->redisDriverProvider() as $driver) { 73 | $this->redis[$driver[0]]->connection()->disconnect(); 74 | } 75 | } 76 | 77 | /** 78 | * Get redis driver provider. 79 | * 80 | * @return array 81 | */ 82 | public function redisDriverProvider() 83 | { 84 | $providers = [ 85 | ['predis'], 86 | ]; 87 | 88 | if (extension_loaded('redis')) { 89 | $providers[] = ['phpredis']; 90 | } 91 | 92 | return $providers; 93 | } 94 | 95 | /** 96 | * Run test if redis is available. 97 | * 98 | * @param callable $callback 99 | * @return void 100 | */ 101 | public function ifRedisAvailable($callback) 102 | { 103 | $this->setUpRedis(); 104 | 105 | $callback(); 106 | 107 | $this->tearDownRedis(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Concerns/RefreshDatabase.php: -------------------------------------------------------------------------------- 1 | usingInMemoryDatabase() 17 | ? $this->refreshInMemoryDatabase() 18 | : $this->refreshTestDatabase(); 19 | } 20 | 21 | /** 22 | * Determine if an in-memory database is being used. 23 | * 24 | * @return bool 25 | */ 26 | protected function usingInMemoryDatabase() 27 | { 28 | return config('database.connections')[ 29 | config('database.default') 30 | ]['database'] == ':memory:'; 31 | } 32 | 33 | /** 34 | * Refresh the in-memory database. 35 | * 36 | * @return void 37 | */ 38 | protected function refreshInMemoryDatabase() 39 | { 40 | $this->artisan('migrate'); 41 | } 42 | 43 | /** 44 | * Refresh a conventional test database. 45 | * 46 | * @return void 47 | */ 48 | protected function refreshTestDatabase() 49 | { 50 | if (! RefreshDatabaseState::$migrated) { 51 | $this->artisan('migrate:fresh', $this->shouldDropViews() ? [ 52 | '--drop-views' => true, 53 | ] : []); 54 | 55 | RefreshDatabaseState::$migrated = true; 56 | } 57 | 58 | $this->beginDatabaseTransaction(); 59 | } 60 | 61 | /** 62 | * Begin a database transaction on the testing database. 63 | * 64 | * @return void 65 | */ 66 | public function beginDatabaseTransaction() 67 | { 68 | $database = $this->app->make('db'); 69 | 70 | foreach ($this->connectionsToTransact() as $name) { 71 | $connection = $database->connection($name); 72 | $dispatcher = $connection->getEventDispatcher(); 73 | 74 | $connection->unsetEventDispatcher(); 75 | $connection->beginTransaction(); 76 | $connection->setEventDispatcher($dispatcher); 77 | } 78 | 79 | $this->beforeApplicationDestroyed(function () use ($database) { 80 | foreach ($this->connectionsToTransact() as $name) { 81 | $connection = $database->connection($name); 82 | $dispatcher = $connection->getEventDispatcher(); 83 | 84 | $connection->unsetEventDispatcher(); 85 | $connection->rollback(); 86 | $connection->setEventDispatcher($dispatcher); 87 | $connection->disconnect(); 88 | } 89 | }); 90 | } 91 | 92 | /** 93 | * The database connections that should have transactions. 94 | * 95 | * @return array 96 | */ 97 | protected function connectionsToTransact() 98 | { 99 | return property_exists($this, 'connectionsToTransact') 100 | ? $this->connectionsToTransact : [null]; 101 | } 102 | 103 | /** 104 | * Determine if views should be dropped when refreshing the database. 105 | * 106 | * @return bool 107 | */ 108 | protected function shouldDropViews() 109 | { 110 | return property_exists($this, 'dropViews') 111 | ? $this->dropViews : false; 112 | } 113 | } -------------------------------------------------------------------------------- /src/Concerns/InteractsWithAuthentication.php: -------------------------------------------------------------------------------- 1 | assertTrue($this->isAuthenticated($guard), 'The user is not authenticated'); 16 | 17 | return $this; 18 | } 19 | 20 | /** 21 | * Assert that the user is not authenticated. 22 | * 23 | * @param string|null $guard 24 | * @return $this 25 | */ 26 | public function assertGuest($guard = null) 27 | { 28 | $this->assertFalse($this->isAuthenticated($guard), 'The user is authenticated'); 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Return true if the user is authenticated, false otherwise. 35 | * 36 | * @param string|null $guard 37 | * @return bool 38 | */ 39 | protected function isAuthenticated($guard = null) 40 | { 41 | return $this->app->make('auth')->guard($guard)->check(); 42 | } 43 | 44 | /** 45 | * Assert that the user is authenticated as the given user. 46 | * 47 | * @param $user 48 | * @param string|null $guard 49 | * @return $this 50 | */ 51 | public function assertAuthenticatedAs($user, $guard = null) 52 | { 53 | $expected = $this->app->make('auth')->guard($guard)->user(); 54 | 55 | $this->assertNotNull($expected, 'The current user is not authenticated.'); 56 | 57 | $this->assertInstanceOf( 58 | get_class($expected), $user, 59 | 'The currently authenticated user is not who was expected' 60 | ); 61 | 62 | $this->assertSame( 63 | $expected->getAuthIdentifier(), $user->getAuthIdentifier(), 64 | 'The currently authenticated user is not who was expected' 65 | ); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Assert that the given credentials are valid. 72 | * 73 | * @param array $credentials 74 | * @param string|null $guard 75 | * @return $this 76 | */ 77 | public function assertCredentials(array $credentials, $guard = null) 78 | { 79 | $this->assertTrue( 80 | $this->hasCredentials($credentials, $guard), 'The given credentials are invalid.' 81 | ); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Assert that the given credentials are invalid. 88 | * 89 | * @param array $credentials 90 | * @param string|null $guard 91 | * @return $this 92 | */ 93 | public function assertInvalidCredentials(array $credentials, $guard = null) 94 | { 95 | $this->assertFalse( 96 | $this->hasCredentials($credentials, $guard), 'The given credentials are valid.' 97 | ); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Set the currently logged in user for the application. 104 | * 105 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 106 | * @param string|null $driver 107 | * @return void 108 | */ 109 | public function be($user, $driver = null) 110 | { 111 | $this->app['auth']->guard($driver)->setUser($user); 112 | } 113 | 114 | /** 115 | * Return true if the credentials are valid, false otherwise. 116 | * 117 | * @param array $credentials 118 | * @param string|null $guard 119 | * @return bool 120 | */ 121 | protected function hasCredentials(array $credentials, $guard = null) 122 | { 123 | $provider = $this->app->make('auth')->guard($guard)->getProvider(); 124 | 125 | $user = $provider->retrieveByCredentials($credentials); 126 | 127 | return $user && $provider->validateCredentials($user, $credentials); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithExceptionHandling.php: -------------------------------------------------------------------------------- 1 | originalExceptionHandler) { 28 | $this->app->instance(ExceptionHandler::class, 29 | $this->originalExceptionHandler); 30 | } 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Only handle the given exceptions via the exception handler. 37 | * 38 | * @param array $exceptions 39 | * 40 | * @return $this 41 | */ 42 | protected function handleExceptions(array $exceptions) 43 | { 44 | return $this->withoutExceptionHandling($exceptions); 45 | } 46 | 47 | /** 48 | * Only handle validation exceptions via the exception handler. 49 | * 50 | * @return $this 51 | */ 52 | protected function handleValidationExceptions() 53 | { 54 | return $this->handleExceptions([ValidationException::class]); 55 | } 56 | 57 | /** 58 | * Disable exception handling for the test. 59 | * 60 | * @param array $except 61 | * 62 | * @return $this 63 | */ 64 | protected function withoutExceptionHandling(array $except = []) 65 | { 66 | if ($this->originalExceptionHandler == null) { 67 | $this->originalExceptionHandler = app(ExceptionHandler::class); 68 | } 69 | 70 | $this->app->instance(ExceptionHandler::class, 71 | new class($this->originalExceptionHandler, $except) implements 72 | ExceptionHandler 73 | { 74 | protected $except; 75 | protected $originalHandler; 76 | 77 | /** 78 | * Create a new class instance. 79 | * 80 | * @param \Illuminate\Contracts\Debug\ExceptionHandler 81 | * @param array $except 82 | * 83 | * @return void 84 | */ 85 | public function __construct($originalHandler, $except = []) 86 | { 87 | $this->except = $except; 88 | $this->originalHandler = $originalHandler; 89 | } 90 | 91 | /** 92 | * Report the given exception. 93 | * 94 | * @param \Exception $e 95 | * 96 | * @return void 97 | */ 98 | public function report(Exception $e) 99 | { 100 | // 101 | } 102 | 103 | /** 104 | * Render the given exception. 105 | * 106 | * @param \Illuminate\Http\Request $request 107 | * @param \Exception $e 108 | * 109 | * @return mixed 110 | * 111 | * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException|\Exception 112 | */ 113 | public function render($request, Exception $e) 114 | { 115 | if ($e instanceof NotFoundHttpException) { 116 | throw new NotFoundHttpException("{$request->method()} {$request->url()}", 117 | null, $e->getCode()); 118 | } 119 | 120 | foreach ($this->except as $class) { 121 | if ($e instanceof $class) { 122 | return $this->originalHandler->render($request, $e); 123 | } 124 | } 125 | 126 | throw $e; 127 | } 128 | 129 | /** 130 | * Render the exception for the console. 131 | * 132 | * @param \Symfony\Component\Console\Output\OutputInterface 133 | * @param \Exception $e 134 | * 135 | * @return void 136 | */ 137 | public function renderForConsole($output, Exception $e) 138 | { 139 | (new ConsoleApplication)->renderException($e, $output); 140 | } 141 | 142 | /** 143 | * Determine if the exception should be reported. 144 | * 145 | * @param \Exception $e 146 | * 147 | * @return bool 148 | */ 149 | public function shouldReport(Exception $e) 150 | { 151 | return false; 152 | } 153 | }); 154 | 155 | return $this; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/PendingCommand.php: -------------------------------------------------------------------------------- 1 | app = $app; 63 | $this->test = $test; 64 | $this->command = $command; 65 | $this->parameters = $parameters; 66 | } 67 | /** 68 | * Specify a question that should be asked when the command runs. 69 | * 70 | * @param string $question 71 | * @param string $answer 72 | * @return $this 73 | */ 74 | public function expectsQuestion($question, $answer) 75 | { 76 | $this->test->expectedQuestions[] = [$question, $answer]; 77 | return $this; 78 | } 79 | /** 80 | * Specify output that should be printed when the command runs. 81 | * 82 | * @param string $output 83 | * @return $this 84 | */ 85 | public function expectsOutput($output) 86 | { 87 | $this->test->expectedOutput[] = $output; 88 | return $this; 89 | } 90 | /** 91 | * Assert that the command has the given exit code. 92 | * 93 | * @param int $exitCode 94 | * @return $this 95 | */ 96 | public function assertExitCode($exitCode) 97 | { 98 | $this->expectedExitCode = $exitCode; 99 | return $this; 100 | } 101 | /** 102 | * Execute the command. 103 | * 104 | * @return int 105 | */ 106 | public function execute() 107 | { 108 | return $this->run(); 109 | } 110 | /** 111 | * Execute the command. 112 | * 113 | * @return int 114 | */ 115 | public function run() 116 | { 117 | $this->hasExecuted = true; 118 | $this->mockConsoleOutput(); 119 | try { 120 | $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters); 121 | } catch (NoMatchingExpectationException $e) { 122 | if ($e->getMethodName() === 'askQuestion') { 123 | $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.'); 124 | } 125 | throw $e; 126 | } 127 | if ($this->expectedExitCode !== null) { 128 | $this->test->assertEquals( 129 | $this->expectedExitCode, $exitCode, 130 | "Expected status code {$this->expectedExitCode} but received {$exitCode}." 131 | ); 132 | } 133 | return $exitCode; 134 | } 135 | /** 136 | * Mock the application's console output. 137 | * 138 | * @return void 139 | */ 140 | protected function mockConsoleOutput() 141 | { 142 | $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [ 143 | (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(), 144 | ]); 145 | foreach ($this->test->expectedQuestions as $i => $question) { 146 | $mock->shouldReceive('askQuestion') 147 | ->once() 148 | ->ordered() 149 | ->with(Mockery::on(function ($argument) use ($question) { 150 | return $argument->getQuestion() == $question[0]; 151 | })) 152 | ->andReturnUsing(function () use ($question, $i) { 153 | unset($this->test->expectedQuestions[$i]); 154 | return $question[1]; 155 | }); 156 | } 157 | $this->app->bind(OutputStyle::class, function () use ($mock) { 158 | return $mock; 159 | }); 160 | } 161 | /** 162 | * Create a mock for the buffered output. 163 | * 164 | * @return \Mockery\MockInterface 165 | */ 166 | private function createABufferedOutputMock() 167 | { 168 | $mock = Mockery::mock(BufferedOutput::class.'[doWrite]') 169 | ->shouldAllowMockingProtectedMethods() 170 | ->shouldIgnoreMissing(); 171 | foreach ($this->test->expectedOutput as $i => $output) { 172 | $mock->shouldReceive('doWrite') 173 | ->once() 174 | ->ordered() 175 | ->with($output, Mockery::any()) 176 | ->andReturnUsing(function () use ($i) { 177 | unset($this->test->expectedOutput[$i]); 178 | }); 179 | } 180 | return $mock; 181 | } 182 | /** 183 | * Handle the object's destruction. 184 | * 185 | * @return void 186 | */ 187 | public function __destruct() 188 | { 189 | if ($this->hasExecuted) { 190 | return; 191 | } 192 | $this->run(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /tests/FoundationTestResponseTest.php: -------------------------------------------------------------------------------- 1 | setContent(\Mockery::mock(View::class, [ 19 | 'render' => 'hello world', 20 | 'getData' => ['foo' => 'bar'], 21 | 'getName' => 'dir.my-view', 22 | ])); 23 | }); 24 | $response = TestResponse::fromBaseResponse($baseResponse); 25 | $response->assertViewIs('dir.my-view'); 26 | } 27 | 28 | public function testAssertViewHas() 29 | { 30 | $baseResponse = tap(new Response, function ($response) { 31 | $response->setContent(\Mockery::mock(View::class, [ 32 | 'render' => 'hello world', 33 | 'getData' => ['foo' => 'bar'], 34 | ])); 35 | }); 36 | $response = TestResponse::fromBaseResponse($baseResponse); 37 | $response->assertViewHas('foo'); 38 | } 39 | 40 | public function testAssertSeeText() 41 | { 42 | $baseResponse = tap(new Response, function ($response) { 43 | $response->setContent(\Mockery::mock(View::class, [ 44 | 'render' => 'foobar', 45 | ])); 46 | }); 47 | $response = TestResponse::fromBaseResponse($baseResponse); 48 | $response->assertSeeText('foobar'); 49 | } 50 | 51 | public function testAssertHeader() 52 | { 53 | $baseResponse = tap(new Response, function ($response) { 54 | $response->header('Location', '/foo'); 55 | }); 56 | $response = TestResponse::fromBaseResponse($baseResponse); 57 | try { 58 | $response->assertHeader('Location', '/bar'); 59 | } catch (\PHPUnit\Framework\ExpectationFailedException $e) { 60 | $this->assertEquals('/bar', $e->getComparisonFailure()->getExpected()); 61 | $this->assertEquals('/foo', $e->getComparisonFailure()->getActual()); 62 | } 63 | } 64 | 65 | public function testAssertJsonWithArray() 66 | { 67 | $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); 68 | $resource = new JsonSerializableSingleResourceStub; 69 | $response->assertJson($resource->jsonSerialize()); 70 | } 71 | 72 | public function testAssertJsonWithMixed() 73 | { 74 | $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); 75 | $resource = new JsonSerializableMixedResourcesStub; 76 | $response->assertJson($resource->jsonSerialize()); 77 | } 78 | 79 | public function testAssertJsonFragment() 80 | { 81 | $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); 82 | $response->assertJsonFragment(['foo' => 'foo 0']); 83 | $response->assertJsonFragment(['foo' => 'foo 0', 'bar' => 'bar 0', 'foobar' => 'foobar 0']); 84 | $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); 85 | $response->assertJsonFragment(['foo' => 'bar']); 86 | $response->assertJsonFragment(['foobar_foo' => 'foo']); 87 | $response->assertJsonFragment(['foobar' => ['foobar_foo' => 'foo', 'foobar_bar' => 'bar']]); 88 | $response->assertJsonFragment(['foo' => 'bar 0', 'bar' => ['foo' => 'bar 0', 'bar' => 'foo 0']]); 89 | } 90 | 91 | public function testAssertJsonStructure() 92 | { 93 | $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); 94 | // Without structure 95 | $response->assertJsonStructure(); 96 | // At root 97 | $response->assertJsonStructure(['foo']); 98 | // Nested 99 | $response->assertJsonStructure(['foobar' => ['foobar_foo', 'foobar_bar']]); 100 | // Wildcard (repeating structure) 101 | $response->assertJsonStructure(['bars' => ['*' => ['bar', 'foo']]]); 102 | // Nested after wildcard 103 | $response->assertJsonStructure(['baz' => ['*' => ['foo', 'bar' => ['foo', 'bar']]]]); 104 | // Wildcard (repeating structure) at root 105 | $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); 106 | $response->assertJsonStructure(['*' => ['foo', 'bar', 'foobar']]); 107 | } 108 | 109 | public function testMacroable() 110 | { 111 | TestResponse::macro('foo', function () { 112 | return 'bar'; 113 | }); 114 | $response = TestResponse::fromBaseResponse(new Response); 115 | $this->assertEquals( 116 | 'bar', $response->foo() 117 | ); 118 | } 119 | 120 | public function testCanBeCreatedFromBinaryFileResponses() 121 | { 122 | $files = new Filesystem; 123 | $tempDir = __DIR__.'/tmp'; 124 | $files->makeDirectory($tempDir, 0755, false, true); 125 | $files->put($tempDir.'/file.txt', 'Hello World'); 126 | $response = TestResponse::fromBaseResponse(new BinaryFileResponse($tempDir.'/file.txt')); 127 | $this->assertEquals($tempDir.'/file.txt', $response->getFile()->getPathname()); 128 | $files->deleteDirectory($tempDir); 129 | } 130 | } 131 | 132 | class JsonSerializableMixedResourcesStub implements JsonSerializable 133 | { 134 | public function jsonSerialize() 135 | { 136 | return [ 137 | 'foo' => 'bar', 138 | 'foobar' => [ 139 | 'foobar_foo' => 'foo', 140 | 'foobar_bar' => 'bar', 141 | ], 142 | 'bars' => [ 143 | ['bar' => 'foo 0', 'foo' => 'bar 0'], 144 | ['bar' => 'foo 1', 'foo' => 'bar 1'], 145 | ['bar' => 'foo 2', 'foo' => 'bar 2'], 146 | ], 147 | 'baz' => [ 148 | ['foo' => 'bar 0', 'bar' => ['foo' => 'bar 0', 'bar' => 'foo 0']], 149 | ['foo' => 'bar 1', 'bar' => ['foo' => 'bar 1', 'bar' => 'foo 1']], 150 | ], 151 | ]; 152 | } 153 | } 154 | 155 | class JsonSerializableSingleResourceStub implements JsonSerializable 156 | { 157 | public function jsonSerialize() 158 | { 159 | return [ 160 | ['foo' => 'foo 0', 'bar' => 'bar 0', 'foobar' => 'foobar 0'], 161 | ['foo' => 'foo 1', 'bar' => 'bar 1', 'foobar' => 'foobar 1'], 162 | ['foo' => 'foo 2', 'bar' => 'bar 2', 'foobar' => 'foobar 2'], 163 | ['foo' => 'foo 3', 'bar' => 'bar 3', 'foobar' => 'foobar 3'], 164 | ]; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lumen Testing 2 | ========== 3 | ![php-badge](https://img.shields.io/packagist/php-v/albertcht/lumen-testing.svg) 4 | [![packagist-badge](https://img.shields.io/packagist/v/albertcht/lumen-testing.svg)](https://packagist.org/packages/albertcht/lumen-testing) 5 | [![Total Downloads](https://poser.pugx.org/albertcht/lumen-testing/downloads)](https://packagist.org/packages/albertcht/lumen-testing) 6 | [![travis-badge](https://api.travis-ci.org/albertcht/lumen-testing.svg?branch=master)](https://travis-ci.org/albertcht/lumen-testing) 7 | 8 | ## Description 9 | 10 | A testing suite for Lumen like Laravel does. 11 | 12 | ### Requirements 13 | 14 | * \>= PHP 7.1 15 | * \>= Lumen 5.3 16 | 17 | ### Installation 18 | 19 | ``` 20 | composer require --dev albertcht/lumen-testing 21 | ``` 22 | 23 | * Make your test case extend `AlbertCht\Lumen\Testing\TestCase` 24 | 25 | You're all done! Enjoy your testing like in Laravel! 26 | 27 | ### Concerns 28 | 29 | There are some traits you can use in your test case (including original ones in Lumen): 30 | 31 | * `AlbertCht\Lumen\Testing\Concerns\RefreshDatabase` 32 | * `AlbertCht\Lumen\Testing\Concerns\WithFaker` 33 | * `AlbertCht\Lumen\Testing\Concerns\InteractsWithRedis` 34 | * `AlbertCht\Lumen\Testing\Concerns\InteractsWithConsole` 35 | * `AlbertCht\Lumen\Testing\Concerns\InteractsWithContainer` 36 | * `Laravel\Lumen\Testing\DatabaseMigrations` 37 | * `Laravel\Lumen\Testing\DatabaseTransactions` 38 | * `Laravel\Lumen\Testing\WithoutMiddleware` 39 | * `Laravel\Lumen\Testing\WithoutEvents` 40 | 41 | > `RefreshDatabase` = `DatabaseMigrations` + `DatabaseTransactions`, so if you use `RefreshDatabase`, you don't need to use the other two traits anymore. 42 | 43 | ### Response Assertions 44 | 45 | Laravel provides a variety of custom assertion methods for your [PHPUnit](https://phpunit.de/) tests. These assertions may be accessed on the response that is returned from the `json`, `get`, `post`, `put`, and `delete` test methods: 46 | 47 | Method | Description 48 | ------------- | ------------- 49 | `$response->assertSuccessful();` | Assert that the response has a successful status code. 50 | `$response->assertStatus($code);` | Assert that the response has a given code. 51 | `$response->assertRedirect($uri);` | Assert that the response is a redirect to a given URI. 52 | `$response->assertHeader($headerName, $value = null);` | Assert that the given header is not present on the response. 53 | `$response->assertHeaderMissing($headerName);` | Assert that the given header is present on the response. 54 | `$response->assertCookie($cookieName, $value = null);` | Assert that the response contains the given cookie. 55 | `$response->assertPlainCookie($cookieName, $value = null);` | Assert that the response contains the given cookie (unencrypted). 56 | `$response->assertCookieExpired($cookieName);` | Assert that the response contains the given cookie and it is expired. 57 | `$response->assertCookieNotExpired($cookieName);` | Assert that the response contains the given cookie and it is not expired. 58 | `$response->assertCookieMissing($cookieName);` | Assert that the response does not contains the given cookie. 59 | `$response->assertJson(array $data);` | Assert that the response contains the given JSON data. 60 | `$response->assertJsonCount(int $count, $key = null);` | Assert that the response JSON has the expected count of items at the given key. 61 | `$response->assertJsonFragment(array $data);` | Assert that the response contains the given JSON fragment. 62 | `$response->assertJsonMissing(array $data);` | Assert that the response does not contain the given JSON fragment. 63 | `$response->assertJsonMissingExact(array $data);` | Assert that the response does not contain the exact JSON fragment. 64 | `$response->assertExactJson(array $data);` | Assert that the response contains an exact match of the given JSON data. 65 | `$response->assertJsonStructure(array $structure);` | Assert that the response has a given JSON structure. 66 | `$response->assertJsonValidationErrors($keys);` | Assert that the response has the given JSON validation errors for the given keys. 67 | `$response->assertViewIs($value);` | Assert that the given view was returned by the route. 68 | `$response->assertViewHas($key, $value = null);` | Assert that the response view was given a piece of data. 69 | `$response->assertViewHasAll(array $data);` | Assert that the response view has a given list of data. 70 | `$response->assertViewMissing($key);` | Assert that the response view is missing a piece of bound data. 71 | `$response->assertSee($value);` | Assert that the given string is contained within the response. 72 | `$response->assertDontSee($value);` | Assert that the given string is not contained within the response. 73 | `$response->assertSeeText($value);` | Assert that the given string is contained within the response text. 74 | `$response->assertDontSeeText($value);` | Assert that the given string is not contained within the response text. 75 | `$response->assertSeeInOrder(array $values);` | Assert that the given strings are contained in order within the response. 76 | `$response->assertSeeTextInOrder(array $values);` | Assert that the given strings are contained in order within the response text. 77 | 78 | ### Authentication Assertions 79 | 80 | Laravel also provides a variety of authentication related assertions for your [PHPUnit](https://phpunit.de/) tests: 81 | 82 | Method | Description 83 | ------------- | ------------- 84 | `$this->assertAuthenticated($guard = null);` | Assert that the user is authenticated. 85 | `$this->assertGuest($guard = null);` | Assert that the user is not authenticated. 86 | `$this->assertAuthenticatedAs($user, $guard = null);` | Assert that the given user is authenticated. 87 | `$this->assertCredentials(array $credentials, $guard = null);` | Assert that the given credentials are valid. 88 | `$this->assertInvalidCredentials(array $credentials, $guard = null);` | Assert that the given credentials are invalid. 89 | 90 | ## Database Assertions 91 | 92 | Laravel provides several database assertions for your [PHPUnit](https://phpunit.de/) tests: 93 | 94 | Method | Description 95 | ------------- | ------------- 96 | `$this->assertDatabaseHas($table, array $data);` | Assert that a table in the database contains the given data. 97 | `$this->assertDatabaseMissing($table, array $data);` | Assert that a table in the database does not contain the given data. 98 | `$this->assertSoftDeleted($table, array $data);` | Assert that the given record has been soft deleted. 99 | 100 | ### Reference 101 | 102 | See full document at Laravel's doc: 103 | 104 | * https://laravel.com/docs/5.6/http-tests 105 | * https://laravel.com/docs/5.6/database-testing 106 | 107 | ## Support on Beerpay 108 | Hey dude! Help me out for a couple of :beers:! 109 | 110 | [![Beerpay](https://beerpay.io/albertcht/lumen-testing/badge.svg?style=beer-square)](https://beerpay.io/albertcht/lumen-testing) [![Beerpay](https://beerpay.io/albertcht/lumen-testing/make-wish.svg?style=flat-square)](https://beerpay.io/albertcht/lumen-testing?focus=wish) 111 | -------------------------------------------------------------------------------- /src/Concerns/MocksApplicationServices.php: -------------------------------------------------------------------------------- 1 | withoutEvents(); 53 | 54 | $this->beforeApplicationDestroyed(function () use ($events) { 55 | $this->assertEmpty( 56 | $fired = $this->getFiredEvents($events), 57 | 'These unexpected events were fired: ['.implode(', ', $fired).']' 58 | ); 59 | }); 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Filter the given events against the fired events. 66 | * 67 | * @param array $events 68 | * @return array 69 | */ 70 | protected function getFiredEvents(array $events) 71 | { 72 | return $this->getDispatched($events, $this->firedEvents); 73 | } 74 | 75 | /** 76 | * Specify a list of jobs that should be dispatched for the given operation. 77 | * 78 | * These jobs will be mocked, so that handlers will not actually be executed. 79 | * 80 | * @param array|string $jobs 81 | * @return $this 82 | */ 83 | protected function expectsJobs($jobs) 84 | { 85 | $jobs = is_array($jobs) ? $jobs : func_get_args(); 86 | $this->withoutJobs(); 87 | $this->beforeApplicationDestroyed(function () use ($jobs) { 88 | $dispatched = $this->getDispatchedJobs($jobs); 89 | if ($jobsNotDispatched = array_diff($jobs, $dispatched)) { 90 | throw new Exception( 91 | 'These expected jobs were not dispatched: ['.implode(', ', $jobsNotDispatched).']' 92 | ); 93 | } 94 | }); 95 | return $this; 96 | } 97 | 98 | /** 99 | * Specify a list of jobs that should not be dispatched for the given operation. 100 | * 101 | * These jobs will be mocked, so that handlers will not actually be executed. 102 | * 103 | * @param array|string $jobs 104 | * @return $this 105 | */ 106 | protected function doesntExpectJobs($jobs) 107 | { 108 | $jobs = is_array($jobs) ? $jobs : func_get_args(); 109 | 110 | $this->withoutJobs(); 111 | 112 | $this->beforeApplicationDestroyed(function () use ($jobs) { 113 | $this->assertEmpty( 114 | $dispatched = $this->getDispatchedJobs($jobs), 115 | 'These unexpected jobs were dispatched: ['.implode(', ', $dispatched).']' 116 | ); 117 | }); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Mock the job dispatcher so all jobs are silenced and collected. 124 | * 125 | * @return $this 126 | */ 127 | protected function withoutJobs() 128 | { 129 | $mock = Mockery::mock('Illuminate\Contracts\Bus\Dispatcher'); 130 | $mock->shouldReceive('dispatch')->andReturnUsing(function ($dispatched) { 131 | $this->dispatchedJobs[] = $dispatched; 132 | }); 133 | $this->app->instance( 134 | 'Illuminate\Contracts\Bus\Dispatcher', $mock 135 | ); 136 | return $this; 137 | } 138 | 139 | /** 140 | * Filter the given jobs against the dispatched jobs. 141 | * 142 | * @param array $jobs 143 | * @return array 144 | */ 145 | protected function getDispatchedJobs(array $jobs) 146 | { 147 | return $this->getDispatched($jobs, $this->dispatchedJobs); 148 | } 149 | 150 | /** 151 | * Filter the given classes against an array of dispatched classes. 152 | * 153 | * @param array $classes 154 | * @param array $dispatched 155 | * @return array 156 | */ 157 | protected function getDispatched(array $classes, array $dispatched) 158 | { 159 | return array_filter($classes, function ($class) use ($dispatched) { 160 | return $this->wasDispatched($class, $dispatched); 161 | }); 162 | } 163 | 164 | /** 165 | * Check if the given class exists in an array of dispatched classes. 166 | * 167 | * @param string $needle 168 | * @param array $haystack 169 | * @return bool 170 | */ 171 | protected function wasDispatched($needle, array $haystack) 172 | { 173 | foreach ($haystack as $dispatched) { 174 | if ((is_string($dispatched) && ($dispatched === $needle || is_subclass_of($dispatched, $needle))) || 175 | $dispatched instanceof $needle) { 176 | return true; 177 | } 178 | } 179 | 180 | return false; 181 | } 182 | 183 | /** 184 | * Mock the notification dispatcher so all notifications are silenced. 185 | * 186 | * @return $this 187 | */ 188 | protected function withoutNotifications() 189 | { 190 | $mock = Mockery::mock(NotificationDispatcher::class); 191 | 192 | $mock->shouldReceive('send')->andReturnUsing(function ($notifiable, $instance, $channels = []) { 193 | $this->dispatchedNotifications[] = compact( 194 | 'notifiable', 'instance', 'channels' 195 | ); 196 | }); 197 | 198 | $this->app->instance(NotificationDispatcher::class, $mock); 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * Specify a notification that is expected to be dispatched. 205 | * 206 | * @param mixed $notifiable 207 | * @param string $notification 208 | * @return $this 209 | */ 210 | protected function expectsNotification($notifiable, $notification) 211 | { 212 | $this->withoutNotifications(); 213 | 214 | $this->beforeApplicationDestroyed(function () use ($notifiable, $notification) { 215 | foreach ($this->dispatchedNotifications as $dispatched) { 216 | $notified = $dispatched['notifiable']; 217 | 218 | if (($notified === $notifiable || 219 | $notified->getKey() == $notifiable->getKey()) && 220 | get_class($dispatched['instance']) === $notification 221 | ) { 222 | return $this; 223 | } 224 | } 225 | 226 | $this->fail('The following expected notification were not dispatched: ['.$notification.']'); 227 | }); 228 | 229 | return $this; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Concerns/MakesHttpRequests.php: -------------------------------------------------------------------------------- 1 | defaultHeaders = array_merge($this->defaultHeaders, $headers); 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Add a header to be sent with the request. 49 | * 50 | * @param string $name 51 | * @param string $value 52 | * @return $this 53 | */ 54 | public function withHeader(string $name, string $value) 55 | { 56 | $this->defaultHeaders[$name] = $value; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Flush all the configured headers. 63 | * 64 | * @return $this 65 | */ 66 | public function flushHeaders() 67 | { 68 | $this->defaultHeaders = []; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Define a set of server variables to be sent with the requests. 75 | * 76 | * @param array $server 77 | * @return $this 78 | */ 79 | public function withServerVariables(array $server) 80 | { 81 | $this->serverVariables = $server; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Disable middleware for the test. 88 | * 89 | * @param string|array $middleware 90 | * @return $this 91 | */ 92 | public function withoutMiddleware($middleware = null) 93 | { 94 | if (is_null($middleware)) { 95 | $this->app->instance('middleware.disable', true); 96 | 97 | return $this; 98 | } 99 | 100 | foreach ((array) $middleware as $abstract) { 101 | $this->app->instance($abstract, new class { 102 | public function handle($request, $next) 103 | { 104 | return $next($request); 105 | } 106 | }); 107 | } 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Enable the given middleware for the test. 114 | * 115 | * @param string|array $middleware 116 | * @return $this 117 | */ 118 | public function withMiddleware($middleware = null) 119 | { 120 | if (is_null($middleware)) { 121 | unset($this->app['middleware.disable']); 122 | 123 | return $this; 124 | } 125 | 126 | foreach ((array) $middleware as $abstract) { 127 | unset($this->app[$abstract]); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Automatically follow any redirects returned from the response. 135 | * 136 | * @return $this 137 | */ 138 | public function followingRedirects() 139 | { 140 | $this->followRedirects = true; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * Set the referer header to simulate a previous request. 147 | * 148 | * @param string $url 149 | * @return $this 150 | */ 151 | public function from(string $url) 152 | { 153 | return $this->withHeader('referer', $url); 154 | } 155 | 156 | /** 157 | * Visit the given URI with a GET request. 158 | * 159 | * @param string $uri 160 | * @param array $headers 161 | * @return TestResponse 162 | */ 163 | public function get($uri, array $headers = []) 164 | { 165 | $server = $this->transformHeadersToServerVars($headers); 166 | 167 | return $this->call('GET', $uri, [], [], [], $server); 168 | } 169 | 170 | /** 171 | * Visit the given URI with a GET request, expecting a JSON response. 172 | * 173 | * @param string $uri 174 | * @param array $headers 175 | * @return TestResponse 176 | */ 177 | public function getJson($uri, array $headers = []) 178 | { 179 | return $this->json('GET', $uri, [], $headers); 180 | } 181 | 182 | /** 183 | * Visit the given URI with a POST request. 184 | * 185 | * @param string $uri 186 | * @param array $data 187 | * @param array $headers 188 | * @return TestResponse 189 | */ 190 | public function post($uri, array $data = [], array $headers = []) 191 | { 192 | $server = $this->transformHeadersToServerVars($headers); 193 | 194 | return $this->call('POST', $uri, $data, [], [], $server); 195 | } 196 | 197 | /** 198 | * Visit the given URI with a POST request, expecting a JSON response. 199 | * 200 | * @param string $uri 201 | * @param array $data 202 | * @param array $headers 203 | * @return TestResponse 204 | */ 205 | public function postJson($uri, array $data = [], array $headers = []) 206 | { 207 | return $this->json('POST', $uri, $data, $headers); 208 | } 209 | 210 | /** 211 | * Visit the given URI with a PUT request. 212 | * 213 | * @param string $uri 214 | * @param array $data 215 | * @param array $headers 216 | * @return TestResponse 217 | */ 218 | public function put($uri, array $data = [], array $headers = []) 219 | { 220 | $server = $this->transformHeadersToServerVars($headers); 221 | 222 | return $this->call('PUT', $uri, $data, [], [], $server); 223 | } 224 | 225 | /** 226 | * Visit the given URI with a PUT request, expecting a JSON response. 227 | * 228 | * @param string $uri 229 | * @param array $data 230 | * @param array $headers 231 | * @return TestResponse 232 | */ 233 | public function putJson($uri, array $data = [], array $headers = []) 234 | { 235 | return $this->json('PUT', $uri, $data, $headers); 236 | } 237 | 238 | /** 239 | * Visit the given URI with a PATCH request. 240 | * 241 | * @param string $uri 242 | * @param array $data 243 | * @param array $headers 244 | * @return TestResponse 245 | */ 246 | public function patch($uri, array $data = [], array $headers = []) 247 | { 248 | $server = $this->transformHeadersToServerVars($headers); 249 | 250 | return $this->call('PATCH', $uri, $data, [], [], $server); 251 | } 252 | 253 | /** 254 | * Visit the given URI with a PATCH request, expecting a JSON response. 255 | * 256 | * @param string $uri 257 | * @param array $data 258 | * @param array $headers 259 | * @return TestResponse 260 | */ 261 | public function patchJson($uri, array $data = [], array $headers = []) 262 | { 263 | return $this->json('PATCH', $uri, $data, $headers); 264 | } 265 | 266 | /** 267 | * Visit the given URI with a DELETE request. 268 | * 269 | * @param string $uri 270 | * @param array $data 271 | * @param array $headers 272 | * @return TestResponse 273 | */ 274 | public function delete($uri, array $data = [], array $headers = []) 275 | { 276 | $server = $this->transformHeadersToServerVars($headers); 277 | 278 | return $this->call('DELETE', $uri, $data, [], [], $server); 279 | } 280 | 281 | /** 282 | * Visit the given URI with a DELETE request, expecting a JSON response. 283 | * 284 | * @param string $uri 285 | * @param array $data 286 | * @param array $headers 287 | * @return TestResponse 288 | */ 289 | public function deleteJson($uri, array $data = [], array $headers = []) 290 | { 291 | return $this->json('DELETE', $uri, $data, $headers); 292 | } 293 | 294 | /** 295 | * Call the given URI with a JSON request. 296 | * 297 | * @param string $method 298 | * @param string $uri 299 | * @param array $data 300 | * @param array $headers 301 | * @return TestResponse 302 | */ 303 | public function json($method, $uri, array $data = [], array $headers = []) 304 | { 305 | $files = $this->extractFilesFromDataArray($data); 306 | 307 | $content = json_encode($data); 308 | 309 | $headers = array_merge([ 310 | 'CONTENT_LENGTH' => mb_strlen($content, '8bit'), 311 | 'CONTENT_TYPE' => 'application/json', 312 | 'Accept' => 'application/json', 313 | ], $headers); 314 | 315 | return $this->call( 316 | $method, $uri, [], [], $files, $this->transformHeadersToServerVars($headers), $content 317 | ); 318 | } 319 | 320 | /** 321 | * Call the given URI and return the Response. 322 | * 323 | * @param string $method 324 | * @param string $uri 325 | * @param array $parameters 326 | * @param array $cookies 327 | * @param array $files 328 | * @param array $server 329 | * @param string $content 330 | * @return TestResponse 331 | */ 332 | public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null) 333 | { 334 | $this->currentUri = $this->prepareUrlForRequest($uri); 335 | 336 | $symfonyRequest = SymfonyRequest::create( 337 | $this->currentUri, $method, $parameters, 338 | $cookies, $files, $server, $content 339 | ); 340 | 341 | $this->app['request'] = Request::createFromBase($symfonyRequest); 342 | 343 | $response = $this->app->prepareResponse( 344 | $this->app->handle($this->app['request']) 345 | ); 346 | $this->response = $this->createTestResponse($response); 347 | 348 | return $this->response; 349 | } 350 | 351 | /** 352 | * Call the given HTTPS URI and return the Response. 353 | * 354 | * @param string $method 355 | * @param string $uri 356 | * @param array $parameters 357 | * @param array $files 358 | * @param array $server 359 | * @param string $content 360 | * @return TestResponse 361 | */ 362 | public function callSecure($method, $uri, $parameters = [], $files = [], $server = [], $content = null) 363 | { 364 | $uri = 'https://localhost/'.ltrim($uri, '/'); 365 | return $this->call($method, $uri, $parameters, $files, $server, $content); 366 | } 367 | 368 | /** 369 | * Turn the given URI into a fully qualified URL. 370 | * 371 | * @param string $uri 372 | * @return string 373 | */ 374 | protected function prepareUrlForRequest($uri) 375 | { 376 | if (Str::startsWith($uri, '/')) { 377 | $uri = substr($uri, 1); 378 | } 379 | 380 | if (! Str::startsWith($uri, 'http')) { 381 | $uri = ($this->baseUrl ?? config('app.url')) . '/' . $uri; 382 | } 383 | 384 | return trim($uri, '/'); 385 | } 386 | 387 | /** 388 | * Transform headers array to array of $_SERVER vars with HTTP_* format. 389 | * 390 | * @param array $headers 391 | * @return array 392 | */ 393 | protected function transformHeadersToServerVars(array $headers) 394 | { 395 | return collect(array_merge($this->defaultHeaders, $headers))->mapWithKeys(function ($value, $name) { 396 | $name = strtr(strtoupper($name), '-', '_'); 397 | 398 | return [$this->formatServerHeaderKey($name) => $value]; 399 | })->all(); 400 | } 401 | 402 | /** 403 | * Format the header name for the server array. 404 | * 405 | * @param string $name 406 | * @return string 407 | */ 408 | protected function formatServerHeaderKey($name) 409 | { 410 | if (! Str::startsWith($name, 'HTTP_') && $name !== 'CONTENT_TYPE' && $name !== 'REMOTE_ADDR') { 411 | return 'HTTP_'.$name; 412 | } 413 | 414 | return $name; 415 | } 416 | 417 | /** 418 | * Extract the file uploads from the given data array. 419 | * 420 | * @param array $data 421 | * @return array 422 | */ 423 | protected function extractFilesFromDataArray(&$data) 424 | { 425 | $files = []; 426 | 427 | foreach ($data as $key => $value) { 428 | if ($value instanceof SymfonyUploadedFile) { 429 | $files[$key] = $value; 430 | 431 | unset($data[$key]); 432 | } 433 | 434 | if (is_array($value)) { 435 | $files[$key] = $this->extractFilesFromDataArray($value); 436 | 437 | $data[$key] = $value; 438 | } 439 | } 440 | 441 | return $files; 442 | } 443 | 444 | /** 445 | * Follow a redirect chain until a non-redirect is received. 446 | * 447 | * @param \Illuminate\Http\Response $response 448 | * @return \Illuminate\Http\Response 449 | */ 450 | protected function followRedirects($response) 451 | { 452 | while ($response->isRedirect()) { 453 | $response = $this->get($response->headers->get('Location')); 454 | } 455 | 456 | $this->followRedirects = false; 457 | 458 | return $response; 459 | } 460 | 461 | /** 462 | * Create the test response instance from the given response. 463 | * 464 | * @param \Illuminate\Http\Response $response 465 | * @return TestResponse 466 | */ 467 | protected function createTestResponse($response) 468 | { 469 | return TestResponse::fromBaseResponse($response); 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/TestResponse.php: -------------------------------------------------------------------------------- 1 | baseResponse = $response; 39 | } 40 | 41 | /** 42 | * Create a new TestResponse from another response. 43 | * 44 | * @param \Illuminate\Http\Response $response 45 | * @return static 46 | */ 47 | public static function fromBaseResponse($response) 48 | { 49 | return new static($response); 50 | } 51 | 52 | /** 53 | * Assert that the response has a successful status code. 54 | * 55 | * @return $this 56 | */ 57 | public function assertSuccessful() 58 | { 59 | PHPUnit::assertTrue( 60 | $this->isSuccessful(), 61 | 'Response status code ['.$this->getStatusCode().'] is not a successful status code.' 62 | ); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Assert that the response has a 200 status code. 69 | * 70 | * @return $this 71 | */ 72 | public function assertOk() 73 | { 74 | PHPUnit::assertTrue( 75 | $this->isOk(), 76 | 'Response status code ['.$this->getStatusCode().'] does not match expected 200 status code.' 77 | ); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Assert that the response has a not found status code. 84 | * 85 | * @return $this 86 | */ 87 | public function assertNotFound() 88 | { 89 | PHPUnit::assertTrue( 90 | $this->isNotFound(), 91 | 'Response status code ['.$this->getStatusCode().'] is not a not found status code.' 92 | ); 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Assert that the response has a forbidden status code. 99 | * 100 | * @return $this 101 | */ 102 | public function assertForbidden() 103 | { 104 | PHPUnit::assertTrue( 105 | $this->isForbidden(), 106 | 'Response status code ['.$this->getStatusCode().'] is not a forbidden status code.' 107 | ); 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Assert that the response has the given status code. 114 | * 115 | * @param int $status 116 | * @return $this 117 | */ 118 | public function assertStatus($status) 119 | { 120 | $actual = $this->getStatusCode(); 121 | 122 | PHPUnit::assertTrue( 123 | $actual === $status, 124 | "Expected status code {$status} but received {$actual}." 125 | ); 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * Assert whether the response is redirecting to a given URI. 132 | * 133 | * @param string $uri 134 | * @return $this 135 | */ 136 | public function assertRedirect($uri = null) 137 | { 138 | PHPUnit::assertTrue( 139 | $this->isRedirect(), 'Response status code ['.$this->getStatusCode().'] is not a redirect status code.' 140 | ); 141 | 142 | if (! is_null($uri)) { 143 | $this->assertLocation($uri); 144 | } 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Asserts that the response contains the given header and equals the optional value. 151 | * 152 | * @param string $headerName 153 | * @param mixed $value 154 | * @return $this 155 | */ 156 | public function assertHeader($headerName, $value = null) 157 | { 158 | PHPUnit::assertTrue( 159 | $this->headers->has($headerName), "Header [{$headerName}] not present on response." 160 | ); 161 | 162 | $actual = $this->headers->get($headerName); 163 | 164 | if (! is_null($value)) { 165 | PHPUnit::assertEquals( 166 | $value, $this->headers->get($headerName), 167 | "Header [{$headerName}] was found, but value [{$actual}] does not match [{$value}]." 168 | ); 169 | } 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * Asserts that the response does not contains the given header. 176 | * 177 | * @param string $headerName 178 | * @return $this 179 | */ 180 | public function assertHeaderMissing($headerName) 181 | { 182 | PHPUnit::assertFalse( 183 | $this->headers->has($headerName), "Unexpected header [{$headerName}] is present on response." 184 | ); 185 | 186 | return $this; 187 | } 188 | 189 | /** 190 | * Assert that the current location header matches the given URI. 191 | * 192 | * @param string $uri 193 | * @return $this 194 | */ 195 | public function assertLocation($uri) 196 | { 197 | PHPUnit::assertEquals( 198 | app('url')->to($uri), app('url')->to($this->headers->get('Location')) 199 | ); 200 | 201 | return $this; 202 | } 203 | 204 | /** 205 | * Asserts that the response contains the given cookie and equals the optional value. 206 | * 207 | * @param string $cookieName 208 | * @param mixed $value 209 | * @return $this 210 | */ 211 | public function assertPlainCookie($cookieName, $value = null) 212 | { 213 | $this->assertCookie($cookieName, $value, false); 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Asserts that the response contains the given cookie and equals the optional value. 220 | * 221 | * @param string $cookieName 222 | * @param mixed $value 223 | * @param bool $encrypted 224 | * @return $this 225 | */ 226 | public function assertCookie($cookieName, $value = null, $encrypted = true) 227 | { 228 | PHPUnit::assertNotNull( 229 | $cookie = $this->getCookie($cookieName), 230 | "Cookie [{$cookieName}] not present on response." 231 | ); 232 | 233 | if (! $cookie || is_null($value)) { 234 | return $this; 235 | } 236 | 237 | $cookieValue = $cookie->getValue(); 238 | 239 | $actual = $encrypted 240 | ? app('encrypter')->decrypt($cookieValue) : $cookieValue; 241 | 242 | PHPUnit::assertEquals( 243 | $value, $actual, 244 | "Cookie [{$cookieName}] was found, but value [{$actual}] does not match [{$value}]." 245 | ); 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * Asserts that the response contains the given cookie and is expired. 252 | * 253 | * @param string $cookieName 254 | * @return $this 255 | */ 256 | public function assertCookieExpired($cookieName) 257 | { 258 | PHPUnit::assertNotNull( 259 | $cookie = $this->getCookie($cookieName), 260 | "Cookie [{$cookieName}] not present on response." 261 | ); 262 | 263 | $expiresAt = Carbon::createFromTimestamp($cookie->getExpiresTime()); 264 | 265 | PHPUnit::assertTrue( 266 | $expiresAt->lessThan(Carbon::now()), 267 | "Cookie [{$cookieName}] is not expired, it expires at [{$expiresAt}]." 268 | ); 269 | 270 | return $this; 271 | } 272 | 273 | /** 274 | * Asserts that the response contains the given cookie and is not expired. 275 | * 276 | * @param string $cookieName 277 | * @return $this 278 | */ 279 | public function assertCookieNotExpired($cookieName) 280 | { 281 | PHPUnit::assertNotNull( 282 | $cookie = $this->getCookie($cookieName), 283 | "Cookie [{$cookieName}] not present on response." 284 | ); 285 | 286 | $expiresAt = Carbon::createFromTimestamp($cookie->getExpiresTime()); 287 | 288 | PHPUnit::assertTrue( 289 | $expiresAt->greaterThan(Carbon::now()), 290 | "Cookie [{$cookieName}] is expired, it expired at [{$expiresAt}]." 291 | ); 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Asserts that the response does not contains the given cookie. 298 | * 299 | * @param string $cookieName 300 | * @return $this 301 | */ 302 | public function assertCookieMissing($cookieName) 303 | { 304 | PHPUnit::assertNull( 305 | $this->getCookie($cookieName), 306 | "Cookie [{$cookieName}] is present on response." 307 | ); 308 | 309 | return $this; 310 | } 311 | 312 | /** 313 | * Get the given cookie from the response. 314 | * 315 | * @param string $cookieName 316 | * @return \Symfony\Component\HttpFoundation\Cookie|null 317 | */ 318 | protected function getCookie($cookieName) 319 | { 320 | foreach ($this->headers->getCookies() as $cookie) { 321 | if ($cookie->getName() === $cookieName) { 322 | return $cookie; 323 | } 324 | } 325 | } 326 | 327 | /** 328 | * Assert that the given string is contained within the response. 329 | * 330 | * @param string $value 331 | * @return $this 332 | */ 333 | public function assertSee($value) 334 | { 335 | PHPUnit::assertContains((string) $value, $this->getContent()); 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * Assert that the given strings are contained in order within the response. 342 | * 343 | * @param array $values 344 | * @return $this 345 | */ 346 | public function assertSeeInOrder(array $values) 347 | { 348 | PHPUnit::assertThat($values, new SeeInOrder($this->getContent())); 349 | 350 | return $this; 351 | } 352 | 353 | /** 354 | * Assert that the given string is contained within the response text. 355 | * 356 | * @param string $value 357 | * @return $this 358 | */ 359 | public function assertSeeText($value) 360 | { 361 | PHPUnit::assertContains((string) $value, strip_tags($this->getContent())); 362 | 363 | return $this; 364 | } 365 | 366 | /** 367 | * Assert that the given strings are contained in order within the response text. 368 | * 369 | * @param array $values 370 | * @return $this 371 | */ 372 | public function assertSeeTextInOrder(array $values) 373 | { 374 | PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->getContent()))); 375 | 376 | return $this; 377 | } 378 | 379 | /** 380 | * Assert that the given string is not contained within the response. 381 | * 382 | * @param string $value 383 | * @return $this 384 | */ 385 | public function assertDontSee($value) 386 | { 387 | PHPUnit::assertNotContains((string) $value, $this->getContent()); 388 | 389 | return $this; 390 | } 391 | 392 | /** 393 | * Assert that the given string is not contained within the response text. 394 | * 395 | * @param string $value 396 | * @return $this 397 | */ 398 | public function assertDontSeeText($value) 399 | { 400 | PHPUnit::assertNotContains((string) $value, strip_tags($this->getContent())); 401 | 402 | return $this; 403 | } 404 | 405 | /** 406 | * Assert that the response is a superset of the given JSON. 407 | * 408 | * @param array $data 409 | * @param bool $strict 410 | * @return $this 411 | */ 412 | public function assertJson(array $data, $strict = false) 413 | { 414 | PHPUnit::assertArraySubset( 415 | $data, $this->decodeResponseJson(), $strict, $this->assertJsonMessage($data) 416 | ); 417 | 418 | return $this; 419 | } 420 | 421 | /** 422 | * Get the assertion message for assertJson. 423 | * 424 | * @param array $data 425 | * @return string 426 | */ 427 | protected function assertJsonMessage(array $data) 428 | { 429 | $expected = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 430 | 431 | $actual = json_encode($this->decodeResponseJson(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 432 | 433 | return 'Unable to find JSON: '.PHP_EOL.PHP_EOL. 434 | "[{$expected}]".PHP_EOL.PHP_EOL. 435 | 'within response JSON:'.PHP_EOL.PHP_EOL. 436 | "[{$actual}].".PHP_EOL.PHP_EOL; 437 | } 438 | 439 | /** 440 | * Assert that the response has the exact given JSON. 441 | * 442 | * @param array $data 443 | * @return $this 444 | */ 445 | public function assertExactJson(array $data) 446 | { 447 | $actual = json_encode(Arr::sortRecursive( 448 | (array) $this->decodeResponseJson() 449 | )); 450 | 451 | PHPUnit::assertEquals(json_encode(Arr::sortRecursive($data)), $actual); 452 | 453 | return $this; 454 | } 455 | 456 | /** 457 | * Assert that the response contains the given JSON fragment. 458 | * 459 | * @param array $data 460 | * @return $this 461 | */ 462 | public function assertJsonFragment(array $data) 463 | { 464 | $actual = json_encode(Arr::sortRecursive( 465 | (array) $this->decodeResponseJson() 466 | )); 467 | 468 | foreach (Arr::sortRecursive($data) as $key => $value) { 469 | $expected = $this->jsonSearchStrings($key, $value); 470 | 471 | PHPUnit::assertTrue( 472 | Str::contains($actual, $expected), 473 | 'Unable to find JSON fragment: '.PHP_EOL.PHP_EOL. 474 | '['.json_encode([$key => $value]).']'.PHP_EOL.PHP_EOL. 475 | 'within'.PHP_EOL.PHP_EOL. 476 | "[{$actual}]." 477 | ); 478 | } 479 | 480 | return $this; 481 | } 482 | 483 | /** 484 | * Assert that the response does not contain the given JSON fragment. 485 | * 486 | * @param array $data 487 | * @param bool $exact 488 | * @return $this 489 | */ 490 | public function assertJsonMissing(array $data, $exact = false) 491 | { 492 | if ($exact) { 493 | return $this->assertJsonMissingExact($data); 494 | } 495 | 496 | $actual = json_encode(Arr::sortRecursive( 497 | (array) $this->decodeResponseJson() 498 | )); 499 | 500 | foreach (Arr::sortRecursive($data) as $key => $value) { 501 | $unexpected = $this->jsonSearchStrings($key, $value); 502 | 503 | PHPUnit::assertFalse( 504 | Str::contains($actual, $unexpected), 505 | 'Found unexpected JSON fragment: '.PHP_EOL.PHP_EOL. 506 | '['.json_encode([$key => $value]).']'.PHP_EOL.PHP_EOL. 507 | 'within'.PHP_EOL.PHP_EOL. 508 | "[{$actual}]." 509 | ); 510 | } 511 | 512 | return $this; 513 | } 514 | 515 | /** 516 | * Assert that the response does not contain the exact JSON fragment. 517 | * 518 | * @param array $data 519 | * @return $this 520 | */ 521 | public function assertJsonMissingExact(array $data) 522 | { 523 | $actual = json_encode(Arr::sortRecursive( 524 | (array) $this->decodeResponseJson() 525 | )); 526 | 527 | foreach (Arr::sortRecursive($data) as $key => $value) { 528 | $unexpected = $this->jsonSearchStrings($key, $value); 529 | 530 | if (! Str::contains($actual, $unexpected)) { 531 | return $this; 532 | } 533 | } 534 | 535 | PHPUnit::fail( 536 | 'Found unexpected JSON fragment: '.PHP_EOL.PHP_EOL. 537 | '['.json_encode($data).']'.PHP_EOL.PHP_EOL. 538 | 'within'.PHP_EOL.PHP_EOL. 539 | "[{$actual}]." 540 | ); 541 | } 542 | 543 | /** 544 | * Get the strings we need to search for when examining the JSON. 545 | * 546 | * @param string $key 547 | * @param string $value 548 | * @return array 549 | */ 550 | protected function jsonSearchStrings($key, $value) 551 | { 552 | $needle = substr(json_encode([$key => $value]), 1, -1); 553 | 554 | return [ 555 | $needle.']', 556 | $needle.'}', 557 | $needle.',', 558 | ]; 559 | } 560 | 561 | /** 562 | * Assert that the response has a given JSON structure. 563 | * 564 | * @param array|null $structure 565 | * @param array|null $responseData 566 | * @return $this 567 | */ 568 | public function assertJsonStructure(array $structure = null, $responseData = null) 569 | { 570 | if (is_null($structure)) { 571 | return $this->assertJson($this->json()); 572 | } 573 | 574 | if (is_null($responseData)) { 575 | $responseData = $this->decodeResponseJson(); 576 | } 577 | 578 | foreach ($structure as $key => $value) { 579 | if (is_array($value) && $key === '*') { 580 | PHPUnit::assertInternalType('array', $responseData); 581 | 582 | foreach ($responseData as $responseDataItem) { 583 | $this->assertJsonStructure($structure['*'], $responseDataItem); 584 | } 585 | } elseif (is_array($value)) { 586 | PHPUnit::assertArrayHasKey($key, $responseData); 587 | 588 | $this->assertJsonStructure($structure[$key], $responseData[$key]); 589 | } else { 590 | PHPUnit::assertArrayHasKey($value, $responseData); 591 | } 592 | } 593 | 594 | return $this; 595 | } 596 | 597 | /** 598 | * Assert that the response JSON has the expected count of items at the given key. 599 | * 600 | * @param int $count 601 | * @param string|null $key 602 | * @return $this 603 | */ 604 | public function assertJsonCount(int $count, $key = null) 605 | { 606 | if ($key) { 607 | PHPUnit::assertCount( 608 | $count, data_get($this->json(), $key), 609 | "Failed to assert that the response count matched the expected {$count}" 610 | ); 611 | 612 | return $this; 613 | } 614 | 615 | PHPUnit::assertCount($count, 616 | $this->json(), 617 | "Failed to assert that the response count matched the expected {$count}" 618 | ); 619 | 620 | return $this; 621 | } 622 | 623 | /** 624 | * Assert that the response has the given JSON validation errors for the given keys. 625 | * 626 | * @param string|array $keys 627 | * @return $this 628 | */ 629 | public function assertJsonValidationErrors($keys) 630 | { 631 | $errors = $this->json()['errors']; 632 | 633 | foreach (Arr::wrap($keys) as $key) { 634 | PHPUnit::assertTrue( 635 | isset($errors[$key]), 636 | "Failed to find a validation error in the response for key: '{$key}'" 637 | ); 638 | } 639 | 640 | return $this; 641 | } 642 | 643 | /** 644 | * Assert that the response has no JSON validation errors for the given keys. 645 | * 646 | * @param string|array $keys 647 | * @return $this 648 | */ 649 | public function assertJsonMissingValidationErrors($keys) 650 | { 651 | $json = $this->json(); 652 | 653 | if (! array_key_exists('errors', $json)) { 654 | PHPUnit::assertArrayNotHasKey('errors', $json); 655 | 656 | return $this; 657 | } 658 | 659 | $errors = $json['errors']; 660 | 661 | foreach (Arr::wrap($keys) as $key) { 662 | PHPUnit::assertFalse( 663 | isset($errors[$key]), 664 | "Found unexpected validation error for key: '{$key}'" 665 | ); 666 | } 667 | 668 | return $this; 669 | } 670 | 671 | /** 672 | * Validate and return the decoded response JSON. 673 | * 674 | * @param string|null $key 675 | * @return mixed 676 | */ 677 | public function decodeResponseJson($key = null) 678 | { 679 | $decodedResponse = json_decode($this->getContent(), true); 680 | 681 | if (is_null($decodedResponse) || $decodedResponse === false) { 682 | if ($this->exception) { 683 | throw $this->exception; 684 | } else { 685 | PHPUnit::fail('Invalid JSON was returned from the route.'); 686 | } 687 | } 688 | 689 | return data_get($decodedResponse, $key); 690 | } 691 | 692 | /** 693 | * Validate and return the decoded response JSON. 694 | * 695 | * @param string|null $key 696 | * @return mixed 697 | */ 698 | public function json($key = null) 699 | { 700 | return $this->decodeResponseJson($key); 701 | } 702 | 703 | /** 704 | * Assert that the response view equals the given value. 705 | * 706 | * @param string $value 707 | * @return $this 708 | */ 709 | public function assertViewIs($value) 710 | { 711 | $this->ensureResponseHasView(); 712 | 713 | PHPUnit::assertEquals($value, $this->original->getName()); 714 | 715 | return $this; 716 | } 717 | 718 | /** 719 | * Assert that the response view has a given piece of bound data. 720 | * 721 | * @param string|array $key 722 | * @param mixed $value 723 | * @return $this 724 | */ 725 | public function assertViewHas($key, $value = null) 726 | { 727 | if (is_array($key)) { 728 | return $this->assertViewHasAll($key); 729 | } 730 | 731 | $this->ensureResponseHasView(); 732 | 733 | if (is_null($value)) { 734 | PHPUnit::assertArrayHasKey($key, $this->original->getData()); 735 | } elseif ($value instanceof Closure) { 736 | PHPUnit::assertTrue($value($this->original->$key)); 737 | } else { 738 | PHPUnit::assertEquals($value, $this->original->$key); 739 | } 740 | 741 | return $this; 742 | } 743 | 744 | /** 745 | * Assert that the response view has a given list of bound data. 746 | * 747 | * @param array $bindings 748 | * @return $this 749 | */ 750 | public function assertViewHasAll(array $bindings) 751 | { 752 | foreach ($bindings as $key => $value) { 753 | if (is_int($key)) { 754 | $this->assertViewHas($value); 755 | } else { 756 | $this->assertViewHas($key, $value); 757 | } 758 | } 759 | 760 | return $this; 761 | } 762 | 763 | /** 764 | * Assert that the response view is missing a piece of bound data. 765 | * 766 | * @param string $key 767 | * @return $this 768 | */ 769 | public function assertViewMissing($key) 770 | { 771 | $this->ensureResponseHasView(); 772 | 773 | PHPUnit::assertArrayNotHasKey($key, $this->original->getData()); 774 | 775 | return $this; 776 | } 777 | 778 | /** 779 | * Ensure that the response has a view as its original content. 780 | * 781 | * @return $this 782 | */ 783 | protected function ensureResponseHasView() 784 | { 785 | if (! isset($this->original) || ! $this->original instanceof View) { 786 | return PHPUnit::fail('The response is not a view.'); 787 | } 788 | 789 | return $this; 790 | } 791 | 792 | /** 793 | * Dump the content from the response. 794 | * 795 | * @return void 796 | */ 797 | public function dump() 798 | { 799 | $content = $this->getContent(); 800 | 801 | $json = json_decode($content); 802 | 803 | if (json_last_error() === JSON_ERROR_NONE) { 804 | $content = $json; 805 | } 806 | 807 | dd($content); 808 | } 809 | 810 | /** 811 | * Dynamically access base response parameters. 812 | * 813 | * @param string $key 814 | * @return mixed 815 | */ 816 | public function __get($key) 817 | { 818 | return $this->baseResponse->{$key}; 819 | } 820 | 821 | /** 822 | * Proxy isset() checks to the underlying base response. 823 | * 824 | * @param string $key 825 | * @return mixed 826 | */ 827 | public function __isset($key) 828 | { 829 | return isset($this->baseResponse->{$key}); 830 | } 831 | 832 | /** 833 | * Handle dynamic calls into macros or pass missing methods to the base response. 834 | * 835 | * @param string $method 836 | * @param array $args 837 | * @return mixed 838 | */ 839 | public function __call($method, $args) 840 | { 841 | if (static::hasMacro($method)) { 842 | return $this->macroCall($method, $args); 843 | } 844 | 845 | return $this->baseResponse->{$method}(...$args); 846 | } 847 | } 848 | --------------------------------------------------------------------------------