feat(helpdesk): add handover protocol schema editor to software products
Add a JSON-based schema editor as a new tab on the software product edit page. Admins can define protocol fields (heading, paragraph, text, textarea, number, date, checkbox, select) via a structured UI with add/remove/reorder controls. The schema is stored as a versioned JSON column on the product row. Includes DB migration, server-side validation (type whitelist, key uniqueness, key format, max 50 fields, select option checks), i18n (DE+EN), CSS, and 10 PHPUnit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,176 @@ class SoftwareProductServiceTest extends TestCase
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
// --- Handover schema tests ---
|
||||
|
||||
private function createServiceWithProduct(array $product = []): array
|
||||
{
|
||||
$product = array_merge([
|
||||
'id' => 1,
|
||||
'code' => 'INFO',
|
||||
'name' => 'Infopoints',
|
||||
'active' => 1,
|
||||
'handover_protocol_schema' => null,
|
||||
], $product);
|
||||
|
||||
$repository = $this->createMock(SoftwareProductRepository::class);
|
||||
$repository->method('findByCode')->willReturn($product);
|
||||
|
||||
return [$this->createService($repository), $repository];
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaWithValidFields(): void
|
||||
{
|
||||
[$service, $repository] = $this->createServiceWithProduct();
|
||||
$repository->expects($this->once())
|
||||
->method('updateHandoverSchema')
|
||||
->with('INFO', $this->callback(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
|
||||
return $data['version'] === 1
|
||||
&& count($data['fields']) === 2
|
||||
&& $data['fields'][0]['key'] === 'handover_date'
|
||||
&& $data['fields'][1]['key'] === 'notes';
|
||||
}))
|
||||
->willReturn(true);
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'date', 'key' => 'handover_date', 'label' => 'Handover date', 'required' => true],
|
||||
['type' => 'textarea', 'key' => 'notes', 'label' => 'Notes', 'required' => false],
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaEmptyFieldsSetsNull(): void
|
||||
{
|
||||
[$service, $repository] = $this->createServiceWithProduct();
|
||||
$repository->expects($this->once())
|
||||
->method('updateHandoverSchema')
|
||||
->with('INFO', null)
|
||||
->willReturn(true);
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', []);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaRejectsDuplicateKeys(): void
|
||||
{
|
||||
[$service] = $this->createServiceWithProduct();
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'text', 'key' => 'name', 'label' => 'Name 1'],
|
||||
['type' => 'text', 'key' => 'name', 'label' => 'Name 2'],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertArrayHasKey('field_2_key', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaRejectsInvalidKeyFormat(): void
|
||||
{
|
||||
[$service] = $this->createServiceWithProduct();
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'text', 'key' => '1invalid', 'label' => 'Bad key'],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertArrayHasKey('field_1_key', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaRejectsUnknownType(): void
|
||||
{
|
||||
[$service] = $this->createServiceWithProduct();
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'unknown_type', 'key' => 'field', 'label' => 'Field'],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertArrayHasKey('field_1_type', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaRejectsMoreThan50Fields(): void
|
||||
{
|
||||
[$service] = $this->createServiceWithProduct();
|
||||
|
||||
$fields = [];
|
||||
for ($i = 0; $i < 51; $i++) {
|
||||
$fields[] = ['type' => 'text', 'key' => 'field_' . $i, 'label' => 'Field ' . $i];
|
||||
}
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', $fields);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertArrayHasKey('schema', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaRejectsSelectWithNoOptions(): void
|
||||
{
|
||||
[$service] = $this->createServiceWithProduct();
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'select', 'key' => 'choice', 'label' => 'Choice', 'options' => []],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertArrayHasKey('field_1_options', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaAllowsHeadingWithoutKey(): void
|
||||
{
|
||||
[$service, $repository] = $this->createServiceWithProduct();
|
||||
$repository->expects($this->once())
|
||||
->method('updateHandoverSchema')
|
||||
->willReturn(true);
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'heading', 'label' => 'Section Title'],
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaIncrementsVersion(): void
|
||||
{
|
||||
$existingSchema = json_encode(['version' => 3, 'fields' => []], JSON_THROW_ON_ERROR);
|
||||
[$service, $repository] = $this->createServiceWithProduct([
|
||||
'handover_protocol_schema' => $existingSchema,
|
||||
]);
|
||||
|
||||
$repository->expects($this->once())
|
||||
->method('updateHandoverSchema')
|
||||
->with('INFO', $this->callback(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
|
||||
return $data['version'] === 4;
|
||||
}))
|
||||
->willReturn(true);
|
||||
|
||||
$result = $service->saveHandoverSchema('INFO', [
|
||||
['type' => 'text', 'key' => 'name', 'label' => 'Name'],
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSaveHandoverSchemaRejectsMissingProduct(): void
|
||||
{
|
||||
$repository = $this->createMock(SoftwareProductRepository::class);
|
||||
$repository->method('findByCode')->willReturn(null);
|
||||
|
||||
$service = $this->createService($repository);
|
||||
$result = $service->saveHandoverSchema('NONEXISTENT', [
|
||||
['type' => 'text', 'key' => 'name', 'label' => 'Name'],
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertArrayHasKey('schema', $result['errors']);
|
||||
}
|
||||
|
||||
public function testListPagedDelegatesToRepository(): void
|
||||
{
|
||||
$expected = ['total' => 2, 'rows' => [['code' => 'A'], ['code' => 'B']]];
|
||||
|
||||
Reference in New Issue
Block a user