feat(resource): let a resource type declare the fields recorded against it
What an operator writes down after treating an area is decided by the device, not by the service: a laser has energy, pulse and shot count, an RF unit has something else. So the field list lives on the resource type, and adding a new kind of device becomes a settings change rather than a migration. One validator covers both directions — the schema when a manager saves it and the values when an operator submits them. Splitting them would let a schema be stored that no value can ever satisfy. A value whose key is not in the schema is rejected rather than stored: silently keeping it means the operator believes they recorded something that will never be shown back to them. Option matching compares as strings so "18" and 18 are one option, not two. The migration seeds the laser type's three fields onto existing rows that have none, so clinics already running laser devices do not start from an empty form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Resource;
|
||||
|
||||
use App\Resource\Service\FieldSchemaValidator;
|
||||
use App\Shared\Exception\AppException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* قرارداد فرم ثبت درمان — بدون کرنل، چون هیچ وابستگیای ندارد.
|
||||
*/
|
||||
class FieldSchemaTest extends TestCase
|
||||
{
|
||||
private FieldSchemaValidator $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->validator = new FieldSchemaValidator();
|
||||
}
|
||||
|
||||
private function laserSchema(): array
|
||||
{
|
||||
return $this->validator->normalizeSchema([
|
||||
['key' => 'energy', 'label' => 'انرژی', 'type' => 'select', 'required' => true, 'options' => [7, 8, 18]],
|
||||
['key' => 'pulse', 'label' => 'پالس', 'type' => 'select', 'required' => true, 'options' => [3, 5]],
|
||||
['key' => 'shots', 'label' => 'شات', 'type' => 'number', 'required' => true],
|
||||
['key' => 'comment', 'label' => 'توضیح', 'type' => 'text'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testSchemaIsNormalizedAndSortedByOrder(): void
|
||||
{
|
||||
$schema = $this->validator->normalizeSchema([
|
||||
['key' => 'shots', 'label' => 'شات', 'type' => 'number', 'sort_order' => 2],
|
||||
['key' => 'energy', 'label' => 'انرژی', 'type' => 'select', 'sort_order' => 0, 'options' => [7, 18]],
|
||||
]);
|
||||
|
||||
self::assertSame(['energy', 'shots'], array_column($schema, 'key'));
|
||||
self::assertFalse($schema[0]['required']);
|
||||
}
|
||||
|
||||
public function testEmptySchemaBecomesNull(): void
|
||||
{
|
||||
self::assertNull($this->validator->normalizeSchema([]));
|
||||
self::assertNull($this->validator->normalizeSchema(null));
|
||||
}
|
||||
|
||||
public function testUnknownFieldTypeIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->normalizeSchema([['key' => 'x', 'label' => 'ایکس', 'type' => 'colour']]);
|
||||
}
|
||||
|
||||
public function testSelectFieldWithoutOptionsIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->normalizeSchema([['key' => 'energy', 'label' => 'انرژی', 'type' => 'select']]);
|
||||
}
|
||||
|
||||
public function testDuplicateKeyIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->normalizeSchema([
|
||||
['key' => 'energy', 'label' => 'انرژی', 'type' => 'number'],
|
||||
['key' => 'energy', 'label' => 'دوباره', 'type' => 'number'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testInvalidKeyIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->normalizeSchema([['key' => 'Energy Level', 'label' => 'انرژی', 'type' => 'number']]);
|
||||
}
|
||||
|
||||
public function testValidValuesPassAndAreCast(): void
|
||||
{
|
||||
$values = $this->validator->validateValues($this->laserSchema(), [
|
||||
'energy' => '18',
|
||||
'pulse' => 3,
|
||||
'shots' => '212',
|
||||
'comment' => ' خوب پیش رفت ',
|
||||
]);
|
||||
|
||||
self::assertSame(18, $values['energy']);
|
||||
self::assertSame(3, $values['pulse']);
|
||||
self::assertSame(212, $values['shots']);
|
||||
self::assertSame('خوب پیش رفت', $values['comment']);
|
||||
}
|
||||
|
||||
/** مقداری که هیچ فیلدی نشانش نمیدهد یعنی اپراتور فکر میکند چیزی ثبت کرده که دیده نمیشود. */
|
||||
public function testUnknownKeyIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->validateValues($this->laserSchema(), [
|
||||
'energy' => 18, 'pulse' => 3, 'shots' => 10, 'wavelength' => 808,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testValueOutsideOptionsIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->validateValues($this->laserSchema(), [
|
||||
'energy' => 99, 'pulse' => 3, 'shots' => 10,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testMissingRequiredValueIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->validateValues($this->laserSchema(), ['energy' => 18, 'pulse' => 3]);
|
||||
}
|
||||
|
||||
public function testOptionalFieldMayBeOmitted(): void
|
||||
{
|
||||
$values = $this->validator->validateValues($this->laserSchema(), [
|
||||
'energy' => 18, 'pulse' => 3, 'shots' => 212,
|
||||
]);
|
||||
|
||||
self::assertArrayNotHasKey('comment', $values);
|
||||
}
|
||||
|
||||
public function testNonNumericValueForNumberFieldIsRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->validateValues($this->laserSchema(), [
|
||||
'energy' => 18, 'pulse' => 3, 'shots' => 'خیلی',
|
||||
]);
|
||||
}
|
||||
|
||||
/** اتاق و پرسنل فرمی ندارند — فرستادن مقدار برایشان خطاست، نه نادیده گرفته شدن. */
|
||||
public function testValuesForATypeWithoutASchemaAreRejected(): void
|
||||
{
|
||||
$this->expectException(AppException::class);
|
||||
$this->validator->validateValues(null, ['energy' => 18]);
|
||||
}
|
||||
|
||||
public function testNoSchemaAndNoValuesIsFine(): void
|
||||
{
|
||||
self::assertNull($this->validator->validateValues(null, []));
|
||||
self::assertNull($this->validator->validateValues(null, null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user