diff --git a/src/Controller/RepoController.php b/src/Controller/RepoController.php index 4b4ba596..452f9ff8 100644 --- a/src/Controller/RepoController.php +++ b/src/Controller/RepoController.php @@ -13,13 +13,13 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Routing\RouterInterface; final class RepoController extends AbstractController { @@ -41,32 +41,42 @@ public function __construct( * @Route("/packages.json", host="{organization}{sep1}repo{sep2}{domain}", name="repo_packages", methods={"GET"}, defaults={"domain":"%domain%","sep1"="%organization_separator%","sep2"="%domain_separator%"}, requirements={"domain"="%domain%","sep1"="%organization_separator%","sep2"="%domain_separator%"}) * @Cache(public=false) */ - public function packages(Request $request, Organization $organization): JsonResponse + public function packages(Request $request, Organization $organization): Response { $packageNames = $this->packageQuery->getAllNames($organization->id()); - [$lastModified, $packages] = $this->packageManager->findProviders($organization->alias(), $packageNames); - - $response = (new JsonResponse([ - 'packages' => $packages, - 'available-packages' => array_map(static fn (PackageName $packageName) => $packageName->name(), $packageNames), - 'metadata-url' => '/p2/%package%.json', - 'notify-batch' => $this->generateUrl('repo_package_downloads', [ - 'organization' => $organization->alias(), - ], UrlGeneratorInterface::ABSOLUTE_URL), - 'search' => 'https://packagist.org/search.json?q=%query%&type=%type%', - 'mirrors' => [ - [ - 'dist-url' => $this->generateUrl( - 'organization_repo_url', - ['organization' => $organization->alias()], - RouterInterface::ABSOLUTE_URL - ).'dists/%package%/%version%/%reference%.%type%', - 'preferred' => true, + [$lastModified, $loader] = $this->packageManager->findProviders($organization->alias(), $packageNames); + + $response = (new StreamedResponse(function () use ($organization, $packageNames, $loader): void { + $outputStream = \fopen('php://output', 'wb'); + if (false === $outputStream) { + throw new HttpException(500, 'Could not open output stream to send binary file.'); // @codeCoverageIgnore + } + + \fwrite($outputStream, ' '); + \flush(); + \fwrite($outputStream, (string) \json_encode([ + 'packages' => $loader(), + 'available-packages' => array_map(static fn (PackageName $packageName) => $packageName->name(), $packageNames), + 'metadata-url' => '/p2/%package%.json', + 'providers-url' => '/p2/%package%.json', + 'notify-batch' => $this->generateUrl('repo_package_downloads', [ + 'organization' => $organization->alias(), + ], UrlGeneratorInterface::ABSOLUTE_URL), + 'search' => 'https://packagist.org/search.json?q=%query%&type=%type%', + 'mirrors' => [ + [ + 'dist-url' => $this->generateUrl( + 'organization_repo_url', + ['organization' => $organization->alias()], + UrlGeneratorInterface::ABSOLUTE_URL + ).'dists/%package%/%version%/%reference%.%type%', + 'preferred' => true, + ], ], - ], - ])) - ->setPrivate() - ->setLastModified($lastModified); + ], JsonResponse::DEFAULT_ENCODING_OPTIONS)); + }, Response::HTTP_OK, ['Content-Type' => 'application/json', 'X-Accel-Buffering' => 'no'])) + ->setLastModified($lastModified) + ->setPrivate(); $response->isNotModified($request); @@ -149,18 +159,27 @@ public function downloads(Request $request, Organization $organization): JsonRes * requirements={"domain"="%domain%","package"="%package_name_pattern%","sep1"="%organization_separator%","sep2"="%domain_separator%"}) * @Cache(public=false) */ - public function providerV2(Request $request, Organization $organization, string $package): JsonResponse + public function providerV2(Request $request, Organization $organization, string $package): Response { - [$lastModified, $providerData] = $this->packageManager->findProviders( + [$lastModified, $loader] = $this->packageManager->findProviders( $organization->alias(), [new PackageName('', $package)] ); - if ($providerData === []) { + if ($lastModified === null) { throw new NotFoundHttpException(); } - $response = (new JsonResponse($providerData)) + $response = (new StreamedResponse(function () use ($loader): void { + $outputStream = \fopen('php://output', 'wb'); + if (false === $outputStream) { + throw new HttpException(500, 'Could not open output stream to send binary file.'); // @codeCoverageIgnore + } + + \fwrite($outputStream, ' '); + \flush(); + \fwrite($outputStream, (string) \json_encode($loader(), JsonResponse::DEFAULT_ENCODING_OPTIONS)); + }, Response::HTTP_OK, ['Content-Type' => 'application/json', 'X-Accel-Buffering' => 'no'])) ->setLastModified($lastModified) ->setPrivate(); diff --git a/src/Service/Organization/PackageManager.php b/src/Service/Organization/PackageManager.php index 55e5c17c..12d55e58 100644 --- a/src/Service/Organization/PackageManager.php +++ b/src/Service/Organization/PackageManager.php @@ -28,12 +28,12 @@ public function __construct(Storage $distStorage, FilesystemInterface $repoFiles /** * @param PackageName[] $packages * - * @return array{\DateTimeImmutable|null, mixed[]} + * @return array{\DateTimeImmutable|null, callable(): mixed[]} */ public function findProviders(string $organizationAlias, array $packages): array { - $data = []; $lastModified = null; + $filepaths = []; foreach ($packages as $package) { $filepath = $this->filepath($organizationAlias, $package->name()); @@ -47,15 +47,22 @@ public function findProviders(string $organizationAlias, array $packages): array $lastModified = $fileModifyDate; } - $json = \unserialize( - (string) $this->repoFilesystem->read($filepath), ['allowed_classes' => false] - ); - $data[] = $json['packages'] ?? []; + $filepaths[] = $filepath; } + $loader = function () use ($filepaths): array { + $data = []; + foreach ($filepaths as $filepath) { + $json = \unserialize((string) $this->repoFilesystem->read($filepath), ['allowed_classes' => false]); + $data[] = $json['packages'] ?? []; + } + + return \array_merge(...$data); + }; + return [ $lastModified, - \array_merge(...$data), + $loader, ]; } diff --git a/src/Service/Security/PackageScanner/SensioLabsPackageScanner.php b/src/Service/Security/PackageScanner/SensioLabsPackageScanner.php index 929f9025..2a647fbc 100644 --- a/src/Service/Security/PackageScanner/SensioLabsPackageScanner.php +++ b/src/Service/Security/PackageScanner/SensioLabsPackageScanner.php @@ -112,11 +112,12 @@ private function findDistribution(Package $package): string $normalizedVersion = $latestReleasedVersion === 'no stable release' ? '9999999-dev' : $this->versionParser->normalize((string) $latestReleasedVersion); - [, $providerData] = $this->packageManager->findProviders( + [, $loader] = $this->packageManager->findProviders( $package->organizationAlias(), [new PackageName($package->id()->toString(), (string) $package->name())] ); + $providerData = $loader(); foreach ($providerData[$packageName] ?? [] as $packageData) { $packageVersion = $packageData['version_normalized'] ?? diff --git a/tests/Functional/Controller/Api/PackageControllerTest.php b/tests/Functional/Controller/Api/PackageControllerTest.php index 6b97572d..78404466 100644 --- a/tests/Functional/Controller/Api/PackageControllerTest.php +++ b/tests/Functional/Controller/Api/PackageControllerTest.php @@ -191,39 +191,30 @@ public function testFindPackage(): void $this->fixtures->addScanResult($packageId, 'ok'); $this->loginApiUser($this->apiToken); - $now = (new \DateTimeImmutable())->format(\DateTime::ATOM); $this->client->request('GET', $this->urlTo('api_package_get', [ 'organization' => self::$organization, 'package' => $packageId, ])); self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode()); - - self::assertJsonStringEqualsJsonString( - $this->lastResponseBody(), - ' - { - "id": "'.$packageId.'", - "type": "vcs", - "url": "https://github.com/buddy-works/repman", - "name": "buddy-works/repman", - "latestReleasedVersion": "2.1.1", - "latestReleaseDate": "'.$release->format(\DateTime::ATOM).'", - "description": "Repository manager", - "enableSecurityScan": true, - "lastSyncAt": "'.$now.'", - "lastSyncError": null, - "webhookCreatedAt": null, - "isSynchronizedSuccessfully": true, - "keepLastReleases": 0, - "scanResultStatus": "ok", - "scanResultDate": "'.$now.'", - "lastScanResultContent": { - "composer.lock": [] - } - } - ' - ); + self::assertEquals([ + 'id' => $packageId, + 'type' => 'vcs', + 'url' => 'https://github.com/buddy-works/repman', + 'name' => 'buddy-works/repman', + 'latestReleasedVersion' => '2.1.1', + 'latestReleaseDate' => $release->format(\DateTimeInterface::ATOM), + 'description' => 'Repository manager', + 'enableSecurityScan' => true, + 'lastSyncError' => null, + 'webhookCreatedAt' => null, + 'isSynchronizedSuccessfully' => true, + 'keepLastReleases' => 0, + 'scanResultStatus' => 'ok', + 'lastScanResultContent' => [ + 'composer.lock' => [], + ], + ], array_diff_key(json_decode($this->lastResponseBody(), true, 512, JSON_THROW_ON_ERROR), ['lastSyncAt' => true, 'scanResultDate' => true])); } public function testFindPackageNonExisting(): void diff --git a/tests/Functional/Controller/RepoControllerTest.php b/tests/Functional/Controller/RepoControllerTest.php index 85ba6445..e8dd5ef6 100644 --- a/tests/Functional/Controller/RepoControllerTest.php +++ b/tests/Functional/Controller/RepoControllerTest.php @@ -80,35 +80,34 @@ public function testOrganizationPackagesAction(): void 'PHP_AUTH_PW' => 'secret-org-token', ]); - self::assertJsonStringEqualsJsonString(' - { - "packages": { - "buddy-works\/repman": { - "1.2.3": { - "version": "1.2.3", - "version_normalized": "1.2.3.0", - "dist": { - "type": "zip", - "url": "\/path\/to\/reference.zip", - "reference": "ac7dcaf888af2324cd14200769362129c8dd8550" - } - } - } - }, - "available-packages": [ - "buddy-works/repman" + echo $this->lastResponseBody(); + self::assertEquals([ + 'packages' => [ + 'buddy-works\/repman' => [ + '1.2.3' => [ + 'version' => '1.2.3', + 'version_normalized' => '1.2.3.0', + 'dist' => [ + 'type' => 'zip', + 'url' => '\/path\/to\/reference.zip', + 'reference' => 'ac7dcaf888af2324cd14200769362129c8dd8550', + ], + ], + ], + ], + 'available-packages' => [ + 'buddy-works/repman', ], - "metadata-url": "/p2/%package%.json", - "notify-batch": "http://buddy.repo.repman.wip/downloads", - "search": "https://packagist.org/search.json?q=%query%&type=%type%", - "mirrors": [ - { - "dist-url": "http://buddy.repo.repman.wip/dists/%package%/%version%/%reference%.%type%", - "preferred": true - } - ] - } - ', $this->lastResponseBody()); + 'metadata-url' => '/p2/%package%.json', + 'notify-batch' => 'http =>//buddy.repo.repman.wip/downloads', + 'search' => 'https =>//packagist.org/search.json?q=%query%&type=%type%', + 'mirrors' => [ + [ + 'dist-url' => 'http =>//buddy.repo.repman.wip/dists/%package%/%version%/%reference%.%type%', + 'preferred' => true, + ], + ], + ], json_decode($this->lastResponseBody(), true, 512, JSON_THROW_ON_ERROR)); // check if last update date is changed $this->client->request('GET', $this->urlTo('organization_tokens', ['organization' => 'buddy'])); @@ -204,22 +203,7 @@ public function testProviderV2Action(): void ]); self::assertTrue($this->client->getResponse()->isOk()); - - self::assertMatchesPattern(' - { - "buddy-works/repman": { - "1.2.3": { - "version": "1.2.3", - "version_normalized": "1.2.3.0", - "dist": { - "type": "zip", - "url": "/path/to/reference.zip", - "reference": "ac7dcaf888af2324cd14200769362129c8dd8550" - } - } - } - } - ', $this->client->getResponse()->getContent()); + self::assertEquals('', $this->lastResponseBody()); } public function testProviderV2ActionWithCache(): void diff --git a/tests/Unit/Service/Organization/PackageManagerTest.php b/tests/Unit/Service/Organization/PackageManagerTest.php index aff3ced7..2aaa307c 100644 --- a/tests/Unit/Service/Organization/PackageManagerTest.php +++ b/tests/Unit/Service/Organization/PackageManagerTest.php @@ -53,7 +53,7 @@ public function testFindProvidersForPackage(): void 'reference' => 'ac7dcaf888af2324cd14200769362129c8dd8550', ], 'version_normalized' => '1.2.3.0', - ]]], $providers); + ]]], $providers()); } public function testReturnDistributionFilenameWhenExist(): void diff --git a/tests/Unit/Service/Security/SensioLabsPackageScannerTest.php b/tests/Unit/Service/Security/SensioLabsPackageScannerTest.php index d1d66ae7..c39e6330 100644 --- a/tests/Unit/Service/Security/SensioLabsPackageScannerTest.php +++ b/tests/Unit/Service/Security/SensioLabsPackageScannerTest.php @@ -126,24 +126,21 @@ private function prepareScanner(string $fixtureType = 'repman'): SensioLabsPacka { $distFile = \realpath(__DIR__.'/../../../Resources/fixtures/buddy/dist/buddy-works/'.$fixtureType.'/1.2.3.0_ac7dcaf888af2324cd14200769362129c8dd8550.zip'); $packageManager = $this->createMock(PackageManager::class); - $packageManager->method('findProviders')->willReturn( - [ - new \DateTimeImmutable(), - [ - 'buddy-works/repman' => [ - self::VERSION => [ - 'version' => self::VERSION, - 'dist' => [ - 'type' => 'zip', - 'url' => $distFile, - 'reference' => 'ac7dcaf888af2324cd14200769362129c8dd8550', - ], - 'version_normalized' => '1.2.3.0', + $packageManager->method('findProviders')->willReturn([new \DateTimeImmutable(), function () use ($distFile): array { + return [ + 'buddy-works/repman' => [ + self::VERSION => [ + 'version' => self::VERSION, + 'dist' => [ + 'type' => 'zip', + 'url' => $distFile, + 'reference' => 'ac7dcaf888af2324cd14200769362129c8dd8550', ], + 'version_normalized' => '1.2.3.0', ], ], - ] - ); + ]; + }]); $packageManager->method('distFilename')->willReturn(Option::some($distFile)); diff --git a/tests/Unit/Service/User/UserOAuthTokenRefresherTest.php b/tests/Unit/Service/User/UserOAuthTokenRefresherTest.php index eecfe9f0..ecc2b033 100644 --- a/tests/Unit/Service/User/UserOAuthTokenRefresherTest.php +++ b/tests/Unit/Service/User/UserOAuthTokenRefresherTest.php @@ -22,14 +22,15 @@ public function testRefreshToken(): void $client->method('getOAuth2Provider')->willReturn($provider); $oauth->method('getClient')->willReturn($client); + $expires = (new \DateTimeImmutable())->setTimestamp(time() + 3600); $provider->method('getAccessToken')->willReturnOnConsecutiveCalls( new LeagueAccessToken(['access_token' => 'new-token']), - new LeagueAccessToken(['access_token' => 'new-token', 'expires_in' => 3600]) + new LeagueAccessToken(['access_token' => 'new-token', 'expires' => $expires->getTimestamp()]) ); $refresher = new UserOAuthTokenRefresher($oauth); self::assertEquals(new AccessToken('new-token'), $refresher->refresh('github', 'refresh-token')); - self::assertEquals(new AccessToken('new-token', (new \DateTimeImmutable())->setTimestamp(time() + 3600)), $refresher->refresh('github', 'refresh-token')); + self::assertEquals(new AccessToken('new-token', $expires), $refresher->refresh('github', 'refresh-token')); } }