├── 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 | 
4 | [](https://packagist.org/packages/albertcht/lumen-testing)
5 | [](https://packagist.org/packages/albertcht/lumen-testing)
6 | [](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 | [](https://beerpay.io/albertcht/lumen-testing) [](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 |
--------------------------------------------------------------------------------