Files
api/services/nginx/app/tests/Unit/Selfserve/SelfserveConfigVersioningTest.php
T
Jeppe Bundgaard 5875371d13 Add attachment payload handling and tests for self-serve tasks
- Introduced `selfserve_task_attachment_payloads` class for managing task attachments, including formatting and download URL generation.
- Added unit and API tests to validate attachment handling in self-serve tasks and customer-scoped workflows.
- Enhanced wash start simulation and studio graph projections to integrate task attachment data.
2026-04-29 16:03:03 +02:00

379 lines
13 KiB
PHP

<?php
app_require('modules/selfserve/classes/selfserve_config_versioning.php');
app_require('objects/selfserve_config_versions_o.php');
use modules\selfserve\classes\selfserve_config_versioning;
use objects\selfserve_config_versions_o;
function selfserve_config_versioning_without_constructor(): selfserve_config_versioning
{
$reflection = new ReflectionClass(selfserve_config_versioning::class);
return $reflection->newInstanceWithoutConstructor();
}
it('validates typed task gates for known references', function (): void {
$service = selfserve_config_versioning_without_constructor();
$validation = $service->validateConfig([
'questions' => [
['id' => 10],
],
'conditions' => [
['id' => 20],
],
'rules' => [],
'tasks' => [
[
'id' => 100,
'gate_type' => 'ALWAYS',
'gate_ref_id' => null,
],
[
'id' => 101,
'gate_type' => 'CONDITION',
'gate_ref_id' => 20,
],
[
'id' => 102,
'gate_type' => 'QUESTION',
'gate_ref_id' => 10,
],
],
]);
expect($validation['valid'])->toBeTrue();
expect($validation['errors'])->toBe([]);
});
it('fails validation when typed task gates reference unknown entities', function (): void {
$service = selfserve_config_versioning_without_constructor();
$validation = $service->validateConfig([
'questions' => [
['id' => 1],
],
'conditions' => [
['id' => 2],
],
'rules' => [],
'tasks' => [
[
'id' => 200,
'gate_type' => 'CONDITION',
'gate_ref_id' => 999,
],
[
'id' => 201,
'gate_type' => 'QUESTION',
'gate_ref_id' => 998,
],
[
'id' => 202,
'gate_type' => 'QUESTION',
'gate_ref_id' => null,
],
],
]);
expect($validation['valid'])->toBeFalse();
expect(implode("\n", $validation['errors']))->toContain('unknown condition gate_ref_id 999');
expect(implode("\n", $validation['errors']))->toContain('unknown question gate_ref_id 998');
expect(implode("\n", $validation['errors']))->toContain('requires gate_ref_id');
});
it('fails validation when nested conditions form cycles', function (): void {
$service = selfserve_config_versioning_without_constructor();
$validation = $service->validateConfig([
'questions' => [],
'conditions' => [
['id' => 10, 'condition_id' => 12],
['id' => 11, 'condition_id' => 10],
['id' => 12, 'condition_id' => 11],
],
'rules' => [],
'tasks' => [],
]);
expect($validation['valid'])->toBeFalse();
expect(implode("\n", $validation['errors']))->toContain('Condition cycle detected');
});
it('migrates legacy AND and OR rules into grouped v2 condition expressions', function (): void {
$service = selfserve_config_versioning_without_constructor();
$config = $service->migrateLegacyConfigToV2([
'department_id' => 6,
'questions' => [
['id' => 1],
['id' => 2],
['id' => 3],
],
'conditions' => [
['id' => 10, 'name' => 'Ready'],
],
'rules' => [
['id' => 100, 'condition_id' => 10, 'type' => 'IS_TRUE', 'object_type' => 'question', 'object_id' => 1],
['id' => 101, 'condition_id' => 10, 'type' => 'IS_TRUE_OR_ANY_TRUE', 'object_type' => 'question', 'object_id' => 2],
['id' => 102, 'condition_id' => 10, 'type' => 'IS_TRUE_OR_ANY_TRUE', 'object_type' => 'question', 'object_id' => 3],
],
'tasks' => [],
]);
$expression = $config['conditions'][0]['expression'];
expect($config['schema_version'])->toBe(2);
expect($config['rules'])->toBe([]);
expect($expression['operator'])->toBe('ALL');
expect($expression['children'][0])->toMatchArray([
'type' => 'predicate',
'subject_type' => 'question',
'subject_id' => 1,
'operator' => 'IS_TRUE',
]);
expect($expression['children'][1]['operator'])->toBe('ANY');
expect(array_column($expression['children'][1]['children'], 'subject_id'))->toBe([2, 3]);
});
it('repairs legacy-defaulted always task gates during v2 normalization', function (): void {
$service = selfserve_config_versioning_without_constructor();
$config = $service->migrateLegacyConfigToV2([
'department_id' => 6,
'questions' => [
['id' => 1],
],
'conditions' => [
['id' => 20, 'name' => 'Machine wash allowed'],
],
'rules' => [
['id' => 100, 'condition_id' => 20, 'type' => 'IS_TRUE', 'object_type' => 'question', 'object_id' => 1],
],
'tasks' => [
[
'id' => 200,
'condition_id' => 20,
'gate_type' => 'ALWAYS',
'gate_ref_id' => null,
],
],
]);
expect($config['tasks'][0]['gate_type'])->toBe('CONDITION');
expect($config['tasks'][0]['gate_ref_id'])->toBe(20);
expect($service->validateConfig($config)['valid'])->toBeTrue();
});
it('does not let legacy-defaulted always task gates bypass validation', function (): void {
$service = selfserve_config_versioning_without_constructor();
$validation = $service->validateConfig([
'schema_version' => 2,
'questions' => [
['id' => 1],
],
'conditions' => [
[
'id' => 20,
'expression' => [
'type' => 'predicate',
'subject_type' => 'question',
'subject_id' => 1,
'operator' => 'IS_TRUE',
],
],
],
'tasks' => [
[
'id' => 201,
'condition_id' => 999,
'gate_type' => 'ALWAYS',
'gate_ref_id' => null,
],
],
]);
expect($validation['valid'])->toBeFalse();
expect(implode("\n", $validation['errors']))->toContain('unknown question gate_ref_id 999');
});
it('rejects unsupported legacy task-target rules after migration', function (): void {
$service = selfserve_config_versioning_without_constructor();
$config = $service->migrateLegacyConfigToV2([
'department_id' => 6,
'questions' => [['id' => 1]],
'conditions' => [['id' => 10, 'name' => 'Ready']],
'rules' => [
['id' => 100, 'condition_id' => 10, 'type' => 'IS_TRUE', 'object_type' => 'task', 'object_id' => 50],
],
'tasks' => [['id' => 50]],
]);
$validation = $service->validateConfig($config);
expect($validation['valid'])->toBeFalse();
expect(implode("\n", $validation['errors']))->toContain('unsupported object_type `task`');
});
it('validates v2 expressions for empty used conditions, missing refs, invalid operators, and cycles', function (): void {
$service = selfserve_config_versioning_without_constructor();
$validation = $service->validateConfig([
'schema_version' => 2,
'questions' => [
['id' => 1, 'condition_id' => 10],
],
'conditions' => [
['id' => 10, 'expression' => ['type' => 'group', 'operator' => 'ALL', 'children' => []]],
[
'id' => 20,
'expression' => [
'type' => 'group',
'operator' => 'ALL',
'children' => [
['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 999, 'operator' => 'IS_TRUE'],
['type' => 'predicate', 'subject_type' => 'condition', 'subject_id' => 30, 'operator' => 'IS_TRUE'],
],
],
],
[
'id' => 30,
'expression' => [
'type' => 'group',
'operator' => 'ALL',
'children' => [
['type' => 'predicate', 'subject_type' => 'condition', 'subject_id' => 20, 'operator' => 'NOPE'],
],
],
],
],
'tasks' => [
['id' => 100, 'gate_type' => 'CONDITION', 'gate_ref_id' => 10],
],
]);
$errors = implode("\n", $validation['errors']);
expect($validation['valid'])->toBeFalse();
expect($errors)->toContain('Condition 10 is used but has an empty expression.');
expect($errors)->toContain('unknown question predicate subject_id 999');
expect($errors)->toContain('invalid predicate operator `NOPE`');
expect($errors)->toContain('Condition cycle detected');
});
it('validates nested v2 branch and case expressions', function (): void {
$service = selfserve_config_versioning_without_constructor();
$validation = $service->validateConfig([
'schema_version' => 2,
'questions' => [
['id' => 1],
['id' => 2],
['id' => 3],
],
'conditions' => [
[
'id' => 10,
'expression' => [
'type' => 'branch',
'branches' => [
[
'kind' => 'if',
'when' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 1, 'operator' => 'IS_TRUE'],
'then' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 2, 'operator' => 'IS_TRUE'],
],
[
'kind' => 'else',
'else' => true,
'then' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 3, 'operator' => 'IS_FALSE_OR_NOT_SET'],
],
],
],
],
[
'id' => 20,
'expression' => [
'type' => 'case',
'subject_type' => 'condition',
'subject_id' => 10,
'cases' => [
[
'value' => true,
'then' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 2, 'operator' => 'IS_TRUE'],
],
[
'value' => false,
'then' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 3, 'operator' => 'IS_FALSE'],
],
],
],
],
],
'tasks' => [
['id' => 100, 'gate_type' => 'CONDITION', 'gate_ref_id' => 20],
],
]);
expect($validation['valid'])->toBeTrue();
expect($validation['errors'])->toBe([]);
});
it('encodes config json payloads with apostrophes before persistence', function (): void {
$version = new class extends selfserve_config_versions_o {
/** @var array<string,mixed> */
public array $capturedPayload = [];
public function __construct()
{
// Skip db bootstrap for this unit test.
}
public function add_object(array $data): int
{
$this->capturedPayload = $data;
return 123;
}
public function getObjectProperties(): void
{
// No-op for this unit test.
}
public function objectChanged(): void
{
// No-op for this unit test.
}
};
$version->add(
42,
selfserve_config_versioning::STATUS_DRAFT,
1,
[
'questions' => [
[
'id' => 1,
'question' => "Driver's side check",
],
],
'conditions' => [],
'rules' => [],
'tasks' => [],
],
[
'valid' => true,
'errors' => [],
'warnings' => [],
],
);
$configJson = $version->capturedPayload['config_json'] ?? null;
$validationJson = $version->capturedPayload['validation_result_json'] ?? null;
expect($configJson)->toBeString();
expect($validationJson)->toBeString();
expect(json_decode($configJson, true)['questions'][0]['question'] ?? null)->toBe("Driver's side check");
expect(json_decode($validationJson, true)['valid'] ?? null)->toBeTrue();
});