laravel-data/tests/Http/RequestTests.php
2026-02-25 17:47:17 -03:00

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');
});