Files
clinicpro/tests/Resource/FieldSchemaTest.php
T
hamedandClaude Opus 5 9af763bfbe 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>
2026-08-06 17:05:48 +03:30

143 lines
5.2 KiB
PHP

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