5e35fefd69
API CI/CD / Validate (composer + pint) (push) Successful in 2m47s
API CI/CD / Test (PHPUnit) (push) Failing after 3m8s
API CI/CD / Build frontend assets (push) Failing after 5m22s
API CI/CD / Security audit (push) Failing after 34s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
82 lines
4.2 KiB
PHP
82 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api\V1\FullSurface;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
|
|
|
class ApiDeepStateTransitionInvariantContractTest extends FullSurfaceE2EContractCase
|
|
{
|
|
public function test_school_year_close_reopen_and_promotion_transition_routes_preserve_controlled_state(): void
|
|
{
|
|
$schoolYearId = (int) $this->ids['schoolYear'];
|
|
$checks = [
|
|
['POST', "/api/v1/school-years/$schoolYearId/validate-close", []],
|
|
['POST', "/api/v1/school-years/$schoolYearId/preview-close", []],
|
|
['POST', "/api/v1/school-years/$schoolYearId/close", ['confirm' => true]],
|
|
['POST', "/api/v1/school-years/$schoolYearId/reopen", ['reason' => 'E2E invariant probe']],
|
|
['GET', '/api/v1/administrator/promotions'],
|
|
['POST', '/api/v1/administrator/promotions/preview', ['school_year' => self::E2E_SCHOOL_YEAR]],
|
|
];
|
|
|
|
$this->assertTransitionChecksAreControlled($checks, 'school year and promotion');
|
|
}
|
|
|
|
public function test_finance_transition_routes_do_not_allow_negative_balance_crashes_or_untracked_state(): void
|
|
{
|
|
$invoiceId = (int) $this->ids['invoiceId'];
|
|
$checks = [
|
|
['POST', '/api/v1/finance/payments', ['invoice_id' => $invoiceId, 'amount' => -100, 'payment_method' => 'cash']],
|
|
['POST', '/api/v1/finance/payments', ['invoice_id' => $invoiceId, 'amount' => 25, 'payment_method' => 'cash']],
|
|
['POST', '/api/v1/finance/refunds', ['invoice_id' => $invoiceId, 'amount' => 999999, 'reason' => 'Over-refund probe']],
|
|
['POST', '/api/v1/finance/installments', ['invoice_id' => $invoiceId, 'amount' => 25, 'due_date' => '2025-11-01']],
|
|
['GET', '/api/v1/finance/payment-followups'],
|
|
];
|
|
|
|
$this->assertTransitionChecksAreControlled($checks, 'finance');
|
|
|
|
$invoice = DB::table('invoices')->where('id', $invoiceId)->first();
|
|
$this->assertNotNull($invoice, 'Finance probes must not delete the invoice fixture just because math got spicy.');
|
|
}
|
|
|
|
public function test_attendance_transition_routes_keep_single_day_student_context_controlled(): void
|
|
{
|
|
$studentId = (int) $this->ids['studentId'];
|
|
$classSectionId = (int) $this->ids['classSectionId'];
|
|
$checks = [
|
|
['POST', '/api/v1/teacher/attendance', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'status' => 'present']],
|
|
['POST', '/api/v1/teacher/attendance', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'status' => 'absent']],
|
|
['POST', '/api/v1/attendance/late-slips', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'arrival_time' => '10:45']],
|
|
['POST', '/api/v1/attendance/early-dismissals', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'dismissal_time' => '12:30']],
|
|
];
|
|
|
|
$this->assertTransitionChecksAreControlled($checks, 'attendance');
|
|
}
|
|
|
|
/** @param list<array{0: string, 1: string, 2?: array<string, mixed>}> $checks */
|
|
private function assertTransitionChecksAreControlled(array $checks, string $domain): void
|
|
{
|
|
$failures = [];
|
|
|
|
foreach ($checks as $check) {
|
|
$method = $check[0];
|
|
$uri = $check[1];
|
|
$payload = $check[2] ?? null;
|
|
$payload ??= $this->payloadFor($method, ltrim($uri, '/'));
|
|
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
|
|
|
if ($response->getStatusCode() >= 500) {
|
|
$failures[] = "$method $uri crashed during $domain transition probe: ".$response->getContent();
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! in_array($response->getStatusCode(), $this->controlledStatuses($method), true)) {
|
|
$failures[] = "$method $uri returned unexpected {$response->getStatusCode()} during $domain transition probe: ".$response->getContent();
|
|
}
|
|
}
|
|
|
|
$this->assertSame([], $failures, ucfirst($domain)." state transitions must be controlled:\n".implode("\n", $failures));
|
|
}
|
|
}
|