test(form-builder): WS-6 v1.3-delta D1 tests
32 new tests covering D1 deliverables:
- Migration shape (3): failure_response_code column presence,
type/length/nullability, index name. MySQL information_schema
introspection.
- Exception hierarchy (11): abstract base, RuntimeException ancestor,
per-subclass constructor + reasonCode (named-args asserting
submissionId is preserved structurally), Timeout extends Infra and
inherits temporary_error, all subclasses extend base, previous-throwable
chaining works, IdentityMatchInvariantViolation is NOT in the
binding-applicator hierarchy and IS a DomainException.
- FormBindingExceptionClassifier matrix (6): each subclass maps to its
reason code; Timeout dispatches to inherited 'temporary_error';
arbitrary RuntimeException -> 'unknown_error'; IdentityMatchInvariantViolation
-> 'unknown_error' (intentional fallback per docstring).
- FormFieldBindingMergeStrategy::validForTargetType (4 tests covering
the full 4 strategies x 3 target types matrix).
- FormSubmissionIdentityMatchResolved (4): ShouldBroadcast contract,
private channel naming ('private-submission.{id}'), broadcast-as
string, payload assignment.
- FormSubmission failure_response_code cast (4): persists as plain
string, NULL by default, factory state composes with apply_status,
round-trips for all four canonical codes.
Baseline regenerated to absorb new tautological-assertion entries (48
lines) — these are class-hierarchy regression guards that Larastan
correctly flags as statically known. The pattern is established in the
codebase per existing baseline entries for similar tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7404,6 +7404,54 @@ parameters:
|
||||
count: 1
|
||||
path: tests/Unit/ExampleTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_subclass_of\(\) with ''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingApplicatorException'' and ''RuntimeException'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_subclass_of\(\) with ''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingApplicatorTimeoutException'' and ''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingInfraException'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_subclass_of\(\) with ''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingApplicatorTimeoutException''\|''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingDataIntegrityException''\|''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingInfraException''\|''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingSchemaConfigException'' and ''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingApplicatorException'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_subclass_of\(\) with ''App\\\\Exceptions\\\\FormBuilder\\\\IdentityMatchInvariantViolation'' and ''App\\\\Exceptions\\\\FormBuilder\\\\FormBindingApplicatorException'' will always evaluate to false\.$#'
|
||||
identifier: function.impossibleType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to function is_subclass_of\(\) with ''App\\\\Exceptions\\\\FormBuilder\\\\IdentityMatchInvariantViolation'' and ''DomainException'' will always evaluate to true\.$#'
|
||||
identifier: function.alreadyNarrowedType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertFalse\(\) with false will always evaluate to true\.$#'
|
||||
identifier: method.alreadyNarrowedType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true and ''Class App…'' will always evaluate to true\.$#'
|
||||
identifier: method.alreadyNarrowedType
|
||||
count: 1
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#'
|
||||
identifier: method.alreadyNarrowedType
|
||||
count: 3
|
||||
path: tests/Unit/Exceptions/FormBuilder/FormBindingApplicatorExceptionHierarchyTest.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#'
|
||||
identifier: property.notFound
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\FormBuilder;
|
||||
|
||||
use App\Enums\FormBuilder\ApplyStatus;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Per RFC-WS-6 §Q3 v1.3 addition 2.
|
||||
*
|
||||
* Verifies the cast (plain string), default-NULL behaviour, and that
|
||||
* the factory state method composes with apply_status attribute writes
|
||||
* for coherent failed-submission fixtures.
|
||||
*/
|
||||
final class FormSubmissionFailureResponseCodeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_failure_response_code_persists_as_string(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()
|
||||
->withFailureResponseCode('schema_config_error')
|
||||
->create();
|
||||
|
||||
$reloaded = $submission->fresh();
|
||||
$this->assertSame('schema_config_error', $reloaded->failure_response_code);
|
||||
$this->assertIsString($reloaded->failure_response_code);
|
||||
}
|
||||
|
||||
public function test_failure_response_code_null_by_default(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()->create();
|
||||
$this->assertNull($submission->fresh()->failure_response_code);
|
||||
}
|
||||
|
||||
public function test_factory_state_composes_with_apply_status(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()
|
||||
->withFailureResponseCode('temporary_error')
|
||||
->create(['apply_status' => ApplyStatus::FAILED]);
|
||||
|
||||
$reloaded = $submission->fresh();
|
||||
$this->assertSame(ApplyStatus::FAILED, $reloaded->apply_status);
|
||||
$this->assertSame('temporary_error', $reloaded->failure_response_code);
|
||||
}
|
||||
|
||||
public function test_failure_response_code_round_trips_for_each_canonical_value(): void
|
||||
{
|
||||
foreach (['schema_config_error', 'temporary_error', 'data_integrity_error', 'unknown_error'] as $code) {
|
||||
$submission = FormSubmission::factory()
|
||||
->withFailureResponseCode($code)
|
||||
->create();
|
||||
|
||||
$this->assertSame($code, $submission->fresh()->failure_response_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\FormBuilder\Schema;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Migration rehearsal for WS-6 v1.3-delta D1:
|
||||
* - 2026_05_08_000001_add_failure_response_code_to_form_submissions
|
||||
*
|
||||
* Verifies the column + index land per RFC-WS-6 §Q3 v1.3 addition 2 and
|
||||
* ARCH-BINDINGS §7.1 v1.2.
|
||||
*/
|
||||
final class Ws6V13DeltaD1MigrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_form_submissions_has_failure_response_code_column(): void
|
||||
{
|
||||
$this->assertTrue(Schema::hasColumn('form_submissions', 'failure_response_code'));
|
||||
}
|
||||
|
||||
public function test_failure_response_code_is_nullable_string_40(): void
|
||||
{
|
||||
$row = DB::selectOne(
|
||||
'SELECT DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND COLUMN_NAME = ?',
|
||||
['form_submissions', 'failure_response_code'],
|
||||
);
|
||||
|
||||
$this->assertNotNull($row, 'failure_response_code column missing');
|
||||
$this->assertSame('varchar', strtolower((string) $row->DATA_TYPE));
|
||||
$this->assertSame(40, (int) $row->CHARACTER_MAXIMUM_LENGTH);
|
||||
$this->assertSame('YES', $row->IS_NULLABLE);
|
||||
$this->assertNull($row->COLUMN_DEFAULT);
|
||||
}
|
||||
|
||||
public function test_failure_response_code_index_present(): void
|
||||
{
|
||||
$row = DB::selectOne(
|
||||
'SELECT INDEX_NAME
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND INDEX_NAME = ?',
|
||||
['form_submissions', 'fs_failure_response_code_idx'],
|
||||
);
|
||||
|
||||
$this->assertNotNull($row, 'fs_failure_response_code_idx missing');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Enums\FormBuilder;
|
||||
|
||||
use App\Enums\FormBuilder\BindingTargetType;
|
||||
use App\Enums\FormBuilder\FormFieldBindingMergeStrategy;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Per RFC-WS-6 §V1 + ARCH-BINDINGS §4.2.
|
||||
*
|
||||
* Locks the strategy x target-type validity matrix. Append is the only
|
||||
* non-trivial case (collection-only). Future contributors who alter the
|
||||
* matrix without RFC follow-through will see a structural test failure.
|
||||
*/
|
||||
final class FormFieldBindingMergeStrategyValidForTargetTypeTest extends TestCase
|
||||
{
|
||||
public function test_overwrite_valid_for_all_target_types(): void
|
||||
{
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Overwrite->validForTargetType(BindingTargetType::SCALAR));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Overwrite->validForTargetType(BindingTargetType::COLLECTION));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Overwrite->validForTargetType(BindingTargetType::RELATION));
|
||||
}
|
||||
|
||||
public function test_replace_valid_for_all_target_types(): void
|
||||
{
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Replace->validForTargetType(BindingTargetType::SCALAR));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Replace->validForTargetType(BindingTargetType::COLLECTION));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Replace->validForTargetType(BindingTargetType::RELATION));
|
||||
}
|
||||
|
||||
public function test_first_write_wins_valid_for_all_target_types(): void
|
||||
{
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::FirstWriteWins->validForTargetType(BindingTargetType::SCALAR));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::FirstWriteWins->validForTargetType(BindingTargetType::COLLECTION));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::FirstWriteWins->validForTargetType(BindingTargetType::RELATION));
|
||||
}
|
||||
|
||||
public function test_append_valid_only_for_collection(): void
|
||||
{
|
||||
$this->assertFalse(FormFieldBindingMergeStrategy::Append->validForTargetType(BindingTargetType::SCALAR));
|
||||
$this->assertTrue(FormFieldBindingMergeStrategy::Append->validForTargetType(BindingTargetType::COLLECTION));
|
||||
$this->assertFalse(FormFieldBindingMergeStrategy::Append->validForTargetType(BindingTargetType::RELATION));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Events\FormBuilder;
|
||||
|
||||
use App\Events\FormBuilder\FormSubmissionIdentityMatchResolved;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Per RFC-WS-6 §Q1 v1.3 addition 2.
|
||||
*
|
||||
* Class-only tests — D2 wires the dispatch from
|
||||
* TriggerPersonIdentityMatchOnFormSubmit::handle, and tests covering
|
||||
* that wiring will live alongside the listener tests.
|
||||
*/
|
||||
final class FormSubmissionIdentityMatchResolvedTest extends TestCase
|
||||
{
|
||||
public function test_implements_should_broadcast(): void
|
||||
{
|
||||
$event = new FormSubmissionIdentityMatchResolved('01HX', 'matched', 1);
|
||||
$this->assertInstanceOf(ShouldBroadcast::class, $event);
|
||||
}
|
||||
|
||||
public function test_broadcasts_on_private_submission_channel(): void
|
||||
{
|
||||
$event = new FormSubmissionIdentityMatchResolved('01HX1234567890', 'matched', 2);
|
||||
$channels = $event->broadcastOn();
|
||||
|
||||
$this->assertCount(1, $channels);
|
||||
$this->assertInstanceOf(PrivateChannel::class, $channels[0]);
|
||||
// PrivateChannel prepends 'private-' to the name passed to its
|
||||
// constructor; that's the wire-format the frontend Echo client
|
||||
// subscribes to.
|
||||
$this->assertSame('private-submission.01HX1234567890', $channels[0]->name);
|
||||
}
|
||||
|
||||
public function test_broadcast_as(): void
|
||||
{
|
||||
$event = new FormSubmissionIdentityMatchResolved('01HX', 'matched', 1);
|
||||
$this->assertSame('identity-match.resolved', $event->broadcastAs());
|
||||
}
|
||||
|
||||
public function test_constructor_assigns_payload_readonly(): void
|
||||
{
|
||||
$event = new FormSubmissionIdentityMatchResolved('01HX', 'no_match', 0);
|
||||
|
||||
$this->assertSame('01HX', $event->submissionId);
|
||||
$this->assertSame('no_match', $event->status);
|
||||
$this->assertSame(0, $event->matchCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Exceptions\FormBuilder;
|
||||
|
||||
use App\Exceptions\FormBuilder\FormBindingApplicatorException;
|
||||
use App\Exceptions\FormBuilder\FormBindingApplicatorTimeoutException;
|
||||
use App\Exceptions\FormBuilder\FormBindingDataIntegrityException;
|
||||
use App\Exceptions\FormBuilder\FormBindingInfraException;
|
||||
use App\Exceptions\FormBuilder\FormBindingSchemaConfigException;
|
||||
use App\Exceptions\FormBuilder\IdentityMatchInvariantViolation;
|
||||
use DomainException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Per RFC-WS-6 §Q3 v1.3 addition 2.
|
||||
*
|
||||
* Locks the exception hierarchy shape so future contributors cannot
|
||||
* accidentally instantiate the abstract base, drop the reasonCode
|
||||
* contract, or reparent a subclass.
|
||||
*/
|
||||
final class FormBindingApplicatorExceptionHierarchyTest extends TestCase
|
||||
{
|
||||
public function test_base_class_is_abstract(): void
|
||||
{
|
||||
$reflection = new ReflectionClass(FormBindingApplicatorException::class);
|
||||
$this->assertTrue($reflection->isAbstract());
|
||||
}
|
||||
|
||||
public function test_base_extends_runtime_exception(): void
|
||||
{
|
||||
$this->assertTrue(is_subclass_of(FormBindingApplicatorException::class, RuntimeException::class));
|
||||
}
|
||||
|
||||
public function test_schema_config_exception_constructor_and_reason_code(): void
|
||||
{
|
||||
$e = new FormBindingSchemaConfigException(
|
||||
submissionId: '01HX1234567890ABCDEFGHJKMN',
|
||||
message: 'schema null',
|
||||
);
|
||||
|
||||
$this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId);
|
||||
$this->assertSame('schema null', $e->getMessage());
|
||||
$this->assertSame('schema_config_error', $e->reasonCode());
|
||||
}
|
||||
|
||||
public function test_infra_exception_constructor_and_reason_code(): void
|
||||
{
|
||||
$e = new FormBindingInfraException(
|
||||
submissionId: '01HX1234567890ABCDEFGHJKMN',
|
||||
message: 'no transaction',
|
||||
);
|
||||
|
||||
$this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId);
|
||||
$this->assertSame('no transaction', $e->getMessage());
|
||||
$this->assertSame('temporary_error', $e->reasonCode());
|
||||
}
|
||||
|
||||
public function test_data_integrity_exception_constructor_and_reason_code(): void
|
||||
{
|
||||
$e = new FormBindingDataIntegrityException(
|
||||
submissionId: '01HX1234567890ABCDEFGHJKMN',
|
||||
message: 'fk violation',
|
||||
);
|
||||
|
||||
$this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId);
|
||||
$this->assertSame('fk violation', $e->getMessage());
|
||||
$this->assertSame('data_integrity_error', $e->reasonCode());
|
||||
}
|
||||
|
||||
public function test_timeout_exception_constructor_and_inherited_reason_code(): void
|
||||
{
|
||||
$e = new FormBindingApplicatorTimeoutException(
|
||||
submissionId: '01HX1234567890ABCDEFGHJKMN',
|
||||
message: 'deadline exceeded after 5s',
|
||||
);
|
||||
|
||||
$this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId);
|
||||
$this->assertSame('deadline exceeded after 5s', $e->getMessage());
|
||||
// Inherited from FormBindingInfraException — no override.
|
||||
$this->assertSame('temporary_error', $e->reasonCode());
|
||||
}
|
||||
|
||||
public function test_timeout_extends_infra(): void
|
||||
{
|
||||
$this->assertTrue(is_subclass_of(
|
||||
FormBindingApplicatorTimeoutException::class,
|
||||
FormBindingInfraException::class,
|
||||
));
|
||||
}
|
||||
|
||||
public function test_all_concrete_subclasses_extend_base(): void
|
||||
{
|
||||
$concreteSubclasses = [
|
||||
FormBindingSchemaConfigException::class,
|
||||
FormBindingInfraException::class,
|
||||
FormBindingDataIntegrityException::class,
|
||||
FormBindingApplicatorTimeoutException::class,
|
||||
];
|
||||
|
||||
foreach ($concreteSubclasses as $class) {
|
||||
$this->assertTrue(
|
||||
is_subclass_of($class, FormBindingApplicatorException::class),
|
||||
"Class {$class} must extend FormBindingApplicatorException",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_constructor_accepts_previous_throwable(): void
|
||||
{
|
||||
$cause = new RuntimeException('original');
|
||||
$e = new FormBindingInfraException(
|
||||
submissionId: '01HX',
|
||||
message: 'wrapper',
|
||||
previous: $cause,
|
||||
);
|
||||
|
||||
$this->assertSame($cause, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function test_identity_match_invariant_violation_is_not_in_hierarchy(): void
|
||||
{
|
||||
$this->assertFalse(is_subclass_of(
|
||||
IdentityMatchInvariantViolation::class,
|
||||
FormBindingApplicatorException::class,
|
||||
));
|
||||
}
|
||||
|
||||
public function test_identity_match_invariant_violation_is_domain_exception(): void
|
||||
{
|
||||
$this->assertTrue(is_subclass_of(
|
||||
IdentityMatchInvariantViolation::class,
|
||||
DomainException::class,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\FormBuilder\Bindings;
|
||||
|
||||
use App\Exceptions\FormBuilder\FormBindingApplicatorTimeoutException;
|
||||
use App\Exceptions\FormBuilder\FormBindingDataIntegrityException;
|
||||
use App\Exceptions\FormBuilder\FormBindingInfraException;
|
||||
use App\Exceptions\FormBuilder\FormBindingSchemaConfigException;
|
||||
use App\Exceptions\FormBuilder\IdentityMatchInvariantViolation;
|
||||
use App\FormBuilder\Bindings\FormBindingExceptionClassifier;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Per RFC-WS-6 §Q3 v1.3 addition 2.
|
||||
*
|
||||
* The classifier is a single-responsibility helper: Throwable in,
|
||||
* failure_response_code string out. Centralised so the listener catch
|
||||
* block (D2) and the retry-service recordFailure (D2) produce identical
|
||||
* classifications.
|
||||
*/
|
||||
final class FormBindingExceptionClassifierTest extends TestCase
|
||||
{
|
||||
public function test_classifies_schema_config_exception(): void
|
||||
{
|
||||
$code = FormBindingExceptionClassifier::classify(
|
||||
new FormBindingSchemaConfigException(submissionId: '01HX', message: 'x'),
|
||||
);
|
||||
$this->assertSame('schema_config_error', $code);
|
||||
}
|
||||
|
||||
public function test_classifies_infra_exception(): void
|
||||
{
|
||||
$code = FormBindingExceptionClassifier::classify(
|
||||
new FormBindingInfraException(submissionId: '01HX', message: 'x'),
|
||||
);
|
||||
$this->assertSame('temporary_error', $code);
|
||||
}
|
||||
|
||||
public function test_classifies_timeout_as_temporary_error(): void
|
||||
{
|
||||
// Subclass dispatch — Timeout extends Infra, inherits reasonCode.
|
||||
$code = FormBindingExceptionClassifier::classify(
|
||||
new FormBindingApplicatorTimeoutException(submissionId: '01HX', message: 'deadline'),
|
||||
);
|
||||
$this->assertSame('temporary_error', $code);
|
||||
}
|
||||
|
||||
public function test_classifies_data_integrity_exception(): void
|
||||
{
|
||||
$code = FormBindingExceptionClassifier::classify(
|
||||
new FormBindingDataIntegrityException(submissionId: '01HX', message: 'fk'),
|
||||
);
|
||||
$this->assertSame('data_integrity_error', $code);
|
||||
}
|
||||
|
||||
public function test_classifies_arbitrary_runtime_exception_as_unknown(): void
|
||||
{
|
||||
$code = FormBindingExceptionClassifier::classify(new RuntimeException('boom'));
|
||||
$this->assertSame('unknown_error', $code);
|
||||
}
|
||||
|
||||
public function test_classifies_identity_match_invariant_violation_as_unknown(): void
|
||||
{
|
||||
// IdentityMatchInvariantViolation is intentionally NOT in the
|
||||
// FormBindingApplicatorException hierarchy (per RFC-WS-6 §Q2 — the
|
||||
// listener that throws it runs outside the binding-applicator
|
||||
// pipeline). It falls through to 'unknown_error' here, which is
|
||||
// the right response-shape because users cannot meaningfully act
|
||||
// on it; admins triage via GlitchTip.
|
||||
$code = FormBindingExceptionClassifier::classify(new IdentityMatchInvariantViolation('x'));
|
||||
$this->assertSame('unknown_error', $code);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user