http tests
This commit is contained in:
parent
b827038df3
commit
30706c3521
8 changed files with 216 additions and 214 deletions
107
tests/Http/RequestTest.php
Normal file
107
tests/Http/RequestTest.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Http;
|
||||
|
||||
use Icefox\DTO\IDataObject;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator as ValidatorFacade;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
readonly class Basic implements IDataObject
|
||||
{
|
||||
public string $reply;
|
||||
public function __construct(string $message)
|
||||
{
|
||||
$this->reply = $message == 'ping' ? 'pong' : 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
test('injects data object', function () {
|
||||
Route::post('/', fn(Basic $object) => ['reply' => $object->reply]);
|
||||
|
||||
/** @var \Tests\TestCase $this */
|
||||
$this->postJson('/', ['message' => 'ping'])->assertJson(['reply' => 'pong']);
|
||||
});
|
||||
|
||||
test('fails on validation error', function () {
|
||||
Route::post('/', fn(Basic $object) => ['reply' => $object->reply]);
|
||||
/** @var \Tests\TestCase $this */
|
||||
$resp = $this->postJson('/', [])
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => 'The message field is required.',
|
||||
'errors' => ['message' => ['The message field is required.']],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class WithCustomValidator implements IDataObject
|
||||
{
|
||||
public string $reply;
|
||||
public function __construct(string $message)
|
||||
{
|
||||
$this->reply = $message == 'ping' ? 'pong' : 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
* @param array<string,mixed> $rules
|
||||
*/
|
||||
public static function withValidator(array $data, array $rules): Validator
|
||||
{
|
||||
return ValidatorFacade::make($data, $rules, $messages = [
|
||||
'message.required' => 'the known message is pong',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
test('replies with custom validator', function () {
|
||||
Route::post('/', fn(WithCustomValidator $object) => []);
|
||||
/** @var \Tests\TestCase $this */
|
||||
$this->postJson('/', [])
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => 'the known message is pong',
|
||||
'errors' => ['message' => ['the known message is pong']],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class WithCustomFailure implements IDataObject
|
||||
{
|
||||
public function __construct(public bool $flag) {}
|
||||
|
||||
public static function fails(Validator $validator): void
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
response(['result' => 'invalid, but that is ok' ], 202),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test('uses custom response', function () {
|
||||
Route::post('/', fn(WithCustomFailure $object) => response('', 204));
|
||||
|
||||
/** @var \Tests\TestCase $this */
|
||||
$this->postJson('/', [])
|
||||
->assertStatus(202)
|
||||
->assertJson(['result' => 'invalid, but that is ok']);
|
||||
});
|
||||
|
||||
readonly class WithDefaultObjectOnFailure implements IDataObject
|
||||
{
|
||||
public function __construct(public bool $flag) {}
|
||||
|
||||
public static function fails(): self
|
||||
{
|
||||
return new self(false);
|
||||
}
|
||||
}
|
||||
|
||||
test('uses default object on failure', function () {
|
||||
Route::post('/', fn(WithDefaultObjectOnFailure $object) => response(['flag' => $object->flag], 200));
|
||||
|
||||
/** @var \Tests\TestCase $this */
|
||||
$this->postJson('/', [])
|
||||
->assertStatus(200)
|
||||
->assertJson(['flag' => false]);
|
||||
});
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Http;
|
||||
|
||||
use Icefox\DTO\Attributes\FromInput;
|
||||
use Icefox\DTO\Attributes\FromRouteParameter;
|
||||
use Icefox\DTO\Factories\DataObjectFactory;
|
||||
use Icefox\DTO\Factories\ValueFactory;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
readonly class SimplePostDTO
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public string $content,
|
||||
) {}
|
||||
}
|
||||
|
||||
test('successful validation returns 200 equivalent', function () {
|
||||
$dto = DataObjectFactory::fromArray(SimplePostDTO::class, [
|
||||
'title' => 'Test Title',
|
||||
'content' => 'Test Content',
|
||||
], []);
|
||||
|
||||
expect($dto)->toBeInstanceOf(SimplePostDTO::class)
|
||||
->and($dto->title)->toBe('Test Title')
|
||||
->and($dto->content)->toBe('Test Content');
|
||||
});
|
||||
|
||||
test('failed validation throws validation exception', function () {
|
||||
try {
|
||||
DataObjectFactory::fromArray(SimplePostDTO::class, [
|
||||
'title' => 'Test',
|
||||
'content' => [],
|
||||
], []);
|
||||
} catch (\Throwable $e) {
|
||||
expect($e)->toBeInstanceOf(ValidationException::class);
|
||||
return;
|
||||
}
|
||||
throw new \Exception('Should have thrown an exception');
|
||||
});
|
||||
|
||||
readonly class PostDTOWithNumeric
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public int $views,
|
||||
) {}
|
||||
}
|
||||
|
||||
test('failed validation returns proper error format', function () {
|
||||
try {
|
||||
DataObjectFactory::fromArray(PostDTOWithNumeric::class, [
|
||||
'title' => 'Test',
|
||||
'views' => 'not-a-number',
|
||||
], []);
|
||||
} catch (ValidationException $e) {
|
||||
$errors = $e->validator->errors();
|
||||
|
||||
expect($errors->has('views'))->toBeTrue()
|
||||
->and($errors->first('views'))->toContain('number');
|
||||
}
|
||||
});
|
||||
|
||||
test('validation error message format matches laravel', function () {
|
||||
try {
|
||||
DataObjectFactory::fromArray(PostDTOWithNumeric::class, [
|
||||
'title' => 'Test',
|
||||
'views' => 'invalid',
|
||||
], []);
|
||||
} catch (ValidationException $e) {
|
||||
$errors = $e->validator->errors()->toArray();
|
||||
|
||||
expect($errors)->toHaveKey('views')
|
||||
->and($errors['views'][0])->toContain('views');
|
||||
}
|
||||
});
|
||||
|
||||
readonly class RestrictedDTO
|
||||
{
|
||||
public function __construct(public string $content) {}
|
||||
|
||||
public static function fails($validator): object
|
||||
{
|
||||
return (object) [
|
||||
'error' => 'Access denied',
|
||||
'details' => $validator->errors()->toArray(),
|
||||
'status' => 403,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
test('custom fails returns custom response structure', function () {
|
||||
$result = DataObjectFactory::fromArray(RestrictedDTO::class, [
|
||||
'content' => [], // invalid - must be string
|
||||
], []);
|
||||
|
||||
expect($result->error)->toBe('Access denied')
|
||||
->and($result->details)->toHaveKey('content')
|
||||
->and($result->status)->toBe(403);
|
||||
});
|
||||
|
||||
readonly class CustomBagDTO
|
||||
{
|
||||
public string $name;
|
||||
public float $price;
|
||||
|
||||
public function __construct(string $name, float $price)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->price = $price;
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'min:3'],
|
||||
'price' => ['required', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
test('named error bag test', function () {
|
||||
try {
|
||||
DataObjectFactory::fromArray(CustomBagDTO::class, [
|
||||
'name' => 'ab',
|
||||
'price' => -10,
|
||||
], []);
|
||||
} catch (ValidationException $e) {
|
||||
$errors = $e->validator->errors()->toArray();
|
||||
|
||||
expect($errors)->toHaveKey('name')
|
||||
->and($errors)->toHaveKey('price');
|
||||
}
|
||||
});
|
||||
|
||||
test('route parameter priority over from input', function () {
|
||||
$dto = new class (123) {
|
||||
public function __construct(
|
||||
#[FromRouteParameter('user_id')]
|
||||
#[FromInput('user_id')]
|
||||
public int $id,
|
||||
) {}
|
||||
};
|
||||
|
||||
$result = DataObjectFactory::mapInput(
|
||||
$dto::class,
|
||||
['user_id' => 456],
|
||||
['user_id' => 123],
|
||||
new \Psr\Log\NullLogger(),
|
||||
);
|
||||
|
||||
expect($result['id'])->toBe(123);
|
||||
});
|
||||
|
||||
test('multiple route parameters', function () {
|
||||
$dto = new class (45, 89, 'A') {
|
||||
public function __construct(
|
||||
#[FromRouteParameter('course_id')]
|
||||
public int $courseId,
|
||||
#[FromRouteParameter('student_id')]
|
||||
public int $studentId,
|
||||
#[FromInput('grade')]
|
||||
public string $grade,
|
||||
) {}
|
||||
};
|
||||
|
||||
$result = DataObjectFactory::mapInput(
|
||||
$dto::class,
|
||||
['grade' => 'A'],
|
||||
['course_id' => 45, 'student_id' => 89],
|
||||
new \Psr\Log\NullLogger(),
|
||||
);
|
||||
|
||||
expect($result['courseId'])->toBe(45)
|
||||
->and($result['studentId'])->toBe(89)
|
||||
->and($result['grade'])->toBe('A');
|
||||
});
|
||||
|
||||
test('route parameter with nested object', function () {
|
||||
$addressDto = new class ('Main St') {
|
||||
public function __construct(public string $street) {}
|
||||
};
|
||||
|
||||
$dto = new class (123, $addressDto) {
|
||||
public function __construct(
|
||||
#[FromRouteParameter('user_id')]
|
||||
public int $userId,
|
||||
public $address,
|
||||
) {}
|
||||
};
|
||||
|
||||
$result = DataObjectFactory::mapInput(
|
||||
$dto::class,
|
||||
['user_id' => 456, 'address' => ['street' => 'Main St']],
|
||||
['user_id' => 123],
|
||||
new \Psr\Log\NullLogger(),
|
||||
);
|
||||
|
||||
expect($result['userId'])->toBe(123)
|
||||
->and($result['address']['street'])->toBe('Main St');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue