From 1955adf76bf75953626a9aecf4acb2397ce3157a Mon Sep 17 00:00:00 2001 From: Vishal Khode Date: Thu, 2 Apr 2026 00:35:04 +0530 Subject: [PATCH 1/3] Fixed the error for some commands that doesn't work in MEO. --- src/AcsfApi/AcsfConnectorFactory.php | 3 +- src/CloudApi/ConnectorFactory.php | 22 +++- src/CloudApi/PathRewriteConnector.php | 124 ++++++++++++++++++ src/ConnectorFactoryInterface.php | 5 +- .../src/CloudApi/ConnectorFactoryTest.php | 35 +++++ .../src/CloudApi/PathRewriteConnectorTest.php | 123 +++++++++++++++++ 6 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 src/CloudApi/PathRewriteConnector.php create mode 100644 tests/phpunit/src/CloudApi/ConnectorFactoryTest.php create mode 100644 tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php diff --git a/src/AcsfApi/AcsfConnectorFactory.php b/src/AcsfApi/AcsfConnectorFactory.php index 186ec5f20..86792799d 100644 --- a/src/AcsfApi/AcsfConnectorFactory.php +++ b/src/AcsfApi/AcsfConnectorFactory.php @@ -5,6 +5,7 @@ namespace Acquia\Cli\AcsfApi; use Acquia\Cli\ConnectorFactoryInterface; +use AcquiaCloudApi\Connector\ConnectorInterface; class AcsfConnectorFactory implements ConnectorFactoryInterface { @@ -15,7 +16,7 @@ public function __construct(protected array $config, protected ?string $baseUri { } - public function createConnector(): AcsfConnector + public function createConnector(): ConnectorInterface { return new AcsfConnector($this->config, $this->baseUri); } diff --git a/src/CloudApi/ConnectorFactory.php b/src/CloudApi/ConnectorFactory.php index 5e1c99e73..9eba24541 100644 --- a/src/CloudApi/ConnectorFactory.php +++ b/src/CloudApi/ConnectorFactory.php @@ -6,6 +6,7 @@ use Acquia\Cli\ConnectorFactoryInterface; use AcquiaCloudApi\Connector\Connector; +use AcquiaCloudApi\Connector\ConnectorInterface; use League\OAuth2\Client\Token\AccessToken; class ConnectorFactory implements ConnectorFactoryInterface @@ -17,10 +18,23 @@ public function __construct(protected array $config, protected ?string $baseUri { } - /** - * @return \Acquia\Cli\CloudApi\AccessTokenConnector|\AcquiaCloudApi\Connector\Connector - */ - public function createConnector(): Connector|AccessTokenConnector + public function createConnector(): ConnectorInterface + { + $connector = $this->buildConnector(); + + // If the AH_CODEBASE_UUID environment variable is set, that means + // it's a MEO subscription and for MEO we need to rewrite the path to + // include the codebase UUID. This is because MEO's uses different + // endpoints, and the codebase UUID is used to determine which endpoint + // to use. + if (getenv('AH_CODEBASE_UUID')) { + return new PathRewriteConnector($connector); + } + + return $connector; + } + + private function buildConnector(): ConnectorInterface { // A defined key & secret takes priority. if ($this->config['key'] && $this->config['secret']) { diff --git a/src/CloudApi/PathRewriteConnector.php b/src/CloudApi/PathRewriteConnector.php new file mode 100644 index 000000000..ade5cdaf5 --- /dev/null +++ b/src/CloudApi/PathRewriteConnector.php @@ -0,0 +1,124 @@ +inner = $inner; + } + + /** + * Creates a PSR-7 request, rewriting the path if it matches a rewrite rule. + * + * @param string $verb HTTP method (e.g., 'GET', 'POST'). + * @param string $path The original API path. + * @return RequestInterface The PSR-7 request with possibly rewritten path. + */ + public function createRequest(string $verb, string $path): RequestInterface + { + return $this->inner->createRequest($verb, $this->rewritePath($path)); + } + + /** + * Sends an HTTP request, rewriting the path if it matches a rewrite rule. + * + * @param string $verb HTTP method (e.g., 'GET', 'POST'). + * @param string $path The original API path. + * @param array $options Additional request options. + * @return ResponseInterface The HTTP response. + */ + public function sendRequest(string $verb, string $path, array $options): ResponseInterface + { + return $this->inner->sendRequest($verb, $this->rewritePath($path), $options); + } + + /** + * Returns the base URI for the API. + * + * @return string The base URI. + */ + public function getBaseUri(): string + { + return $this->inner->getBaseUri(); + } + + /** + * Returns the access token for URL authentication. + * + * @return string The access token. + */ + public function getUrlAccessToken(): string + { + return $this->inner->getUrlAccessToken(); + } + + /** + * Rewrites the API path if it matches any rewrite rule. + * + * @param string $path The original API path. + * @return string The rewritten path, or the original if no rule matches. + */ + private function rewritePath(string $path): string + { + foreach ($this->getPathsToRewrite() as $pattern => $replacement) { + if (preg_match($pattern, $path) === 1) { + // Replace the entire path with the replacement if the pattern matches. + return $replacement; + } + } + + // Return the original path if no rewrite rule matches. + return $path; + } + + /** + * Returns an array of regex patterns and their corresponding replacement paths for rewriting API request paths. + * + * @return array An array of regex patterns and their corresponding replacement paths. + * The replacement paths may use the codebaseUuid property. + */ + private function getPathsToRewrite(): array + { + $codebaseUuid = $this->getCodeBaseUuid(); + return [ + '#^/applications/[0-9a-f\-]+/environments$#i' => "/translation/codebases/$codebaseUuid/environments", + '#^/applications/[0-9a-f\-]+/permissions$#i' => "/translation/codebases/$codebaseUuid/permissions", + ]; + } + + /** + * Retrieves the codebase UUID. + */ + private function getCodeBaseUuid(): string + { + $codebaseUuid = getenv('AH_CODEBASE_UUID'); + if (!$codebaseUuid) { + throw new \RuntimeException('Environment variable AH_CODEBASE_UUID is not set.'); + } + return $codebaseUuid; + } +} diff --git a/src/ConnectorFactoryInterface.php b/src/ConnectorFactoryInterface.php index ded5c0138..97c9b5a11 100644 --- a/src/ConnectorFactoryInterface.php +++ b/src/ConnectorFactoryInterface.php @@ -4,10 +4,9 @@ namespace Acquia\Cli; -use Acquia\Cli\CloudApi\AccessTokenConnector; -use AcquiaCloudApi\Connector\Connector; +use AcquiaCloudApi\Connector\ConnectorInterface; interface ConnectorFactoryInterface { - public function createConnector(): Connector|AccessTokenConnector; + public function createConnector(): ConnectorInterface; } diff --git a/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php b/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php new file mode 100644 index 000000000..5d657542c --- /dev/null +++ b/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php @@ -0,0 +1,35 @@ + 'k', 'secret' => 's'], 'https://api.example.com'); + $connector = $factory->createConnector(); + $this->assertInstanceOf(PathRewriteConnector::class, $connector); + } + + /** + * @runInSeparateProcess + */ + public function testCreateConnectorReturnsRegularConnectorIfEnvNotSet(): void + { + putenv('AH_CODEBASE_UUID'); + $factory = new ConnectorFactory(['key' => 'k', 'secret' => 's'], 'https://api.example.com'); + $connector = $factory->createConnector(); + $this->assertInstanceOf(Connector::class, $connector); + } +} diff --git a/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php b/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php new file mode 100644 index 000000000..760bd3a88 --- /dev/null +++ b/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php @@ -0,0 +1,123 @@ +inner = $this->createMock(ConnectorInterface::class); + $this->connector = new PathRewriteConnector($this->inner); + } + + /** + * @runInSeparateProcess + */ + public function testCreateRequestRewritesMatchingPath(): void + { + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); + $expectedPath = '/translation/codebases/1234-5678-uuid/environments'; + $request = $this->createMock(RequestInterface::class); + $this->inner->expects($this->once()) + ->method('createRequest') + ->with('GET', $expectedPath) + ->willReturn($request); + $result = $this->connector->createRequest('GET', '/applications/abcd-ef01/environments'); + $this->assertSame($request, $result); + } + + /** + * @runInSeparateProcess + */ + public function testCreateRequestDoesNotRewriteUnmatchedPath(): void + { + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); + $request = $this->createMock(RequestInterface::class); + $this->inner->expects($this->once()) + ->method('createRequest') + ->with('GET', '/other/path') + ->willReturn($request); + $result = $this->connector->createRequest('GET', '/other/path'); + $this->assertSame($request, $result); + } + + /** + * @runInSeparateProcess + */ + public function testSendRequestRewritesMatchingPath(): void + { + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); + $expectedPath = '/translation/codebases/1234-5678-uuid/permissions'; + $response = $this->createMock(ResponseInterface::class); + $this->inner->expects($this->once()) + ->method('sendRequest') + ->with('POST', $expectedPath, ['foo' => 'bar']) + ->willReturn($response); + $result = $this->connector->sendRequest('POST', '/applications/abcd-ef01/permissions', ['foo' => 'bar']); + $this->assertSame($response, $result); + } + + /** + * @runInSeparateProcess + */ + public function testSendRequestDoesNotRewriteUnmatchedPath(): void + { + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); + $response = $this->createMock(ResponseInterface::class); + $this->inner->expects($this->once()) + ->method('sendRequest') + ->with('GET', '/other/path', []) + ->willReturn($response); + $result = $this->connector->sendRequest('GET', '/other/path', []); + $this->assertSame($response, $result); + } + + /** + * @runInSeparateProcess + */ + public function testGetBaseUriDelegates(): void + { + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); + $this->inner->expects($this->once()) + ->method('getBaseUri') + ->willReturn('https://api.example.com'); + $this->assertSame('https://api.example.com', $this->connector->getBaseUri()); + } + + /** + * @runInSeparateProcess + */ + public function testGetUrlAccessTokenDelegates(): void + { + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); + $this->inner->expects($this->once()) + ->method('getUrlAccessToken') + ->willReturn('token123'); + $this->assertSame('token123', $this->connector->getUrlAccessToken()); + } + + /** + * @runInSeparateProcess + */ + public function testThrowsIfCodebaseUuidNotSet(): void + { + putenv('AH_CODEBASE_UUID'); + $connector = new PathRewriteConnector($this->inner); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Environment variable AH_CODEBASE_UUID is not set.'); + // This will trigger getCodeBaseUuid() + $connector->createRequest('GET', '/applications/abcd-ef01/environments'); + } +} From 93d2687e1dfd8c0baa602feafe0e79fff67cfee9 Mon Sep 17 00:00:00 2001 From: Vishal Khode Date: Thu, 2 Apr 2026 11:35:26 +0530 Subject: [PATCH 2/3] IDE-4857: Fixed the co-polit suggestions. --- src/CloudApi/ConnectorFactory.php | 7 +- src/CloudApi/PathRewriteConnector.php | 2 +- .../src/CloudApi/ConnectorFactoryTest.php | 62 +++++-- .../src/CloudApi/PathRewriteConnectorTest.php | 168 +++++++++++------- 4 files changed, 164 insertions(+), 75 deletions(-) diff --git a/src/CloudApi/ConnectorFactory.php b/src/CloudApi/ConnectorFactory.php index 9eba24541..74cd33fe5 100644 --- a/src/CloudApi/ConnectorFactory.php +++ b/src/CloudApi/ConnectorFactory.php @@ -23,10 +23,9 @@ public function createConnector(): ConnectorInterface $connector = $this->buildConnector(); // If the AH_CODEBASE_UUID environment variable is set, that means - // it's a MEO subscription and for MEO we need to rewrite the path to - // include the codebase UUID. This is because MEO's uses different - // endpoints, and the codebase UUID is used to determine which endpoint - // to use. + // it's a MEO subscription. For MEO, we need to rewrite the API request + // path so that MEO-specific endpoints are used and the correct + // endpoint can be selected based on the codebase. if (getenv('AH_CODEBASE_UUID')) { return new PathRewriteConnector($connector); } diff --git a/src/CloudApi/PathRewriteConnector.php b/src/CloudApi/PathRewriteConnector.php index ade5cdaf5..c157a40a4 100644 --- a/src/CloudApi/PathRewriteConnector.php +++ b/src/CloudApi/PathRewriteConnector.php @@ -99,7 +99,7 @@ private function rewritePath(string $path): string * Returns an array of regex patterns and their corresponding replacement paths for rewriting API request paths. * * @return array An array of regex patterns and their corresponding replacement paths. - * The replacement paths may use the codebaseUuid property. + * The replacement paths may include the codebase UUID obtained from the AH_CODEBASE_UUID environment variable. */ private function getPathsToRewrite(): array { diff --git a/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php b/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php index 5d657542c..43c9e521e 100644 --- a/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php +++ b/tests/phpunit/src/CloudApi/ConnectorFactoryTest.php @@ -9,27 +9,69 @@ use AcquiaCloudApi\Connector\Connector; use PHPUnit\Framework\TestCase; +/** + * @covers \Acquia\Cli\CloudApi\ConnectorFactory + * + * Unit tests for the ConnectorFactory. Ensures that the factory returns the correct + * connector type depending on the presence of the AH_CODEBASE_UUID environment variable. + */ class ConnectorFactoryTest extends TestCase { /** - * @runInSeparateProcess + * Stores the original value of AH_CODEBASE_UUID to restore after each test. */ - public function testCreateConnectorReturnsPathRewriteConnectorIfEnvSet(): void + private string|false $originalEnv; + + /** + * Saves the original environment variable before each test. + */ + protected function setUp(): void { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $factory = new ConnectorFactory(['key' => 'k', 'secret' => 's'], 'https://api.example.com'); - $connector = $factory->createConnector(); - $this->assertInstanceOf(PathRewriteConnector::class, $connector); + parent::setUp(); + $this->originalEnv = getenv('AH_CODEBASE_UUID'); } + /** - * @runInSeparateProcess + * @dataProvider connectorFactoryProvider */ - public function testCreateConnectorReturnsRegularConnectorIfEnvNotSet(): void + public function testCreateConnectorFactoryBehavior(?string $envValue, string $expectedClass): void { - putenv('AH_CODEBASE_UUID'); + if ($envValue !== null) { + putenv("AH_CODEBASE_UUID=$envValue"); + } else { + putenv('AH_CODEBASE_UUID'); + } $factory = new ConnectorFactory(['key' => 'k', 'secret' => 's'], 'https://api.example.com'); $connector = $factory->createConnector(); - $this->assertInstanceOf(Connector::class, $connector); + $this->assertInstanceOf($expectedClass, $connector); + } + + /** + * Data provider for testCreateConnectorFactoryBehavior() test. + * + * @return array + */ + public static function connectorFactoryProvider(): array + { + return [ + // Env set: should return PathRewriteConnector. + ['1234-5678-uuid', PathRewriteConnector::class], + // Env not set: should return Connector. + [null, Connector::class], + ]; + } + + /** + * Restores the original environment variable after each test. + */ + protected function tearDown(): void + { + if ($this->originalEnv === false) { + putenv('AH_CODEBASE_UUID'); + } else { + putenv('AH_CODEBASE_UUID=' . $this->originalEnv); + } + parent::tearDown(); } } diff --git a/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php b/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php index 760bd3a88..04488d5cb 100644 --- a/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php +++ b/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php @@ -10,114 +10,162 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +/** + * @covers \Acquia\Cli\CloudApi\PathRewriteConnector + * + * Unit tests for the PathRewriteConnector decorator. Ensures all path rewriting logic, + * delegation, and error handling are correct. + */ class PathRewriteConnectorTest extends TestCase { + /** + * Mocked inner connector to verify delegation and path rewriting. + */ private ConnectorInterface $inner; + + /** + * The PathRewriteConnector under test. + */ private ConnectorInterface $connector; + /** + * Stores the original value of AH_CODEBASE_UUID to restore after each test. + */ + private string|bool $originalEnv; + + /** + * Sets up a fresh PathRewriteConnector and mocks before each test. + * Ensures AH_CODEBASE_UUID is set for tests that require it. + */ protected function setUp(): void { parent::setUp(); + $this->originalEnv = getenv('AH_CODEBASE_UUID'); + putenv('AH_CODEBASE_UUID=1234-5678-uuid'); $this->inner = $this->createMock(ConnectorInterface::class); $this->connector = new PathRewriteConnector($this->inner); } /** - * @runInSeparateProcess + * @dataProvider createRequestProvider + * @param string $verb The HTTP verb to test. + * @param string $inputPath The input path to test. + * @param string $expectedPath The expected path after rewriting. */ - public function testCreateRequestRewritesMatchingPath(): void + public function testCreateRequestPathRewriting(string $verb, string $inputPath, string $expectedPath): void { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $expectedPath = '/translation/codebases/1234-5678-uuid/environments'; - $request = $this->createMock(RequestInterface::class); + $mock = $this->createMock(RequestInterface::class); $this->inner->expects($this->once()) ->method('createRequest') - ->with('GET', $expectedPath) - ->willReturn($request); - $result = $this->connector->createRequest('GET', '/applications/abcd-ef01/environments'); - $this->assertSame($request, $result); + ->with($verb, $expectedPath) + ->willReturn($mock); + $result = $this->connector->createRequest($verb, $inputPath); + $this->assertSame($mock, $result); } + + /** - * @runInSeparateProcess + * @dataProvider sendRequestProvider + * @param string $verb The HTTP verb to test. + * @param string $inputPath The input path to test. + * @param string $expectedPath The expected path after rewriting. + * @param array $options The options to pass to sendRequest. */ - public function testCreateRequestDoesNotRewriteUnmatchedPath(): void + public function testSendRequestPathRewriting(string $verb, string $inputPath, string $expectedPath, array $options): void { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $request = $this->createMock(RequestInterface::class); + $mock = $this->createMock(ResponseInterface::class); $this->inner->expects($this->once()) - ->method('createRequest') - ->with('GET', '/other/path') - ->willReturn($request); - $result = $this->connector->createRequest('GET', '/other/path'); - $this->assertSame($request, $result); + ->method('sendRequest') + ->with($verb, $expectedPath, $options) + ->willReturn($mock); + $result = $this->connector->sendRequest($verb, $inputPath, $options); + $this->assertSame($mock, $result); } /** - * @runInSeparateProcess + * @dataProvider delegationProvider + * @param string $method The method to test delegation for. + * @param mixed $expected The expected return value from the inner connector. */ - public function testSendRequestRewritesMatchingPath(): void + public function testDelegation(string $method, string $expected): void { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $expectedPath = '/translation/codebases/1234-5678-uuid/permissions'; - $response = $this->createMock(ResponseInterface::class); $this->inner->expects($this->once()) - ->method('sendRequest') - ->with('POST', $expectedPath, ['foo' => 'bar']) - ->willReturn($response); - $result = $this->connector->sendRequest('POST', '/applications/abcd-ef01/permissions', ['foo' => 'bar']); - $this->assertSame($response, $result); + ->method($method) + ->willReturn($expected); + $this->assertTrue(method_exists($this->connector, $method)); + $this->assertSame($expected, $this->connector->{$method}()); } /** - * @runInSeparateProcess + * Ensures an exception is thrown if AH_CODEBASE_UUID is not set when required. */ - public function testSendRequestDoesNotRewriteUnmatchedPath(): void + public function testThrowsIfCodebaseUuidNotSet(): void { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $response = $this->createMock(ResponseInterface::class); - $this->inner->expects($this->once()) - ->method('sendRequest') - ->with('GET', '/other/path', []) - ->willReturn($response); - $result = $this->connector->sendRequest('GET', '/other/path', []); - $this->assertSame($response, $result); + putenv('AH_CODEBASE_UUID'); + $connector = new PathRewriteConnector($this->inner); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Environment variable AH_CODEBASE_UUID is not set.'); + // This will trigger getCodeBaseUuid() + $connector->createRequest('GET', '/applications/abcd-ef01/environments'); } /** - * @runInSeparateProcess + * Data provider for createRequest tests. Ensures that paths are rewritten + * correctly based on the presence of the code base environment variable. + * + * @return array */ - public function testGetBaseUriDelegates(): void + public static function createRequestProvider(): array { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $this->inner->expects($this->once()) - ->method('getBaseUri') - ->willReturn('https://api.example.com'); - $this->assertSame('https://api.example.com', $this->connector->getBaseUri()); + return [ + // Rewrite. + ['GET', '/applications/abcd-ef01/environments', '/translation/codebases/1234-5678-uuid/environments'], + // No rewrite. + ['GET', '/other/path', '/other/path'], + ]; } /** - * @runInSeparateProcess + * Data provider for sendRequest tests. Ensures that both path rewriting + * and options are handled correctly. + * + * @return array}> */ - public function testGetUrlAccessTokenDelegates(): void + public static function sendRequestProvider(): array { - putenv('AH_CODEBASE_UUID=1234-5678-uuid'); - $this->inner->expects($this->once()) - ->method('getUrlAccessToken') - ->willReturn('token123'); - $this->assertSame('token123', $this->connector->getUrlAccessToken()); + return [ + // Rewrite. + ['POST', '/applications/abcd-ef01/permissions', '/translation/codebases/1234-5678-uuid/permissions', ['foo' => 'bar']], + // No rewrite. + ['GET', '/other/path', '/other/path', []], + ]; } /** - * @runInSeparateProcess + * Data provider for delegation tests. Ensures that methods not related to + * path rewriting are properly delegated to the inner connector. + * + * @return array */ - public function testThrowsIfCodebaseUuidNotSet(): void + public static function delegationProvider(): array { - putenv('AH_CODEBASE_UUID'); - $connector = new PathRewriteConnector($this->inner); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Environment variable AH_CODEBASE_UUID is not set.'); - // This will trigger getCodeBaseUuid() - $connector->createRequest('GET', '/applications/abcd-ef01/environments'); + return [ + ['getBaseUri', 'https://api.example.com'], + ['getUrlAccessToken', 'token123'], + ]; + } + + /** + * Restores the original AH_CODEBASE_UUID environment variable after each test. + */ + protected function tearDown(): void + { + if ($this->originalEnv === false) { + putenv('AH_CODEBASE_UUID'); + } else { + putenv('AH_CODEBASE_UUID=' . $this->originalEnv); + } + parent::tearDown(); } } From d9897dc859672004733cc38c22d7ef9ec741212f Mon Sep 17 00:00:00 2001 From: Vishal Khode Date: Thu, 2 Apr 2026 18:31:15 +0530 Subject: [PATCH 3/3] IDE-4857: Use regex to search and replace path instead of hardcoding paths. --- src/CloudApi/PathRewriteConnector.php | 21 ++++-- .../src/CloudApi/PathRewriteConnectorTest.php | 69 ++++++++++++++++--- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/CloudApi/PathRewriteConnector.php b/src/CloudApi/PathRewriteConnector.php index c157a40a4..517789885 100644 --- a/src/CloudApi/PathRewriteConnector.php +++ b/src/CloudApi/PathRewriteConnector.php @@ -77,7 +77,7 @@ public function getUrlAccessToken(): string } /** - * Rewrites the API path if it matches any rewrite rule. + * Rewrites the API path using preg_replace if it matches any rewrite rule. * * @param string $path The original API path. * @return string The rewritten path, or the original if no rule matches. @@ -86,8 +86,7 @@ private function rewritePath(string $path): string { foreach ($this->getPathsToRewrite() as $pattern => $replacement) { if (preg_match($pattern, $path) === 1) { - // Replace the entire path with the replacement if the pattern matches. - return $replacement; + return (string) preg_replace($pattern, $replacement, $path); } } @@ -98,15 +97,23 @@ private function rewritePath(string $path): string /** * Returns an array of regex patterns and their corresponding replacement paths for rewriting API request paths. * - * @return array An array of regex patterns and their corresponding replacement paths. - * The replacement paths may include the codebase UUID obtained from the AH_CODEBASE_UUID environment variable. + * Two rules cover all cases: + * - Paths with a trailing segment: /applications/{uuid}/foo/bar → /translation/codebases/{codebaseUuid}/foo/bar + * - Bare application UUID paths: /applications/{uuid} → /translation/codebases/{codebaseUuid} + * + * The first rule uses a capture group ($1) so any trailing path is preserved automatically, + * avoiding the need to enumerate every possible sub-path. + * + * @return array Regex pattern => preg_replace replacement string. */ private function getPathsToRewrite(): array { $codebaseUuid = $this->getCodeBaseUuid(); return [ - '#^/applications/[0-9a-f\-]+/environments$#i' => "/translation/codebases/$codebaseUuid/environments", - '#^/applications/[0-9a-f\-]+/permissions$#i' => "/translation/codebases/$codebaseUuid/permissions", + // Matches bare /applications/{uuid} with no trailing segment. + '#^/applications/[0-9a-f\-]+$#i' => '/translation/codebases/' . $codebaseUuid, + // Matches /applications/{uuid}/{anything} and preserves the trailing segment via $1. + '#^/applications/[0-9a-f\-]+/(.+)$#i' => '/translation/codebases/' . $codebaseUuid . '/$1', ]; } diff --git a/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php b/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php index 04488d5cb..6cd0957de 100644 --- a/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php +++ b/tests/phpunit/src/CloudApi/PathRewriteConnectorTest.php @@ -119,10 +119,39 @@ public function testThrowsIfCodebaseUuidNotSet(): void public static function createRequestProvider(): array { return [ - // Rewrite. - ['GET', '/applications/abcd-ef01/environments', '/translation/codebases/1234-5678-uuid/environments'], - // No rewrite. - ['GET', '/other/path', '/other/path'], + 'account path is not rewritten' => [ + 'GET', + '/account', + '/account', + ], + // Bare UUID (no trailing segment) is also rewritten. + 'bare application UUID is rewritten' => [ + 'GET', + '/applications/abcd-ef01', + '/translation/codebases/1234-5678-uuid', + ], + // Deep sub-path: entire trailing part is preserved by $1. + 'deep sub-path is rewritten' => ['GET', + '/applications/abcd-ef01/environments/env-1/tags', + '/translation/codebases/1234-5678-uuid/environments/env-1/tags', + ], + // Single trailing segment is rewritten via capture group ($1). + 'environments path is rewritten' => [ + 'GET', + '/applications/abcd-ef01/environments', + '/translation/codebases/1234-5678-uuid/environments', + ], + 'permissions path is rewritten' => [ + 'GET', + '/applications/abcd-ef01/permissions', + '/translation/codebases/1234-5678-uuid/permissions', + ], + // Paths that do not start with /applications/{uuid} are left unchanged. + 'unrelated path is not rewritten' => [ + 'GET', + '/other/path', + '/other/path', + ], ]; } @@ -135,10 +164,34 @@ public static function createRequestProvider(): array public static function sendRequestProvider(): array { return [ - // Rewrite. - ['POST', '/applications/abcd-ef01/permissions', '/translation/codebases/1234-5678-uuid/permissions', ['foo' => 'bar']], - // No rewrite. - ['GET', '/other/path', '/other/path', []], + // Bare UUID (no trailing segment) is also rewritten. + 'bare application UUID is rewritten' => [ + 'GET', + '/applications/abcd-ef01', + '/translation/codebases/1234-5678-uuid', + [], + ], + // Deep sub-path: entire trailing part is preserved by $1. + 'deep sub-path is rewritten' => [ + 'POST', + '/applications/abcd-ef01/environments/env-1/tags', + '/translation/codebases/1234-5678-uuid/environments/env-1/tags', + [], + ], + // Single trailing segment is rewritten via capture group ($1). + 'permissions path is rewritten' => [ + 'POST', + '/applications/abcd-ef01/permissions', + '/translation/codebases/1234-5678-uuid/permissions', + ['foo' => 'bar'], + ], + // Paths that do not start with /applications/{uuid} are left unchanged. + 'unrelated path is not rewritten' => [ + 'GET', + '/other/path', + '/other/path', + [], + ], ]; }