Skip to content

Commit

Permalink
TASK: improve tests and add CorsHeader constants
Browse files Browse the repository at this point in the history
  • Loading branch information
3m5/frohberg committed Oct 22, 2024
1 parent ad79d13 commit a21f0c5
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 37 deletions.
15 changes: 15 additions & 0 deletions Classes/Domain/CorsHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);

namespace Flowpack\Cors\Domain;

class CorsHeader {
public const ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
public const ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers';
public const ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods';
public const ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials';
public const ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age';
public const ACCESS_CONTROL_REQUEST_METHOD = 'Access-Control-Request-Method';
public const ACCESS_CONTROL_REQUEST_HEADERS = 'Access-Control-Request-Headers';
public const ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers';
}
25 changes: 12 additions & 13 deletions Classes/Http/CorsHeaderMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Flowpack\Cors\Http;

use GuzzleHttp\Psr7\MessageTrait;
use Flowpack\Cors\Domain\CorsHeader;
use Neos\Flow\Annotations as Flow;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -131,10 +131,9 @@ private function handlePreflight(ServerRequestInterface $request, ResponseInterf
* Always set Vary headers, see
* https://github.com/rs/cors/issues/10 and
* https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
* @var MessageTrait $response
*/
$response = $response->withHeader(
'Vary', ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']
'Vary', ['Origin', CorsHeader::ACCESS_CONTROL_REQUEST_METHOD, CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS]
);

$origin = $request->getHeader('Origin')[0] ?? '';
Expand Down Expand Up @@ -170,28 +169,28 @@ private function handlePreflight(ServerRequestInterface $request, ResponseInterf
}

if ($this->allowedOriginsAll && !$this->allowCredentials) {
$headersToAdd['Access-Control-Allow-Origin'] = '*';
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN] = '*';
} else {
$headersToAdd['Access-Control-Allow-Origin'] = $origin;
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN] = $origin;
}

// Spec says: Since the list of methods can be unbounded, simply returning the method indicated
// by Access-Control-Request-Method (if supported) can be enough

$headersToAdd['Access-Control-Allow-Methods'] = strtoupper($requestMethod);
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_METHODS] = strtoupper($requestMethod);

if ($headerList !== []) {
// Spec says: Since the list of headers can be unbounded, simply returning supported headers
// from Access-Control-Request-Headers can be enough
$headersToAdd['Access-Control-Allow-Headers'] = implode(', ', $headerList);
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_HEADERS] = implode(', ', $headerList);
}

if ($this->allowCredentials) {
$headersToAdd['Access-Control-Allow-Credentials'] = 'true';
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true';
}

if ($this->maxAge > 0) {
$headersToAdd['Access-Control-Max-Age'] = $this->maxAge;
$headersToAdd[CorsHeader::ACCESS_CONTROL_MAX_AGE] = $this->maxAge;
}

$this->logger->debug('Preflight response headers', ['headers' => $response->getHeaders(),]);
Expand Down Expand Up @@ -246,16 +245,16 @@ private function handleRequest(ServerRequestInterface $request, ResponseInterfac
}

if ($this->allowedOriginsAll && !$this->allowCredentials) {
$headersToAdd['Access-Control-Allow-Origin'] = '*';
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN] = '*';
} else {
$headersToAdd['Access-Control-Allow-Origin'] = $origin;
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN] = $origin;
}

if ($this->exposedHeaders !== []) {
$headersToAdd['Access-Control-Expose-Headers'] = implode(', ', $this->exposedHeaders);
$headersToAdd[CorsHeader::ACCESS_CONTROL_EXPOSE_HEADERS] = implode(', ', $this->exposedHeaders);
}

$headersToAdd['Access-Control-Allow-Credentials'] = 'true';
$headersToAdd[CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true';

$this->logger->debug('Actual response added headers', ['headers' => $response->getHeaders()]);
foreach ($headersToAdd as $header => $value) {
Expand Down
149 changes: 125 additions & 24 deletions Tests/Unit/Http/CorsHeaderMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Flowpack\Cors\Tests\Unit\Http;

use Flowpack\Cors\Http\CorsHeaderMiddleware;
use Flowpack\Cors\Domain\CorsHeader;
use GuzzleHttp\Psr7\Response;
use Neos\Utility\ObjectAccess;
use PHPUnit\Framework\Attributes\CoversClass;
Expand All @@ -15,13 +16,15 @@
use Psr\Log\LoggerInterface;

#[CoversClass(CorsHeaderMiddleware::class)]
class CorsHeaderMiddlewareTest extends TestCase {
class CorsHeaderMiddlewareTest extends TestCase
{

private CorsHeaderMiddleware $middleware;
private ServerRequestInterface|MockObject $mockRequest;
private ResponseInterface $response;
private RequestHandlerInterface|MockObject $mockHandler;
private LoggerInterface|MockObject $logger;

protected function setUp(): void
{
parent::setUp();
Expand Down Expand Up @@ -55,6 +58,63 @@ private function injectConfiguration(): void
],
true
);
ObjectAccess::setProperty(
$this->middleware,
'exposedHeaders',
['Custom-Header'],
true
);
ObjectAccess::setProperty(
$this->middleware,
'allowCredentials',
true,
true,
);
ObjectAccess::setProperty(
$this->middleware,
'maxAge',
60,
true
);
}

private function injectWildCardConfiguration(): void
{
ObjectAccess::setProperty(
$this->middleware,
'allowedOrigins',
[
0 => '*',
],
true
);
ObjectAccess::setProperty(
$this->middleware,
'allowedMethods',
[
0 => 'GET',
1 => 'POST',
],
true
);
ObjectAccess::setProperty(
$this->middleware,
'exposedHeaders',
['Custom-Header'],
true
);
ObjectAccess::setProperty(
$this->middleware,
'allowCredentials',
false,
true,
);
ObjectAccess::setProperty(
$this->middleware,
'maxAge',
60,
true
);
}


Expand All @@ -65,53 +125,94 @@ public function testMiddlewareIsNotEnabled(): void
ObjectAccess::setProperty($this->middleware, 'enabled', false, true);
$response = $this->middleware->process($this->mockRequest, $this->mockHandler);

$this->assertSame($response->getHeader('Access-Control-Allow-Origin'), []);
$this->assertSame($response->getHeader('Access-Control-Allow-Methods'), []);
$this->assertSame($response->getHeader('Access-Control-Allow-Headers'), []);
$this->assertSame($response->getHeader('Access-Control-Allow-Credentials'), []);
$this->assertSame($response->getHeader('Access-Control-Max-Age'), []);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN), []);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_METHODS), []);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_HEADERS), []);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS), []);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_MAX_AGE), []);
}

public function testMiddlewarePreflightWithConfig(): void
{
$this->injectConfiguration();
$this->mockHandler->expects($this->once())->method('handle')->willReturn($this->response);
$this->mockRequest->expects($this->once())->method('getMethod')->willReturn('OPTIONS');
$this->mockRequest->expects($this->any())->method('getHeader')->willReturnCallback(function (string $value) {
return match($value) {
$this->mockRequest->expects($this->atLeastOnce())->method('getHeader')->willReturnCallback(function (string $value) {
return match ($value) {
'Origin' => ['https://google.com'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => [],
CorsHeader::ACCESS_CONTROL_REQUEST_METHOD => ['GET'],
CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS => [],
};
});

$response = $this->middleware->process($this->mockRequest, $this->mockHandler);
$this->assertCount(3, $response->getHeaders());
$this->assertCount(5, $response->getHeaders());
$this->assertSame($response->getHeader('Vary'), [
0 => 'Origin',
1 => 'Access-Control-Request-Method',
2 => 'Access-Control-Request-Headers',
'Origin',
CorsHeader::ACCESS_CONTROL_REQUEST_METHOD,
CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS,
]);
$this->assertSame($response->getHeader('Access-Control-Allow-Origin'), ['https://google.com']);
$this->assertSame($response->getHeader('Access-Control-Allow-Methods'), ['GET']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN), ['https://google.com']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_METHODS), ['GET']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS), ['true']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_MAX_AGE), ['60']);
}

public function testMiddlewarePreflightWithWildcardConfig(): void
{
$this->injectWildCardConfiguration();
$this->mockHandler->expects($this->once())->method('handle')->willReturn($this->response);
$this->mockRequest->expects($this->atLeastOnce())->method('getMethod')->willReturn('OPTIONS');
$this->mockRequest->expects($this->atLeastOnce())->method('getHeader')->willReturnCallback(function (string $value) {
return match ($value) {
'Origin' => ['https://google.com'],
CorsHeader::ACCESS_CONTROL_REQUEST_METHOD => ['GET'],
CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS => [],
};
});
$response = $this->middleware->process($this->mockRequest, $this->mockHandler);
$this->assertCount(4, $response->getHeaders());
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS), []);
$this->assertSame($response->getHeader('Vary'), ['Origin', CorsHeader::ACCESS_CONTROL_REQUEST_METHOD, CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS]);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN), ['*']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_MAX_AGE), ['60']);
}

public function testMiddlewareActualRequestWithConfig(): void
{
$this->injectConfiguration();
$this->mockHandler->expects($this->once())->method('handle')->willReturn($this->response);
$this->mockRequest->expects($this->any())->method('getMethod')->willReturn('POST');
$this->mockRequest->expects($this->any())->method('getHeader')->willReturnCallback(function (string $value) {
return match($value) {
$this->mockRequest->expects($this->atLeastOnce())->method('getMethod')->willReturn('POST');
$this->mockRequest->expects($this->atLeastOnce())->method('getHeader')->willReturnCallback(function (string $value) {
return match ($value) {
'Origin' => ['https://google.com'],
CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS => [],
};
});
$response = $this->middleware->process($this->mockRequest, $this->mockHandler);
$this->assertCount(4, $response->getHeaders());
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS), ['true']);
$this->assertSame($response->getHeader('Vary'), ['Origin']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN), ['https://google.com']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_EXPOSE_HEADERS), ['custom-header']);
}

public function testMiddlewareActualRequestWithWildcardConfig(): void
{
$this->injectWildCardConfiguration();
$this->mockHandler->expects($this->once())->method('handle')->willReturn($this->response);
$this->mockRequest->expects($this->atLeastOnce())->method('getMethod')->willReturn('POST');
$this->mockRequest->expects($this->atLeastOnce())->method('getHeader')->willReturnCallback(function (string $value) {
return match ($value) {
'Origin' => ['https://google.com'],
'Access-Control-Request-Method' => ['GET'],
'Access-Control-Request-Headers' => [],
CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS => [],
};
});
$response = $this->middleware->process($this->mockRequest, $this->mockHandler);
$this->assertCount(3, $response->getHeaders());
$this->assertSame($response->getHeader('Access-Control-Allow-Credentials'), ['true']);
$this->assertCount(4, $response->getHeaders());
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS), ['true']);
$this->assertSame($response->getHeader('Vary'), ['Origin']);
$this->assertSame($response->getHeader('Access-Control-Allow-Origin'), ['https://google.com']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN), ['*']);
$this->assertSame($response->getHeader(CorsHeader::ACCESS_CONTROL_EXPOSE_HEADERS), ['custom-header']);
}
}

0 comments on commit a21f0c5

Please sign in to comment.