From f4307ba5a31bcb1b3d3c70934cf719ea1d0a17ce Mon Sep 17 00:00:00 2001 From: Joshua Gigg Date: Thu, 8 Apr 2021 09:56:21 +0100 Subject: [PATCH] Dependency/dependant tracking (#426) * Basic dependency/dependant tracking * Add other link types to test a design * Allow searching for dependants * Slightly nicer looking package list * Move dependant link to overview & update/fix tests * Fix doctrine schema validation * Add missing foreign key * Rename index to what doctrine suggests * Test each of the link types * Improve test coverage --- phpstan.neon | 5 + src/Controller/OrganizationController.php | 19 +++ src/Entity/Organization/Package.php | 31 ++++- src/Entity/Organization/Package/Link.php | 111 +++++++++++++++++ src/Migrations/Version20210309201702.php | 41 +++++++ src/Query/User/PackageQuery.php | 8 ++ .../User/PackageQuery/DbalPackageQuery.php | 113 +++++++++++++----- src/Query/User/PackageQuery/Filter.php | 15 +++ .../ComposerPackageSynchronizer.php | 39 ++++++ .../organization/package/details.html.twig | 46 ++++++- tests/Doubles/FakePackageSynchronizer.php | 18 ++- .../Controller/OrganizationControllerTest.php | 43 ++++++- tests/Integration/FixturesManager.php | 6 +- .../SynchronizePackageHandlerTest.php | 13 +- tests/MotherObject/PackageMother.php | 4 +- .../artifacts/buddy-works-alpha-1.1.0.zip | Bin 309 -> 300 bytes .../artifacts/buddy-works-alpha-1.2.0.zip | Bin 309 -> 362 bytes tests/Unit/Entity/PackageTest.php | 26 +++- .../ComposerPackageSynchronizerTest.php | 17 +++ 19 files changed, 514 insertions(+), 41 deletions(-) create mode 100644 src/Entity/Organization/Package/Link.php create mode 100644 src/Migrations/Version20210309201702.php diff --git a/phpstan.neon b/phpstan.neon index 1b604913..04fcb4bb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,6 +8,11 @@ parameters: count: 1 path: src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php + - + message: "#^Variable method call on Composer\\\\Package\\\\PackageInterface\\.$#" + count: 1 + path: src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php + - message: "#^Variable property access on DateInterval\\.$#" count: 1 diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 7d5cae4e..13dac790 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -110,6 +110,23 @@ public function packageDetails(Organization $organization, PackageDetails $packa { $filter = Filter::fromRequest($request); + $packageLinks = $this->packageQuery->getLinks($package->id(), $organization->id()); + + /** @var string $packageName */ + $packageName = $package->name(); + + $dependantCount = $this->packageQuery->getDependantCount($packageName, $organization->id()); + + $groupedPackageLinks = []; + + foreach ($packageLinks as $packageLink) { + if (!isset($groupedPackageLinks[$packageLink->type()])) { + $groupedPackageLinks[$packageLink->type()] = []; + } + + $groupedPackageLinks[$packageLink->type()][] = $packageLink; + } + return $this->render('organization/package/details.html.twig', [ 'organization' => $organization, 'package' => $package, @@ -117,6 +134,8 @@ public function packageDetails(Organization $organization, PackageDetails $packa 'count' => $this->packageQuery->versionCount($package->id()), 'versions' => $this->packageQuery->getVersions($package->id(), $filter), 'installs' => $this->packageQuery->getInstalls($package->id(), 0), + 'packageLinks' => $groupedPackageLinks, + 'dependantCount' => $dependantCount, ]); } diff --git a/src/Entity/Organization/Package.php b/src/Entity/Organization/Package.php index 58406083..f3280f9f 100644 --- a/src/Entity/Organization/Package.php +++ b/src/Entity/Organization/Package.php @@ -5,6 +5,7 @@ namespace Buddy\Repman\Entity\Organization; use Buddy\Repman\Entity\Organization; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Entity\Organization\Package\Version; use Buddy\Repman\Entity\User\OAuthToken; use Doctrine\Common\Collections\ArrayCollection; @@ -120,6 +121,12 @@ class Package */ private Collection $versions; + /** + * @var Collection|Link[] + * @ORM\OneToMany(targetEntity="Buddy\Repman\Entity\Organization\Package\Link", mappedBy="package", cascade={"persist"}, orphanRemoval=true) + */ + private Collection $links; + /** * @ORM\Column(type="integer") */ @@ -136,6 +143,7 @@ public function __construct(UuidInterface $id, string $type, string $url, array $this->metadata = $metadata; $this->keepLastReleases = $keepLastReleases; $this->versions = new ArrayCollection(); + $this->links = new ArrayCollection(); } public function id(): UuidInterface @@ -163,8 +171,9 @@ public function repositoryUrl(): string /** * @param string[] $encounteredVersions + * @param string[] $encounteredLinks */ - public function syncSuccess(string $name, string $description, string $latestReleasedVersion, array $encounteredVersions, \DateTimeImmutable $latestReleaseDate): void + public function syncSuccess(string $name, string $description, string $latestReleasedVersion, array $encounteredVersions, array $encounteredLinks, \DateTimeImmutable $latestReleaseDate): void { $this->setName($name); $this->description = $description; @@ -175,6 +184,11 @@ public function syncSuccess(string $name, string $description, string $latestRel $this->versions->removeElement($version); } } + foreach ($this->links as $link) { + if (!in_array($link->type().'-'.$link->target(), $encounteredLinks, true)) { + $this->links->removeElement($link); + } + } $this->lastSyncAt = new \DateTimeImmutable(); $this->lastSyncError = null; } @@ -320,6 +334,21 @@ public function addOrUpdateVersion(Version $version): void $this->versions->add($version); } + /** + * @return Collection|Link[] + */ + public function links(): Collection + { + return $this->links; + } + + public function addLink(Link $link): void + { + $link->setPackage($this); + $link->setOrganization($this->organization); + $this->links->add($link); + } + public function removeVersion(Version $version): void { $this->versions->removeElement($version); diff --git a/src/Entity/Organization/Package/Link.php b/src/Entity/Organization/Package/Link.php new file mode 100644 index 00000000..816fb743 --- /dev/null +++ b/src/Entity/Organization/Package/Link.php @@ -0,0 +1,111 @@ +id = $id; + $this->type = $type; + $this->target = $target; + $this->constraint = $constraint; + $this->packageId = $packageId; + $this->targetPackageId = $targetPackageId; + } + + public function type(): string + { + return $this->type; + } + + public function target(): string + { + return $this->target; + } + + public function constraint(): string + { + return $this->constraint; + } + + public function targetPackageId(): ?string + { + return $this->targetPackageId; + } + + public function setOrganization(Organization $organization): void + { + if (isset($this->organization)) { + throw new \RuntimeException('You can not change link organization'); + } + $this->organization = $organization; + } + + public function setPackage(Package $package): void + { + if (isset($this->package)) { + throw new \RuntimeException('You can not change link package'); + } + $this->package = $package; + } +} diff --git a/src/Migrations/Version20210309201702.php b/src/Migrations/Version20210309201702.php new file mode 100644 index 00000000..3a7086ac --- /dev/null +++ b/src/Migrations/Version20210309201702.php @@ -0,0 +1,41 @@ +addSql('CREATE TABLE organization_package_link (id UUID NOT NULL, organization_id UUID NOT NULL, package_id UUID NOT NULL, target VARCHAR(255) NOT NULL, "constraint" VARCHAR(255) NOT NULL, type VARCHAR (255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX link_package_id_idx ON organization_package_link (package_id)'); + $this->addSql('CREATE INDEX IDX_4A06082932C8A3DE ON organization_package_link (organization_id)'); + $this->addSql('CREATE INDEX link_target_idx ON organization_package_link (target)'); + $this->addSql('COMMENT ON COLUMN organization_package_link.id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN organization_package_link.package_id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN organization_package_link.organization_id IS \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE organization_package_link ADD CONSTRAINT FK_CAKE4LIFE FOREIGN KEY (package_id) REFERENCES organization_package (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE organization_package_link ADD CONSTRAINT FK_4A06082932C8A3DE FOREIGN KEY (organization_id) REFERENCES organization (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + + $this->addSql('DROP TABLE organization_package_link'); + } +} diff --git a/src/Query/User/PackageQuery.php b/src/Query/User/PackageQuery.php index 3267c620..6465acd5 100644 --- a/src/Query/User/PackageQuery.php +++ b/src/Query/User/PackageQuery.php @@ -4,6 +4,7 @@ namespace Buddy\Repman\Query\User; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Query\Filter; use Buddy\Repman\Query\User\Model\Installs; use Buddy\Repman\Query\User\Model\Package; @@ -46,6 +47,13 @@ public function versionCount(string $packageId): int; */ public function getVersions(string $packageId, Filter $filter): array; + /** + * @return Link[] + */ + public function getLinks(string $packageId, string $organizationId): array; + + public function getDependantCount(string $packageName, string $organizationId): int; + public function getInstalls(string $packageId, int $lastDays = 30, ?string $version = null): Installs; /** diff --git a/src/Query/User/PackageQuery/DbalPackageQuery.php b/src/Query/User/PackageQuery/DbalPackageQuery.php index 8e0ba081..231a5608 100644 --- a/src/Query/User/PackageQuery/DbalPackageQuery.php +++ b/src/Query/User/PackageQuery/DbalPackageQuery.php @@ -4,6 +4,7 @@ namespace Buddy\Repman\Query\User\PackageQuery; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Entity\Organization\Package\Version as VersionEntity; use Buddy\Repman\Query\Filter as BaseFilter; use Buddy\Repman\Query\User\Model\Installs; @@ -16,6 +17,7 @@ use Buddy\Repman\Query\User\PackageQuery; use Doctrine\DBAL\Connection; use Munus\Control\Option; +use Ramsey\Uuid\Uuid; final class DbalPackageQuery implements PackageQuery { @@ -31,24 +33,28 @@ public function __construct(Connection $connection) */ public function findAll(string $organizationId, Filter $filter): array { - $filterSQL = ''; + $filterSQL = $joinSQL = ''; $params = [ ':organization_id' => $organizationId, ':limit' => $filter->getLimit(), ':offset' => $filter->getOffset(), ]; - if ($filter->hasSearchTerm()) { - $filterSQL = ' AND (name ILIKE :term OR description ILIKE :term) '; + if ($filter->hasLinkSearch()) { + $filterSQL = ' AND l.target = :link'; + $params[':link'] = $filter->getLinkSearch(); + $joinSQL = 'JOIN organization_package_link l ON (p.id = l.package_id AND p.organization_id = :organization_id) '; + } elseif ($filter->hasSearchTerm()) { + $filterSQL = ' AND (p.name ILIKE :term OR p.description ILIKE :term)'; $params[':term'] = '%'.$filter->getSearchTerm().'%'; } $sortSQL = 'name ASC'; $sortColumnMappings = [ - 'name' => 'name', - 'version' => 'latest_released_version', - 'date' => 'latest_release_date', + 'name' => 'p.name', + 'version' => 'p.latest_released_version', + 'date' => 'p.latest_release_date', ]; if ($filter->hasSort() && isset($sortColumnMappings[$filter->getSortColumn()])) { @@ -61,26 +67,26 @@ function (array $data): Package { }, $this->connection->fetchAllAssociative( 'SELECT - id, - organization_id, - type, - repository_url, - name, - latest_released_version, - latest_release_date, - description, - last_sync_at, - last_sync_error, - webhook_created_at, - webhook_created_error, - last_scan_date, - last_scan_status, - last_scan_result, - keep_last_releases - FROM organization_package - WHERE organization_id = :organization_id + p.id, + p.organization_id, + p.type, + p.repository_url, + p.name, + p.latest_released_version, + p.latest_release_date, + p.description, + p.last_sync_at, + p.last_sync_error, + p.webhook_created_at, + p.webhook_created_error, + p.last_scan_date, + p.last_scan_status, + p.last_scan_result, + p.keep_last_releases + FROM organization_package p '.$joinSQL + .'WHERE p.organization_id = :organization_id '.$filterSQL.' - GROUP BY id + GROUP BY p.id ORDER BY '.$sortSQL.' LIMIT :limit OFFSET :offset', $params @@ -106,21 +112,25 @@ public function getAllNames(string $organizationId): array public function count(string $organizationId, Filter $filter): int { - $filterSQL = ''; + $filterSQL = $joinSQL = ''; $params = [ ':organization_id' => $organizationId, ]; - if ($filter->hasSearchTerm()) { - $filterSQL = ' AND (name ILIKE :term OR description ILIKE :term)'; + if ($filter->hasLinkSearch()) { + $filterSQL = ' AND l.target = :link'; + $params[':link'] = $filter->getLinkSearch(); + $joinSQL = 'JOIN organization_package_link l ON (p.id = l.package_id AND p.organization_id = :organization_id) '; + } elseif ($filter->hasSearchTerm()) { + $filterSQL = ' AND (p.name ILIKE :term OR p.description ILIKE :term)'; $params[':term'] = '%'.$filter->getSearchTerm().'%'; } return (int) $this ->connection ->fetchOne( - 'SELECT COUNT(id) FROM "organization_package" - WHERE organization_id = :organization_id'.$filterSQL, + 'SELECT COUNT(DISTINCT p.id) FROM organization_package p '.$joinSQL + .'WHERE p.organization_id = :organization_id'.$filterSQL, $params ); } @@ -206,6 +216,49 @@ public function versionCount(string $packageId): int ); } + public function getDependantCount(string $packageName, string $organizationId): int + { + return (int) $this->connection->fetchOne( + 'SELECT + COUNT(DISTINCT package_id) + FROM organization_package_link + WHERE target = :package_name + AND organization_id = :organization_id', [ + ':package_name' => $packageName, + ':organization_id' => $organizationId, + ]); + } + + /** + * @return Link[] + */ + public function getLinks(string $packageId, string $organizationId): array + { + return array_map(function (array $data): Link { + return new Link( + Uuid::fromString($data['id']), + $data['type'], + $data['target'], + $data['constraint'], + $data['package_id'], + $data['target_package_id'] + ); + }, $this->connection->fetchAllAssociative( + 'SELECT + l.id, + l.type, + l.target, + l.constraint, + l.package_id, + p.id as target_package_id + FROM organization_package_link l + LEFT JOIN organization_package p ON (p.name = l.target AND p.organization_id = :organization_id) + WHERE package_id = :package_id', [ + ':organization_id' => $organizationId, + ':package_id' => $packageId, + ])); + } + /** * @return Version[] */ diff --git a/src/Query/User/PackageQuery/Filter.php b/src/Query/User/PackageQuery/Filter.php index d1f1c170..26a20acd 100644 --- a/src/Query/User/PackageQuery/Filter.php +++ b/src/Query/User/PackageQuery/Filter.php @@ -27,6 +27,21 @@ public function hasSearchTerm(): bool return $this->searchTerm !== null; } + public function hasLinkSearch(): bool + { + // @todo Allow more search types? + return $this->searchTerm !== null && str_starts_with($this->searchTerm, 'depends:'); + } + + public function getLinkSearch(): ?string + { + if ($this->searchTerm === null) { + return null; + } + + return substr($this->searchTerm, strlen('depends:')); + } + public function getQueryStringParams(): array { $params = parent::getQueryStringParams(); diff --git a/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php b/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php index 720c5c96..460c7826 100644 --- a/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php +++ b/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php @@ -19,6 +19,7 @@ use Composer\IO\BufferIO; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; +use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryInterface; @@ -114,6 +115,7 @@ public function synchronize(Package $package): void usort($versions, fn ($item1, $item2) => $item2['releaseDate'] <=> $item1['releaseDate']); $encounteredVersions = []; + $encounteredLinks = []; foreach ($versions as $version) { $dist = new Dist( $version['organizationAlias'], @@ -149,6 +151,42 @@ public function synchronize(Package $package): void if ($latest->getVersion() === $version['version']) { $this->readmeExtractor->extractReadme($package, $dist); + + // Set the version links + $types = ['requires', 'devRequires', 'provides', 'replaces', 'conflicts']; + + foreach ($types as $type) { + /** @var Link[] $links */ + $functionName = 'get'.$type; + if (method_exists($latest, $functionName)) { + $links = $latest->{$functionName}(); + + foreach ($links as $link) { + $package->addLink( + new Package\Link( + Uuid::uuid4(), + $type, + $link->getTarget(), + $link->getPrettyConstraint(), + ) + ); + $encounteredLinks[] = $type.'-'.$link->getTarget(); + } + } + } + + // suggests are different + foreach ($latest->getSuggests() as $linkName => $linkDescription) { + $package->addLink( + new Package\Link( + Uuid::uuid4(), + 'suggests', + $linkName, + $linkDescription, + ) + ); + $encounteredLinks[] = 'suggests-'.$linkName; + } } $package->addOrUpdateVersion( @@ -170,6 +208,7 @@ public function synchronize(Package $package): void $latest instanceof CompletePackage ? ($latest->getDescription() ?? 'n/a') : 'n/a', $latest->getStability() === Version::STABILITY_STABLE ? $latest->getPrettyVersion() : 'no stable release', $encounteredVersions, + $encounteredLinks, \DateTimeImmutable::createFromMutable($latest->getReleaseDate() ?? new \DateTime()), ); diff --git a/templates/organization/package/details.html.twig b/templates/organization/package/details.html.twig index 5d6f302f..77d6fc99 100644 --- a/templates/organization/package/details.html.twig +++ b/templates/organization/package/details.html.twig @@ -28,7 +28,7 @@
-
+

Total Installs

+

Security

@@ -146,6 +154,42 @@

{% endif %} + {% if packageLinks|length > 0 %} +
+ +
+ {% set types = { + 'requires': 'Requirements', + 'devRequires': 'Dev Requirements', + 'suggests': 'Suggestions', + 'provides': 'Provides', + 'conflicts': 'Conflicts', + 'replaces': 'Replaces', + } %} + + {% for type,label in types %} +
+

{{ label }}

+ {% if packageLinks[type] is defined and packageLinks[type]|length > 0 %} +
    + {% for link in packageLinks[type] %} +
  • + {% if link.targetPackageId %} + {{ link.target }}: {{ link.constraint }} + {% else %} + {{ link.target }}: {{ link.constraint }} + {% endif %} +
  • + {% endfor %} +
+ {% else %} + None + {% endif %} +
+ {% endfor %} +
+ {% endif %} + {% if package.readme() %}
diff --git a/tests/Doubles/FakePackageSynchronizer.php b/tests/Doubles/FakePackageSynchronizer.php index 62ff18fd..02718f65 100644 --- a/tests/Doubles/FakePackageSynchronizer.php +++ b/tests/Doubles/FakePackageSynchronizer.php @@ -5,6 +5,7 @@ namespace Buddy\Repman\Tests\Doubles; use Buddy\Repman\Entity\Organization\Package; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Entity\Organization\Package\Version; use Buddy\Repman\Service\PackageSynchronizer; @@ -22,6 +23,11 @@ final class FakePackageSynchronizer implements PackageSynchronizer */ private array $versions = []; + /** + * @var Link[] + */ + private array $links = []; + public function __construct() { $this->latestReleaseDate = new \DateTimeImmutable(); @@ -29,8 +35,9 @@ public function __construct() /** * @param Version[] $versions + * @param Link[] $links */ - public function setData(string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = [], ?string $readme = null): void + public function setData(string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = [], array $links = [], ?string $readme = null): void { $this->name = $name; $this->description = $description; @@ -38,6 +45,7 @@ public function setData(string $name, string $description, string $latestRelease $this->latestReleaseDate = $latestReleaseDate; $this->error = null; $this->versions = $versions; + $this->links = $links; $this->readme = $readme; } @@ -64,11 +72,19 @@ public function synchronize(Package $package): void return $version->version(); }, $this->versions); + $encounteredLinks = []; + + foreach ($this->links as $link) { + $package->addLink($link); + $encounteredLinks[] = $link->type().'-'.$link->target(); + } + $package->syncSuccess( $this->name, $this->description, $this->latestReleasedVersion, $encounteredVersions, + $encounteredLinks, $this->latestReleaseDate ); } diff --git a/tests/Functional/Controller/OrganizationControllerTest.php b/tests/Functional/Controller/OrganizationControllerTest.php index 137087de..e48abbb1 100644 --- a/tests/Functional/Controller/OrganizationControllerTest.php +++ b/tests/Functional/Controller/OrganizationControllerTest.php @@ -4,6 +4,7 @@ namespace Buddy\Repman\Tests\Functional\Controller; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Entity\Organization\Package\Metadata; use Buddy\Repman\Entity\Organization\Package\Version; use Buddy\Repman\Entity\User\OAuthToken; @@ -163,6 +164,29 @@ public function testPackageSearch(): void self::assertStringContainsString('search=buddy', $response); } + public function testDependantSearch(): void + { + $buddyId = $this->fixtures->createOrganization('buddy', $this->userId); + + $packageId = $this->fixtures->addPackage($buddyId, 'https://buddy.com'); + $this->fixtures->syncPackageWithData($packageId, 'buddy-works/testing', '1', '1.1.1', new \DateTimeImmutable()); + + $packageId2 = $this->fixtures->addPackage($buddyId, 'https://buddy.com'); + $links = [ + new Link(Uuid::uuid4(), 'requires', 'buddy-works/testing', '^1.5'), + ]; + $this->fixtures->syncPackageWithData($packageId2, 'buddy-works/example', '2', '1.1.1', new \DateTimeImmutable(), [], $links); + + // Search for 'testing' (which is in name) + $this->client->request('GET', $this->urlTo('organization_packages', ['organization' => 'buddy', 'search' => 'depends:buddy-works/testing'])); + + self::assertTrue($this->client->getResponse()->isOk()); + $response = (string) $this->client->getResponse()->getContent(); + self::assertStringContainsString('1 entries', $response); + self::assertStringNotContainsString($packageId, $response); + self::assertStringContainsString($packageId2, $response); + } + public function testPagination(): void { $buddyId = $this->fixtures->createOrganization('buddy', $this->userId); @@ -459,10 +483,14 @@ public function testPackageDetails(): void new Version(Uuid::uuid4(), '1.0.1', 'ref2', 1048576, new \DateTimeImmutable(), Version::STABILITY_STABLE), new Version(Uuid::uuid4(), '1.1.0', 'lastref', 1073741824, new \DateTimeImmutable(), Version::STABILITY_STABLE), ]; - $this->fixtures->syncPackageWithData($packageId, 'buddy-works/buddy', 'Test', '1.1.1', new \DateTimeImmutable(), $versions, 'This is a readme'); + $links = [ + new Link(Uuid::uuid4(), 'requires', 'buddy-works/target', '^1.5'), + new Link(Uuid::uuid4(), 'suggests', 'buddy-works/buddy', '^2.0'), // Suggest self to test dependant link + ]; + $this->fixtures->syncPackageWithData($packageId, 'buddy-works/buddy', 'Test', '1.1.1', new \DateTimeImmutable(), $versions, $links, 'This is a readme'); $this->fixtures->addScanResult($packageId, 'ok'); - $this->client->request('GET', $this->urlTo('organization_package_details', [ + $crawler = $this->client->request('GET', $this->urlTo('organization_package_details', [ 'organization' => 'buddy', 'package' => $packageId, ])); @@ -475,6 +503,17 @@ public function testPackageDetails(): void self::assertStringContainsString($version->version(), $this->lastResponseBody()); self::assertStringContainsString($version->reference(), $this->lastResponseBody()); } + + $crawlerText = $crawler->text(null, true); + + self::assertStringContainsString('Requirements', $this->lastResponseBody()); + foreach ($links as $link) { + self::assertStringContainsString("{$link->target()}: {$link->constraint()}", $crawlerText); + } + + self::assertStringContainsString('Dependant Packages 1', $crawlerText); + self::assertStringContainsString('depends:buddy-works/buddy', $this->lastResponseBody()); + self::assertStringContainsString('This is a readme', $this->lastResponseBody()); $this->client->request('GET', $this->urlTo('organization_package_details', [ diff --git a/tests/Integration/FixturesManager.php b/tests/Integration/FixturesManager.php index ebf5d1e6..d4e2fe08 100644 --- a/tests/Integration/FixturesManager.php +++ b/tests/Integration/FixturesManager.php @@ -5,6 +5,7 @@ namespace Buddy\Repman\Tests\Integration; use Buddy\Repman\Entity\Organization\Member; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Entity\Organization\Package\ScanResult; use Buddy\Repman\Entity\Organization\Package\Version; use Buddy\Repman\Message\Admin\ChangeConfig; @@ -200,10 +201,11 @@ public function syncPackageWithError(string $packageId, string $error): void /** * @param Version[] $versions + * @param Link[] $links */ - public function syncPackageWithData(string $packageId, string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = [], ?string $readme = null): void + public function syncPackageWithData(string $packageId, string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = [], array $links = [], ?string $readme = null): void { - $this->container->get(PackageSynchronizer::class)->setData($name, $description, $latestReleasedVersion, $latestReleaseDate, $versions, $readme); + $this->container->get(PackageSynchronizer::class)->setData($name, $description, $latestReleasedVersion, $latestReleaseDate, $versions, $links, $readme); $this->dispatchMessage(new SynchronizePackage($packageId)); $this->container->get(EntityManagerInterface::class)->flush(); } diff --git a/tests/Integration/MessageHandler/Organization/SynchronizePackageHandlerTest.php b/tests/Integration/MessageHandler/Organization/SynchronizePackageHandlerTest.php index 2bd16872..7c0b46c2 100644 --- a/tests/Integration/MessageHandler/Organization/SynchronizePackageHandlerTest.php +++ b/tests/Integration/MessageHandler/Organization/SynchronizePackageHandlerTest.php @@ -4,11 +4,13 @@ namespace Buddy\Repman\Tests\Integration\MessageHandler\Organization; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Message\Organization\SynchronizePackage; use Buddy\Repman\Query\User\Model\Package; use Buddy\Repman\Query\User\PackageQuery\DbalPackageQuery; use Buddy\Repman\Service\PackageSynchronizer; use Buddy\Repman\Tests\Integration\IntegrationTestCase; +use Ramsey\Uuid\Uuid; final class SynchronizePackageHandlerTest extends IntegrationTestCase { @@ -16,11 +18,14 @@ public function testSuccess(): void { $organizationId = $this->fixtures->createOrganization('Buddy', $this->fixtures->createUser()); $packageId = $this->fixtures->addPackage($organizationId, 'https://github.com/buddy-works/repman', 'vcs'); + $link = new Link(Uuid::uuid4(), 'requires', 'buddy-works/target', '^1.5'); $this->container()->get(PackageSynchronizer::class)->setData( $name = 'buddy-works/repman', $description = 'Repman - PHP repository manager', $version = '2.0.0', - $date = new \DateTimeImmutable() + $date = new \DateTimeImmutable(), + [], + [$link], ); $this->dispatchMessage(new SynchronizePackage($packageId)); @@ -35,6 +40,12 @@ public function testSuccess(): void /** @var \DateTimeImmutable $releaseDate */ $releaseDate = $package->latestReleaseDate(); self::assertEquals($date->format('Y-m-d H:i:s'), $releaseDate->format('Y-m-d H:i:s')); + + /** @var Link[] $packageLinks */ + $packageLinks = $this->container()->get(DbalPackageQuery::class)->getLinks($packageId, $organizationId); + self::assertCount(1, $packageLinks); + self::assertEquals($link->target(), $packageLinks[0]->target()); + self::assertEquals($link->constraint(), $packageLinks[0]->constraint()); } public function testHandlePackageNotFoundWithoutError(): void diff --git a/tests/MotherObject/PackageMother.php b/tests/MotherObject/PackageMother.php index f53cdf4d..6d4547c4 100644 --- a/tests/MotherObject/PackageMother.php +++ b/tests/MotherObject/PackageMother.php @@ -55,8 +55,9 @@ public static function withOrganizationAndToken(string $type, string $url, strin /** * @param string[] $unencounteredVersions + * @param string[] $unencounteredLinks */ - public static function synchronized(string $name, string $latestVersion, string $url = '', array $unencounteredVersions = []): Package + public static function synchronized(string $name, string $latestVersion, string $url = '', array $unencounteredVersions = [], array $unencounteredLinks = []): Package { $package = new Package(Uuid::uuid4(), 'path', $url); $package->setOrganization(new Organization( @@ -70,6 +71,7 @@ public static function synchronized(string $name, string $latestVersion, string 'Package description', $latestVersion, $unencounteredVersions, + $unencounteredLinks, new \DateTimeImmutable() ); diff --git a/tests/Resources/artifacts/buddy-works-alpha-1.1.0.zip b/tests/Resources/artifacts/buddy-works-alpha-1.1.0.zip index 75736bd94245f5529cc5c2bca198b6822aa017ae..96f7a50807c9f992a97a1ef64468c635d3660fb4 100644 GIT binary patch literal 300 zcmWIWW@Zs#W?)rV+=Be#)FQpC;`}__zG$vP1_G}C zcXv(jH#A(?bRqYLe$UwiXWz+cD?^u5oA)0&aA0S8+wW|Sg~Ad`WEb|kuGEiSD}R_X zE{~z=%*I0(s^2c!7Q8L*nnI(f^F6*_g7YGdn*ZA1J;_^7t9MsjzN_|(2|jZ=b9Nhr zez4HLm9DSL47!*Ka42%pC3=AyCtn$^aGosT0-mGj8HH-}LK*n|uhXDZnm13*_ literal 309 zcmWIWW@Zs#-~hty+1>#RP|yUVc^MQKlJj#5@{3c8^slFA-B0PZgVz z^6YtI2_?66DeVZ$>5&W`r-0K+7T>hc#466;U6Od122$H&d)8#FHSAe%PP*#TiWNyb;v-# z^}DU>iCf2(a5P;nchiihRe~j7H^2(EGqoPw7RBvXWO^4|H_Ur*dF|`e_GMki&9s8 zwC9)9oQ+;Mjz{nuM&``0+`n%e7MszyBo0ScshLIs2$oLB4 GFaQ7q>WPd1 literal 309 zcmWIWW@Zs#-~hsh+1>#RP|yUVc^MQKlJj#5@{3c8^slFA-B0PZgVz z^6YtI2expectException(\RuntimeException::class); - $this->package->syncSuccess('../invalid/name', 'desc', '1.2.0.0', [], new \DateTimeImmutable()); + $this->package->syncSuccess('../invalid/name', 'desc', '1.2.0.0', [], [], new \DateTimeImmutable()); } public function testSyncSuccessRemovesUnencounteredVersions(): void @@ -32,7 +33,7 @@ public function testSyncSuccessRemovesUnencounteredVersions(): void $this->package->addOrUpdateVersion($version2 = new Version(Uuid::uuid4(), '1.0.1', 'anotherref', 5678, new \DateTimeImmutable(), Version::STABILITY_STABLE)); $this->package->addOrUpdateVersion($version3 = new Version(Uuid::uuid4(), '1.1.0', 'lastref', 6543, new \DateTimeImmutable(), Version::STABILITY_STABLE)); - $this->package->syncSuccess('some/package', 'desc', '1.1.0', ['1.0.0', '1.1.0'], new \DateTimeImmutable()); + $this->package->syncSuccess('some/package', 'desc', '1.1.0', ['1.0.0', '1.1.0'], [], new \DateTimeImmutable()); self::assertCount(2, $this->package->versions()); self::assertContains($version1, $this->package->versions()); @@ -40,6 +41,27 @@ public function testSyncSuccessRemovesUnencounteredVersions(): void self::assertContains($version3, $this->package->versions()); } + public function testSyncSuccessRemovesUnencounteredLinks(): void + { + $this->package->addLink($link1 = new Link(Uuid::uuid4(), 'replaces', 'buddy-works/testone', '^1.0')); + $this->package->addLink($link2 = new Link(Uuid::uuid4(), 'replaces', 'buddy-works/testtwo', '^1.0')); + $this->package->addLink($link3 = new Link(Uuid::uuid4(), 'replaces', 'buddy-works/testthree', '^1.0')); + + $this->package->syncSuccess( + 'some/package', + 'desc', + '1.1.0', + [], + ['replaces-buddy-works/testone', 'replaces-buddy-works/testthree'], + new \DateTimeImmutable() + ); + + self::assertCount(2, $this->package->links()); + self::assertContains($link1, $this->package->links()); + self::assertNotContains($link2, $this->package->links()); + self::assertContains($link3, $this->package->links()); + } + public function testOuathTokenNotFound(): void { $this->expectException(\RuntimeException::class); diff --git a/tests/Unit/Service/PackageSynchronizer/ComposerPackageSynchronizerTest.php b/tests/Unit/Service/PackageSynchronizer/ComposerPackageSynchronizerTest.php index 4347d9d9..5b3a10cd 100644 --- a/tests/Unit/Service/PackageSynchronizer/ComposerPackageSynchronizerTest.php +++ b/tests/Unit/Service/PackageSynchronizer/ComposerPackageSynchronizerTest.php @@ -4,6 +4,7 @@ namespace Buddy\Repman\Tests\Unit\Service\PackageSynchronizer; +use Buddy\Repman\Entity\Organization\Package\Link; use Buddy\Repman\Entity\Organization\Package\Version; use Buddy\Repman\Repository\PackageRepository; use Buddy\Repman\Service\Dist\Storage; @@ -92,6 +93,22 @@ public function testSynchronizePackageFromArtifacts(): void }, $package->versions()->toArray()); sort($versionStrings, SORT_NATURAL); self::assertEquals(['1.0.0', '1.1.0', '1.1.1', '1.2.0'], $versionStrings); + + /** @var Link[] $links */ + $links = $package->links()->toArray(); + self::assertCount(6, $links); + + $linkStrings = array_map( + fn (Link $link): string => $link->type().'-'.$link->target().'-'.$link->constraint(), + $links + ); + + self::assertContains('requires-php-^7.4.1', $linkStrings); + self::assertContains('devRequires-buddy-works/dev-^1.0', $linkStrings); + self::assertContains('provides-buddy-works/provide-^1.0', $linkStrings); + self::assertContains('replaces-buddy-works/replace-^1.0', $linkStrings); + self::assertContains('conflicts-buddy-works/conflict-^1.0', $linkStrings); + self::assertContains('suggests-buddy-works/suggests-You really should', $linkStrings); } public function testSynchronizePackageThatAlreadyExists(): void