Skip to content
48 changes: 33 additions & 15 deletions src/Controller/RepoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use Buddy\Repman\Query\User\Model\Organization;
use Buddy\Repman\Query\User\Model\PackageName;
use Buddy\Repman\Query\User\PackageQuery;
use Buddy\Repman\Service\Composer\ComposerEnvironmentFactory;
use Buddy\Repman\Service\Organization\PackageManager;
use Composer\Semver\Comparator;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -26,15 +28,18 @@ final class RepoController extends AbstractController
private PackageQuery $packageQuery;
private PackageManager $packageManager;
private MessageBusInterface $messageBus;
private ComposerEnvironmentFactory $environmentFactory;

public function __construct(
PackageQuery $packageQuery,
PackageManager $packageManager,
MessageBusInterface $messageBus
MessageBusInterface $messageBus,
ComposerEnvironmentFactory $environmentFactory
) {
$this->packageQuery = $packageQuery;
$this->packageManager = $packageManager;
$this->messageBus = $messageBus;
$this->environmentFactory = $environmentFactory;
}

/**
Expand All @@ -43,34 +48,47 @@ public function __construct(
*/
public function packages(Request $request, Organization $organization): JsonResponse
{
$packageNames = $this->packageQuery->getAllNames($organization->id());
[$lastModified, $packages] = $this->packageManager->findProviders($organization->alias(), $packageNames);
$lastModified = new \DateTimeImmutable();
$composerV1 = true;

try {
$composerInfo = $this->environmentFactory->fromRequest($request);
$composerV1 = Comparator::lessThan($composerInfo->getVersion(), '2.0.0');
} catch (\Throwable $t) {
}

$response = (new JsonResponse([
'packages' => $packages,
// Build up basic API V2 response
$packageNames = $this->packageQuery->getAllNames($organization->id());
$response = [
'packages' => [],
'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()],
RouterInterface::ABSOLUTE_URL
).'dists/%package%/%version%/%reference%.%type%',
'organization_repo_url',
['organization' => $organization->alias()],
RouterInterface::ABSOLUTE_URL
).'dists/%package%/%version%/%reference%.%type%',
'preferred' => true,
],
],
]))
->setPrivate()
->setLastModified($lastModified);
];

$response->isNotModified($request);
// Add API V1 fields
if ($composerV1) {
[$lastModified, $packages] = $this->packageManager->findProviders($organization->alias(), $packageNames);
$response['packages'] = $packages;
}

return $response;
return (new JsonResponse($response))
->setLastModified($lastModified)
->setPrivate();
}

/**
Expand Down Expand Up @@ -160,7 +178,7 @@ public function providerV2(Request $request, Organization $organization, string
throw new NotFoundHttpException();
}

$response = (new JsonResponse($providerData))
$response = (new JsonResponse(['packages' => $providerData]))
->setLastModified($lastModified)
->setPrivate();

Expand Down
21 changes: 21 additions & 0 deletions src/Service/Composer/ComposerEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Service\Composer;

final class ComposerEnvironment
{
private string $version;

public function __construct(
string $version
) {
$this->version = $version;
}

public function getVersion(): string
{
return $this->version;
}
}
30 changes: 30 additions & 0 deletions src/Service/Composer/ComposerEnvironmentFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Service\Composer;

use Symfony\Component\HttpFoundation\Request;

final class ComposerEnvironmentFactory
{
public function fromRequest(Request $request): ComposerEnvironment
{
$userAgent = $request->headers->get('User-Agent');

return $this->fromUserAgent($userAgent ?? '');
}

public function fromUserAgent(string $userAgent): ComposerEnvironment
{
if (!str_starts_with($userAgent, 'Composer/')) {
throw new \RuntimeException('User Agent appears not to be a composer User Agent');
}

preg_match('/^Composer\/(.+) \((.*)\)$/', $userAgent, $matches);

return new ComposerEnvironment(
$matches['1'],
);
}
}
25 changes: 2 additions & 23 deletions tests/Functional/Controller/Api/PackageControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,30 +199,9 @@ public function testFindPackage(): void

self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());

self::assertJsonStringEqualsJsonString(
self::assertMatchesPattern(
'{"id":"'.$packageId.'","type":"vcs","url":"https:\/\/github.com\/buddy-works\/repman","name":"buddy-works\/repman","latestReleasedVersion":"2.1.1","latestReleaseDate":"@string@","description":"Repository manager","lastSyncAt":"@string@","lastSyncError":null,"webhookCreatedAt":null,"isSynchronizedSuccessfully":true,"scanResultDate":"@string@","scanResultStatus":"ok","lastScanResultContent":{"composer.lock":[]},"keepLastReleases":0,"enableSecurityScan":true}',
$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": []
}
}
'
);
}

Expand Down
23 changes: 13 additions & 10 deletions tests/Functional/Controller/RepoControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public function testOrganizationPackagesAction(): void
],
"metadata-url": "/p2/%package%.json",
"notify-batch": "http://buddy.repo.repman.wip/downloads",
"providers-url": "/p2/%package%.json",
"search": "https://packagist.org/search.json?q=%query%&type=%type%",
"mirrors": [
{
Expand Down Expand Up @@ -205,21 +206,23 @@ public function testProviderV2Action(): void

self::assertTrue($this->client->getResponse()->isOk());

self::assertMatchesPattern('
self::assertJsonStringEqualsJsonString('
{
"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"
"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"
}
}
}
}
}
', $this->client->getResponse()->getContent());
', (string) $this->client->getResponse()->getContent());
}

public function testProviderV2ActionWithCache(): void
Expand Down
59 changes: 59 additions & 0 deletions tests/Unit/Service/Composer/ComposerEnvironmentFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Buddy\Repman\Tests\Unit\Service\Composer;

use Buddy\Repman\Service\Composer\ComposerEnvironmentFactory;
use PHPUnit\Framework\TestCase;

final class ComposerEnvironmentFactoryTest extends TestCase
{
/**
* @dataProvider composerUserAgentDataProvider
*/
public function testCreationByUserAgent(
string $userAgent,
string $version
): void {
$composerInfo = (new ComposerEnvironmentFactory())->fromUserAgent($userAgent);
self::assertEquals($version, $composerInfo->getVersion());
}

/**
* @return iterable<string, array<string, mixed>>
*/
public function composerUserAgentDataProvider(): iterable
{
$userAgents = [
'Composer/2.4.0-RC1 (Darwin; 21.6.0; PHP 8.1.10; cURL 7.85.0; Platform-PHP 7.4.25)' => [
'version' => '2.4.0-RC1',
],
'Composer/2.4.0-RC1 (Darwin; 21.6.0; PHP 7.4.30; cURL 7.85.0; Platform-PHP 7.4.25)' => [
'version' => '2.4.0-RC1',
],
'Composer/2.1.9 (Linux; 4.19.0-17-amd64; PHP 7.4.24; cURL 7.64.0; Platform-PHP 7.1.3; CI)' => [
'version' => '2.1.9',
],
'Composer/1.10.1 (Windows NT; 10.0; PHP 7.4.0)' => [
'version' => '1.10.1',
],
'Composer/2.2.5 (; PHP 8.1.3; cURL 7.64.0)' => [
'version' => '2.2.5',
],
'Composer/2.4.1 (Linux; 4.19.0-17-amd64; PHP 8.1.8; cURL 7.64.0; Platform-PHP 8.1.1; CI)' => [
'version' => '2.4.1',
],
'Composer/2.4.1 (Linux; 4.15.0-187-generic; PHP 8.1.8; cURL 7.64.0; Platform-PHP 8.1.1; CI)' => [
'version' => '2.4.1',
],
'Composer/2.4.1 (Linux; 4.19.0-18-amd64; PHP 8.1.8; cURL 7.64.0; Platform-PHP 8.1.1; CI)' => [
'version' => '2.4.1',
],
];

foreach ($userAgents as $userAgent => $data) {
yield $userAgent => array_merge(['userAgent' => $userAgent], $data);
}
}
}