Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/database/src/IsDatabaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ public static function updateOrCreate(array $find, array $update): self
return $model->save();
}

public function refresh(): self
{
$model = self::find(id: $this->id)->first();
foreach (new ClassReflector($model)->getPublicProperties() as $property) {
$property->setValue($this, $property->getValue($model));
}

return $this;
}

public function __get(string $name): mixed
{
$property = PropertyReflector::fromParts($this, $name);
Expand Down
146 changes: 145 additions & 1 deletion src/Tempest/Framework/Testing/Http/TestResponseHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPUnit\Framework\Assert;
use Tempest\Http\Cookie\CookieManager;
use Tempest\Http\Response;
use Tempest\Http\Responses\Invalid;
use Tempest\Http\Session\Session;
use Tempest\Http\Status;
use Tempest\Validation\Rule;
Expand Down Expand Up @@ -329,9 +330,152 @@ public function assertViewModel(string $expected, ?Closure $callback = null): se
return $this;
}

/**
* Assert the response body is an exact match to the given array.
*
* The keys can also be specified using dot notation.
*
* ### Example
* ```
* // build the expected array with dot notation
* $this->http->get(uri([BookController::class, 'index']))
* ->assertJson([
* 'id' => 1,
* 'title' => 'Timeline Taxi',
* 'author.name' => 'Brent',
* ]);
*
* // build the expected array with a normal array
* $this->http->get(uri([BookController::class, 'index']))
* ->assertJson([
* 'id' => 1,
* 'title' => 'Timeline Taxi',
* 'author' => [
* 'name' => 'Brent',
* ],
* ]);
* ```
*
* @param array<string, mixed> $expected
*/
public function assertJson(array $expected): self
{
Assert::assertEquals(
expected: arr($expected)->undot()->toArray(),
actual: $this->response->body,
);

return $this;
}

/**
* Asserts the response contains the given keys.
*
* The keys can also be specified using dot notation.
*
* ### Example
* ```
* $this->http->get(uri([BookController::class, 'index']))
* ->assertJsonHasKeys('id', 'title', 'author.name');
* ```
*/
public function assertJsonHasKeys(string ...$keys): self
{
foreach ($keys as $key) {
Assert::assertArrayHasKey($key, arr($this->response->body)->dot());
}

return $this;
}

/**
* Asserts the response contains the given keys and values.
*
* The keys can also be specified using dot notation.
*
* ### Example
* ```
* $this->http->get(uri([BookController::class, 'index']))
* ->assertJsonContains([
* 'id' => 1,
* 'title' => 'Timeline Taxi',
* ])
* ->assertJsonContains(['author' => ['name' => 'Brent']])
* ->assertJsonContains(['author.name' => 'Brent']);
* ```
*
* @template TKey of array-key
* @template TValue
*
* @param array<TKey, TValue> $expected
*/
public function assertJsonContains(array $expected): self
{
foreach (arr($expected)->undot() as $key => $value) {
Assert::assertEquals($this->response->body[$key], $value);
}

return $this;
}

/**
* Asserts the response contains the given JSON validation errors.
*
* The keys can also be specified using dot notation.
*
* ### Example
* ```
* $this->http->get(uri([BookController::class, 'index']))
* ->assertJsonValidationErrors([
* 'title' => 'The title field is required.',
* ]);
* ```
*
* @param array<string, string|string[]> $expectedErrors
*/
public function assertHasJsonValidationErrors(array $expectedErrors): self
{
Assert::assertInstanceOf(Invalid::class, $this->response);
Assert::assertContains($this->response->status, [Status::BAD_REQUEST, Status::FOUND]);
Assert::assertNotNull($this->response->getHeader('x-validation'));

$session = get(Session::class);
$validationRules = arr($session->get(Session::VALIDATION_ERRORS))->dot();

$dottedExpectedErrors = arr($expectedErrors)->dot();
arr($dottedExpectedErrors)
->each(fn ($expectedErrorValue, $expectedErrorKey) => Assert::assertEquals(
$expectedErrorValue,
$validationRules->get($expectedErrorKey)->message(),
));

return $this;
}

/**
* Asserts the response does not contain any JSON validation errors.
*
* ### Example
* ```
* $this->http->get(uri([BookController::class, 'index']))
* ->assertHasNoJsonValidationErrors();
* ```
*/
public function assertHasNoJsonValidationErrors(): self
{
Assert::assertNotContains($this->response->status, [Status::BAD_REQUEST, Status::FOUND]);
Assert::assertNotInstanceOf(Invalid::class, $this->response);
Assert::assertNull($this->response->getHeader('x-validation'));

return $this;
}

public function dd(): void
{
// @phpstan-ignore disallowed.function
/**
* @noinspection ForgottenDebugOutputInspection
* @phpstan-ignore disallowed.function
*/
dd($this->response); // @mago-expect best-practices/no-debug-symbols
}
}
13 changes: 0 additions & 13 deletions tests/Fixtures/Controllers/RequestItemForValidationController.php

This file was deleted.

40 changes: 39 additions & 1 deletion tests/Fixtures/Controllers/ValidationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
namespace Tests\Tempest\Fixtures\Controllers;

use Tempest\Http\Response;
use Tempest\Http\Responses\Json;
use Tempest\Http\Responses\Ok;
use Tempest\Http\Responses\Redirect;
use Tempest\Router\Get;
use Tempest\Router\Post;
use Tests\Tempest\Fixtures\Modules\Books\Models\Book;
use Tests\Tempest\Fixtures\Requests\BookRequest;
use Tests\Tempest\Fixtures\Requests\ValidationRequest;

use function Tempest\uri;

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

#[Post('/test-validation-responses')]
public function store(RequestForValidationController $request): Response // @mago-expect best-practices/no-unused-parameter
public function store(ValidationRequest $request): Response // @mago-expect best-practices/no-unused-parameter
{
return new Redirect(uri([self::class, 'get']));
}

#[Get(uri: '/test-validation-responses-json/{book}')]
public function book(Book $book): Response
{
$book->load('author');

return new Json([
'id' => $book->id->id,
'title' => $book->title,
'author' => [
'id' => $book->author->id->id,
'name' => $book->author->name,
],
]);
}

#[Post(uri: '/test-validation-responses-json/{book}')]
public function updateBook(BookRequest $request, Book $book): Response
{
$book->load('author');

$book->update(title: $request->get('title'));

return new Json([
'id' => $book->id->id,
'title' => $book->title,
'author' => [
'id' => $book->author->id->id,
'name' => $book->author->name,
],
'chapters' => $book->chapters,
'isbn' => $book->isbn,
]);
}
}
28 changes: 28 additions & 0 deletions tests/Fixtures/Requests/BookRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Fixtures\Requests;

use Tempest\Database\HasOne;
use Tempest\Http\IsRequest;
use Tempest\Http\Request;
use Tempest\Validation\Rules\Length;
use Tests\Tempest\Fixtures\Modules\Books\Models\Author;
use Tests\Tempest\Fixtures\Modules\Books\Models\Isbn;

final class BookRequest implements Request
{
use IsRequest;

#[Length(min: 1, max: 120)]
public string $title;

public ?Author $author = null;

/** @var \Tests\Tempest\Fixtures\Modules\Books\Models\Chapter[] */
public array $chapters = [];

#[HasOne]
public ?Isbn $isbn = null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

declare(strict_types=1);

namespace Tests\Tempest\Fixtures\Controllers;
namespace Tests\Tempest\Fixtures\Requests;

use Tempest\Http\IsRequest;
use Tempest\Http\Request;
use Tempest\Validation\Rules\Between;

final class RequestForValidationController implements Request
final class ValidationRequest implements Request
{
use IsRequest;

public RequestItemForValidationController $item;

#[Between(min: 1, max: 10)]
public int $number;
}
57 changes: 57 additions & 0 deletions tests/Integration/Http/ValidationResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

namespace Tests\Tempest\Integration\Http;

use Tempest\Database\Migrations\CreateMigrationsTable;
use Tempest\Http\Session\Session;
use Tests\Tempest\Fixtures\Controllers\ValidationController;
use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable;
use Tests\Tempest\Fixtures\Migrations\CreateBookTable;
use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable;
use Tests\Tempest\Fixtures\Modules\Books\Models\Author;
use Tests\Tempest\Fixtures\Modules\Books\Models\Book;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;

use function Tempest\uri;
Expand Down Expand Up @@ -43,4 +49,55 @@ public function test_original_values(): void
$this->assertEquals($values, $data);
});
}

public function test_update_book(): void
{
$this->migrate(
CreateMigrationsTable::class,
CreatePublishersTable::class,
CreateAuthorTable::class,
CreateBookTable::class,
);

$book = Book::create(
title: 'Timeline Taxi',
author: Author::create(name: 'Brent'),
);

$this->http
->post(
uri([ValidationController::class, 'updateBook'], book: 1),
body: ['title' => 'Beyond the Odyssee'],
)
->assertOk()
->assertHasNoJsonValidationErrors();

$book->refresh();

$this->assertSame($book->title, 'Beyond the Odyssee');
}

public function test_failing_post_request(): void
{
$this->migrate(
CreateMigrationsTable::class,
CreatePublishersTable::class,
CreateAuthorTable::class,
CreateBookTable::class,
);

Book::create(
title: 'Timeline Taxi',
author: Author::create(name: 'Brent'),
);

$this->http
->post(
uri([ValidationController::class, 'updateBook'], book: 1),
body: ['book' => ['title' => 1]],
)
->assertHasJsonValidationErrors(['title' => ['Value should be between 1 and 120']]);

$this->assertSame('Timeline Taxi', Book::find(id: 1)->first()->title);
}
}
Loading