206 lines
5.4 KiB
PHP
206 lines
5.4 KiB
PHP
<?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');
|
|
});
|