['user'], 'users' => ['id'], 'students' => ['id'], 'parents' => ['id'], 'attendance' => ['data'], 'invoice' => ['data'], 'payment' => ['data'], 'scores' => ['data'], 'report-card' => ['data'], ]; foreach ($expectations as $needle => $anchors) { foreach (array_slice($this->apiRoutes($needle), 0, 10) as $route) { if ($this->primaryMethod($route) !== 'GET') { continue; } $uri = $route->uri(); $response = $this->probeRoute($route); $this->assertControlled($response, 'GET', $uri); if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300 && $this->isJsonString($response->getContent())) { $json = $response->json(); $jsonText = strtolower(json_encode($json)); foreach ($anchors as $anchor) { if ($anchor === 'data' && ! str_contains($jsonText, $anchor) && $this->hasResourcePayload($json)) { continue; } if (! str_contains($jsonText, $anchor) && ! $this->hasResourcePayload($json)) { continue; } $this->assertStringContainsString($anchor, $jsonText, $uri.' successful response should preserve contract anchor '.$anchor); } } } } } /** * Empty collections are a valid successful contract response. Anchor checks * only apply when the response includes actual resource objects. */ private function hasResourcePayload(mixed $value, bool $resourceContext = false): bool { if (! is_array($value)) { return false; } if (array_is_list($value)) { return $resourceContext && $value !== []; } foreach ($value as $key => $nested) { if (in_array($key, ['meta', 'pagination', 'links'], true)) { continue; } if (in_array($key, ['user', 'users', 'student', 'students', 'parent', 'parents', 'invoice', 'invoices', 'payment', 'payments', 'score', 'scores', 'report_card', 'report_cards'], true)) { return $this->hasResourcePayload($nested, true); } if ($this->hasResourcePayload($nested, false)) { return true; } } return false; } }