DocsControllerTest.php 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Public;
  4. use App\Tests\Integration\Support\AppTestCase;
  5. /**
  6. * SEC_REVIEW F58 — `/api/docs` loads the RapiDoc viewer from jsDelivr.
  7. * The `<script>` tag must carry an SRI `integrity` hash and
  8. * `crossorigin="anonymous"` so the browser refuses to execute the JS
  9. * if a CDN compromise or in-flight content modification serves
  10. * different bytes.
  11. *
  12. * Also covers SEC_REVIEW F68 — the docs routes are gated behind
  13. * `API_DOCS_PUBLIC` (default off). Tests that reach the routes
  14. * enable the flag and rebuild the app; the dedicated F68 cases
  15. * leave it off and assert 404.
  16. */
  17. final class DocsControllerTest extends AppTestCase
  18. {
  19. public function testDocsPageEmbedsRapiDocWithSriIntegrity(): void
  20. {
  21. $this->enableDocs();
  22. $resp = $this->request('GET', '/api/docs');
  23. self::assertSame(200, $resp->getStatusCode());
  24. self::assertStringContainsString('text/html', $resp->getHeaderLine('Content-Type'));
  25. $html = (string) $resp->getBody();
  26. // Script tag points at the locked RapiDoc version.
  27. self::assertStringContainsString(
  28. 'https://cdn.jsdelivr.net/npm/rapidoc@9.3.4/dist/rapidoc-min.js',
  29. $html,
  30. );
  31. // Integrity hash present and well-formed (sha384- + base64).
  32. self::assertMatchesRegularExpression(
  33. '/integrity="sha384-[A-Za-z0-9+\/=]{64}"/',
  34. $html,
  35. 'expected sha384 SRI on the rapidoc <script> tag',
  36. );
  37. // crossorigin=anonymous required alongside SRI for cross-origin scripts.
  38. self::assertStringContainsString('crossorigin="anonymous"', $html);
  39. }
  40. public function testOpenapiSpecIsServed(): void
  41. {
  42. // Sanity check that we didn't break the spec endpoint while
  43. // editing the controller.
  44. $this->enableDocs();
  45. $resp = $this->request('GET', '/api/v1/openapi.yaml');
  46. // The spec file isn't shipped in tests; either 200 with content
  47. // or 500 with `spec_unavailable`. Both are acceptable here —
  48. // we just want to prove the route still wires through the
  49. // edited controller.
  50. self::assertContains($resp->getStatusCode(), [200, 500]);
  51. }
  52. public function testDocsPageIs404ByDefault(): void
  53. {
  54. // SEC_REVIEW F68: `/api/docs` is gated behind
  55. // `API_DOCS_PUBLIC`. AppTestCase builds the container with
  56. // the env var unset, so the default (false) applies and the
  57. // route is never registered → Slim returns 404.
  58. $resp = $this->request('GET', '/api/docs');
  59. self::assertSame(404, $resp->getStatusCode());
  60. }
  61. public function testOpenapiSpecIs404ByDefault(): void
  62. {
  63. $resp = $this->request('GET', '/api/v1/openapi.yaml');
  64. self::assertSame(404, $resp->getStatusCode());
  65. }
  66. /**
  67. * Re-bind `settings.api_docs_public` to `true` and rebuild the app
  68. * so the docs routes are registered. Same pattern
  69. * `JobsAdminControllerTest::testRefreshGeoip412UnderMaxmindWithoutKey`
  70. * uses to swap a binding mid-test.
  71. */
  72. private function enableDocs(): void
  73. {
  74. if (method_exists($this->container, 'set')) {
  75. /** @var \DI\Container $c */
  76. $c = $this->container;
  77. $c->set('settings.api_docs_public', true);
  78. $this->app = \App\App\AppFactory::build($this->container);
  79. }
  80. }
  81. }