Skip to content

Commit 754a657

Browse files
authored
feat(testing): add assert response json assertions (#1433)
Signed-off-by: Tonko Mulder <[email protected]>
1 parent acace86 commit 754a657

File tree

8 files changed

+311
-19
lines changed

8 files changed

+311
-19
lines changed

packages/database/src/IsDatabaseModel.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ public static function updateOrCreate(array $find, array $update): self
116116
return $model->save();
117117
}
118118

119+
public function refresh(): self
120+
{
121+
$model = self::find(id: $this->id)->first();
122+
foreach (new ClassReflector($model)->getPublicProperties() as $property) {
123+
$property->setValue($this, $property->getValue($model));
124+
}
125+
126+
return $this;
127+
}
128+
119129
public function __get(string $name): mixed
120130
{
121131
$property = PropertyReflector::fromParts($this, $name);

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

Lines changed: 145 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,152 @@ 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::assertNotContains($this->response->status, [Status::BAD_REQUEST, Status::FOUND]);
467+
Assert::assertNotInstanceOf(Invalid::class, $this->response);
468+
Assert::assertNull($this->response->getHeader('x-validation'));
469+
470+
return $this;
471+
}
472+
332473
public function dd(): void
333474
{
334-
// @phpstan-ignore disallowed.function
475+
/**
476+
* @noinspection ForgottenDebugOutputInspection
477+
* @phpstan-ignore disallowed.function
478+
*/
335479
dd($this->response); // @mago-expect best-practices/no-debug-symbols
336480
}
337481
}

tests/Fixtures/Controllers/RequestItemForValidationController.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/Fixtures/Controllers/ValidationController.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
namespace Tests\Tempest\Fixtures\Controllers;
66

77
use Tempest\Http\Response;
8+
use Tempest\Http\Responses\Json;
89
use Tempest\Http\Responses\Ok;
910
use Tempest\Http\Responses\Redirect;
1011
use Tempest\Router\Get;
1112
use Tempest\Router\Post;
13+
use Tests\Tempest\Fixtures\Modules\Books\Models\Book;
14+
use Tests\Tempest\Fixtures\Requests\BookRequest;
15+
use Tests\Tempest\Fixtures\Requests\ValidationRequest;
1216

1317
use function Tempest\uri;
1418

@@ -21,8 +25,42 @@ public function get(): Response
2125
}
2226

2327
#[Post('/test-validation-responses')]
24-
public function store(RequestForValidationController $request): Response // @mago-expect best-practices/no-unused-parameter
28+
public function store(ValidationRequest $request): Response // @mago-expect best-practices/no-unused-parameter
2529
{
2630
return new Redirect(uri([self::class, 'get']));
2731
}
32+
33+
#[Get(uri: '/test-validation-responses-json/{book}')]
34+
public function book(Book $book): Response
35+
{
36+
$book->load('author');
37+
38+
return new Json([
39+
'id' => $book->id->id,
40+
'title' => $book->title,
41+
'author' => [
42+
'id' => $book->author->id->id,
43+
'name' => $book->author->name,
44+
],
45+
]);
46+
}
47+
48+
#[Post(uri: '/test-validation-responses-json/{book}')]
49+
public function updateBook(BookRequest $request, Book $book): Response
50+
{
51+
$book->load('author');
52+
53+
$book->update(title: $request->get('title'));
54+
55+
return new Json([
56+
'id' => $book->id->id,
57+
'title' => $book->title,
58+
'author' => [
59+
'id' => $book->author->id->id,
60+
'name' => $book->author->name,
61+
],
62+
'chapters' => $book->chapters,
63+
'isbn' => $book->isbn,
64+
]);
65+
}
2866
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Fixtures\Requests;
6+
7+
use Tempest\Database\HasOne;
8+
use Tempest\Http\IsRequest;
9+
use Tempest\Http\Request;
10+
use Tempest\Validation\Rules\Length;
11+
use Tests\Tempest\Fixtures\Modules\Books\Models\Author;
12+
use Tests\Tempest\Fixtures\Modules\Books\Models\Isbn;
13+
14+
final class BookRequest implements Request
15+
{
16+
use IsRequest;
17+
18+
#[Length(min: 1, max: 120)]
19+
public string $title;
20+
21+
public ?Author $author = null;
22+
23+
/** @var \Tests\Tempest\Fixtures\Modules\Books\Models\Chapter[] */
24+
public array $chapters = [];
25+
26+
#[HasOne]
27+
public ?Isbn $isbn = null;
28+
}

tests/Fixtures/Controllers/RequestForValidationController.php renamed to tests/Fixtures/Requests/ValidationRequest.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@
22

33
declare(strict_types=1);
44

5-
namespace Tests\Tempest\Fixtures\Controllers;
5+
namespace Tests\Tempest\Fixtures\Requests;
66

77
use Tempest\Http\IsRequest;
88
use Tempest\Http\Request;
99
use Tempest\Validation\Rules\Between;
1010

11-
final class RequestForValidationController implements Request
11+
final class ValidationRequest implements Request
1212
{
1313
use IsRequest;
1414

15-
public RequestItemForValidationController $item;
16-
1715
#[Between(min: 1, max: 10)]
1816
public int $number;
1917
}

tests/Integration/Http/ValidationResponseTest.php

Lines changed: 57 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,55 @@ 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+
->assertHasNoJsonValidationErrors();
74+
75+
$book->refresh();
76+
77+
$this->assertSame($book->title, 'Beyond the Odyssee');
78+
}
79+
80+
public function test_failing_post_request(): void
81+
{
82+
$this->migrate(
83+
CreateMigrationsTable::class,
84+
CreatePublishersTable::class,
85+
CreateAuthorTable::class,
86+
CreateBookTable::class,
87+
);
88+
89+
Book::create(
90+
title: 'Timeline Taxi',
91+
author: Author::create(name: 'Brent'),
92+
);
93+
94+
$this->http
95+
->post(
96+
uri([ValidationController::class, 'updateBook'], book: 1),
97+
body: ['book' => ['title' => 1]],
98+
)
99+
->assertHasJsonValidationErrors(['title' => ['Value should be between 1 and 120']]);
100+
101+
$this->assertSame('Timeline Taxi', Book::find(id: 1)->first()->title);
102+
}
46103
}

0 commit comments

Comments
 (0)