| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Integration\Public;
- use App\Tests\Integration\Support\AppTestCase;
- /**
- * SEC_REVIEW F58 — `/api/docs` loads the RapiDoc viewer from jsDelivr.
- * The `<script>` tag must carry an SRI `integrity` hash and
- * `crossorigin="anonymous"` so the browser refuses to execute the JS
- * if a CDN compromise or in-flight content modification serves
- * different bytes.
- *
- * Also covers SEC_REVIEW F68 — the docs routes are gated behind
- * `API_DOCS_PUBLIC` (default off). Tests that reach the routes
- * enable the flag and rebuild the app; the dedicated F68 cases
- * leave it off and assert 404.
- */
- final class DocsControllerTest extends AppTestCase
- {
- public function testDocsPageEmbedsRapiDocWithSriIntegrity(): void
- {
- $this->enableDocs();
- $resp = $this->request('GET', '/api/docs');
- self::assertSame(200, $resp->getStatusCode());
- self::assertStringContainsString('text/html', $resp->getHeaderLine('Content-Type'));
- $html = (string) $resp->getBody();
- // Script tag points at the locked RapiDoc version.
- self::assertStringContainsString(
- 'https://cdn.jsdelivr.net/npm/rapidoc@9.3.4/dist/rapidoc-min.js',
- $html,
- );
- // Integrity hash present and well-formed (sha384- + base64).
- self::assertMatchesRegularExpression(
- '/integrity="sha384-[A-Za-z0-9+\/=]{64}"/',
- $html,
- 'expected sha384 SRI on the rapidoc <script> tag',
- );
- // crossorigin=anonymous required alongside SRI for cross-origin scripts.
- self::assertStringContainsString('crossorigin="anonymous"', $html);
- }
- public function testOpenapiSpecIsServed(): void
- {
- // Sanity check that we didn't break the spec endpoint while
- // editing the controller.
- $this->enableDocs();
- $resp = $this->request('GET', '/api/v1/openapi.yaml');
- // The spec file isn't shipped in tests; either 200 with content
- // or 500 with `spec_unavailable`. Both are acceptable here —
- // we just want to prove the route still wires through the
- // edited controller.
- self::assertContains($resp->getStatusCode(), [200, 500]);
- }
- public function testDocsPageIs404ByDefault(): void
- {
- // SEC_REVIEW F68: `/api/docs` is gated behind
- // `API_DOCS_PUBLIC`. AppTestCase builds the container with
- // the env var unset, so the default (false) applies and the
- // route is never registered → Slim returns 404.
- $resp = $this->request('GET', '/api/docs');
- self::assertSame(404, $resp->getStatusCode());
- }
- public function testOpenapiSpecIs404ByDefault(): void
- {
- $resp = $this->request('GET', '/api/v1/openapi.yaml');
- self::assertSame(404, $resp->getStatusCode());
- }
- /**
- * Re-bind `settings.api_docs_public` to `true` and rebuild the app
- * so the docs routes are registered. Same pattern
- * `JobsAdminControllerTest::testRefreshGeoip412UnderMaxmindWithoutKey`
- * uses to swap a binding mid-test.
- */
- private function enableDocs(): void
- {
- if (method_exists($this->container, 'set')) {
- /** @var \DI\Container $c */
- $c = $this->container;
- $c->set('settings.api_docs_public', true);
- $this->app = \App\App\AppFactory::build($this->container);
- }
- }
- }
|