Skip to content

Commit 7ef901e

Browse files
committed
add json assertions on the TestResponseHelper
Signed-off-by: Tonko Mulder <[email protected]>
1 parent d8bbb9a commit 7ef901e

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed

src/Tempest/Framework/Testing/Http/TestResponseHelper.php

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPUnit\Framework\Assert;
1010
use Tempest\Http\Cookie\CookieManager;
1111
use Tempest\Http\Response;
12+
use Tempest\Http\Responses\Invalid;
1213
use Tempest\Http\Session\Session;
1314
use Tempest\Http\Status;
1415
use Tempest\Validation\Rule;
@@ -329,9 +330,151 @@ public function assertViewModel(string $expected, ?Closure $callback = null): se
329330
return $this;
330331
}
331332

333+
/**
334+
* Assert the response body is an exact match to the given array.
335+
*
336+
* The keys can also be specified using dot notation.
337+
*
338+
* ### Example
339+
* ```
340+
* // build the expected array with dot notation
341+
* $this->http->get(uri([BookController::class, 'index']))
342+
* ->assertJson([
343+
* 'id' => 1,
344+
* 'title' => 'Timeline Taxi',
345+
* 'author.name' => 'Brent',
346+
* ]);
347+
*
348+
* // build the expected array with a normal array
349+
* $this->http->get(uri([BookController::class, 'index']))
350+
* ->assertJson([
351+
* 'id' => 1,
352+
* 'title' => 'Timeline Taxi',
353+
* 'author' => [
354+
* 'name' => 'Brent',
355+
* ],
356+
* ]);
357+
* ```
358+
*
359+
* @param array<string, mixed> $expected
360+
*/
361+
public function assertJson(array $expected): self
362+
{
363+
Assert::assertEquals(
364+
expected: arr($expected)->undot()->toArray(),
365+
actual: $this->response->body,
366+
);
367+
368+
return $this;
369+
}
370+
371+
/**
372+
* Asserts the response contains the given keys.
373+
*
374+
* The keys can also be specified using dot notation.
375+
*
376+
* ### Example
377+
* ```
378+
* $this->http->get(uri([BookController::class, 'index']))
379+
* ->assertJsonHasKeys('id', 'title', 'author.name');
380+
* ```
381+
*/
382+
public function assertJsonHasKeys(string ...$keys): self
383+
{
384+
foreach ($keys as $key) {
385+
Assert::assertArrayHasKey($key, arr($this->response->body)->dot());
386+
}
387+
388+
return $this;
389+
}
390+
391+
/**
392+
* Asserts the response contains the given keys and values.
393+
*
394+
* The keys can also be specified using dot notation.
395+
*
396+
* ### Example
397+
* ```
398+
* $this->http->get(uri([BookController::class, 'index']))
399+
* ->assertJsonContains([
400+
* 'id' => 1,
401+
* 'title' => 'Timeline Taxi',
402+
* ])
403+
* ->assertJsonContains(['author' => ['name' => 'Brent']])
404+
* ->assertJsonContains(['author.name' => 'Brent']);
405+
* ```
406+
*
407+
* @template TKey of array-key
408+
* @template TValue
409+
*
410+
* @param array<TKey, TValue> $expected
411+
*/
412+
public function assertJsonContains(array $expected): self
413+
{
414+
foreach (arr($expected)->undot() as $key => $value) {
415+
Assert::assertEquals($this->response->body[$key], $value);
416+
}
417+
418+
return $this;
419+
}
420+
421+
/**
422+
* Asserts the response contains the given JSON validation errors.
423+
*
424+
* The keys can also be specified using dot notation.
425+
*
426+
* ### Example
427+
* ```
428+
* $this->http->get(uri([BookController::class, 'index']))
429+
* ->assertJsonValidationErrors([
430+
* 'title' => 'The title field is required.',
431+
* ]);
432+
* ```
433+
*
434+
* @param array<string, string|string[]> $expectedErrors
435+
*/
436+
public function assertHasJsonValidationErrors(array $expectedErrors): self
437+
{
438+
Assert::assertInstanceOf(Invalid::class, $this->response);
439+
Assert::assertContains($this->response->status, [Status::BAD_REQUEST, Status::FOUND]);
440+
Assert::assertNotNull($this->response->getHeader('x-validation'));
441+
442+
$session = get(Session::class);
443+
$validationRules = arr($session->get(Session::VALIDATION_ERRORS))->dot();
444+
445+
$dottedExpectedErrors = arr($expectedErrors)->dot();
446+
arr($dottedExpectedErrors)
447+
->each(fn ($expectedErrorValue, $expectedErrorKey) => Assert::assertEquals(
448+
$expectedErrorValue,
449+
$validationRules->get($expectedErrorKey)->message(),
450+
));
451+
452+
return $this;
453+
}
454+
455+
/**
456+
* Asserts the response does not contain any JSON validation errors.
457+
*
458+
* ### Example
459+
* ```
460+
* $this->http->get(uri([BookController::class, 'index']))
461+
* ->assertHasNoJsonValidationErrors();
462+
* ```
463+
*/
464+
public function assertHasNoJsonValidationErrors(): self
465+
{
466+
Assert::assertNotInstanceOf(Invalid::class, $this->response);
467+
Assert::assertNull($this->response->getHeader('x-validation'));
468+
469+
return $this;
470+
}
471+
332472
public function dd(): void
333473
{
334-
// @phpstan-ignore disallowed.function
474+
/**
475+
* @noinspection ForgottenDebugOutputInspection
476+
* @phpstan-ignore disallowed.function
477+
*/
335478
dd($this->response); // @mago-expect best-practices/no-debug-symbols
336479
}
337480
}

tests/Integration/Http/ValidationResponseTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44

55
namespace Tests\Tempest\Integration\Http;
66

7+
use Tempest\Database\Migrations\CreateMigrationsTable;
78
use Tempest\Http\Session\Session;
89
use Tests\Tempest\Fixtures\Controllers\ValidationController;
10+
use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable;
11+
use Tests\Tempest\Fixtures\Migrations\CreateBookTable;
12+
use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable;
13+
use Tests\Tempest\Fixtures\Modules\Books\Models\Author;
14+
use Tests\Tempest\Fixtures\Modules\Books\Models\Book;
915
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
1016

1117
use function Tempest\uri;
@@ -43,4 +49,56 @@ public function test_original_values(): void
4349
$this->assertEquals($values, $data);
4450
});
4551
}
52+
53+
public function test_update_book(): void
54+
{
55+
$this->migrate(
56+
CreateMigrationsTable::class,
57+
CreatePublishersTable::class,
58+
CreateAuthorTable::class,
59+
CreateBookTable::class,
60+
);
61+
62+
$book = Book::create(
63+
title: 'Timeline Taxi',
64+
author: Author::create(name: 'Brent'),
65+
);
66+
67+
$this->http
68+
->post(
69+
uri([ValidationController::class, 'updateBook'], book: 1),
70+
body: ['title' => 'Beyond the Odyssee'],
71+
)
72+
->assertOk();
73+
74+
$book->refresh();
75+
76+
$this->assertSame($book->title, 'Beyond the Odyssee');
77+
}
78+
79+
public function test_failing_post_request(): void
80+
{
81+
$this->migrate(
82+
CreateMigrationsTable::class,
83+
CreatePublishersTable::class,
84+
CreateAuthorTable::class,
85+
CreateBookTable::class,
86+
);
87+
88+
Book::create(
89+
title: 'Timeline Taxi',
90+
author: Author::create(name: 'Brent'),
91+
);
92+
93+
$this->http
94+
->post(
95+
uri([ValidationController::class, 'updateBook'], book: 1),
96+
body: ['book' => ['title' => 1]],
97+
)
98+
->assertHasJsonValidationError('title', function (array $errors): void {
99+
$this->assertContains('Value should be between 1 and 120', $errors);
100+
});
101+
102+
$this->assertSame('Timeline Taxi', Book::find(id: 1)->first()->title);
103+
}
46104
}

tests/Integration/Testing/Http/TestResponseHelperTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,36 @@ public function test_assert_status_fails_when_status_does_not_match(Status $expe
181181
$helper->assertStatus($expectedStatus);
182182
}
183183

184+
public function test_assert_json_has_keys(): void
185+
{
186+
$helper = new TestResponseHelper(
187+
new GenericResponse(status: Status::OK, body: ['title' => 'Timeline Taxi', 'author' => ['name' => 'John']]),
188+
);
189+
190+
$helper->assertJsonHasKeys('title', 'author.name');
191+
}
192+
193+
public function test_assert_json_contains(): void
194+
{
195+
$helper = new TestResponseHelper(
196+
new GenericResponse(status: Status::OK, body: ['title' => 'Timeline Taxi', 'author' => ['name' => 'John']]),
197+
);
198+
199+
$helper->assertJsonContains(['title' => 'Timeline Taxi']);
200+
$helper->assertJsonContains(['author' => ['name' => 'John']]);
201+
$helper->assertJsonContains(['author.name' => 'John']);
202+
}
203+
204+
public function test_assert_json(): void
205+
{
206+
$helper = new TestResponseHelper(
207+
new GenericResponse(status: Status::OK, body: ['title' => 'Timeline Taxi', 'author' => ['name' => 'John']]),
208+
);
209+
210+
$helper->assertJson(['title' => 'Timeline Taxi', 'author' => ['name' => 'John']]);
211+
$helper->assertJson(['title' => 'Timeline Taxi', 'author.name' => 'John']);
212+
}
213+
184214
public static function provide_assert_status_cases(): iterable
185215
{
186216
return [

0 commit comments

Comments
 (0)