Skip to content

Commit 2e3e653

Browse files
committed
feat: support authentication headers for remote OpenAPI specifications
1 parent 0047e96 commit 2e3e653

5 files changed

Lines changed: 241 additions & 9 deletions

File tree

config/routes.php

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,103 @@
11
<?php
22
declare(strict_types=1);
33

4+
use Laminas\Diactoros\Response\HtmlResponse;
5+
use Laminas\Diactoros\Response\JsonResponse;
6+
use Laminas\Diactoros\Response\TextResponse;
47
use Mezzio\Application;
58
use Mezzio\MiddlewareFactory;
69
use Psr\Http\Message\ResponseInterface;
710
use Psr\Http\Message\ServerRequestInterface;
11+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Utils\RemoteSpecificationLoader;
812

9-
return static function (Application $app, MiddlewareFactory $factory): void {
10-
// Fallback for routes not in the spec but defined in Mezzio
11-
$app->get('/', static function (ServerRequestInterface $request): ResponseInterface {
12-
return new Laminas\Diactoros\Response\JsonResponse([
13-
'message' => 'OpenAPI Mock Server is running!',
14-
'instructions' => 'Point your requests to endpoints defined in your OpenAPI spec.',
15-
'spec_path' => getenv('OPENAPI_SPEC') ?: 'data/openapi.yaml',
16-
]);
13+
return static function (Application $application, MiddlewareFactory $middlewareFactory): void {
14+
$specPath = getenv('OPENAPI_SPEC') ?: 'data/openapi.yaml';
15+
$projectRoot = realpath(__DIR__ . '/..') ?: '/app';
16+
17+
$specHandler = static function (ServerRequestInterface $serverRequest) use ($specPath, $projectRoot): ResponseInterface {
18+
try {
19+
$path = $specPath;
20+
if (!str_starts_with((string) $path, '/') && !str_starts_with((string) $path, 'http')) {
21+
$path = $projectRoot . DIRECTORY_SEPARATOR . $path;
22+
}
23+
24+
if (str_starts_with((string) $path, 'http')) {
25+
$content = file_get_contents($path, false, RemoteSpecificationLoader::createStreamContext());
26+
} else {
27+
$content = file_exists((string) $path) ? file_get_contents((string) $path) : null;
28+
}
29+
30+
if ($content === false || $content === null) {
31+
return new TextResponse('OpenAPI spec not found at ' . $path, 404);
32+
}
33+
34+
$contentType = str_ends_with((string) $path, '.json') ? 'application/json' : 'text/yaml';
35+
if (str_ends_with($serverRequest->getUri()->getPath(), '.json')) {
36+
$contentType = 'application/json';
37+
}
38+
39+
return new TextResponse($content, 200, [
40+
'Content-Type' => $contentType,
41+
]);
42+
} catch (Throwable $e) {
43+
return new TextResponse('Error loading spec: ' . $e->getMessage(), 500);
44+
}
45+
};
46+
47+
// Route to serve the raw OpenAPI specification
48+
$application->get('/openapi.yaml', $specHandler);
49+
$application->get('/openapi.json', $specHandler);
50+
51+
// Swagger UI at root
52+
$application->get('/', static function (ServerRequestInterface $serverRequest) use ($specPath): ResponseInterface {
53+
$accept = $serverRequest->getHeaderLine('Accept');
54+
if (str_contains($accept, 'application/json')) {
55+
return new JsonResponse([
56+
'message' => 'OpenAPI Mock Server is running!',
57+
'instructions' => 'Point your requests to endpoints defined in your OpenAPI spec.',
58+
'spec_path' => $specPath,
59+
]);
60+
}
61+
62+
$html = <<<HTML
63+
<!DOCTYPE html>
64+
<html lang="en">
65+
<head>
66+
<meta charset="utf-8" />
67+
<meta name="viewport" content="width=device-width, initial-scale=1" />
68+
<meta name="description" content="SwaggerUI" />
69+
<title>OpenAPI Mock Server - Swagger UI</title>
70+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
71+
<style>
72+
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
73+
*, *:before, *:after { box-sizing: inherit; }
74+
body { margin: 0; background: #fafafa; }
75+
</style>
76+
</head>
77+
<body>
78+
<div id="swagger-ui"></div>
79+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
80+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js" crossorigin></script>
81+
<script>
82+
window.onload = () => {
83+
window.ui = SwaggerUIBundle({
84+
url: '/openapi.yaml',
85+
dom_id: '#swagger-ui',
86+
deepLinking: true,
87+
presets: [
88+
SwaggerUIBundle.presets.apis,
89+
SwaggerStandalonePreset
90+
],
91+
plugins: [
92+
SwaggerUIBundle.plugins.DownloadUrl
93+
],
94+
layout: "BaseLayout",
95+
});
96+
};
97+
</script>
98+
</body>
99+
</html>
100+
HTML;
101+
return new HtmlResponse($html);
17102
});
18103
};

src/Factory/OpenApiMockMiddlewareFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use function sprintf;
2525
use function str_ends_with;
2626
use function str_starts_with;
27-
use function stream_context_create;
2827

2928
use const DIRECTORY_SEPARATOR;
3029

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Utils;
6+
7+
use function base64_encode;
8+
use function explode;
9+
use function getenv;
10+
use function implode;
11+
use function is_string;
12+
use function sprintf;
13+
use function stream_context_create;
14+
use function trim;
15+
16+
class RemoteSpecificationLoader
17+
{
18+
/**
19+
* @return resource
20+
*/
21+
public static function createStreamContext()
22+
{
23+
$headers = [
24+
'User-Agent: PHP-OpenAPI-Mock-Server',
25+
];
26+
27+
$bearer = getenv('OPENAPI_SPEC_AUTH_BEARER');
28+
if (is_string($bearer) && $bearer !== '') {
29+
$headers[] = sprintf('Authorization: Bearer %s', $bearer);
30+
}
31+
32+
$basic = getenv('OPENAPI_SPEC_AUTH_BASIC');
33+
if (is_string($basic) && $basic !== '') {
34+
$headers[] = sprintf('Authorization: Basic %s', base64_encode($basic));
35+
}
36+
37+
$customHeaders = getenv('OPENAPI_SPEC_HEADERS');
38+
if (is_string($customHeaders) && $customHeaders !== '') {
39+
// Assume semicolon separated
40+
foreach (explode(';', $customHeaders) as $header) {
41+
if (trim($header) !== '') {
42+
$headers[] = trim($header);
43+
}
44+
}
45+
}
46+
47+
return stream_context_create([
48+
'http' => [
49+
'header' => implode("\r\n", $headers) . "\r\n",
50+
],
51+
]);
52+
}
53+
}

tests/RemoteAcceptance.suite.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
actor: AcceptanceTester
2+
modules:
3+
enabled:
4+
- PhpBrowser:
5+
url: http://localhost:8082
6+
- REST:
7+
depends: PhpBrowser
8+
- Asserts
9+
coverage:
10+
remote: true
11+
c3_url: http://localhost:8082/index.php
12+
extensions:
13+
enabled:
14+
- Codeception\Extension\RunProcess:
15+
- OPENAPI_SPEC=https://raw.githubusercontent.com/Redocly/openapi-template/gh-pages/openapi.yaml php -S localhost:8082 -t public
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WebProject\PhpOpenApiMockServer\Unit;
6+
7+
use Codeception\Test\Unit;
8+
use function putenv;
9+
use function stream_context_get_options;
10+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Utils\RemoteSpecificationLoader;
11+
12+
class RemoteSpecificationLoaderTest extends Unit
13+
{
14+
protected function _after(): void
15+
{
16+
putenv('OPENAPI_SPEC_AUTH_BEARER');
17+
putenv('OPENAPI_SPEC_AUTH_BASIC');
18+
putenv('OPENAPI_SPEC_HEADERS');
19+
}
20+
21+
public function testDefaultHeaders(): void
22+
{
23+
$context = RemoteSpecificationLoader::createStreamContext();
24+
$options = stream_context_get_options($context);
25+
26+
self::assertArrayHasKey('http', $options);
27+
self::assertStringContainsString('User-Agent: PHP-OpenAPI-Mock-Server', $options['http']['header']);
28+
}
29+
30+
public function testBearerToken(): void
31+
{
32+
putenv('OPENAPI_SPEC_AUTH_BEARER=my-token');
33+
$context = RemoteSpecificationLoader::createStreamContext();
34+
$options = stream_context_get_options($context);
35+
36+
self::assertStringContainsString('Authorization: Bearer my-token', $options['http']['header']);
37+
}
38+
39+
public function testBasicAuth(): void
40+
{
41+
putenv('OPENAPI_SPEC_AUTH_BASIC=user:pass');
42+
$context = RemoteSpecificationLoader::createStreamContext();
43+
$options = stream_context_get_options($context);
44+
45+
// base64_encode('user:pass') === 'dXNlcjpwYXNz'
46+
self::assertStringContainsString('Authorization: Basic dXNlcjpwYXNz', $options['http']['header']);
47+
}
48+
49+
public function testCustomHeaders(): void
50+
{
51+
putenv('OPENAPI_SPEC_HEADERS=X-Custom: Value; X-Another: One');
52+
$context = RemoteSpecificationLoader::createStreamContext();
53+
$options = stream_context_get_options($context);
54+
55+
self::assertStringContainsString('X-Custom: Value', $options['http']['header']);
56+
self::assertStringContainsString('X-Another: One', $options['http']['header']);
57+
}
58+
59+
public function testCombinedHeaders(): void
60+
{
61+
putenv('OPENAPI_SPEC_AUTH_BEARER=my-token');
62+
putenv('OPENAPI_SPEC_HEADERS=X-Custom: Value');
63+
$context = RemoteSpecificationLoader::createStreamContext();
64+
$options = stream_context_get_options($context);
65+
66+
self::assertStringContainsString('Authorization: Bearer my-token', $options['http']['header']);
67+
self::assertStringContainsString('X-Custom: Value', $options['http']['header']);
68+
}
69+
70+
public function testInvalidHeadersFormat(): void
71+
{
72+
putenv('OPENAPI_SPEC_HEADERS=InvalidHeaderFormat; ; ;;');
73+
$context = RemoteSpecificationLoader::createStreamContext();
74+
$options = stream_context_get_options($context);
75+
76+
self::assertStringContainsString('User-Agent: PHP-OpenAPI-Mock-Server', $options['http']['header']);
77+
// Should not contain empty lines or malformed parts in a way that breaks things
78+
self::assertStringNotContainsString(';;', $options['http']['header']);
79+
}
80+
}

0 commit comments

Comments
 (0)