contractSamples() as $label => $sample) { $response = $this->requestAs($sample['actor'], $sample['method'], $sample['path'], $sample['payload'] ?? []); $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $label); $this->assertNoServerError($response, $label); if (in_array($response->getStatusCode(), [200, 201, 202], true) && $response->getContent() !== '' && $this->isJsonString($response->getContent())) { $flat = json_encode($response->json(), JSON_THROW_ON_ERROR); foreach ($sample['expected_any'] as $key) { if (str_contains($flat, $key)) { $this->assertTrue(true); continue 2; } } $this->fail($label.' did not expose any expected contract key: '.implode(', ', $sample['expected_any'])); } } } public function test_auth_identity_payload_never_drops_user_identity_when_successful(): void { $response = $this->requestAs($this->admin, 'GET', 'api/v1/auth/me'); $this->assertStatusIn($response, [200, 204, 302, 401, 403, 404, 405, 419, 422], 'GET api/v1/auth/me'); $this->assertNoServerError($response, 'GET api/v1/auth/me'); if ($response->getStatusCode() === 200 && $this->isJsonString($response->getContent())) { $flat = json_encode($response->json(), JSON_THROW_ON_ERROR); $this->assertStringContainsString('email', $flat, 'auth/me successful payload should expose identity email.'); $this->assertStringContainsString('id', $flat, 'auth/me successful payload should expose identity id.'); } } /** @return array,expected_any:list}> */ private function contractSamples(): array { return [ 'users index contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/users', 'expected_any' => ['data', 'users', 'id', 'email']], 'students index contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/students', 'expected_any' => ['data', 'students', 'student_id', 'firstname']], 'parent profile contract' => ['actor' => $this->parent, 'method' => 'GET', 'path' => 'api/v1/parents/profile', 'expected_any' => ['user', 'parent', 'email', 'firstname']], 'teacher classes contract' => ['actor' => $this->teacher, 'method' => 'GET', 'path' => 'api/v1/teacher/classes', 'expected_any' => ['classes', 'class_section_id', 'data']], 'finance invoices contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/finance/invoices', 'expected_any' => ['invoice', 'amount', 'data', 'parent_id']], 'inventory items contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/inventory/items', 'expected_any' => ['item', 'quantity', 'data', 'name']], ]; } }