diff --git a/src/VCS/Adapter/Git/Bitbucket.php b/src/VCS/Adapter/Git/Bitbucket.php index 95f2eff8..938a741e 100644 --- a/src/VCS/Adapter/Git/Bitbucket.php +++ b/src/VCS/Adapter/Git/Bitbucket.php @@ -256,15 +256,26 @@ private function normalizeRepository(array $repository): array return $repository; } - public function createRepository(string $owner, string $repositoryName, bool $private): array + /** + * Bitbucket alone groups repositories under a project, so the argument + * stays optional and off the shared three-argument contract. + * + * @return array Details of new repository + */ + public function createRepository(string $owner, string $repositoryName, bool $private, string $project = ''): array { $url = "/repositories/{$owner}/{$repositoryName}"; - - $response = $this->call(self::METHOD_POST, $url, ['Authorization' => $this->authorizationHeader()], [ + $payload = [ 'scm' => 'git', 'name' => $repositoryName, 'is_private' => $private, - ]); + ]; + + if ($project !== '') { + $payload['project'] = ['key' => $project]; + } + + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => $this->authorizationHeader()], $payload); $responseHeaders = $response['headers'] ?? []; $statusCode = $responseHeaders['status-code'] ?? 0; diff --git a/tests/VCS/Adapter/BitbucketTest.php b/tests/VCS/Adapter/BitbucketTest.php index 3bb9cffb..b3233a02 100644 --- a/tests/VCS/Adapter/BitbucketTest.php +++ b/tests/VCS/Adapter/BitbucketTest.php @@ -6,6 +6,7 @@ use Utopia\Cache\Cache; use Utopia\System\System; use Utopia\Tests\Base; +use Exception; use Utopia\VCS\Adapter\Git\Bitbucket; class BitbucketTest extends Base @@ -45,6 +46,10 @@ class BitbucketTest extends Base // Bitbucket Cloud can't reach a local test catcher protected static bool $supportsWebhookDelivery = false; + // Projects are Bitbucket's alone, so they are reached off the adapter + // itself rather than through the shared contract + private Bitbucket $bitbucket; + protected function signWebhookPayload(string $payload, string $secret): string { return 'sha256=' . hash_hmac('sha256', $payload, $secret); @@ -75,6 +80,7 @@ protected function setupAdapter(): void } $this->vcsAdapter = $adapter; + $this->bitbucket = $adapter; } /** @@ -171,10 +177,50 @@ private function eventActor(): array ]; } - /** - * Bitbucket only names the author in a raw "Name " string; a commit - * linked to an account is named by the account instead. - */ + public function testCreateRepositoryProject(): void + { + // Naming no project files the repository under the workspace default, + // which is also the only place an existing key can be read from + $withoutProject = 'test-create-repository-no-project-' . \uniqid(); + $repository = $this->bitbucket->createRepository(static::$owner, $withoutProject, false); + + try { + $this->assertSame($withoutProject, $repository['name']); + $this->assertArrayHasKey('project', $repository); + $this->assertIsArray($repository['project']); + $this->assertArrayHasKey('key', $repository['project']); + $this->assertIsString($repository['project']['key']); + $projectKey = $repository['project']['key']; + } finally { + $this->discardRepositories($withoutProject); + } + + $inProject = 'test-create-repository-project-' . \uniqid(); + $repository = $this->bitbucket->createRepository(static::$owner, $inProject, false, $projectKey); + + try { + $this->assertSame($inProject, $repository['name']); + $this->assertArrayHasKey('project', $repository); + $this->assertIsArray($repository['project']); + $this->assertArrayHasKey('key', $repository['project']); + $this->assertSame($projectKey, $repository['project']['key']); + } finally { + $this->discardRepositories($inProject); + } + } + + public function testCreateRepositoryInAnUnknownProjectFails(): void + { + $this->expectException(Exception::class); + + $this->bitbucket->createRepository( + static::$owner, + 'test-create-repository-unknown-project-' . \uniqid(), + false, + 'NOSUCHPROJECTKEY' + ); + } + public function testGetEventPushWithLinkedAuthor(): void { $payload = json_decode($this->pushPayload(static::$defaultBranch), true);