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>
140 lines
4.7 KiB
PHP
140 lines
4.7 KiB
PHP
<?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,
|
|
));
|
|
}
|
|
}
|