diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index 41918414..56fafbb4 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -37,9 +37,9 @@ protected function parseJson(Request $request): array return []; } - $data = json_decode($request->getContent(), true); - - if (json_last_error() !== JSON_ERROR_NONE) { + try { + $data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $th) { throw new BadRequestHttpException(); } @@ -60,11 +60,9 @@ protected function getErrors(FormInterface $form): Errors } /** - * @param callable $listFunction - * * @return array */ - protected function paginate($listFunction, int $total, int $perPage, int $page, string $baseUrl): array + protected function paginate(callable $listFunction, int $total, int $perPage, int $page, string $baseUrl): array { $pages = (int) ceil($total / $perPage); if ($pages === 0) { diff --git a/src/Controller/Api/PackageController.php b/src/Controller/Api/PackageController.php index 744c8edf..316ce907 100644 --- a/src/Controller/Api/PackageController.php +++ b/src/Controller/Api/PackageController.php @@ -7,10 +7,12 @@ use Buddy\Repman\Entity\Organization\Package\Metadata; use Buddy\Repman\Entity\User\OAuthToken; use Buddy\Repman\Form\Type\Api\AddPackageType; +use Buddy\Repman\Form\Type\Api\EditPackageType; use Buddy\Repman\Message\Organization\AddPackage; use Buddy\Repman\Message\Organization\Package\AddBitbucketHook; use Buddy\Repman\Message\Organization\Package\AddGitHubHook; use Buddy\Repman\Message\Organization\Package\AddGitLabHook; +use Buddy\Repman\Message\Organization\Package\Update; use Buddy\Repman\Message\Organization\RemovePackage; use Buddy\Repman\Message\Organization\SynchronizePackage; use Buddy\Repman\Query\Api\Model\Errors; @@ -174,12 +176,48 @@ public function removePackage(Organization $organization, Package $package): Jso return new JsonResponse(); } + /** + * Synchronize package. + * + * @Route("/api/organization/{organization}/package/{package}", + * name="api_synchronize_update", + * methods={"PUT"}, + * requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"} + * ) + * + * @Oa\Parameter( + * name="package", + * in="path", + * description="UUID" + * ) + * + * @OA\Response( + * response=200, + * description="Package updated" + * ) + * + * @OA\Response( + * response=404, + * description="Package not found" + * ) + * + * @OA\Tag(name="Package") + */ + public function synchronizePackage(Organization $organization, Package $package): JsonResponse + { + $this->dispatchMessage(new SynchronizePackage($package->getId())); + + return new JsonResponse(); + } + /** * Update and synchronize package. * + * @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization") + * * @Route("/api/organization/{organization}/package/{package}", * name="api_package_update", - * methods={"PUT"}, + * methods={"PATCH"}, * requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"} * ) * @@ -189,6 +227,10 @@ public function removePackage(Organization $organization, Package $package): Jso * description="UUID" * ) * + * @OA\RequestBody( + * @Model(type=EditPackageType::class) + * ) + * * @OA\Response( * response=200, * description="Package updated" @@ -204,10 +246,32 @@ public function removePackage(Organization $organization, Package $package): Jso * description="Forbidden" * ) * + * @OA\Response( + * response=400, + * description="Bad request" + * ) + * * @OA\Tag(name="Package") */ - public function updatePackage(Organization $organization, Package $package): JsonResponse + public function updatePackage(Organization $organization, Package $package, Request $request): JsonResponse { + $form = $this->createApiForm(EditPackageType::class); + + $form->submit(array_merge([ + 'url' => $package->getUrl(), + 'keepLastReleases' => $package->getKeepLastReleases(), + ], $this->parseJson($request))); + + if (!$form->isValid()) { + return $this->badRequest($this->getErrors($form)); + } + + $this->dispatchMessage(new Update( + $package->getId(), + $form->get('url')->getData(), + $form->get('keepLastReleases')->getData(), + )); + $this->dispatchMessage(new SynchronizePackage($package->getId())); return new JsonResponse(); diff --git a/src/Controller/Organization/PackageController.php b/src/Controller/Organization/PackageController.php index 98ce164f..0f9bbba6 100644 --- a/src/Controller/Organization/PackageController.php +++ b/src/Controller/Organization/PackageController.php @@ -7,12 +7,15 @@ use Buddy\Repman\Entity\Organization\Package\Metadata; use Buddy\Repman\Entity\User\OAuthToken; use Buddy\Repman\Form\Type\Organization\AddPackageType; +use Buddy\Repman\Form\Type\Organization\EditPackageType; use Buddy\Repman\Message\Organization\AddPackage; use Buddy\Repman\Message\Organization\Package\AddBitbucketHook; use Buddy\Repman\Message\Organization\Package\AddGitHubHook; use Buddy\Repman\Message\Organization\Package\AddGitLabHook; +use Buddy\Repman\Message\Organization\Package\Update; use Buddy\Repman\Message\Organization\SynchronizePackage; use Buddy\Repman\Query\User\Model\Organization; +use Buddy\Repman\Query\User\Model\Package; use Buddy\Repman\Query\User\UserQuery; use Buddy\Repman\Security\Model\User; use Buddy\Repman\Service\BitbucketApi; @@ -110,6 +113,53 @@ public function packageNew(Organization $organization, Request $request, ?string ]); } + /** + * @Route("/organization/{organization}/package/{package}", name="organization_package_update", methods={"POST"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"}) + */ + public function updatePackage(Organization $organization, Package $package): Response + { + $this->dispatchMessage(new SynchronizePackage($package->id())); + + $this->addFlash('success', 'Package will be synchronized in the background'); + + return $this->redirectToRoute('organization_packages', ['organization' => $organization->alias()]); + } + + /** + * @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization") + * @Route("/organization/{organization}/package/{package}/edit", name="organization_package_edit", methods={"GET","POST"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"}) + */ + public function editPackage(Organization $organization, Package $package, Request $request): Response + { + $form = $this->createForm(EditPackageType::class, [ + 'url' => $package->url(), + 'keepLastReleases' => $package->keepLastReleases(), + ]); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + + $this->dispatchMessage(new Update( + $package->id(), + $data['url'], + $data['keepLastReleases'], + )); + + $this->dispatchMessage(new SynchronizePackage($package->id())); + + $this->addFlash('success', 'Package will be synchronized in the background'); + + return $this->redirectToRoute('organization_packages', ['organization' => $organization->alias()]); + } + + return $this->render('organization/package/edit.html.twig', [ + 'organization' => $organization, + 'package' => $package, + 'form' => $form->createView(), + ]); + } + /** * @param string[] $choices */ diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 44c4df77..5c318302 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -22,7 +22,6 @@ use Buddy\Repman\Message\Organization\RemoveOrganization; use Buddy\Repman\Message\Organization\RemovePackage; use Buddy\Repman\Message\Organization\RemoveToken; -use Buddy\Repman\Message\Organization\SynchronizePackage; use Buddy\Repman\Message\Security\ScanPackage; use Buddy\Repman\Query\Filter; use Buddy\Repman\Query\User\Model\Installs\Day; @@ -88,18 +87,6 @@ public function packages(Organization $organization, Request $request): Response ]); } - /** - * @Route("/organization/{organization}/package/{package}", name="organization_package_update", methods={"POST"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"}) - */ - public function updatePackage(Organization $organization, Package $package): Response - { - $this->dispatchMessage(new SynchronizePackage($package->id())); - - $this->addFlash('success', 'Package will be updated in the background'); - - return $this->redirectToRoute('organization_packages', ['organization' => $organization->alias()]); - } - /** * @IsGranted("ROLE_ORGANIZATION_OWNER", subject="organization") * @Route("/organization/{organization}/package/{package}", name="organization_package_remove", methods={"DELETE"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"}) diff --git a/src/Entity/Organization/Package.php b/src/Entity/Organization/Package.php index 20f93ffa..cbb3e02e 100644 --- a/src/Entity/Organization/Package.php +++ b/src/Entity/Organization/Package.php @@ -310,4 +310,10 @@ public function keepLastReleases(): int { return $this->keepLastReleases; } + + public function update(string $url, int $keepLastReleases): void + { + $this->keepLastReleases = $keepLastReleases; + $this->repositoryUrl = $url; + } } diff --git a/src/Form/Type/Api/AddPackageType.php b/src/Form/Type/Api/AddPackageType.php index f03eb9b4..6cc2b8b9 100644 --- a/src/Form/Type/Api/AddPackageType.php +++ b/src/Form/Type/Api/AddPackageType.php @@ -10,6 +10,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\PositiveOrZero; class AddPackageType extends AbstractType { @@ -57,6 +58,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('keepLastReleases', IntegerType::class, [ 'data' => 0, 'required' => false, + 'constraints' => [ + new PositiveOrZero(), + ], ]); } } diff --git a/src/Form/Type/Api/EditPackageType.php b/src/Form/Type/Api/EditPackageType.php new file mode 100644 index 00000000..24e8f996 --- /dev/null +++ b/src/Form/Type/Api/EditPackageType.php @@ -0,0 +1,43 @@ + $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('url', TextType::class, [ + 'required' => false, + 'label' => 'Repository URL', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('keepLastReleases', IntegerType::class, [ + 'required' => false, + 'label' => 'Keep last releases', + 'help' => 'Number of last releases that will be downloaded. Put "0" to download all.', + 'constraints' => [ + new PositiveOrZero(), + ], + ]); + } +} diff --git a/src/Form/Type/Organization/AddPackageType.php b/src/Form/Type/Organization/AddPackageType.php index 976e4376..f226a834 100644 --- a/src/Form/Type/Organization/AddPackageType.php +++ b/src/Form/Type/Organization/AddPackageType.php @@ -11,6 +11,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\PositiveOrZero; class AddPackageType extends AbstractType { @@ -68,6 +69,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'data' => 0, 'help' => 'Number of last releases that will be downloaded. Put "0" to download all.', 'required' => false, + 'constraints' => [ + new PositiveOrZero(), + ], ]) ->add('Add', SubmitType::class); } diff --git a/src/Form/Type/Organization/EditPackageType.php b/src/Form/Type/Organization/EditPackageType.php new file mode 100644 index 00000000..dfcfe170 --- /dev/null +++ b/src/Form/Type/Organization/EditPackageType.php @@ -0,0 +1,43 @@ + $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('url', TextType::class, [ + 'label' => 'Repository URL', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('keepLastReleases', IntegerType::class, [ + 'label' => 'Keep last releases', + 'help' => 'Number of last releases that will be downloaded. Put "0" to download all.', + 'constraints' => [ + new PositiveOrZero(), + ], + ]) + ->add('Update', SubmitType::class); + } +} diff --git a/src/Message/Organization/Package/Update.php b/src/Message/Organization/Package/Update.php new file mode 100644 index 00000000..c1ddd5e8 --- /dev/null +++ b/src/Message/Organization/Package/Update.php @@ -0,0 +1,34 @@ +packageId = $packageId; + $this->url = $url; + $this->keepLastReleases = $keepLastReleases; + } + + public function packageId(): string + { + return $this->packageId; + } + + public function url(): string + { + return $this->url; + } + + public function keepLastReleases(): int + { + return $this->keepLastReleases; + } +} diff --git a/src/MessageHandler/Organization/Package/UpdateHandler.php b/src/MessageHandler/Organization/Package/UpdateHandler.php new file mode 100644 index 00000000..e98281d6 --- /dev/null +++ b/src/MessageHandler/Organization/Package/UpdateHandler.php @@ -0,0 +1,31 @@ +packages = $packages; + } + + public function __invoke(Update $message): void + { + $package = $this->packages->find(Uuid::fromString($message->packageId())); + if (!$package instanceof Package) { + return; + } + + $package->update($message->url(), $message->keepLastReleases()); + } +} diff --git a/src/Query/Api/Model/Package.php b/src/Query/Api/Model/Package.php index 2bd04105..7235a6b1 100644 --- a/src/Query/Api/Model/Package.php +++ b/src/Query/Api/Model/Package.php @@ -122,7 +122,7 @@ public function getLastScanResultContent(): array return $this->scanResult !== null ? $this->scanResult->content() : []; } - public function keepLastReleases(): int + public function getKeepLastReleases(): int { return $this->keepLastReleases; } @@ -147,7 +147,7 @@ public function jsonSerialize(): array 'scanResultDate' => $this->getScanResultDate() === null ? null : $this->getScanResultDate()->format(\DateTime::ATOM), 'scanResultStatus' => $this->getScanResultStatus(), 'lastScanResultContent' => $this->getLastScanResultContent(), - 'keepLastReleases' => $this->keepLastReleases(), + 'keepLastReleases' => $this->getKeepLastReleases(), ]; } } diff --git a/templates/component/packageActions.html.twig b/templates/component/packageActions.html.twig index 8b1a85aa..973526cc 100644 --- a/templates/component/packageActions.html.twig +++ b/templates/component/packageActions.html.twig @@ -41,6 +41,13 @@ {% include 'svg/refresh.svg' %} Update {% if is_granted('ROLE_ORGANIZATION_OWNER', organization) %} + + + {% include 'svg/edit.svg' %} Edit + - @@ -128,7 +128,7 @@ diff --git a/templates/organization/package/edit.html.twig b/templates/organization/package/edit.html.twig new file mode 100644 index 00000000..d8f23d5a --- /dev/null +++ b/templates/organization/package/edit.html.twig @@ -0,0 +1,22 @@ +{% extends 'base.html.twig' %} + +{% block header %} + + « + {% include 'svg/package.svg' %} + + + {{ package.name }} edit +{% endblock %} +{% block header_btn %} + {% include 'component/packageActions.html.twig' %} +{% endblock %} + +{% block content %} +
+
+ {{ form(form) }} +
+
+{% endblock %} diff --git a/templates/organization/package/webhook.html.twig b/templates/organization/package/webhook.html.twig index 14cefb27..19eedc1b 100644 --- a/templates/organization/package/webhook.html.twig +++ b/templates/organization/package/webhook.html.twig @@ -52,7 +52,7 @@
- diff --git a/templates/organization/tokens.html.twig b/templates/organization/tokens.html.twig index ac69076e..ae771f60 100644 --- a/templates/organization/tokens.html.twig +++ b/templates/organization/tokens.html.twig @@ -41,7 +41,7 @@
- diff --git a/templates/svg/edit.svg b/templates/svg/edit.svg new file mode 100644 index 00000000..0e561c26 --- /dev/null +++ b/templates/svg/edit.svg @@ -0,0 +1 @@ + diff --git a/templates/user/apiTokens.html.twig b/templates/user/apiTokens.html.twig index 5520fffe..d0e6e5aa 100644 --- a/templates/user/apiTokens.html.twig +++ b/templates/user/apiTokens.html.twig @@ -49,7 +49,7 @@
- diff --git a/tests/Functional/Controller/Api/PackageControllerTest.php b/tests/Functional/Controller/Api/PackageControllerTest.php index 1943b822..3e93ce27 100644 --- a/tests/Functional/Controller/Api/PackageControllerTest.php +++ b/tests/Functional/Controller/Api/PackageControllerTest.php @@ -262,7 +262,7 @@ public function testRemovePackageNonExisting(): void self::assertEquals(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode()); } - public function testUpdatePackage(): void + public function testSynchronizePackage(): void { $packageId = Uuid::uuid4()->toString(); $this->fixtures->createPackage($packageId, '', $this->organizationId); @@ -274,18 +274,76 @@ public function testUpdatePackage(): void ])); self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode()); - self::assertFalse( - $this->container() - ->get(DbalPackageQuery::class) - ->getById($packageId) - ->isEmpty() + } + + public function testSynchronizePackageNonExisting(): void + { + $this->loginApiUser($this->apiToken); + $this->client->request('PUT', $this->urlTo('api_package_update', [ + 'organization' => self::$organization, + 'package' => self::$fakeId, + ])); + + self::assertEquals(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode()); + } + + public function testUpdatePackage(): void + { + $packageId = Uuid::uuid4()->toString(); + $this->fixtures->createPackage($packageId, '', $this->organizationId); + + $this->loginApiUser($this->apiToken); + $this->client->request('PATCH', $this->urlTo('api_package_update', [ + 'organization' => self::$organization, + 'package' => $packageId, + ]), [], [], [], (string) json_encode([ + 'url' => 'new-url', + 'keepLastReleases' => 6, + ])); + + $package = $this->container()->get(DbalPackageQuery::class)->getById($packageId)->get(); + + self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode()); + self::assertEquals($package->url(), 'new-url'); + self::assertEquals($package->keepLastReleases(), 6); + } + + public function testUpdatePackageBadRequest(): void + { + $packageId = Uuid::uuid4()->toString(); + $this->fixtures->createPackage($packageId, '', $this->organizationId); + + $this->loginApiUser($this->apiToken); + $this->client->request('PATCH', $this->urlTo('api_package_update', [ + 'organization' => self::$organization, + 'package' => $packageId, + ]), [], [], [], (string) json_encode([ + 'keepLastReleases' => 10.5, + ])); + + $package = $this->container()->get(DbalPackageQuery::class)->getById($packageId)->get(); + + self::assertEquals(Response::HTTP_BAD_REQUEST, $this->client->getResponse()->getStatusCode()); + self::assertJsonStringEqualsJsonString( + $this->lastResponseBody(), + ' + { + "errors": [ + { + "field": "keepLastReleases", + "message": "This value is not valid." + } + ] + } + ' ); + self::assertEquals($package->keepLastReleases(), 0); } public function testUpdatePackageNonExisting(): void { $this->loginApiUser($this->apiToken); - $this->client->request('PUT', $this->urlTo('api_package_update', [ + $this->client->request('PATCH', $this->urlTo('api_package_update', [ 'organization' => self::$organization, 'package' => self::$fakeId, ])); @@ -304,7 +362,6 @@ public function testAddPackageByUrl(): void ])); self::assertEquals(Response::HTTP_CREATED, $this->client->getResponse()->getStatusCode()); - self::assertFalse( $this->container() ->get(DbalPackageQuery::class) diff --git a/tests/Functional/Controller/Organization/PackageControllerTest.php b/tests/Functional/Controller/Organization/PackageControllerTest.php index 15f23887..0f5d3717 100644 --- a/tests/Functional/Controller/Organization/PackageControllerTest.php +++ b/tests/Functional/Controller/Organization/PackageControllerTest.php @@ -213,4 +213,52 @@ public function testNewPackageUnsupportedType(): void self::assertEquals(404, $this->client->getResponse()->getStatusCode()); } + + public function testUpdatePackage(): void + { + $buddyId = $this->fixtures->createOrganization('buddy', $this->userId); + $packageId = $this->fixtures->addPackage($buddyId, 'https://buddy.com'); + + $this->client->request('POST', $this->urlTo('organization_package_update', [ + 'organization' => 'buddy', + 'package' => $packageId, + ])); + + self::assertTrue($this->client->getResponse()->isRedirect( + $this->urlTo('organization_packages', ['organization' => 'buddy']) + )); + + $this->fixtures->syncPackageWithData($packageId, 'buddy-works/repman', 'Repository manager', '2.1.1', new \DateTimeImmutable('2020-01-01 12:12:12')); + + $this->client->followRedirect(); + self::assertStringContainsString('Package will be synchronized in the background', $this->lastResponseBody()); + self::assertStringContainsString('buddy-works/repman', $this->lastResponseBody()); + self::assertStringContainsString('2.1.1', $this->lastResponseBody()); + } + + public function testEditPackage(): void + { + $buddyId = $this->fixtures->createOrganization('buddy', $this->userId); + $packageId = $this->fixtures->addPackage($buddyId, 'https://buddy.com'); + $this->fixtures->syncPackageWithData($packageId, 'buddy-works/buddy', 'Test', '1.1.1', new \DateTimeImmutable()); + + $this->client->request('GET', $this->urlTo('organization_package_edit', ['organization' => 'buddy', 'package' => $packageId])); + + self::assertTrue($this->client->getResponse()->isOk()); + + $this->client->submitForm('Update', [ + 'url' => 'http://github.com/test/test', + 'keepLastReleases' => '6', + ]); + + self::assertTrue( + $this->client->getResponse()->isRedirect($this->urlTo('organization_packages', ['organization' => 'buddy'])) + ); + + $this->client->followRedirect(); + self::assertStringContainsString('Package will be synchronized in the background', $this->lastResponseBody()); + self::assertStringContainsString('http://github.com/test/test', $this->lastResponseBody()); + + self::assertTrue($this->client->getResponse()->isOk()); + } } diff --git a/tests/Functional/Controller/OrganizationControllerTest.php b/tests/Functional/Controller/OrganizationControllerTest.php index c2fa412f..f33f4fff 100644 --- a/tests/Functional/Controller/OrganizationControllerTest.php +++ b/tests/Functional/Controller/OrganizationControllerTest.php @@ -382,28 +382,6 @@ public function testSynchronizeWebhookFromBitbucketPackage(): void self::assertStringContainsString('will be synchronized in background', $this->lastResponseBody()); } - public function testUpdatePackage(): void - { - $buddyId = $this->fixtures->createOrganization('buddy', $this->userId); - $packageId = $this->fixtures->addPackage($buddyId, 'https://buddy.com'); - - $this->client->request('POST', $this->urlTo('organization_package_update', [ - 'organization' => 'buddy', - 'package' => $packageId, - ])); - - self::assertTrue($this->client->getResponse()->isRedirect( - $this->urlTo('organization_packages', ['organization' => 'buddy']) - )); - - $this->fixtures->syncPackageWithData($packageId, 'buddy-works/repman', 'Repository manager', '2.1.1', new \DateTimeImmutable('2020-01-01 12:12:12')); - - $this->client->followRedirect(); - self::assertStringContainsString('Package will be updated in the background', $this->lastResponseBody()); - self::assertStringContainsString('buddy-works/repman', $this->lastResponseBody()); - self::assertStringContainsString('2.1.1', $this->lastResponseBody()); - } - public function testUpdateNonExistingPackage(): void { $buddyId = $this->fixtures->createOrganization('buddy', $this->userId); diff --git a/tests/Integration/MessageHandler/Organization/Package/UpdateHandlerTest.php b/tests/Integration/MessageHandler/Organization/Package/UpdateHandlerTest.php new file mode 100644 index 00000000..6dc106bf --- /dev/null +++ b/tests/Integration/MessageHandler/Organization/Package/UpdateHandlerTest.php @@ -0,0 +1,22 @@ +dispatchMessage(new Update('e0ea4d32-4144-4a67-9310-6dae483a6377', 'test', 0)); + } catch (\Exception $exception) { + } + + self::assertNull($exception); + } +}