From dad3a20f52c370670c2f3af6fa6f115d0fd9d5ba Mon Sep 17 00:00:00 2001 From: Xheni Myrtaj Date: Tue, 5 Mar 2019 10:34:58 +0100 Subject: [PATCH 01/26] change branch name Signed-off-by: Xheni Myrtaj --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fa1ecf2..c49a6f6 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "require": { "php": "~7.0.0 || ~7.1.0 || ~7.2.0", - "phplist/core": "4.0.x-dev", + "phplist/core": "dev-phplist3", "friendsofsymfony/rest-bundle": "^2.3", "sensio/framework-extra-bundle": "5.1.0", From 9e4e996c17a57636619ecb00c85b3ce28c471815 Mon Sep 17 00:00:00 2001 From: Xheni Myrtaj Date: Wed, 6 Mar 2019 11:56:59 +0100 Subject: [PATCH 02/26] Disable REST API by default Signed-off-by: Xheni Myrtaj --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c49a6f6..01b2fa7 100644 --- a/composer.json +++ b/composer.json @@ -119,7 +119,7 @@ }, "view": { "view_response_listener": { - "enabled": true + "enabled": false } }, From 7d2145f7f719aeaa2dc0978cb424f28a1a617210 Mon Sep 17 00:00:00 2001 From: Xheni Myrtaj Date: Tue, 26 Mar 2019 12:16:01 +0000 Subject: [PATCH 03/26] Change path Signed-off-by: Xheni Myrtaj --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 01b2fa7..df9b156 100644 --- a/composer.json +++ b/composer.json @@ -108,7 +108,7 @@ "enabled": true, "rules": [ { - "path": "^/api/v2/", + "path": "^/", "fallback_format": "json" }, { From 81fd3390f6d2a5540d48410423f920d2a91ceab9 Mon Sep 17 00:00:00 2001 From: Xheni Myrtaj Date: Thu, 4 Apr 2019 22:36:53 +0100 Subject: [PATCH 04/26] Remove prefix Signed-off-by: Xheni Myrtaj --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index df9b156..5b8e9c8 100644 --- a/composer.json +++ b/composer.json @@ -96,7 +96,7 @@ "rest-api": { "resource": "@PhpListRestBundle/Controller/", "type": "rest", - "prefix": "/api/v2" + "prefix": "/" } }, "configuration": { From 30fde0d0ca44c3c7673384451adeb93eac655f16 Mon Sep 17 00:00:00 2001 From: Xheni Myrtaj Date: Fri, 31 May 2019 17:18:25 +0200 Subject: [PATCH 05/26] [FEATURE] Get the number of subscribers of list and added tests (#116) Closes #115 --- src/Controller/ListController.php | 16 ++++ .../Controller/AbstractControllerTest.php | 10 ++ .../Controller/Fixtures/SubscriberList.csv | 1 + .../Controller/ListControllerTest.php | 92 ++++++++++++++++++- 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index f54006f..d73bb00 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -16,6 +16,7 @@ * This controller provides REST API access to subscriber lists. * * @author Oliver Klee + * @author Xheni Myrtaj */ class ListController extends FOSRestController implements ClassResourceInterface { @@ -96,4 +97,19 @@ public function getMembersAction(Request $request, SubscriberList $list): View return View::create()->setData($list->getSubscribers()); } + + /** + * Gets the total number of subscribers of a list. + * + * @param Request $request + * @param SubscriberList $list + * + * @return View + */ + public function getSubscribersCountAction(Request $request, SubscriberList $list): View + { + $this->requireAuthentication($request); + + return View::create()->setData(count($list->getSubscribers())); + } } diff --git a/tests/Integration/Controller/AbstractControllerTest.php b/tests/Integration/Controller/AbstractControllerTest.php index bcf3857..169ece2 100644 --- a/tests/Integration/Controller/AbstractControllerTest.php +++ b/tests/Integration/Controller/AbstractControllerTest.php @@ -102,6 +102,16 @@ protected function getDecodedJsonResponseContent(): array return json_decode($this->client->getResponse()->getContent(), true); } + /** + * Returns the response content as int. + * + * @return int + */ + protected function getResponseContentAsInt(): int + { + return json_decode($this->client->getResponse()->getContent(), true); + } + /** * Asserts that the (decoded) JSON response content is the same as the expected array. * diff --git a/tests/Integration/Controller/Fixtures/SubscriberList.csv b/tests/Integration/Controller/Fixtures/SubscriberList.csv index 0213ea1..59f6e18 100644 --- a/tests/Integration/Controller/Fixtures/SubscriberList.csv +++ b/tests/Integration/Controller/Fixtures/SubscriberList.csv @@ -1,3 +1,4 @@ id,name,description,entered,modified,listorder,prefix,active,category,owner 1,"News","News (and some fun stuff)","2016-06-22 15:01:17","2016-06-23 19:50:43",12,"phpList",1,"news",1 2,"More news","","2016-06-22 15:01:17","2016-06-23 19:50:43",12,"",1,"",1 +3,"Tech news","","2019-02-11 15:01:15","2019-02-11 19:50:43",12,"",1,"",1 diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Controller/ListControllerTest.php index ff19769..4b449e4 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Controller/ListControllerTest.php @@ -10,6 +10,7 @@ * Testcase. * * @author Oliver Klee + * @author Xheni Myrtaj */ class ListControllerTest extends AbstractControllerTest { @@ -107,7 +108,17 @@ public function getListsWithCurrentSessionKeyReturnsListData() 'public' => true, 'category' => '', 'id' => 2, - ] + ], + [ + 'name' => 'Tech news', + 'description' => '', + 'creation_date' => '2019-02-11T15:01:15+00:00', + 'list_position' => 12, + 'subject_prefix' => '', + 'public' => true, + 'category' => '', + 'id' => 3, + ], ] ); } @@ -320,4 +331,83 @@ public function getListMembersWithCurrentSessionKeyForExistingListWithSubscriber ] ); } + + /** + * @test + */ + public function getListSubscribersCountForExistingListWithoutSessionKeyReturnsForbiddenStatus() + { + $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); + $this->applyDatabaseChanges(); + + $this->client->request('get', '/api/v2/lists/1/subscribers/count'); + + $this->assertHttpForbidden(); + } + + /** + * @test + */ + public function getListSubscribersCountForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() + { + $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); + $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); + $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); + $this->applyDatabaseChanges(); + + $this->client->request( + 'get', + '/api/v2/lists/1/subscribers/count', + [], + [], + ['PHP_AUTH_USER' => 'unused', 'PHP_AUTH_PW' => 'cfdf64eecbbf336628b0f3071adba763'] + ); + + $this->assertHttpForbidden(); + } + + /** + * @test + */ + public function getListSubscribersCountWithCurrentSessionKeyForExistingListReturnsOkayStatus() + { + $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); + $this->applyDatabaseChanges(); + + $this->authenticatedJsonRequest('get', '/api/v2/lists/1/subscribers/count'); + + $this->assertHttpOkay(); + } + + /** + * @test + */ + public function getListSubscribersCountWithCurrentSessionKeyForExistingListWithNoSubscribersReturnsZero() + { + $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); + $this->getDataSet()->addTable(static::SUBSCRIBER_TABLE_NAME, __DIR__ . '/Fixtures/Subscriber.csv'); + $this->getDataSet()->addTable(static::SUBSCRIPTION_TABLE_NAME, __DIR__ . '/Fixtures/Subscription.csv'); + $this->applyDatabaseChanges(); + + $this->authenticatedJsonRequest('get', '/api/v2/lists/3/subscribers/count'); + $responseContent = $this->getResponseContentAsInt(); + + static::assertSame(0, $responseContent); + } + + /** + * @test + */ + public function getListSubscribersCountWithCurrentSessionKeyForExistingListWithSubscribersReturnsSubscribersCount() + { + $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); + $this->getDataSet()->addTable(static::SUBSCRIBER_TABLE_NAME, __DIR__ . '/Fixtures/Subscriber.csv'); + $this->getDataSet()->addTable(static::SUBSCRIPTION_TABLE_NAME, __DIR__ . '/Fixtures/Subscription.csv'); + $this->applyDatabaseChanges(); + + $this->authenticatedJsonRequest('get', '/api/v2/lists/2/subscribers/count'); + $responseContent = $this->getResponseContentAsInt(); + + static::assertSame(1, $responseContent); + } } From 8fca0ef5ccb9b91f5c81d56830521f8d74415c2d Mon Sep 17 00:00:00 2001 From: Xheni Myrtaj Date: Sat, 1 Jun 2019 00:15:59 +0200 Subject: [PATCH 06/26] [BUGFIX] Fix the expected number in an integration test (#119) Signed-off-by: Xheni Myrtaj --- tests/Integration/Controller/ListControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Controller/ListControllerTest.php index 4b449e4..db5f8e3 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Controller/ListControllerTest.php @@ -408,6 +408,6 @@ public function getListSubscribersCountWithCurrentSessionKeyForExistingListWithS $this->authenticatedJsonRequest('get', '/api/v2/lists/2/subscribers/count'); $responseContent = $this->getResponseContentAsInt(); - static::assertSame(1, $responseContent); + static::assertSame(2, $responseContent); } } From 4364ded05d66fea3a7adb0a0ea0382c8b05cf45e Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 5 Dec 2019 15:52:32 +0100 Subject: [PATCH 07/26] [CLEANUP] Fix a warning with newer PHPMD versions (#125) --- src/Controller/SessionController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 2085f7e..963c282 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -35,7 +35,7 @@ class SessionController extends FOSRestController implements ClassResourceInterf /** * @var AdministratorTokenRepository */ - private $administratorTokenRepository = null; + private $tokenRepository = null; /** * @param Authentication $authentication @@ -49,7 +49,7 @@ public function __construct( ) { $this->authentication = $authentication; $this->administratorRepository = $administratorRepository; - $this->administratorTokenRepository = $tokenRepository; + $this->tokenRepository = $tokenRepository; } /** @@ -96,7 +96,7 @@ public function deleteAction(Request $request, AdministratorToken $token): View throw new AccessDeniedHttpException('You do not have access to this session.', null, 1519831644); } - $this->administratorTokenRepository->remove($token); + $this->tokenRepository->remove($token); return View::create(); } @@ -131,7 +131,7 @@ private function createAndPersistToken(Administrator $administrator): Administra $token->setAdministrator($administrator); $token->generateExpiry(); $token->generateKey(); - $this->administratorTokenRepository->save($token); + $this->tokenRepository->save($token); return $token; } From 26ea3c8d3efbf1e4df0c7602e8bb541333b21d1c Mon Sep 17 00:00:00 2001 From: Michiel Dethmers Date: Tue, 4 Oct 2022 20:17:24 +0100 Subject: [PATCH 08/26] update php dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5b8e9c8..540bd98 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "source": "https://github.com/phpList/rest-api" }, "require": { - "php": "~7.0.0 || ~7.1.0 || ~7.2.0", + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0", "phplist/core": "dev-phplist3", "friendsofsymfony/rest-bundle": "^2.3", From 110b25d55532c6ca65b0095885c0b9f5a51284ca Mon Sep 17 00:00:00 2001 From: Michiel Dethmers Date: Thu, 9 Feb 2023 21:51:39 +0000 Subject: [PATCH 09/26] update to pass building --- composer.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 540bd98..27b858f 100644 --- a/composer.json +++ b/composer.json @@ -29,9 +29,7 @@ "phplist/core": "dev-phplist3", "friendsofsymfony/rest-bundle": "^2.3", - "sensio/framework-extra-bundle": "5.1.0", - - "roave/security-advisories": "dev-master" + "sensio/framework-extra-bundle": "5.1.0" }, "require-dev": { "phpunit/phpunit": "^6.5.6", From c07cee1ee98263add05cca6798372cd712336a31 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 24 Nov 2024 16:45:33 +0400 Subject: [PATCH 10/26] ISSUE-337: update package versions --- composer.json | 23 ++++++++----------- src/Controller/ListController.php | 8 +++---- src/Controller/SessionController.php | 12 +++++----- src/Controller/SubscriberController.php | 10 ++++---- .../PhpListRestExtension.php | 11 +++++---- src/PhpListRestBundle.php | 1 + src/ViewHandler/SecuredViewHandler.php | 2 +- 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/composer.json b/composer.json index 27b858f..498ba42 100644 --- a/composer.json +++ b/composer.json @@ -25,22 +25,20 @@ "source": "https://github.com/phpList/rest-api" }, "require": { - "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0", - - "phplist/core": "dev-phplist3", - "friendsofsymfony/rest-bundle": "^2.3", - "sensio/framework-extra-bundle": "5.1.0" + "php": "^8.1", + "phplist/core": "dev-ISSUE-337", + "friendsofsymfony/rest-bundle": "*" }, "require-dev": { - "phpunit/phpunit": "^6.5.6", - "phpunit/phpunit-mock-objects": "^5.0.6", - "phpunit/dbunit": "^3.0.0", + "phpunit/phpunit": "^10.0", "guzzlehttp/guzzle": "^6.3.0", "squizlabs/php_codesniffer": "^3.2.0", - "phpstan/phpstan": "^0.7.0", - "nette/caching": "^2.5.0 || ^3.0.0", - "nikic/php-parser": "^3.1.0", - "phpmd/phpmd": "^2.6.0" + "phpstan/phpstan": "^0.7.0|0.12.57", + "nette/caching": "^2.5.0|^3.0.0", + "nikic/php-parser": "^4.19.1", + "phpmd/phpmd": "^2.6.0", + "composer/composer": "^1.6.0", + "doctrine/instantiator": "^1.0.5" }, "autoload": { "psr-4": { @@ -119,7 +117,6 @@ "view_response_listener": { "enabled": false } - }, "exception": { "enabled": true, diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index d73bb00..1811780 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -1,10 +1,10 @@ * @author Xheni Myrtaj */ -class ListController extends FOSRestController implements ClassResourceInterface +class ListController extends AbstractController { use AuthenticationTrait; /** * @var SubscriberListRepository */ - private $subscriberListRepository = null; + private SubscriberListRepository $subscriberListRepository; /** * @param Authentication $authentication diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 963c282..8998e03 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -1,10 +1,10 @@ */ -class SessionController extends FOSRestController implements ClassResourceInterface +class SessionController extends AbstractController { use AuthenticationTrait; /** * @var AdministratorRepository */ - private $administratorRepository = null; + private AdministratorRepository $administratorRepository; /** * @var AdministratorTokenRepository */ - private $tokenRepository = null; + private AdministratorTokenRepository $tokenRepository; /** * @param Authentication $authentication @@ -110,7 +110,7 @@ public function deleteAction(Request $request, AdministratorToken $token): View * * @throws BadRequestHttpException */ - private function validateCreateRequest(Request $request) + private function validateCreateRequest(Request $request): void { if ($request->getContent() === '') { throw new BadRequestHttpException('Empty JSON data', null, 1500559729); diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index 07b196d..4a92fb0 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -1,10 +1,10 @@ */ -class SubscriberController extends FOSRestController implements ClassResourceInterface +class SubscriberController extends AbstractController { use AuthenticationTrait; /** * @var SubscriberRepository */ - private $subscriberRepository = null; + private SubscriberRepository $subscriberRepository; /** * @param Authentication $authentication @@ -78,7 +78,7 @@ public function postAction(Request $request): View * * @throws UnprocessableEntityHttpException */ - private function validateSubscriber(Request $request) + private function validateSubscriber(Request $request): void { /** @var string[] $invalidFields */ $invalidFields = []; diff --git a/src/DependencyInjection/PhpListRestExtension.php b/src/DependencyInjection/PhpListRestExtension.php index e865fbe..04fce6e 100644 --- a/src/DependencyInjection/PhpListRestExtension.php +++ b/src/DependencyInjection/PhpListRestExtension.php @@ -1,8 +1,11 @@ load('services.yml'); } } diff --git a/src/PhpListRestBundle.php b/src/PhpListRestBundle.php index 876c331..f67b343 100644 --- a/src/PhpListRestBundle.php +++ b/src/PhpListRestBundle.php @@ -1,4 +1,5 @@ Date: Sun, 24 Nov 2024 16:45:46 +0400 Subject: [PATCH 11/26] ISSUE-337: use local --- composer.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 498ba42..de5a0f8 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,15 @@ "PhpList\\RestBundle\\Tests\\": "tests/" } }, + "repositories": [ + { + "type": "path", + "url": "../core", + "options": { + "symlink": true + } + } + ], "scripts": { "list-modules": [ "PhpList\\Core\\Composer\\ScriptHandler::listModules" @@ -76,7 +85,7 @@ }, "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-ISSUE-337": "v5.0.x-dev" }, "symfony-app-dir": "bin", "symfony-bin-dir": "bin", @@ -97,9 +106,6 @@ }, "configuration": { "fos_rest": { - "routing_loader": { - "include_format": false - }, "format_listener": { "enabled": true, "rules": [ @@ -125,7 +131,8 @@ } }, "service": { - "view_handler": "my.secure_view_handler" + "view_handler": "my.secure_view_handler", + "serializer": "fos_rest.serializer.symfony" } } } From 85ab9ceb9ae1a517f4b4b9cd6ebef5e572b14f6c Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sat, 30 Nov 2024 14:24:20 +0400 Subject: [PATCH 12/26] ISSUE-337: symfony 6.4 --- composer.json | 37 +-------- config/services.yml | 31 ++++++- src/Controller/ListController.php | 104 +++++++++++------------- src/Controller/SessionController.php | 44 +++++----- src/Controller/SubscriberController.php | 46 ++++++----- src/EventListener/ExceptionListener.php | 38 +++++++++ src/EventListener/ResponseListener.php | 22 +++++ 7 files changed, 180 insertions(+), 142 deletions(-) create mode 100644 src/EventListener/ExceptionListener.php create mode 100644 src/EventListener/ResponseListener.php diff --git a/composer.json b/composer.json index de5a0f8..504f74e 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,6 @@ "nette/caching": "^2.5.0|^3.0.0", "nikic/php-parser": "^4.19.1", "phpmd/phpmd": "^2.6.0", - "composer/composer": "^1.6.0", "doctrine/instantiator": "^1.0.5" }, "autoload": { @@ -100,40 +99,8 @@ "routes": { "rest-api": { "resource": "@PhpListRestBundle/Controller/", - "type": "rest", - "prefix": "/" - } - }, - "configuration": { - "fos_rest": { - "format_listener": { - "enabled": true, - "rules": [ - { - "path": "^/", - "fallback_format": "json" - }, - { - "path": "^/", - "fallback_format": "html" - } - ] - }, - "view": { - "view_response_listener": { - "enabled": false - } - }, - "exception": { - "enabled": true, - "messages": { - "Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException": true - } - }, - "service": { - "view_handler": "my.secure_view_handler", - "serializer": "fos_rest.serializer.symfony" - } + "type": "attribute", + "prefix": "/api/v2" } } } diff --git a/config/services.yml b/config/services.yml index a7cb41b..4aede00 100644 --- a/config/services.yml +++ b/config/services.yml @@ -1,14 +1,37 @@ services: + Psr\Container\ContainerInterface: + alias: 'service_container' + PhpList\RestBundle\Controller\: resource: '../src/Controller' public: true autowire: true tags: ['controller.service_arguments'] +# Symfony\Component\Serializer\SerializerInterface: +# autowire: true +# autoconfigure: true + my.secure_handler: - class: \PhpList\RestBundle\ViewHandler\SecuredViewHandler + class: \PhpList\RestBundle\ViewHandler\SecuredViewHandler my.secure_view_handler: - parent: fos_rest.view_handler.default - calls: - - ['registerHandler', [ 'json', ['@my.secure_handler', 'createResponse'] ] ] + parent: fos_rest.view_handler.default + calls: + - ['registerHandler', [ 'json', ['@my.secure_handler', 'createResponse'] ] ] + + PhpList\Core\Security\Authentication: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Repository\Messaging\SubscriberListRepository: + autowire: true + autoconfigure: true + + PhpList\RestBundle\EventListener\ExceptionListener: + tags: + - { name: kernel.event_listener, event: kernel.exception } + + PhpList\RestBundle\EventListener\ResponseListener: + tags: + - { name: kernel.event_listener, event: kernel.response } diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index 1811780..37773c4 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -4,13 +4,18 @@ namespace PhpList\RestBundle\Controller; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use FOS\RestBundle\View\View; use PhpList\Core\Domain\Model\Messaging\SubscriberList; +use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use PhpList\Core\Domain\Repository\Messaging\SubscriberListRepository; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Controller\Traits\AuthenticationTrait; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\SerializerInterface; /** * This controller provides REST API access to subscriber lists. @@ -22,94 +27,81 @@ class ListController extends AbstractController { use AuthenticationTrait; - /** - * @var SubscriberListRepository - */ private SubscriberListRepository $subscriberListRepository; + private SubscriberRepository $subscriberRepository; + private SerializerInterface $serializer; /** * @param Authentication $authentication * @param SubscriberListRepository $repository + * @param SubscriberRepository $subscriberRepository + * @param SerializerInterface $serializer */ - public function __construct(Authentication $authentication, SubscriberListRepository $repository) - { + public function __construct( + Authentication $authentication, + SubscriberListRepository $repository, + SubscriberRepository $subscriberRepository, + SerializerInterface $serializer + ) { $this->authentication = $authentication; $this->subscriberListRepository = $repository; + $this->subscriberRepository = $subscriberRepository; + $this->serializer = $serializer; } - /** - * Gets a list of all subscriber lists. - * - * @param Request $request - * - * @return View - */ - public function cgetAction(Request $request): View + #[Route('/lists', name: 'get_lists', methods: ['GET'])] + public function getLists(Request $request): JsonResponse { $this->requireAuthentication($request); + $data = $this->subscriberListRepository->findAll(); + $json = $this->serializer->serialize($data, 'json', [ + AbstractNormalizer::GROUPS => 'SubscriberList', + ]); - return View::create()->setData($this->subscriberListRepository->findAll()); + return new JsonResponse($json, Response::HTTP_OK, [], true); } - /** - * Gets a subscriber list. - * - * @param Request $request - * @param SubscriberList $list - * - * @return View - */ - public function getAction(Request $request, SubscriberList $list): View + #[Route('/lists/{id}', name: 'get_list', methods: ['GET'])] + public function getList(Request $request, SubscriberList $list): JsonResponse { $this->requireAuthentication($request); + $json = $this->serializer->serialize($list, 'json', [ + AbstractNormalizer::GROUPS => 'SubscriberList', + ]); - return View::create()->setData($list); + return new JsonResponse($json, Response::HTTP_OK, [], true); } - /** - * Deletes a subscriber list. - * - * @param Request $request - * @param SubscriberList $list - * - * @return View - */ - public function deleteAction(Request $request, SubscriberList $list): View + #[Route('/lists/{id}', name: 'delete_list', methods: ['DELETE'])] + public function deleteList(Request $request, SubscriberList $list): JsonResponse { $this->requireAuthentication($request); $this->subscriberListRepository->remove($list); - return View::create(); + return new JsonResponse(null, Response::HTTP_OK, [], true); } - /** - * Gets a list of all subscribers (members) of a subscriber list. - * - * @param Request $request - * @param SubscriberList $list - * - * @return View - */ - public function getMembersAction(Request $request, SubscriberList $list): View + #[Route('/lists/{id}/members', name: 'get_subscriber_from_list', methods: ['GET'])] + public function getListMembers(Request $request, SubscriberList $list): JsonResponse { $this->requireAuthentication($request); - return View::create()->setData($list->getSubscribers()); + $subscribers = $this->subscriberRepository->findSubscribersBySubscribedList($list->getId()); + + $json = $this->serializer->serialize($subscribers, 'json', [ + AbstractNormalizer::GROUPS => 'SubscriberListMembers', + ]); + + return new JsonResponse($json, Response::HTTP_OK, [], true); } - /** - * Gets the total number of subscribers of a list. - * - * @param Request $request - * @param SubscriberList $list - * - * @return View - */ - public function getSubscribersCountAction(Request $request, SubscriberList $list): View + #[Route('/lists/{id}/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] + public function getSubscribersCount(Request $request, SubscriberList $list): JsonResponse { $this->requireAuthentication($request); + $json = $this->serializer->serialize(count($list->getSubscribers()), 'json'); - return View::create()->setData(count($list->getSubscribers())); + return new JsonResponse($json, Response::HTTP_OK, [], true); } } diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 8998e03..28ae648 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -5,76 +5,74 @@ namespace PhpList\RestBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use FOS\RestBundle\View\View; use PhpList\Core\Domain\Model\Identity\Administrator; use PhpList\Core\Domain\Model\Identity\AdministratorToken; use PhpList\Core\Domain\Repository\Identity\AdministratorRepository; use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Controller\Traits\AuthenticationTrait; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Serializer\SerializerInterface; /** * This controller provides methods to create and destroy REST API sessions. * * @author Oliver Klee + * @author Tatevik Grigoryan */ class SessionController extends AbstractController { use AuthenticationTrait; - /** - * @var AdministratorRepository - */ private AdministratorRepository $administratorRepository; - - /** - * @var AdministratorTokenRepository - */ private AdministratorTokenRepository $tokenRepository; + private SerializerInterface $serializer; /** * @param Authentication $authentication * @param AdministratorRepository $administratorRepository * @param AdministratorTokenRepository $tokenRepository + * @param SerializerInterface $serializer */ public function __construct( Authentication $authentication, AdministratorRepository $administratorRepository, - AdministratorTokenRepository $tokenRepository + AdministratorTokenRepository $tokenRepository, + SerializerInterface $serializer ) { $this->authentication = $authentication; $this->administratorRepository = $administratorRepository; $this->tokenRepository = $tokenRepository; + $this->serializer = $serializer; } /** * Creates a new session (if the provided credentials are valid). * - * @param Request $request - * - * @return View - * * @throws UnauthorizedHttpException */ - public function postAction(Request $request): View + #[Route('/sessions', name: 'create_session', methods: ['POST'])] + public function createSession(Request $request): JsonResponse { $this->validateCreateRequest($request); $administrator = $this->administratorRepository->findOneByLoginCredentials( - $request->get('login_name'), - $request->get('password') + $request->getPayload()->get('login_name'), + $request->getPayload()->get('password') ); if ($administrator === null) { throw new UnauthorizedHttpException('', 'Not authorized', null, 1500567098); } $token = $this->createAndPersistToken($administrator); + $json = $this->serializer->serialize($token, 'json'); - return View::create()->setStatusCode(Response::HTTP_CREATED)->setData($token); + return new JsonResponse($json, Response::HTTP_OK, [], true); } /** @@ -82,14 +80,10 @@ public function postAction(Request $request): View * * This action may only be called for sessions that are owned by the authenticated administrator. * - * @param Request $request - * @param AdministratorToken $token - * - * @return View - * * @throws AccessDeniedHttpException */ - public function deleteAction(Request $request, AdministratorToken $token): View + #[Route('/sessions/{id}', name: 'delete_session', methods: ['DELETE'])] + public function deleteAction(Request $request, AdministratorToken $token): JsonResponse { $administrator = $this->requireAuthentication($request); if ($token->getAdministrator() !== $administrator) { @@ -98,7 +92,7 @@ public function deleteAction(Request $request, AdministratorToken $token): View $this->tokenRepository->remove($token); - return View::create(); + return new JsonResponse(null, Response::HTTP_OK, [], true); } /** @@ -115,7 +109,7 @@ private function validateCreateRequest(Request $request): void if ($request->getContent() === '') { throw new BadRequestHttpException('Empty JSON data', null, 1500559729); } - if (empty($request->get('login_name')) || empty($request->get('password'))) { + if (empty($request->getPayload()->get('login_name')) || empty($request->getPayload()->get('password'))) { throw new BadRequestHttpException('Incomplete credentials', null, 1500562647); } } diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index 4a92fb0..e4a7800 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -5,15 +5,17 @@ namespace PhpList\RestBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use FOS\RestBundle\View\View; use PhpList\Core\Domain\Model\Subscription\Subscriber; use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Controller\Traits\AuthenticationTrait; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Serializer\SerializerInterface; /** * This controller provides REST API access to subscribers. @@ -24,51 +26,51 @@ class SubscriberController extends AbstractController { use AuthenticationTrait; - /** - * @var SubscriberRepository - */ private SubscriberRepository $subscriberRepository; + private SerializerInterface $serializer; /** * @param Authentication $authentication * @param SubscriberRepository $repository + * @param SerializerInterface $serializer */ - public function __construct(Authentication $authentication, SubscriberRepository $repository) - { + public function __construct( + Authentication $authentication, + SubscriberRepository $repository, + SerializerInterface $serializer + ) { $this->authentication = $authentication; $this->subscriberRepository = $repository; + $this->serializer = $serializer; } /** * Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email * address yet). - * - * @param Request $request - * - * @return View - * - * @throws ConflictHttpException */ - public function postAction(Request $request): View + #[Route('/subscriber', name: 'create_subscriber', methods: ['POST'])] + public function postAction(Request $request): JsonResponse { $this->requireAuthentication($request); - + $data = $request->getPayload(); $this->validateSubscriber($request); - $email = $request->get('email'); + $email = $data->get('email'); if ($this->subscriberRepository->findOneByEmail($email) !== null) { throw new ConflictHttpException('This resource already exists.', null, 1513439108); } $subscriber = new Subscriber(); $subscriber->setEmail($email); - $subscriber->setConfirmed((bool)$request->get('confirmed')); - $subscriber->setBlacklisted((bool)$request->get('blacklisted')); - $subscriber->setHtmlEmail((bool)$request->get('html_email')); - $subscriber->setDisabled((bool)$request->get('disabled')); + $subscriber->setConfirmed((bool)$data->get('confirmed')); + $subscriber->setBlacklisted((bool)$data->get('blacklisted')); + $subscriber->setHtmlEmail((bool)$data->get('html_email')); + $subscriber->setDisabled((bool)$data->get('disabled')); $this->subscriberRepository->save($subscriber); - return View::create()->setStatusCode(Response::HTTP_CREATED)->setData($subscriber); + $json = $this->serializer->serialize($subscriber, 'json'); + + return new JsonResponse($json, Response::HTTP_CREATED, [], true); } /** @@ -82,13 +84,13 @@ private function validateSubscriber(Request $request): void { /** @var string[] $invalidFields */ $invalidFields = []; - if (filter_var($request->get('email'), FILTER_VALIDATE_EMAIL) === false) { + if (filter_var($request->getPayload()->get('email'), FILTER_VALIDATE_EMAIL) === false) { $invalidFields[] = 'email'; } $booleanFields = ['confirmed', 'blacklisted', 'html_email', 'disabled']; foreach ($booleanFields as $fieldKey) { - if ($request->get($fieldKey) !== null && !is_bool($request->get($fieldKey))) { + if ($request->getPayload()->get($fieldKey) !== null && !is_bool($request->getPayload()->get($fieldKey))) { $invalidFields[] = $fieldKey; } } diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php new file mode 100644 index 0000000..43c4d1a --- /dev/null +++ b/src/EventListener/ExceptionListener.php @@ -0,0 +1,38 @@ +getThrowable(); + + if ($exception instanceof AccessDeniedHttpException) { + $response = new JsonResponse([ + 'message' => $exception->getMessage(), + ], 403); + + $event->setResponse($response); + } elseif ($exception instanceof HttpExceptionInterface) { + $response = new JsonResponse([ + 'message' => $exception->getMessage(), + ], $exception->getStatusCode()); + + $event->setResponse($response); + } elseif ($exception instanceof \Exception) { + $response = new JsonResponse([ + 'message' => $exception->getMessage(), + ], 500); + + $event->setResponse($response); + } + } +} diff --git a/src/EventListener/ResponseListener.php b/src/EventListener/ResponseListener.php new file mode 100644 index 0000000..2bfb915 --- /dev/null +++ b/src/EventListener/ResponseListener.php @@ -0,0 +1,22 @@ +getResponse(); + + if ($response instanceof JsonResponse) { + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('Content-Security-Policy', "default-src 'none'"); + $response->headers->set('X-Frame-Options', 'DENY'); + } + } +} From f09b1f95405de8eb904f8d5bbd17f90eff0e5a42 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Thu, 5 Dec 2024 22:02:40 +0400 Subject: [PATCH 13/26] ISSUE-337: test fix --- .gitignore | 1 + composer.json | 10 +- tests/Integration/Composer/ScriptsTest.php | 94 +++---- .../Controller/AbstractControllerTest.php | 72 +++--- .../Fixtures/AdministratorFixture.php | 48 ++++ .../Fixtures/AdministratorTokenFixture.php | 50 ++++ .../Controller/Fixtures/SubscriberFixture.php | 68 +++++ .../Fixtures/SubscriberListFixture.php | 56 +++++ .../Fixtures/SubscriptionFixture.php | 56 +++++ .../Controller/ListControllerTest.php | 232 +++++------------- .../Controller/SessionControllerTest.php | 2 +- .../Controller/SubscriberControllerTest.php | 2 +- tests/Integration/Routing/RoutingTest.php | 17 +- .../Controller/SecuredViewHandlerTest.php | 15 +- .../Controller/SessionControllerTest.php | 24 +- tests/Unit/PhpListRestBundleTest.php | 13 +- 16 files changed, 435 insertions(+), 325 deletions(-) create mode 100644 tests/Integration/Controller/Fixtures/AdministratorFixture.php create mode 100644 tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php create mode 100644 tests/Integration/Controller/Fixtures/SubscriberFixture.php create mode 100644 tests/Integration/Controller/Fixtures/SubscriberListFixture.php create mode 100644 tests/Integration/Controller/Fixtures/SubscriptionFixture.php diff --git a/.gitignore b/.gitignore index 9cd86d7..8bf5efa 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /public/ /var/ /vendor/ +.phpunit.result.cache diff --git a/composer.json b/composer.json index 504f74e..d91c721 100644 --- a/composer.json +++ b/composer.json @@ -27,17 +27,19 @@ "require": { "php": "^8.1", "phplist/core": "dev-ISSUE-337", - "friendsofsymfony/rest-bundle": "*" + "friendsofsymfony/rest-bundle": "*", + "symfony/test-pack": "^1.0", + "symfony/process": "^6.4" }, "require-dev": { "phpunit/phpunit": "^10.0", "guzzlehttp/guzzle": "^6.3.0", "squizlabs/php_codesniffer": "^3.2.0", - "phpstan/phpstan": "^0.7.0|0.12.57", - "nette/caching": "^2.5.0|^3.0.0", + "phpstan/phpstan": "^0.12.57", + "nette/caching": "^3.0.0", "nikic/php-parser": "^4.19.1", "phpmd/phpmd": "^2.6.0", - "doctrine/instantiator": "^1.0.5" + "doctrine/instantiator": "^2.0." }, "autoload": { "psr-4": { diff --git a/tests/Integration/Composer/ScriptsTest.php b/tests/Integration/Composer/ScriptsTest.php index bfd6a9c..9a077ac 100644 --- a/tests/Integration/Composer/ScriptsTest.php +++ b/tests/Integration/Composer/ScriptsTest.php @@ -1,4 +1,5 @@ getAbsolutePublicDirectoryPath()); + self::assertDirectoryExists($this->getAbsolutePublicDirectoryPath()); } - /** - * @return string - */ private function getAbsolutePublicDirectoryPath(): string { return dirname(__DIR__, 3) . '/public/'; @@ -31,7 +26,7 @@ private function getAbsolutePublicDirectoryPath(): string /** * @return string[][] */ - public function publicDirectoryFilesDataProvider(): array + public static function publicDirectoryFilesDataProvider(): array { return [ 'production entry point' => ['app.php'], @@ -42,26 +37,18 @@ public function publicDirectoryFilesDataProvider(): array } /** - * @test - * @param string $fileName * @dataProvider publicDirectoryFilesDataProvider */ - public function publicDirectoryFilesExist(string $fileName) + public function testPublicDirectoryFilesExist(string $fileName) { - static::assertFileExists($this->getAbsolutePublicDirectoryPath() . $fileName); + self::assertFileExists($this->getAbsolutePublicDirectoryPath() . $fileName); } - /** - * @test - */ - public function binariesDirectoryHasBeenCreated() + public function testBinariesDirectoryHasBeenCreated() { - static::assertDirectoryExists($this->getAbsoluteBinariesDirectoryPath()); + self::assertDirectoryExists($this->getAbsoluteBinariesDirectoryPath()); } - /** - * @return string - */ private function getAbsoluteBinariesDirectoryPath(): string { return dirname(__DIR__, 3) . '/bin/'; @@ -70,7 +57,7 @@ private function getAbsoluteBinariesDirectoryPath(): string /** * @return string[][] */ - public function binariesDataProvider(): array + public static function binariesDataProvider(): array { return [ 'Symfony console' => ['console'], @@ -78,107 +65,82 @@ public function binariesDataProvider(): array } /** - * @test - * @param string $fileName * @dataProvider binariesDataProvider */ - public function binariesExist(string $fileName) + public function testBinariesExist(string $fileName) { - static::assertFileExists($this->getAbsoluteBinariesDirectoryPath() . $fileName); + self::assertFileExists($this->getAbsoluteBinariesDirectoryPath() . $fileName); } - /** - * @return string - */ private function getBundleConfigurationFilePath(): string { return dirname(__DIR__, 3) . '/config/bundles.yml'; } - /** - * @test - */ - public function bundleConfigurationFileExists() + public function testBundleConfigurationFileExists() { - static::assertFileExists($this->getBundleConfigurationFilePath()); + self::assertFileExists($this->getBundleConfigurationFilePath()); } /** * @return string[][] */ - public function bundleClassNameDataProvider(): array + public static function bundleClassNameDataProvider(): array { return [ - 'framework bundle' => ['Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], - 'rest bundle' => ['PhpList\\RestBundle\\PhpListRestBundle'], + 'framework bundle' => ['Symfony\Bundle\FrameworkBundle\FrameworkBundle'], + 'rest bundle' => ['PhpList\RestBundle\PhpListRestBundle'], ]; } /** - * @test - * @param string $bundleClassName * @dataProvider bundleClassNameDataProvider */ - public function bundleConfigurationFileContainsModuleBundles(string $bundleClassName) + public function testBundleConfigurationFileContainsModuleBundles(string $bundleClassName) { $fileContents = file_get_contents($this->getBundleConfigurationFilePath()); - static::assertContains($bundleClassName, $fileContents); + self::assertStringContainsString($bundleClassName, $fileContents); } - /** - * @return string - */ private function getModuleRoutesConfigurationFilePath(): string { return dirname(__DIR__, 3) . '/config/routing_modules.yml'; } - - /** - * @test - */ - public function moduleRoutesConfigurationFileExists() + public function testModuleRoutesConfigurationFileExists() { - static::assertFileExists($this->getModuleRoutesConfigurationFilePath()); + self::assertFileExists($this->getModuleRoutesConfigurationFilePath()); } /** * @return string[][] */ - public function moduleRoutingDataProvider(): array + public static function moduleRoutingDataProvider(): array { return [ 'route name' => ['phplist/rest-api.rest-api'], 'resource' => ["resource: '@PhpListRestBundle/Controller/'"], - 'type' => ['type: annotation'], + 'type' => ['type: attribute'], ]; } /** - * @test - * @param string $routeSearchString * @dataProvider moduleRoutingDataProvider */ - public function moduleRoutesConfigurationFileContainsModuleRoutes(string $routeSearchString) + public function testModuleRoutesConfigurationFileContainsModuleRoutes(string $routeSearchString) { $fileContents = file_get_contents($this->getModuleRoutesConfigurationFilePath()); - static::assertContains($routeSearchString, $fileContents); + self::assertStringContainsString($routeSearchString, $fileContents); } - /** - * @test - */ - public function parametersConfigurationFileExists() + public function testParametersConfigurationFileExists() { - static::assertFileExists(dirname(__DIR__, 3) . '/config/parameters.yml'); + self::assertFileExists(dirname(__DIR__, 3) . '/config/parameters.yml'); } - /** - * @test - */ - public function modulesConfigurationFileExists() + public function testModulesConfigurationFileExists() { - static::assertFileExists(dirname(__DIR__, 3) . '/config/config_modules.yml'); + self::assertFileExists(dirname(__DIR__, 3) . '/config/config_modules.yml'); } } diff --git a/tests/Integration/Controller/AbstractControllerTest.php b/tests/Integration/Controller/AbstractControllerTest.php index 169ece2..8110c3e 100644 --- a/tests/Integration/Controller/AbstractControllerTest.php +++ b/tests/Integration/Controller/AbstractControllerTest.php @@ -1,10 +1,13 @@ */ -abstract class AbstractControllerTest extends AbstractWebTest +abstract class AbstractControllerTest extends WebTestCase { use DatabaseTestTrait; - /** - * @var string - */ - const ADMINISTRATOR_TABLE_NAME = 'phplist_admin'; - - /** - * @var string - */ - const TOKEN_TABLE_NAME = 'phplist_admintoken'; - - protected function setUp() + protected function setUp(): void { $this->setUpDatabaseTest(); - $this->setUpWebTest(); } /** @@ -58,7 +50,7 @@ protected function jsonRequest( $serverWithContentType = $server; $serverWithContentType['CONTENT_TYPE'] = 'application/json'; - return $this->client->request($method, $uri, $parameters, $files, $serverWithContentType, $content); + return self::getClient()->request($method, $uri, $parameters, $files, $serverWithContentType, $content); } /** @@ -81,9 +73,7 @@ protected function authenticatedJsonRequest( array $server = [], string $content = null ): Crawler { - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); $serverWithAuthentication = $server; $serverWithAuthentication['PHP_AUTH_USER'] = 'unused'; @@ -99,7 +89,7 @@ protected function authenticatedJsonRequest( */ protected function getDecodedJsonResponseContent(): array { - return json_decode($this->client->getResponse()->getContent(), true); + return json_decode(self::getClient()->getResponse()->getContent(), true); } /** @@ -109,7 +99,7 @@ protected function getDecodedJsonResponseContent(): array */ protected function getResponseContentAsInt(): int { - return json_decode($this->client->getResponse()->getContent(), true); + return json_decode(self::getClient()->getResponse()->getContent(), true); } /** @@ -119,9 +109,9 @@ protected function getResponseContentAsInt(): int * * @return void */ - protected function assertJsonResponseContentEquals(array $expected) + protected function assertJsonResponseContentEquals(array $expected): void { - static::assertSame($expected, $this->getDecodedJsonResponseContent()); + self::assertSame($expected, $this->getDecodedJsonResponseContent()); } /** @@ -131,12 +121,12 @@ protected function assertJsonResponseContentEquals(array $expected) * * @return void */ - protected function assertHttpStatusWithJsonContentType(int $status) + protected function assertHttpStatusWithJsonContentType(int $status): void { - $response = $this->client->getResponse(); + $response = self::getClient()->getResponse(); - static::assertSame($status, $response->getStatusCode()); - static::assertContains('application/json', (string)$response->headers); + self::assertSame($status, $response->getStatusCode()); + self::assertStringContainsString('application/json', (string)$response->headers); } /** @@ -144,7 +134,7 @@ protected function assertHttpStatusWithJsonContentType(int $status) * * @return void */ - protected function assertHttpOkay() + protected function assertHttpOkay(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_OK); } @@ -154,7 +144,7 @@ protected function assertHttpOkay() * * @return void */ - protected function assertHttpCreated() + protected function assertHttpCreated(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_CREATED); } @@ -164,11 +154,11 @@ protected function assertHttpCreated() * * @return void */ - protected function assertHttpNoContent() + protected function assertHttpNoContent(): void { - $response = $this->client->getResponse(); + $response = self::getClient()->getResponse(); - static::assertSame(Response::HTTP_NO_CONTENT, $response->getStatusCode()); + self::assertSame(Response::HTTP_NO_CONTENT, $response->getStatusCode()); } /** @@ -176,7 +166,7 @@ protected function assertHttpNoContent() * * @return void */ - protected function assertHttpBadRequest() + protected function assertHttpBadRequest(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_BAD_REQUEST); } @@ -186,7 +176,7 @@ protected function assertHttpBadRequest() * * @return void */ - protected function assertHttpUnauthorized() + protected function assertHttpUnauthorized(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_UNAUTHORIZED); } @@ -196,7 +186,7 @@ protected function assertHttpUnauthorized() * * @return void */ - protected function assertHttpNotFound() + protected function assertHttpNotFound(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_NOT_FOUND); } @@ -206,7 +196,7 @@ protected function assertHttpNotFound() * * @return void */ - protected function assertHttpForbidden() + protected function assertHttpForbidden(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_FORBIDDEN); } @@ -216,11 +206,11 @@ protected function assertHttpForbidden() * * @return void */ - protected function assertHttpMethodNotAllowed() + protected function assertHttpMethodNotAllowed(): void { - $response = $this->client->getResponse(); + $response = self::getClient()->getResponse(); - static::assertSame(Response::HTTP_METHOD_NOT_ALLOWED, $response->getStatusCode()); + self::assertSame(Response::HTTP_METHOD_NOT_ALLOWED, $response->getStatusCode()); } /** @@ -229,11 +219,11 @@ protected function assertHttpMethodNotAllowed() * * @return void */ - protected function assertHttpConflict() + protected function assertHttpConflict(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_CONFLICT); - static::assertSame( + self::assertSame( [ 'code' => Response::HTTP_CONFLICT, 'message' => 'This resource already exists.', @@ -247,7 +237,7 @@ protected function assertHttpConflict() * * @return void */ - protected function assertHttpUnprocessableEntity() + protected function assertHttpUnprocessableEntity(): void { $this->assertHttpStatusWithJsonContentType(Response::HTTP_UNPROCESSABLE_ENTITY); } diff --git a/tests/Integration/Controller/Fixtures/AdministratorFixture.php b/tests/Integration/Controller/Fixtures/AdministratorFixture.php new file mode 100644 index 0000000..2541446 --- /dev/null +++ b/tests/Integration/Controller/Fixtures/AdministratorFixture.php @@ -0,0 +1,48 @@ +setLoginName($row['loginname']); + $admin->setEmailAddress($row['email']); + $this->setSubjectProperty($admin,'creationDate', new DateTime($row['created'])); + $admin->setPasswordHash($row['password']); + $this->setSubjectProperty($admin,'passwordChangeDate', new DateTime($row['passwordchanged'])); + $admin->setDisabled((bool) $row['disabled']); + $admin->setSuperUser((bool) $row['superuser']); + $manager->persist($admin); + } + + fclose($handle); + } +} diff --git a/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php b/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php new file mode 100644 index 0000000..9d3d9be --- /dev/null +++ b/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php @@ -0,0 +1,50 @@ +getRepository(Administrator::class); + + while (($data = fgetcsv($handle)) !== false) { + $row = array_combine($headers, $data); + + $adminToken = new AdministratorToken(); + $this->setSubjectId($adminToken,$row['id']); + $adminToken->setKey($row['value']); + $this->setSubjectProperty($adminToken,'expiry', new DateTime($row['expires'])); + $adminToken->setAdministrator($administratorRepository->find($row['adminid'])); + $this->setSubjectProperty($adminToken,'creationDate', new DateTime($row['created'])); + + $manager->persist($adminToken); + } + + fclose($handle); + } +} diff --git a/tests/Integration/Controller/Fixtures/SubscriberFixture.php b/tests/Integration/Controller/Fixtures/SubscriberFixture.php new file mode 100644 index 0000000..4451574 --- /dev/null +++ b/tests/Integration/Controller/Fixtures/SubscriberFixture.php @@ -0,0 +1,68 @@ +getConnection(); + + $insertQuery = " + INSERT INTO phplist_user_user ( + id, entered, modified, email, confirmed, blacklisted, bouncecount, + uniqid, htmlemail, disabled, extradata + ) VALUES ( + :id, :creation_date, :modification_date, :email, :confirmed, :blacklisted, :bounce_count, + :unique_id, :html_email, :disabled, :extra_data + ) + "; + + $stmt = $connection->prepare($insertQuery); + + while (($data = fgetcsv($handle)) !== false) { + $row = array_combine($headers, $data); + + $stmt->executeStatement([ + 'id' => (int) $row['id'], + 'creation_date' => (new DateTime($row['entered']))->format('Y-m-d H:i:s'), + 'modification_date' => (new DateTime($row['modified']))->format('Y-m-d H:i:s'), + 'email' => $row['email'], + 'confirmed' => (bool) $row['confirmed'] ? 1 : 0, + 'blacklisted' => (bool) $row['blacklisted'] ? 1 : 0, + 'bounce_count' => (int) $row['bouncecount'], + 'unique_id' => $row['uniqueid'], + 'html_email' => (bool) $row['htmlemail'] ? 1 : 0, + 'disabled' => (bool) $row['disabled'] ? 1 : 0, + 'extra_data' => $row['extradata'], + ]); + } + + fclose($handle); + } +} diff --git a/tests/Integration/Controller/Fixtures/SubscriberListFixture.php b/tests/Integration/Controller/Fixtures/SubscriberListFixture.php new file mode 100644 index 0000000..6a4c86e --- /dev/null +++ b/tests/Integration/Controller/Fixtures/SubscriberListFixture.php @@ -0,0 +1,56 @@ +setSubjectId($admin,(int)$row['owner']); + + $subscriberList = new SubscriberList(); + $this->setSubjectId($subscriberList,(int)$row['id']); + $subscriberList->setName($row['name']); + $subscriberList->setDescription($row['description']); + $this->setSubjectProperty($subscriberList,'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscriberList,'modificationDate', new DateTime($row['modified'])); + $subscriberList->setListPosition((int)$row['listorder']); + $subscriberList->setSubjectPrefix($row['prefix']); + $subscriberList->setPublic((bool) $row['active']); + $subscriberList->setCategory($row['category']); + $subscriberList->setOwner($admin); + + $manager->persist($admin); + $manager->persist($subscriberList); + } + + fclose($handle); + } +} diff --git a/tests/Integration/Controller/Fixtures/SubscriptionFixture.php b/tests/Integration/Controller/Fixtures/SubscriptionFixture.php new file mode 100644 index 0000000..76728b9 --- /dev/null +++ b/tests/Integration/Controller/Fixtures/SubscriptionFixture.php @@ -0,0 +1,56 @@ +setSubjectId($subscriber,(int)$row['userid']); + $manager->persist($subscriber); + + $subscriberList = new SubscriberList(); + $this->setSubjectId($subscriberList,(int)$row['listid']); + $manager->persist($subscriberList); + + $subscription = new Subscription(); + $this->setSubjectProperty($subscription,'subscriber', $subscriber); + $this->setSubjectProperty($subscription,'subscriberList', $subscriberList); + $this->setSubjectProperty($subscription,'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscription,'modificationDate', new DateTime($row['modified'])); + $manager->persist($subscription); + } + + fclose($handle); + } +} diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Controller/ListControllerTest.php index db5f8e3..c461121 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Controller/ListControllerTest.php @@ -1,10 +1,16 @@ client->getContainer()->get(ListController::class)); + self::assertInstanceOf(ListController::class, self::getClient()->getContainer()->get(ListController::class)); } - /** - * @test - */ - public function getListsWithoutSessionKeyReturnsForbiddenStatus() + public function testGetListsWithoutSessionKeyReturnsForbiddenStatus() { - $this->client->request('get', '/api/v2/lists'); + self::getClient()->request('get', '/api/v2/lists'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListsWithExpiredSessionKeyReturnsForbiddenStatus() + public function testGetListsWithExpiredSessionKeyReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); - $this->client->request( + self::getClient()->request( 'get', '/api/v2/lists', [], @@ -67,23 +47,16 @@ public function getListsWithExpiredSessionKeyReturnsForbiddenStatus() $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListsWithCurrentSessionKeyReturnsOkayStatus() + public function testGetListsWithCurrentSessionKeyReturnsOkayStatus() { $this->authenticatedJsonRequest('get', '/api/v2/lists'); $this->assertHttpOkay(); } - /** - * @test - */ - public function getListsWithCurrentSessionKeyReturnsListData() + public function testGetListsWithCurrentSessionKeyReturnsListData() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists'); @@ -123,49 +96,34 @@ public function getListsWithCurrentSessionKeyReturnsListData() ); } - /** - * @test - */ - public function getListWithoutSessionKeyForExistingListReturnsForbiddenStatus() + public function testGetListWithoutSessionKeyForExistingListReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); - $this->client->request('get', '/api/v2/lists/1'); + self::getClient()->request('get', '/api/v2/lists/1'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListWithCurrentSessionKeyForExistingListReturnsOkayStatus() + public function testGetListWithCurrentSessionKeyForExistingListReturnsOkayStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/1'); $this->assertHttpOkay(); } - /** - * @test - */ - public function getListWithCurrentSessionKeyForInexistentListReturnsNotFoundStatus() + public function testGetListWithCurrentSessionKeyForInexistentListReturnsNotFoundStatus() { $this->authenticatedJsonRequest('get', '/api/v2/lists/999'); $this->assertHttpNotFound(); } - /** - * @test - */ - public function getListWithCurrentSessionKeyReturnsListData() + public function testGetListWithCurrentSessionKeyReturnsListData() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/1'); @@ -183,80 +141,55 @@ public function getListWithCurrentSessionKeyReturnsListData() ); } - /** - * @test - */ - public function deleteListWithoutSessionKeyForExistingListReturnsForbiddenStatus() + public function testDeleteListWithoutSessionKeyForExistingListReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); - $this->client->request('delete', '/api/v2/lists/1'); + self::getClient()->request('delete', '/api/v2/lists/1'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function deleteListWithCurrentSessionKeyForExistingListReturnsNoContentStatus() + public function testDeleteListWithCurrentSessionKeyForExistingListReturnsNoContentStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('delete', '/api/v2/lists/1'); $this->assertHttpNoContent(); } - /** - * @test - */ - public function deleteListWithCurrentSessionKeyForInexistentListReturnsNotFoundStatus() + public function testDeleteListWithCurrentSessionKeyForInexistentListReturnsNotFoundStatus() { $this->authenticatedJsonRequest('delete', '/api/v2/lists/999'); $this->assertHttpNotFound(); } - /** - * @test - */ - public function deleteListWithCurrentSessionKeyDeletesList() + public function testDeleteListWithCurrentSessionKeyDeletesList() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('delete', '/api/v2/lists/1'); - $listRepository = $this->container->get(SubscriberListRepository::class); - static::assertNull($listRepository->find(1)); + $listRepository = self::getContainer()->get(SubscriberListRepository::class); + self::assertNull($listRepository->find(1)); } - /** - * @test - */ - public function getListMembersForExistingListWithoutSessionKeyReturnsForbiddenStatus() + public function testGetListMembersForExistingListWithoutSessionKeyReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); - $this->client->request('get', '/api/v2/lists/1/members'); + self::getClient()->request('get', '/api/v2/lists/1/members'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListMembersForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() + public function testGetListMembersForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class, AdministratorFixture::class, AdministratorTokenFixture::class]); - $this->client->request( + self::getClient()->request( 'get', '/api/v2/lists/1/members', [], @@ -267,51 +200,34 @@ public function getListMembersForExistingListWithExpiredSessionKeyReturnsForbidd $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListMembersWithCurrentSessionKeyForInexistentListReturnsNotFoundStatus() + public function testGetListMembersWithCurrentSessionKeyForInexistentListReturnsNotFoundStatus() { $this->authenticatedJsonRequest('get', '/api/v2/lists/999/members'); $this->assertHttpNotFound(); } - /** - * @test - */ - public function getListMembersWithCurrentSessionKeyForExistingListReturnsOkayStatus() + public function testGetListMembersWithCurrentSessionKeyForExistingListReturnsOkayStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/1/members'); $this->assertHttpOkay(); } - /** - * @test - */ - public function getListMembersWithCurrentSessionKeyForExistingListWithoutSubscribersReturnsEmptyArray() + public function testGetListMembersWithCurrentSessionKeyForExistingListWithoutSubscribersReturnsEmptyArray() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/1/members'); $this->assertJsonResponseContentEquals([]); } - /** - * @test - */ - public function getListMembersWithCurrentSessionKeyForExistingListWithSubscribersReturnsSubscribers() + public function testGetListMembersWithCurrentSessionKeyForExistingListWithSubscribersReturnsSubscribers() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->getDataSet()->addTable(static::SUBSCRIBER_TABLE_NAME, __DIR__ . '/Fixtures/Subscriber.csv'); - $this->getDataSet()->addTable(static::SUBSCRIPTION_TABLE_NAME, __DIR__ . '/Fixtures/Subscription.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class, SubscriberFixture::class, SubscriptionFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/2/members'); @@ -332,30 +248,20 @@ public function getListMembersWithCurrentSessionKeyForExistingListWithSubscriber ); } - /** - * @test - */ - public function getListSubscribersCountForExistingListWithoutSessionKeyReturnsForbiddenStatus() + public function testGetListSubscribersCountForExistingListWithoutSessionKeyReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); - $this->client->request('get', '/api/v2/lists/1/subscribers/count'); + self::getClient()->request('get', '/api/v2/lists/1/subscribers/count'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListSubscribersCountForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() + public function testGetListSubscribersCountForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class, AdministratorFixture::class, AdministratorTokenFixture::class]); - $this->client->request( + self::getClient()->request( 'get', '/api/v2/lists/1/subscribers/count', [], @@ -366,48 +272,32 @@ public function getListSubscribersCountForExistingListWithExpiredSessionKeyRetur $this->assertHttpForbidden(); } - /** - * @test - */ - public function getListSubscribersCountWithCurrentSessionKeyForExistingListReturnsOkayStatus() + public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListReturnsOkayStatus() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/1/subscribers/count'); $this->assertHttpOkay(); } - /** - * @test - */ - public function getListSubscribersCountWithCurrentSessionKeyForExistingListWithNoSubscribersReturnsZero() + public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListWithNoSubscribersReturnsZero() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->getDataSet()->addTable(static::SUBSCRIBER_TABLE_NAME, __DIR__ . '/Fixtures/Subscriber.csv'); - $this->getDataSet()->addTable(static::SUBSCRIPTION_TABLE_NAME, __DIR__ . '/Fixtures/Subscription.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class, SubscriberFixture::class, SubscriptionFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/3/subscribers/count'); $responseContent = $this->getResponseContentAsInt(); - static::assertSame(0, $responseContent); + self::assertSame(0, $responseContent); } - /** - * @test - */ - public function getListSubscribersCountWithCurrentSessionKeyForExistingListWithSubscribersReturnsSubscribersCount() + public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListWithSubscribersReturnsSubscribersCount() { - $this->getDataSet()->addTable(static::LISTS_TABLE_NAME, __DIR__ . '/Fixtures/SubscriberList.csv'); - $this->getDataSet()->addTable(static::SUBSCRIBER_TABLE_NAME, __DIR__ . '/Fixtures/Subscriber.csv'); - $this->getDataSet()->addTable(static::SUBSCRIPTION_TABLE_NAME, __DIR__ . '/Fixtures/Subscription.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberListFixture::class, SubscriberFixture::class, SubscriptionFixture::class]); $this->authenticatedJsonRequest('get', '/api/v2/lists/2/subscribers/count'); $responseContent = $this->getResponseContentAsInt(); - static::assertSame(2, $responseContent); + self::assertSame(2, $responseContent); } } diff --git a/tests/Integration/Controller/SessionControllerTest.php b/tests/Integration/Controller/SessionControllerTest.php index 0e6d296..88ba518 100644 --- a/tests/Integration/Controller/SessionControllerTest.php +++ b/tests/Integration/Controller/SessionControllerTest.php @@ -21,7 +21,7 @@ class SessionControllerTest extends AbstractControllerTest */ private $administratorTokenRepository = null; - protected function setUp() + protected function setUp(): void { $this->setUpDatabaseTest(); $this->setUpWebTest(); diff --git a/tests/Integration/Controller/SubscriberControllerTest.php b/tests/Integration/Controller/SubscriberControllerTest.php index 6d2b1ff..3bb8aad 100644 --- a/tests/Integration/Controller/SubscriberControllerTest.php +++ b/tests/Integration/Controller/SubscriberControllerTest.php @@ -24,7 +24,7 @@ class SubscriberControllerTest extends AbstractControllerTest */ private $subscriberRepository = null; - protected function setUp() + protected function setUp(): void { $this->setUpDatabaseTest(); $this->setUpWebTest(); diff --git a/tests/Integration/Routing/RoutingTest.php b/tests/Integration/Routing/RoutingTest.php index c022c1a..32ed90c 100644 --- a/tests/Integration/Routing/RoutingTest.php +++ b/tests/Integration/Routing/RoutingTest.php @@ -1,26 +1,25 @@ */ -class RoutingTest extends AbstractWebTest +class RoutingTest extends WebTestCase { - /** - * @test - */ - public function rootUrlHasHtmlContentType() + public function testRootUrlHasHtmlContentType() { - $this->client->request('get', '/'); + $client = self::createClient(); + $client->request('get', '/'); - $response = $this->client->getResponse(); + $response = $client->getResponse(); - static::assertContains('text/html', (string)$response->headers); + self::assertStringContainsString('text/html', (string)$response->headers); } } diff --git a/tests/System/Controller/SecuredViewHandlerTest.php b/tests/System/Controller/SecuredViewHandlerTest.php index 0675241..5791e24 100644 --- a/tests/System/Controller/SecuredViewHandlerTest.php +++ b/tests/System/Controller/SecuredViewHandlerTest.php @@ -1,4 +1,5 @@ httpClient = new Client(['http_errors' => false]); } - protected function tearDown() + protected function tearDown(): void { $this->stopSymfonyServer(); } @@ -35,7 +33,7 @@ protected function tearDown() /** * @return string[][] */ - public function environmentDataProvider(): array + public static function environmentDataProvider(): array { return [ 'test' => ['test'], @@ -44,7 +42,6 @@ public function environmentDataProvider(): array } /** - * @test * @param string $environment * @dataProvider environmentDataProvider */ @@ -63,7 +60,7 @@ public function testSecurityHeaders(string $environment) ]; foreach ($expectedHeaders as $key => $value) { - static::assertSame([$value], $response->getHeader($key)); + self::assertSame([$value], $response->getHeader($key)); } } } diff --git a/tests/System/Controller/SessionControllerTest.php b/tests/System/Controller/SessionControllerTest.php index 9f4a9c4..34d10cd 100644 --- a/tests/System/Controller/SessionControllerTest.php +++ b/tests/System/Controller/SessionControllerTest.php @@ -1,4 +1,5 @@ httpClient = new Client(['http_errors' => false]); } - protected function tearDown() + protected function tearDown(): void { $this->stopSymfonyServer(); } @@ -35,7 +33,7 @@ protected function tearDown() /** * @return string[][] */ - public function environmentDataProvider(): array + public static function environmentDataProvider(): array { return [ 'test' => ['test'], @@ -44,11 +42,9 @@ public function environmentDataProvider(): array } /** - * @test - * @param string $environment * @dataProvider environmentDataProvider */ - public function postSessionsWithInvalidCredentialsReturnsNotAuthorized(string $environment) + public function testPostSessionsWithInvalidCredentialsReturnsNotAuthorized(string $environment) { $this->startSymfonyServer($environment); @@ -58,15 +54,15 @@ public function postSessionsWithInvalidCredentialsReturnsNotAuthorized(string $e $response = $this->httpClient->post( '/api/v2/sessions', - ['base_uri' => $this->getBaseUrl(), 'body' => \json_encode($jsonData)] + ['base_uri' => $this->getBaseUrl(), 'body' => json_encode($jsonData)] ); - static::assertSame(Response::HTTP_UNAUTHORIZED, $response->getStatusCode()); - static::assertSame( + self::assertSame(Response::HTTP_UNAUTHORIZED, $response->getStatusCode()); + self::assertSame( [ 'code' => Response::HTTP_UNAUTHORIZED, 'message' => 'Not authorized', ], - \json_decode($response->getBody()->getContents(), true) + json_decode($response->getBody()->getContents(), true) ); } } diff --git a/tests/Unit/PhpListRestBundleTest.php b/tests/Unit/PhpListRestBundleTest.php index b8577e6..305cc86 100644 --- a/tests/Unit/PhpListRestBundleTest.php +++ b/tests/Unit/PhpListRestBundleTest.php @@ -1,4 +1,5 @@ subject = new PhpListRestBundle(); } - /** - * @test - */ - public function classIsBundle() + public function testClassIsBundle() { static::assertInstanceOf(Bundle::class, $this->subject); } From bf004d062c17b3e30c96aa49b6c7befc1dcfae8d Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 10 Dec 2024 21:13:02 +0400 Subject: [PATCH 14/26] ISSUE-337: fix the rest of tests --- src/Controller/ListController.php | 6 +- src/Controller/SessionController.php | 4 +- src/Controller/SubscriberController.php | 2 +- src/EventListener/ExceptionListener.php | 3 +- ...lerTest.php => AbstractTestController.php} | 23 ++- .../Fixtures/AdministratorFixture.php | 7 +- .../Fixtures/AdministratorTokenFixture.php | 21 ++- .../Controller/Fixtures/Subscriber.csv | 2 + .../Controller/Fixtures/SubscriberFixture.php | 51 +++--- .../Fixtures/SubscriberListFixture.php | 18 ++- .../Fixtures/SubscriptionFixture.php | 20 +-- .../Controller/ListControllerTest.php | 18 ++- .../Controller/SessionControllerTest.php | 150 ++++++------------ .../Controller/SubscriberControllerTest.php | 90 +++-------- tests/Integration/Routing/RoutingTest.php | 2 +- .../Controller/SecuredViewHandlerTest.php | 48 +----- .../Controller/SessionControllerTest.php | 48 ++---- 17 files changed, 194 insertions(+), 319 deletions(-) rename tests/Integration/Controller/{AbstractControllerTest.php => AbstractTestController.php} (92%) diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index 37773c4..f5f4d59 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -79,7 +79,7 @@ public function deleteList(Request $request, SubscriberList $list): JsonResponse $this->subscriberListRepository->remove($list); - return new JsonResponse(null, Response::HTTP_OK, [], true); + return new JsonResponse(null, Response::HTTP_NO_CONTENT, [], false); } #[Route('/lists/{id}/members', name: 'get_subscriber_from_list', methods: ['GET'])] @@ -87,7 +87,7 @@ public function getListMembers(Request $request, SubscriberList $list): JsonResp { $this->requireAuthentication($request); - $subscribers = $this->subscriberRepository->findSubscribersBySubscribedList($list->getId()); + $subscribers = $this->subscriberRepository->getSubscribersBySubscribedListId($list->getId()); $json = $this->serializer->serialize($subscribers, 'json', [ AbstractNormalizer::GROUPS => 'SubscriberListMembers', @@ -96,7 +96,7 @@ public function getListMembers(Request $request, SubscriberList $list): JsonResp return new JsonResponse($json, Response::HTTP_OK, [], true); } - #[Route('/lists/{id}/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] + #[Route('/lists/{id}/subscribers/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] public function getSubscribersCount(Request $request, SubscriberList $list): JsonResponse { $this->requireAuthentication($request); diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 28ae648..cc9ee84 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -72,7 +72,7 @@ public function createSession(Request $request): JsonResponse $token = $this->createAndPersistToken($administrator); $json = $this->serializer->serialize($token, 'json'); - return new JsonResponse($json, Response::HTTP_OK, [], true); + return new JsonResponse($json, Response::HTTP_CREATED, [], true); } /** @@ -92,7 +92,7 @@ public function deleteAction(Request $request, AdministratorToken $token): JsonR $this->tokenRepository->remove($token); - return new JsonResponse(null, Response::HTTP_OK, [], true); + return new JsonResponse(null, Response::HTTP_NO_CONTENT, [], false); } /** diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index e4a7800..18a966d 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -48,7 +48,7 @@ public function __construct( * Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email * address yet). */ - #[Route('/subscriber', name: 'create_subscriber', methods: ['POST'])] + #[Route('/subscribers', name: 'create_subscriber', methods: ['POST'])] public function postAction(Request $request): JsonResponse { $this->requireAuthentication($request); diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index 43c4d1a..8cd80b9 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\EventListener; +use Exception; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -27,7 +28,7 @@ public function onKernelException(ExceptionEvent $event): void ], $exception->getStatusCode()); $event->setResponse($response); - } elseif ($exception instanceof \Exception) { + } elseif ($exception instanceof Exception) { $response = new JsonResponse([ 'message' => $exception->getMessage(), ], 500); diff --git a/tests/Integration/Controller/AbstractControllerTest.php b/tests/Integration/Controller/AbstractTestController.php similarity index 92% rename from tests/Integration/Controller/AbstractControllerTest.php rename to tests/Integration/Controller/AbstractTestController.php index 8110c3e..22dc053 100644 --- a/tests/Integration/Controller/AbstractControllerTest.php +++ b/tests/Integration/Controller/AbstractTestController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Tests\Integration\Controller; +use Doctrine\ORM\Tools\SchemaTool; use PhpList\Core\TestingSupport\Traits\DatabaseTestTrait; use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\AdministratorFixture; use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\AdministratorTokenFixture; @@ -18,13 +19,23 @@ * * @author Oliver Klee */ -abstract class AbstractControllerTest extends WebTestCase +abstract class AbstractTestController extends WebTestCase { use DatabaseTestTrait; protected function setUp(): void { + parent::setUp(); + self::createClient(); $this->setUpDatabaseTest(); + $this->loadSchema(); + } + + protected function tearDown(): void + { + $schemaTool = new SchemaTool($this->entityManager); + $schemaTool->dropDatabase(); + parent::tearDown(); } /** @@ -50,7 +61,14 @@ protected function jsonRequest( $serverWithContentType = $server; $serverWithContentType['CONTENT_TYPE'] = 'application/json'; - return self::getClient()->request($method, $uri, $parameters, $files, $serverWithContentType, $content); + return self::getClient()->request( + $method, + $uri, + $parameters, + $files, + $serverWithContentType, + $content + ); } /** @@ -225,7 +243,6 @@ protected function assertHttpConflict(): void self::assertSame( [ - 'code' => Response::HTTP_CONFLICT, 'message' => 'This resource already exists.', ], $this->getDecodedJsonResponseContent() diff --git a/tests/Integration/Controller/Fixtures/AdministratorFixture.php b/tests/Integration/Controller/Fixtures/AdministratorFixture.php index 2541446..f6976f8 100644 --- a/tests/Integration/Controller/Fixtures/AdministratorFixture.php +++ b/tests/Integration/Controller/Fixtures/AdministratorFixture.php @@ -33,14 +33,17 @@ public function load(ObjectManager $manager): void $row = array_combine($headers, $data); $admin = new Administrator(); + $this->setSubjectId($admin, (int)$row['id']); $admin->setLoginName($row['loginname']); $admin->setEmailAddress($row['email']); - $this->setSubjectProperty($admin,'creationDate', new DateTime($row['created'])); $admin->setPasswordHash($row['password']); - $this->setSubjectProperty($admin,'passwordChangeDate', new DateTime($row['passwordchanged'])); $admin->setDisabled((bool) $row['disabled']); $admin->setSuperUser((bool) $row['superuser']); + $manager->persist($admin); + + $this->setSubjectProperty($admin,'creationDate', new DateTime($row['created'])); + $this->setSubjectProperty($admin,'passwordChangeDate', new DateTime($row['passwordchanged'])); } fclose($handle); diff --git a/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php b/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php index 9d3d9be..c372039 100644 --- a/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php +++ b/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php @@ -29,20 +29,27 @@ public function load(ObjectManager $manager): void } $headers = fgetcsv($handle); - - $administratorRepository = $manager->getRepository(Administrator::class); + $adminRepository = $manager->getRepository(Administrator::class); while (($data = fgetcsv($handle)) !== false) { $row = array_combine($headers, $data); + $admin = $adminRepository->find($row['adminid']); + if ($admin === null) { + $admin = new Administrator(); + $this->setSubjectId($admin,(int)$row['adminid']); + $admin->setSuperUser(true); + $manager->persist($admin); + } + $adminToken = new AdministratorToken(); - $this->setSubjectId($adminToken,$row['id']); + $this->setSubjectId($adminToken,(int)$row['id']); $adminToken->setKey($row['value']); - $this->setSubjectProperty($adminToken,'expiry', new DateTime($row['expires'])); - $adminToken->setAdministrator($administratorRepository->find($row['adminid'])); - $this->setSubjectProperty($adminToken,'creationDate', new DateTime($row['created'])); - + $adminToken->setAdministrator($admin); $manager->persist($adminToken); + + $this->setSubjectProperty($adminToken,'expiry', new DateTime($row['expires'])); + $this->setSubjectProperty($adminToken, 'creationDate', (bool) $row['entered']); } fclose($handle); diff --git a/tests/Integration/Controller/Fixtures/Subscriber.csv b/tests/Integration/Controller/Fixtures/Subscriber.csv index deaf955..1fb2471 100644 --- a/tests/Integration/Controller/Fixtures/Subscriber.csv +++ b/tests/Integration/Controller/Fixtures/Subscriber.csv @@ -1,2 +1,4 @@ id,entered,modified,email,confirmed,blacklisted,bouncecount,uniqid,htmlemail,disabled 1,"2016-07-22 15:01:17","2016-08-23 19:50:43","oliver@example.com",1,1,17,"95feb7fe7e06e6c11ca8d0c48cb46e89",1,1 +2,"2016-07-22 15:01:17","2016-08-23 19:50:43","oliver1@example.com",1,1,17,"95feb7fe7e06e6c11ca8d0c48cb46e87",1,1 +3,"2016-07-22 15:01:17","2016-08-23 19:50:43","oliver2@example.com",1,1,17,"95feb7fe7e06e6c11ca8d0c48cb46e86",1,1 diff --git a/tests/Integration/Controller/Fixtures/SubscriberFixture.php b/tests/Integration/Controller/Fixtures/SubscriberFixture.php index 4451574..9dbe782 100644 --- a/tests/Integration/Controller/Fixtures/SubscriberFixture.php +++ b/tests/Integration/Controller/Fixtures/SubscriberFixture.php @@ -6,12 +6,15 @@ use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; -use Doctrine\DBAL\Connection; use Doctrine\Persistence\ObjectManager; +use PhpList\Core\Domain\Model\Subscription\Subscriber; +use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; class SubscriberFixture extends Fixture { + use ModelTestTrait; + public function load(ObjectManager $manager): void { $csvFile = __DIR__ . '/Subscriber.csv'; @@ -26,41 +29,25 @@ public function load(ObjectManager $manager): void } $headers = fgetcsv($handle); - if ($headers === false) { - throw new RuntimeException('Could not read headers from CSV file.'); - } - - /** @var Connection $connection */ - $connection = $manager->getConnection(); - - $insertQuery = " - INSERT INTO phplist_user_user ( - id, entered, modified, email, confirmed, blacklisted, bouncecount, - uniqid, htmlemail, disabled, extradata - ) VALUES ( - :id, :creation_date, :modification_date, :email, :confirmed, :blacklisted, :bounce_count, - :unique_id, :html_email, :disabled, :extra_data - ) - "; - - $stmt = $connection->prepare($insertQuery); while (($data = fgetcsv($handle)) !== false) { $row = array_combine($headers, $data); - $stmt->executeStatement([ - 'id' => (int) $row['id'], - 'creation_date' => (new DateTime($row['entered']))->format('Y-m-d H:i:s'), - 'modification_date' => (new DateTime($row['modified']))->format('Y-m-d H:i:s'), - 'email' => $row['email'], - 'confirmed' => (bool) $row['confirmed'] ? 1 : 0, - 'blacklisted' => (bool) $row['blacklisted'] ? 1 : 0, - 'bounce_count' => (int) $row['bouncecount'], - 'unique_id' => $row['uniqueid'], - 'html_email' => (bool) $row['htmlemail'] ? 1 : 0, - 'disabled' => (bool) $row['disabled'] ? 1 : 0, - 'extra_data' => $row['extradata'], - ]); + $subscriber = new Subscriber(); + $this->setSubjectId($subscriber,(int)$row['id']); + + $subscriber->setEmail($row['email']); + $subscriber->setConfirmed((bool) $row['confirmed']); + $subscriber->setBlacklisted((bool) $row['blacklisted']); + $subscriber->setBounceCount((int) $row['bouncecount']); + $subscriber->setHtmlEmail((bool) $row['htmlemail']); + $subscriber->setDisabled((bool) $row['disabled']); + + $manager->persist($subscriber); + // avoid pre-persist + $subscriber->setUniqueId($row['uniqid']); + $this->setSubjectProperty($subscriber,'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscriber,'modificationDate', new DateTime($row['modified'])); } fclose($handle); diff --git a/tests/Integration/Controller/Fixtures/SubscriberListFixture.php b/tests/Integration/Controller/Fixtures/SubscriberListFixture.php index 6a4c86e..68204dc 100644 --- a/tests/Integration/Controller/Fixtures/SubscriberListFixture.php +++ b/tests/Integration/Controller/Fixtures/SubscriberListFixture.php @@ -30,25 +30,33 @@ public function load(ObjectManager $manager): void $headers = fgetcsv($handle); + $adminRepository = $manager->getRepository(Administrator::class); + while (($data = fgetcsv($handle)) !== false) { $row = array_combine($headers, $data); - $admin = new Administrator(); - $this->setSubjectId($admin,(int)$row['owner']); + $admin = $adminRepository->find($row['owner']); + if ($admin === null) { + $admin = new Administrator(); + $this->setSubjectId($admin,(int)$row['owner']); + $admin->setSuperUser(true); + $admin->setDisabled(false); + $manager->persist($admin); + } $subscriberList = new SubscriberList(); $this->setSubjectId($subscriberList,(int)$row['id']); $subscriberList->setName($row['name']); $subscriberList->setDescription($row['description']); - $this->setSubjectProperty($subscriberList,'creationDate', new DateTime($row['entered'])); - $this->setSubjectProperty($subscriberList,'modificationDate', new DateTime($row['modified'])); $subscriberList->setListPosition((int)$row['listorder']); $subscriberList->setSubjectPrefix($row['prefix']); $subscriberList->setPublic((bool) $row['active']); $subscriberList->setCategory($row['category']); $subscriberList->setOwner($admin); - $manager->persist($admin); $manager->persist($subscriberList); + + $this->setSubjectProperty($subscriberList,'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscriberList,'modificationDate', new DateTime($row['modified'])); } fclose($handle); diff --git a/tests/Integration/Controller/Fixtures/SubscriptionFixture.php b/tests/Integration/Controller/Fixtures/SubscriptionFixture.php index 76728b9..5026dac 100644 --- a/tests/Integration/Controller/Fixtures/SubscriptionFixture.php +++ b/tests/Integration/Controller/Fixtures/SubscriptionFixture.php @@ -30,25 +30,25 @@ public function load(ObjectManager $manager): void throw new RuntimeException(sprintf('Could not open fixture file "%s".', $csvFile)); } + $subscriberRepository = $manager->getRepository(Subscriber::class); + $subscriberListRepository = $manager->getRepository(SubscriberList::class); + $headers = fgetcsv($handle); while (($data = fgetcsv($handle)) !== false) { $row = array_combine($headers, $data); - $subscriber = new Subscriber(); - $this->setSubjectId($subscriber,(int)$row['userid']); - $manager->persist($subscriber); - - $subscriberList = new SubscriberList(); - $this->setSubjectId($subscriberList,(int)$row['listid']); - $manager->persist($subscriberList); + $subscriber = $subscriberRepository->find((int)$row['userid']); + $subscriberList = $subscriberListRepository->find((int)$row['listid']); $subscription = new Subscription(); - $this->setSubjectProperty($subscription,'subscriber', $subscriber); - $this->setSubjectProperty($subscription,'subscriberList', $subscriberList); + $subscriberList->addSubscription($subscription); + $subscriber->addSubscription($subscription); + + $manager->persist($subscription); + $this->setSubjectProperty($subscription,'creationDate', new DateTime($row['entered'])); $this->setSubjectProperty($subscription,'modificationDate', new DateTime($row['modified'])); - $manager->persist($subscription); } fclose($handle); diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Controller/ListControllerTest.php index c461121..0f97da0 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Controller/ListControllerTest.php @@ -18,7 +18,7 @@ * @author Oliver Klee * @author Xheni Myrtaj */ -class ListControllerTest extends AbstractControllerTest +class ListControllerTest extends AbstractTestController { public function testControllerIsAvailableViaContainer() { @@ -152,7 +152,7 @@ public function testDeleteListWithoutSessionKeyForExistingListReturnsForbiddenSt public function testDeleteListWithCurrentSessionKeyForExistingListReturnsNoContentStatus() { - $this->loadFixtures([SubscriberListFixture::class]); + $this->loadFixtures([SubscriberFixture::class, SubscriberListFixture::class, SubscriptionFixture::class]); $this->authenticatedJsonRequest('delete', '/api/v2/lists/1'); @@ -243,7 +243,17 @@ public function testGetListMembersWithCurrentSessionKeyForExistingListWithSubscr 'html_email' => true, 'disabled' => true, 'id' => 1, - ] + ], [ + 'creation_date' => '2016-07-22T15:01:17+00:00', + 'email' => 'oliver1@example.com', + 'confirmed' => true, + 'blacklisted' => true, + 'bounce_count' => 17, + 'unique_id' => '95feb7fe7e06e6c11ca8d0c48cb46e87', + 'html_email' => true, + 'disabled' => true, + 'id' => 2, + ], ] ); } @@ -266,7 +276,7 @@ public function testGetListSubscribersCountForExistingListWithExpiredSessionKeyR '/api/v2/lists/1/subscribers/count', [], [], - ['PHP_AUTH_USER' => 'unused', 'PHP_AUTH_PW' => 'cfdf64eecbbf336628b0f3071adba763'] + ['PHP_AUTH_USER' => 'unused', 'PHP_AUTH_PW' => 'cfdf64eecbbf336628b0f3071adba764'] ); $this->assertHttpForbidden(); diff --git a/tests/Integration/Controller/SessionControllerTest.php b/tests/Integration/Controller/SessionControllerTest.php index 88ba518..e55b5cf 100644 --- a/tests/Integration/Controller/SessionControllerTest.php +++ b/tests/Integration/Controller/SessionControllerTest.php @@ -1,100 +1,78 @@ */ -class SessionControllerTest extends AbstractControllerTest +class SessionControllerTest extends AbstractTestController { - /** - * @var AdministratorTokenRepository|ObjectRepository - */ - private $administratorTokenRepository = null; + private ?AdministratorTokenRepository $administratorTokenRepository = null; protected function setUp(): void { - $this->setUpDatabaseTest(); - $this->setUpWebTest(); - - $this->administratorTokenRepository = $this->bootstrap->getContainer() - ->get(AdministratorTokenRepository::class); + parent::setUp(); + $this->administratorTokenRepository = self::getContainer()->get(AdministratorTokenRepository::class); } - /** - * @test - */ - public function controllerIsAvailableViaContainer() + public function testControllerIsAvailableViaContainer() { - static::assertInstanceOf( + self::assertInstanceOf( SessionController::class, - $this->client->getContainer()->get(SessionController::class) + self::getClient()->getContainer()->get(SessionController::class) ); } - /** - * @test - */ - public function getSessionsIsNotAllowed() + public function testGetSessionsIsNotAllowed() { - $this->client->request('get', '/api/v2/sessions'); + self::getClient()->request('get', '/api/v2/sessions'); $this->assertHttpMethodNotAllowed(); } - /** - * @test - */ - public function postSessionsWithNoJsonReturnsError400() + public function testPostSessionsWithNoJsonReturnsError400() { $this->jsonRequest('post', '/api/v2/sessions'); $this->assertHttpBadRequest(); $this->assertJsonResponseContentEquals( [ - 'code' => Response::HTTP_BAD_REQUEST, 'message' => 'Empty JSON data', ] ); } - /** - * @test - */ - public function postSessionsWithInvalidJsonWithJsonContentTypeReturnsError400() + public function testPostSessionsWithInvalidJsonWithJsonContentTypeReturnsError400() { $this->jsonRequest('post', '/api/v2/sessions', [], [], [], 'Here be dragons, but no JSON.'); $this->assertHttpBadRequest(); $this->assertJsonResponseContentEquals( [ - 'code' => Response::HTTP_BAD_REQUEST, - 'message' => 'Invalid json message received' + 'message' => 'Could not decode request body.', ] ); } - /** - * @test - */ - public function postSessionsWithValidEmptyJsonWithOtherTypeReturnsError400() + public function testPostSessionsWithValidEmptyJsonWithOtherTypeReturnsError400() { - $this->client->request('post', '/api/v2/sessions', [], [], ['CONTENT_TYPE' => 'application/xml'], '[]'); + self::getClient()->request('post', '/api/v2/sessions', [], [], ['CONTENT_TYPE' => 'application/xml'], '[]'); $this->assertHttpBadRequest(); $this->assertJsonResponseContentEquals( [ - 'code' => Response::HTTP_BAD_REQUEST, - 'message' => 'Invalid xml message received' + 'message' => 'Incomplete credentials', ] ); } @@ -102,7 +80,7 @@ public function postSessionsWithValidEmptyJsonWithOtherTypeReturnsError400() /** * @return string[][] */ - public function incompleteCredentialsDataProvider(): array + public static function incompleteCredentialsDataProvider(): array { return [ 'neither login_name nor password' => ['{}'], @@ -112,30 +90,23 @@ public function incompleteCredentialsDataProvider(): array } /** - * @test - * @param string $jsonData * @dataProvider incompleteCredentialsDataProvider */ - public function postSessionsWithValidIncompleteJsonReturnsError400(string $jsonData) + public function testPostSessionsWithValidIncompleteJsonReturnsError400(string $jsonData) { $this->jsonRequest('post', '/api/v2/sessions', [], [], [], $jsonData); $this->assertHttpBadRequest(); $this->assertJsonResponseContentEquals( [ - 'code' => Response::HTTP_BAD_REQUEST, 'message' => 'Incomplete credentials', ] ); } - /** - * @test - */ - public function postSessionsWithInvalidCredentialsReturnsNotAuthorized() + public function testPostSessionsWithInvalidCredentialsReturnsNotAuthorized() { - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([AdministratorFixture::class]); $loginName = 'john.doe'; $password = 'a sandwich and a cup of coffee'; @@ -146,19 +117,14 @@ public function postSessionsWithInvalidCredentialsReturnsNotAuthorized() $this->assertHttpUnauthorized(); $this->assertJsonResponseContentEquals( [ - 'code' => Response::HTTP_UNAUTHORIZED, 'message' => 'Not authorized', ] ); } - /** - * @test - */ - public function postSessionsActionWithValidCredentialsReturnsCreatedHttpStatus() + public function testPostSessionsActionWithValidCredentialsReturnsCreatedHttpStatus() { - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([AdministratorFixture::class]); $loginName = 'john.doe'; $password = 'Bazinga!'; @@ -169,95 +135,71 @@ public function postSessionsActionWithValidCredentialsReturnsCreatedHttpStatus() $this->assertHttpCreated(); } - /** - * @test - */ - public function postSessionsActionWithValidCredentialsCreatesToken() + public function testPostSessionsActionWithValidCredentialsCreatesToken() { $administratorId = 1; - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->applyDatabaseChanges(); - - $loginName = 'john.doe'; - $password = 'Bazinga!'; - $jsonData = ['login_name' => $loginName, 'password' => $password]; + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); + $jsonData = ['login_name' => 'john.doe', 'password' => 'Bazinga!']; $this->jsonRequest('post', '/api/v2/sessions', [], [], [], json_encode($jsonData)); $responseContent = $this->getDecodedJsonResponseContent(); $tokenId = $responseContent['id']; $key = $responseContent['key']; - $expiry = $responseContent['expiry']; + $expiry = $responseContent['expiry_date']; /** @var AdministratorToken $token */ $token = $this->administratorTokenRepository->find($tokenId); - static::assertNotNull($token); - static::assertSame($key, $token->getKey()); - static::assertSame($expiry, $token->getExpiry()->format(\DateTime::ATOM)); - static::assertSame($administratorId, $token->getAdministrator()->getId()); + + self::assertNotNull($token); + self::assertSame($key, $token->getKey()); + self::assertSame($expiry, $token->getExpiry()->format(DateTime::ATOM)); + self::assertSame($administratorId, $token->getAdministrator()->getId()); } - /** - * @test - */ - public function deleteSessionWithoutSessionKeyForExistingSessionReturnsForbiddenStatus() + public function testDeleteSessionWithoutSessionKeyForExistingSessionReturnsForbiddenStatus() { - $this->getDataSet()->addTable(static::ADMINISTRATOR_TABLE_NAME, __DIR__ . '/Fixtures/Administrator.csv'); - $this->getDataSet()->addTable(static::TOKEN_TABLE_NAME, __DIR__ . '/Fixtures/AdministratorToken.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); - $this->client->request('delete', '/api/v2/sessions/1'); + self::getClient()->request('delete', '/api/v2/sessions/1'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function deleteSessionWithCurrentSessionKeyForExistingSessionReturnsNoContentStatus() + public function testDeleteSessionWithCurrentSessionKeyForExistingSessionReturnsNoContentStatus() { $this->authenticatedJsonRequest('delete', '/api/v2/sessions/1'); $this->assertHttpNoContent(); } - /** - * @test - */ - public function deleteSessionWithCurrentSessionKeyForInexistentSessionReturnsNotFoundStatus() + public function testDeleteSessionWithCurrentSessionKeyForInexistentSessionReturnsNotFoundStatus() { $this->authenticatedJsonRequest('delete', '/api/v2/sessions/999'); $this->assertHttpNotFound(); } - /** - * @test - */ - public function deleteSessionWithCurrentSessionAndOwnSessionKeyDeletesSession() + public function testDeleteSessionWithCurrentSessionAndOwnSessionKeyDeletesSession() { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); $this->authenticatedJsonRequest('delete', '/api/v2/sessions/1'); - static::assertNull($this->administratorTokenRepository->find(1)); + self::assertNull($this->administratorTokenRepository->find(1)); } - /** - * @test - */ - public function deleteSessionWithCurrentSessionAndOwnSessionKeyKeepsReturnsForbiddenStatus() + public function testDeleteSessionWithCurrentSessionAndOwnSessionKeyKeepsReturnsForbiddenStatus() { $this->authenticatedJsonRequest('delete', '/api/v2/sessions/3'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function deleteSessionWithCurrentSessionAndOwnSessionKeyKeepsSession() + public function testDeleteSessionWithCurrentSessionAndOwnSessionKeyKeepsSession() { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); $this->authenticatedJsonRequest('delete', '/api/v2/sessions/3'); - static::assertNotNull($this->administratorTokenRepository->find(3)); + self::assertNotNull($this->administratorTokenRepository->find(3)); } } diff --git a/tests/Integration/Controller/SubscriberControllerTest.php b/tests/Integration/Controller/SubscriberControllerTest.php index 3bb8aad..5e2fa9e 100644 --- a/tests/Integration/Controller/SubscriberControllerTest.php +++ b/tests/Integration/Controller/SubscriberControllerTest.php @@ -1,4 +1,5 @@ */ -class SubscriberControllerTest extends AbstractControllerTest +class SubscriberControllerTest extends AbstractTestController { - /** - * @var string - */ - const SUBSCRIBER_TABLE_NAME = 'phplist_user_user'; - - /** - * @var SubscriberRepository - */ - private $subscriberRepository = null; + private ?SubscriberRepository $subscriberRepository = null; protected function setUp(): void { - $this->setUpDatabaseTest(); - $this->setUpWebTest(); + parent::setUp(); - $this->subscriberRepository = $this->bootstrap->getContainer() - ->get(SubscriberRepository::class); + $this->subscriberRepository = self::getContainer()->get(SubscriberRepository::class); } - /** - * @test - */ - public function controllerIsAvailableViaContainer() + public function testControllerIsAvailableViaContainer() { - static::assertInstanceOf( + self::assertInstanceOf( SubscriberController::class, - $this->client->getContainer()->get(SubscriberController::class) + self::getClient()->getContainer()->get(SubscriberController::class) ); } - /** - * @test - */ - public function getSubscribersIsNotAllowed() + public function testGetSubscribersIsNotAllowed() { - $this->client->request('get', '/api/v2/subscribers'); + self::getClient()->request('get', '/api/v2/subscribers'); $this->assertHttpMethodNotAllowed(); } - /** - * @test - */ - public function postSubscribersWithoutSessionKeyReturnsForbiddenStatus() + public function testPostSubscribersWithoutSessionKeyReturnsForbiddenStatus() { $this->jsonRequest('post', '/api/v2/subscribers'); $this->assertHttpForbidden(); } - /** - * @test - */ - public function postSubscribersWithValidSessionKeyAndMinimalValidSubscriberDataCreatesResource() + public function testPostSubscribersWithValidSessionKeyAndMinimalValidSubscriberDataCreatesResource() { - $this->touchDatabaseTable(static::SUBSCRIBER_TABLE_NAME); - $email = 'subscriber@example.com'; $jsonData = ['email' => $email]; @@ -79,13 +57,8 @@ public function postSubscribersWithValidSessionKeyAndMinimalValidSubscriberDataC $this->assertHttpCreated(); } - /** - * @test - */ - public function postSubscribersWithValidSessionKeyAndMinimalValidDataReturnsIdAndUniqueId() + public function testPostSubscribersWithValidSessionKeyAndMinimalValidDataReturnsIdAndUniqueId() { - $this->touchDatabaseTable(static::SUBSCRIBER_TABLE_NAME); - $email = 'subscriber@example.com'; $jsonData = ['email' => $email]; @@ -93,17 +66,12 @@ public function postSubscribersWithValidSessionKeyAndMinimalValidDataReturnsIdAn $responseContent = $this->getDecodedJsonResponseContent(); - static::assertGreaterThan(0, $responseContent['id']); - static::assertRegExp('/^[0-9a-f]{32}$/', $responseContent['unique_id']); + self::assertGreaterThan(0, $responseContent['id']); + self::assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $responseContent['unique_id']); } - /** - * @test - */ - public function postSubscribersWithValidSessionKeyAndValidDataCreatesSubscriber() + public function testPostSubscribersWithValidSessionKeyAndValidDataCreatesSubscriber() { - $this->touchDatabaseTable(static::SUBSCRIBER_TABLE_NAME); - $email = 'subscriber@example.com'; $jsonData = ['email' => $email]; @@ -112,16 +80,12 @@ public function postSubscribersWithValidSessionKeyAndValidDataCreatesSubscriber( $responseContent = $this->getDecodedJsonResponseContent(); $subscriberId = $responseContent['id']; - static::assertInstanceOf(Subscriber::class, $this->subscriberRepository->find($subscriberId)); + self::assertInstanceOf(Subscriber::class, $this->subscriberRepository->find($subscriberId)); } - /** - * @test - */ - public function postSubscribersWithValidSessionKeyAndExistingEmailAddressCreatesConflictStatus() + public function testPostSubscribersWithValidSessionKeyAndExistingEmailAddressCreatesConflictStatus() { - $this->getDataSet()->addTable(static::SUBSCRIBER_TABLE_NAME, __DIR__ . '/Fixtures/Subscriber.csv'); - $this->applyDatabaseChanges(); + $this->loadFixtures([SubscriberFixture::class]); $email = 'oliver@example.com'; $jsonData = ['email' => $email]; @@ -134,7 +98,7 @@ public function postSubscribersWithValidSessionKeyAndExistingEmailAddressCreates /** * @return array[][] */ - public function invalidSubscriberDataProvider(): array + public static function invalidSubscriberDataProvider(): array { return [ 'no data' => [[]], @@ -154,26 +118,18 @@ public function invalidSubscriberDataProvider(): array } /** - * @test * @dataProvider invalidSubscriberDataProvider * @param array[] $jsonData */ - public function postSubscribersWithInvalidDataCreatesUnprocessableEntityStatus(array $jsonData) + public function testPostSubscribersWithInvalidDataCreatesUnprocessableEntityStatus(array $jsonData) { - $this->touchDatabaseTable(static::SUBSCRIBER_TABLE_NAME); - $this->authenticatedJsonRequest('post', '/api/v2/subscribers', [], [], [], json_encode($jsonData)); $this->assertHttpUnprocessableEntity(); } - /** - * @test - */ - public function postSubscribersWithValidSessionKeyAssignsProvidedSubscriberData() + public function testPostSubscribersWithValidSessionKeyAssignsProvidedSubscriberData() { - $this->touchDatabaseTable(static::SUBSCRIBER_TABLE_NAME); - $email = 'subscriber@example.com'; $jsonData = [ 'email' => $email, diff --git a/tests/Integration/Routing/RoutingTest.php b/tests/Integration/Routing/RoutingTest.php index 32ed90c..b0731ff 100644 --- a/tests/Integration/Routing/RoutingTest.php +++ b/tests/Integration/Routing/RoutingTest.php @@ -16,7 +16,7 @@ class RoutingTest extends WebTestCase public function testRootUrlHasHtmlContentType() { $client = self::createClient(); - $client->request('get', '/'); + $client->request('get', '/api/v2'); $response = $client->getResponse(); diff --git a/tests/System/Controller/SecuredViewHandlerTest.php b/tests/System/Controller/SecuredViewHandlerTest.php index 5791e24..c823ed4 100644 --- a/tests/System/Controller/SecuredViewHandlerTest.php +++ b/tests/System/Controller/SecuredViewHandlerTest.php @@ -4,55 +4,23 @@ namespace PhpList\RestBundle\Tests\System\Controller; -use GuzzleHttp\Client; -use PhpList\Core\TestingSupport\Traits\SymfonyServerTrait; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Response; +use PhpList\RestBundle\Tests\Integration\Controller\AbstractTestController; /** * Test for security headers * * @author Xheni Myrtaj */ -class SecuredViewHandlerTest extends TestCase +class SecuredViewHandlerTest extends AbstractTestController { - use SymfonyServerTrait; - - private ?Client $httpClient = null; - - protected function setUp(): void - { - $this->httpClient = new Client(['http_errors' => false]); - } - - protected function tearDown(): void - { - $this->stopSymfonyServer(); - } - - /** - * @return string[][] - */ - public static function environmentDataProvider(): array + public function testSecurityHeaders() { - return [ - 'test' => ['test'], - 'dev' => ['dev'], - ]; - } - - /** - * @param string $environment - * @dataProvider environmentDataProvider - */ - public function testSecurityHeaders(string $environment) - { - $this->startSymfonyServer($environment); - - $response = $this->httpClient->get( + self::getClient()->request( + 'GET', '/api/v2/sessions', - ['base_uri' => $this->getBaseUrl()] ); + + $response = self::getClient()->getResponse(); $expectedHeaders = [ 'X-Content-Type-Options' => 'nosniff', 'Content-Security-Policy' => "default-src 'none'", @@ -60,7 +28,7 @@ public function testSecurityHeaders(string $environment) ]; foreach ($expectedHeaders as $key => $value) { - self::assertSame([$value], $response->getHeader($key)); + self::assertSame($value, $response->headers->get($key)); } } } diff --git a/tests/System/Controller/SessionControllerTest.php b/tests/System/Controller/SessionControllerTest.php index 34d10cd..e5d6cdd 100644 --- a/tests/System/Controller/SessionControllerTest.php +++ b/tests/System/Controller/SessionControllerTest.php @@ -4,9 +4,8 @@ namespace PhpList\RestBundle\Tests\System\Controller; -use GuzzleHttp\Client; use PhpList\Core\TestingSupport\Traits\SymfonyServerTrait; -use PHPUnit\Framework\TestCase; +use PhpList\RestBundle\Tests\Integration\Controller\AbstractTestController; use Symfony\Component\HttpFoundation\Response; /** @@ -14,55 +13,30 @@ * * @author Oliver Klee */ -class SessionControllerTest extends TestCase +class SessionControllerTest extends AbstractTestController { use SymfonyServerTrait; - private ?Client $httpClient = null; - - protected function setUp(): void - { - $this->httpClient = new Client(['http_errors' => false]); - } - - protected function tearDown(): void + public function testPostSessionsWithInvalidCredentialsReturnsNotAuthorized() { - $this->stopSymfonyServer(); - } - - /** - * @return string[][] - */ - public static function environmentDataProvider(): array - { - return [ - 'test' => ['test'], - 'dev' => ['dev'], - ]; - } - - /** - * @dataProvider environmentDataProvider - */ - public function testPostSessionsWithInvalidCredentialsReturnsNotAuthorized(string $environment) - { - $this->startSymfonyServer($environment); - $loginName = 'john.doe'; $password = 'a sandwich and a cup of coffee'; $jsonData = ['login_name' => $loginName, 'password' => $password]; - $response = $this->httpClient->post( + self::getClient()->request( + 'POST', '/api/v2/sessions', - ['base_uri' => $this->getBaseUrl(), 'body' => json_encode($jsonData)] + [], + [], + [], + json_encode($jsonData) ); - self::assertSame(Response::HTTP_UNAUTHORIZED, $response->getStatusCode()); + self::assertSame(Response::HTTP_UNAUTHORIZED, self::getClient()->getResponse()->getStatusCode()); self::assertSame( [ - 'code' => Response::HTTP_UNAUTHORIZED, 'message' => 'Not authorized', ], - json_decode($response->getBody()->getContents(), true) + json_decode(self::getClient()->getResponse()->getContent(), true) ); } } From 86b26f66b08bc7359b4ecc9c68db9d450f89cb92 Mon Sep 17 00:00:00 2001 From: "F. E Noel Nfebe" Date: Fri, 12 Feb 2021 02:55:17 +0400 Subject: [PATCH 15/26] GitHub actions (#132) * added github workflow(action) for build and test ci tasks Signed-off-by: fenn-cs * updated php_codesniffer dep Signed-off-by: fenn-cs * removed travis ci config file Signed-off-by: fenn-cs (cherry picked from commit 70d45ad9853007d39ced57facd172066e1cb9b16) --- .github/workflows/ci.yml | 72 ++++++++++++++++++++++++++++++ .travis.yml | 94 ---------------------------------------- README.md | 2 +- composer.json | 2 +- 4 files changed, 74 insertions(+), 96 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49b5d50 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: phpList REST API Build +on: [push, pull_request] +jobs: + main: + name: phpList Base Dist on PHP ${{ matrix.php-versions }}, with dist ${{ matrix.dependencies }} [Build, Test] + runs-on: ubuntu-20.04 + env: + DB_DATABASE: phplist + DB_USERNAME: root + DB_PASSWORD: phplist + BROADCAST_DRIVER: log + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: false + MYSQL_ROOT_PASSWORD: ${{ env.DB_PASSWORD }} + MYSQL_DATABASE: ${{ env.DB_DATABASE }} + ports: + - 3306/tcp + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + strategy: + fail-fast: false + matrix: + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] + dependencies: ['latest', 'oldest'] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php-versions }} + extensions: mbstring, dom, fileinfo, mysql + coverage: xdebug #optional + - name: Start mysql service + run: sudo /etc/init.d/mysql start + - name: Verify MySQL connection on host + run: mysql --host 127.0.0.1 --port ${{ job.services.mysql.ports['3306'] }} -u${{ env.DB_USERNAME }} -p${{ env.DB_PASSWORD }} -e "SHOW DATABASES" + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + # Use composer.json for key, if composer.lock is not committed. + # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install the lowest dependencies + run: composer update --with-dependencies --prefer-stable --prefer-dist --prefer-lowest + if: ${{ matrix.dependencies }} == "latest" + - name: Install current dependencies from composer.lock + run: composer install + if: ${{ matrix.dependencies }} == "oldest" + - name: Set up database schema + run: mysql --host 127.0.0.1 --port ${{ job.services.mysql.ports['3306'] }} -u${{ env.DB_USERNAME }} -p${{ env.DB_PASSWORD }} ${{ env.DB_DATABASE }} < vendor/phplist/core/resources/Database/Schema.sql + - name: Validating composer.json + run: composer validate --no-check-all --no-check-lock --strict; + - name: Linting all php files + run: find src/ tests/ public/ -name ''*.php'' -print0 | xargs -0 -n 1 -P 4 php -l; php -l; + - name: Run integration tests with phpunit + run: vendor/bin/phpunit tests/Integration/ + - name: Running the system tests + run: vendor/bin/phpunit tests/Integration/; + - name: Running static analysis + run: vendor/bin/phpstan analyse -l 5 src/ tests/; + - name: Running PHPMD + run: vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml; + - name: Running PHP_CodeSniffer + run: vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/; \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 88b1bc0..e69de29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,94 +0,0 @@ -language: php - -php: -- 7.0 -- 7.1 -- 7.2 - -services: -- mysql - -env: - global: - - PHPLIST_DATABASE_NAME=phplist PHPLIST_DATABASE_USER=travis PHPLIST_DATABASE_PASSWORD='' - matrix: - - DEPENDENCIES=latest - - DEPENDENCIES=oldest - -notifications: - slack: - rooms: - secure: cwCoc5P28/IG7s+P9Hj5cw4jbVXHDuYcTQoj9CJNNzWb5/Mfu7472EzjRkFP9FL86LGa3JfnoTHBRxfvDS4I1soapPX8hHNZFL68iOgXtKxpWpYTeJszV09W2Cc2EzOWn/Th1Q6D5DfLPGmDTgaIiFS70FJ+3VOwNl0GFjtryAJxRdzGTRkbKDZUd/jA6JXOEu3+Rk9rZeHl90bI7CWDJS5sz5Ubzy7mmEFyn4+S1ktFkKeCOdD+CS3r4DCv1TufAg6fRMflBU5chw8sypqtEE6FqlH1bYZ3OMLaQ4MZZH+4yL+EfnUx4zmXN0L7tOSGKNeoCwiTk5AnlJzUaop0+d3FQXNEvsXvq1UuIEe8lm8bkn1gMvMBvo0rh2YpWLErHBFXclS03uSUYU0EQPz/kAuNfDD7a+v62vgSTi8t/7Pqr099J/V6b7cAtlqf1x38/U5QKVOZ4g2zUV3nVbPpoaR8pC/PJ0Jvz4g6vIaWiSqtCWkUV2CDHblSNt+ySIfadwgFBBbylPOXNBorzkRn+QUFbeq2iOMM0FLnhKc+XPIyK1ewmCfm5bj1kUOfoN09zSwauigNI3Ag6MWPAZNY0qpDyCqGOm8KcFCX5X57eekNHjRNM3Zac3OSTjlQ9jV9MmDCHWxvrmKBGM9a5d5u6bS/8Mqxgm/FsNk4b1HgsN8= - on_success: change - on_failure: always - -sudo: false - -cache: - directories: - - vendor - - "$HOME/.composer/cache" - -before_install: -- phpenv config-rm xdebug.ini - -install: -- > - echo; - if [ "$DEPENDENCIES" = "latest" ]; then - echo "Installing the latest dependencies"; - composer update --with-dependencies --prefer-stable --prefer-dist - else - echo "Installing the lowest dependencies"; - composer update --with-dependencies --prefer-stable --prefer-dist --prefer-lowest - fi; - composer show; - -before_script: -- > - echo; - echo "Creating the database and importing the database schema"; - mysql -e "CREATE DATABASE ${PHPLIST_DATABASE_NAME};"; - mysql -u root -e "GRANT ALL ON ${PHPLIST_DATABASE_NAME}.* TO '${PHPLIST_DATABASE_USER}'@'%';"; - mysql ${PHPLIST_DATABASE_NAME} < vendor/phplist/core/resources/Database/Schema.sql; - -script: -- > - echo; - echo "Validating the composer.json"; - composer validate --no-check-all --no-check-lock --strict; - -- > - echo; - echo "Linting all PHP files"; - find src/ tests/ -name ''*.php'' -print0 | xargs -0 -n 1 -P 4 php -l; - -- > - echo; - echo "Running the unit tests"; - vendor/bin/phpunit tests/Unit/; - -- > - echo; - echo "Running the integration tests"; - vendor/bin/phpunit tests/Integration/; - -- > - echo; - echo "Running the system tests"; - vendor/bin/phpunit tests/System/; - -- > - echo; - echo "Running the static analysis"; - vendor/bin/phpstan analyse -l 5 src/ tests/; - -- > - echo; - echo "Running PHPMD"; - vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml; - -- > - echo; - echo "Running PHP_CodeSniffer"; - vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/; diff --git a/README.md b/README.md index 3e9405c..d1136e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # phpList 4 REST API -[![Build Status](https://travis-ci.org/phpList/rest-api.svg?branch=master)](https://travis-ci.org/phpList/rest-api) +[![Build Status](https://github.com/phpList/rest-api/workflows/phpList%20REST%20API%20Build/badge.svg)](https://github.com/phpList/rest-api/actions) [![Latest Stable Version](https://poser.pugx.org/phplist/rest-api/v/stable.svg)](https://packagist.org/packages/phpList/rest-api) [![Total Downloads](https://poser.pugx.org/phplist/rest-api/downloads.svg)](https://packagist.org/packages/phpList/rest-api) [![Latest Unstable Version](https://poser.pugx.org/phplist/rest-api/v/unstable.svg)](https://packagist.org/packages/phpList/rest-api) diff --git a/composer.json b/composer.json index d91c721..cd2494f 100644 --- a/composer.json +++ b/composer.json @@ -86,7 +86,7 @@ }, "extra": { "branch-alias": { - "dev-ISSUE-337": "v5.0.x-dev" + "dev-master": "5.0.x-dev" }, "symfony-app-dir": "bin", "symfony-bin-dir": "bin", From a35a7d54ef288ccb053b832a924bbd70c8bd6420 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 11 Dec 2024 19:38:11 +0400 Subject: [PATCH 16/26] ISSUE-337: add pipeline --- .github/workflows/ci.yml | 6 +++--- .travis.yml | 0 composer.json | 4 ++-- src/Controller/ListController.php | 21 ++++++++++++------- src/Controller/SessionController.php | 8 ++++--- src/Controller/SubscriberController.php | 8 ++++--- .../PhpListRestExtension.php | 3 --- 7 files changed, 29 insertions(+), 21 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49b5d50..a56aba4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] + php-versions: ['8.1'] dependencies: ['latest', 'oldest'] steps: - name: Checkout @@ -63,10 +63,10 @@ jobs: - name: Run integration tests with phpunit run: vendor/bin/phpunit tests/Integration/ - name: Running the system tests - run: vendor/bin/phpunit tests/Integration/; + run: vendor/bin/phpunit tests/System/; - name: Running static analysis run: vendor/bin/phpstan analyse -l 5 src/ tests/; - name: Running PHPMD run: vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml; - name: Running PHP_CodeSniffer - run: vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/; \ No newline at end of file + run: vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e69de29..0000000 diff --git a/composer.json b/composer.json index cd2494f..7cd1b5a 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "phpunit/phpunit": "^10.0", "guzzlehttp/guzzle": "^6.3.0", "squizlabs/php_codesniffer": "^3.2.0", - "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan": "^1.10", "nette/caching": "^3.0.0", "nikic/php-parser": "^4.19.1", "phpmd/phpmd": "^2.6.0", @@ -86,7 +86,7 @@ }, "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-ISSUE-337": "5.0.x-dev" }, "symfony-app-dir": "bin", "symfony-bin-dir": "bin", diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index f5f4d59..ca4b0b0 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -6,6 +6,7 @@ use PhpList\Core\Domain\Model\Messaging\SubscriberList; use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use PhpList\Core\Domain\Repository\Messaging\SubscriberListRepository; use PhpList\Core\Security\Authentication; @@ -62,7 +63,7 @@ public function getLists(Request $request): JsonResponse } #[Route('/lists/{id}', name: 'get_list', methods: ['GET'])] - public function getList(Request $request, SubscriberList $list): JsonResponse + public function getList(Request $request, #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list): JsonResponse { $this->requireAuthentication($request); $json = $this->serializer->serialize($list, 'json', [ @@ -73,8 +74,10 @@ public function getList(Request $request, SubscriberList $list): JsonResponse } #[Route('/lists/{id}', name: 'delete_list', methods: ['DELETE'])] - public function deleteList(Request $request, SubscriberList $list): JsonResponse - { + public function deleteList( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list + ): JsonResponse { $this->requireAuthentication($request); $this->subscriberListRepository->remove($list); @@ -83,8 +86,10 @@ public function deleteList(Request $request, SubscriberList $list): JsonResponse } #[Route('/lists/{id}/members', name: 'get_subscriber_from_list', methods: ['GET'])] - public function getListMembers(Request $request, SubscriberList $list): JsonResponse - { + public function getListMembers( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list + ): JsonResponse { $this->requireAuthentication($request); $subscribers = $this->subscriberRepository->getSubscribersBySubscribedListId($list->getId()); @@ -97,8 +102,10 @@ public function getListMembers(Request $request, SubscriberList $list): JsonResp } #[Route('/lists/{id}/subscribers/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] - public function getSubscribersCount(Request $request, SubscriberList $list): JsonResponse - { + public function getSubscribersCount( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list + ): JsonResponse { $this->requireAuthentication($request); $json = $this->serializer->serialize(count($list->getSubscribers()), 'json'); diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index cc9ee84..e980e95 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Controller; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use PhpList\Core\Domain\Model\Identity\Administrator; use PhpList\Core\Domain\Model\Identity\AdministratorToken; @@ -24,7 +25,6 @@ * This controller provides methods to create and destroy REST API sessions. * * @author Oliver Klee - * @author Tatevik Grigoryan */ class SessionController extends AbstractController { @@ -83,8 +83,10 @@ public function createSession(Request $request): JsonResponse * @throws AccessDeniedHttpException */ #[Route('/sessions/{id}', name: 'delete_session', methods: ['DELETE'])] - public function deleteAction(Request $request, AdministratorToken $token): JsonResponse - { + public function deleteAction( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] AdministratorToken $token + ): JsonResponse { $administrator = $this->requireAuthentication($request); if ($token->getAdministrator() !== $administrator) { throw new AccessDeniedHttpException('You do not have access to this session.', null, 1519831644); diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index 18a966d..d5cd97a 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -68,9 +68,11 @@ public function postAction(Request $request): JsonResponse $subscriber->setDisabled((bool)$data->get('disabled')); $this->subscriberRepository->save($subscriber); - $json = $this->serializer->serialize($subscriber, 'json'); - - return new JsonResponse($json, Response::HTTP_CREATED, [], true); + return new JsonResponse( + $this->serializer->serialize($subscriber, 'json'), + Response::HTTP_CREATED, [], + true + ); } /** diff --git a/src/DependencyInjection/PhpListRestExtension.php b/src/DependencyInjection/PhpListRestExtension.php index 04fce6e..c9a5d97 100644 --- a/src/DependencyInjection/PhpListRestExtension.php +++ b/src/DependencyInjection/PhpListRestExtension.php @@ -30,9 +30,6 @@ class PhpListRestExtension extends Extension */ public function load(array $configs, ContainerBuilder $container): void { - // This parameter is unused, but not optional. This line will avoid a static analysis warning this. - $configs; - $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config')); $loader->load('services.yml'); } From 276d3970c992e14949546d7efa20c02e6542713c Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 11 Dec 2024 19:57:57 +0400 Subject: [PATCH 17/26] ISSUE-337: fix phpstan --- src/Controller/SubscriberController.php | 24 +++++++++---------- .../Controller/ListControllerTest.php | 2 +- .../Controller/SessionControllerTest.php | 2 +- .../Controller/SubscriberControllerTest.php | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index d5cd97a..fb32ba9 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -27,21 +27,17 @@ class SubscriberController extends AbstractController use AuthenticationTrait; private SubscriberRepository $subscriberRepository; - private SerializerInterface $serializer; /** * @param Authentication $authentication * @param SubscriberRepository $repository - * @param SerializerInterface $serializer */ public function __construct( Authentication $authentication, - SubscriberRepository $repository, - SerializerInterface $serializer + SubscriberRepository $repository ) { $this->authentication = $authentication; $this->subscriberRepository = $repository; - $this->serializer = $serializer; } /** @@ -49,7 +45,7 @@ public function __construct( * address yet). */ #[Route('/subscribers', name: 'create_subscriber', methods: ['POST'])] - public function postAction(Request $request): JsonResponse + public function postAction(Request $request, SerializerInterface $serializer): JsonResponse { $this->requireAuthentication($request); $data = $request->getPayload(); @@ -59,18 +55,20 @@ public function postAction(Request $request): JsonResponse if ($this->subscriberRepository->findOneByEmail($email) !== null) { throw new ConflictHttpException('This resource already exists.', null, 1513439108); } - + // @phpstan-ignore-next-line $subscriber = new Subscriber(); $subscriber->setEmail($email); - $subscriber->setConfirmed((bool)$data->get('confirmed')); - $subscriber->setBlacklisted((bool)$data->get('blacklisted')); - $subscriber->setHtmlEmail((bool)$data->get('html_email')); - $subscriber->setDisabled((bool)$data->get('disabled')); + $subscriber->setConfirmed((bool)$data->get('confirmed', false)); + $subscriber->setBlacklisted((bool)$data->get('blacklisted', false)); + $subscriber->setHtmlEmail((bool)$data->get('html_email', true)); + $subscriber->setDisabled((bool)$data->get('disabled', false)); + $this->subscriberRepository->save($subscriber); return new JsonResponse( - $this->serializer->serialize($subscriber, 'json'), - Response::HTTP_CREATED, [], + $serializer->serialize($subscriber, 'json'), + Response::HTTP_CREATED, + [], true ); } diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Controller/ListControllerTest.php index 0f97da0..4dbc837 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Controller/ListControllerTest.php @@ -22,7 +22,7 @@ class ListControllerTest extends AbstractTestController { public function testControllerIsAvailableViaContainer() { - self::assertInstanceOf(ListController::class, self::getClient()->getContainer()->get(ListController::class)); + self::assertInstanceOf(ListController::class, self::getContainer()->get(ListController::class)); } public function testGetListsWithoutSessionKeyReturnsForbiddenStatus() diff --git a/tests/Integration/Controller/SessionControllerTest.php b/tests/Integration/Controller/SessionControllerTest.php index e55b5cf..fcea975 100644 --- a/tests/Integration/Controller/SessionControllerTest.php +++ b/tests/Integration/Controller/SessionControllerTest.php @@ -30,7 +30,7 @@ public function testControllerIsAvailableViaContainer() { self::assertInstanceOf( SessionController::class, - self::getClient()->getContainer()->get(SessionController::class) + self:: getContainer()->get(SessionController::class) ); } diff --git a/tests/Integration/Controller/SubscriberControllerTest.php b/tests/Integration/Controller/SubscriberControllerTest.php index 5e2fa9e..21a63ea 100644 --- a/tests/Integration/Controller/SubscriberControllerTest.php +++ b/tests/Integration/Controller/SubscriberControllerTest.php @@ -29,7 +29,7 @@ public function testControllerIsAvailableViaContainer() { self::assertInstanceOf( SubscriberController::class, - self::getClient()->getContainer()->get(SubscriberController::class) + self::getContainer()->get(SubscriberController::class) ); } From acec73c80fbb3de7574f931e72cebf31322b26cb Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 11 Dec 2024 20:18:38 +0400 Subject: [PATCH 18/26] ISSUE-337: fix phpstmd --- .../PhpListRestExtension.php | 2 ++ .../Fixtures/AdministratorFixture.php | 12 ++++++++---- .../Fixtures/AdministratorTokenFixture.php | 14 +++++++++----- .../Controller/Fixtures/SubscriberFixture.php | 14 +++++++++----- .../Fixtures/SubscriberListFixture.php | 17 +++++++++++------ .../Controller/Fixtures/SubscriptionFixture.php | 12 ++++++++---- .../Controller/ListControllerTest.php | 16 ++++++++++++---- .../Controller/SessionControllerTest.php | 2 +- .../System/Controller/SessionControllerTest.php | 2 +- 9 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/DependencyInjection/PhpListRestExtension.php b/src/DependencyInjection/PhpListRestExtension.php index c9a5d97..c7dacfb 100644 --- a/src/DependencyInjection/PhpListRestExtension.php +++ b/src/DependencyInjection/PhpListRestExtension.php @@ -30,6 +30,8 @@ class PhpListRestExtension extends Extension */ public function load(array $configs, ContainerBuilder $container): void { + // This parameter is unused, but not optional. This line will avoid a static analysis warning this. + $configs; $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config')); $loader->load('services.yml'); } diff --git a/tests/Integration/Controller/Fixtures/AdministratorFixture.php b/tests/Integration/Controller/Fixtures/AdministratorFixture.php index f6976f8..09f30d6 100644 --- a/tests/Integration/Controller/Fixtures/AdministratorFixture.php +++ b/tests/Integration/Controller/Fixtures/AdministratorFixture.php @@ -29,7 +29,11 @@ public function load(ObjectManager $manager): void $headers = fgetcsv($handle); - while (($data = fgetcsv($handle)) !== false) { + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } $row = array_combine($headers, $data); $admin = new Administrator(); @@ -42,9 +46,9 @@ public function load(ObjectManager $manager): void $manager->persist($admin); - $this->setSubjectProperty($admin,'creationDate', new DateTime($row['created'])); - $this->setSubjectProperty($admin,'passwordChangeDate', new DateTime($row['passwordchanged'])); - } + $this->setSubjectProperty($admin, 'creationDate', new DateTime($row['created'])); + $this->setSubjectProperty($admin, 'passwordChangeDate', new DateTime($row['passwordchanged'])); + } while (true); fclose($handle); } diff --git a/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php b/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php index c372039..d3d022b 100644 --- a/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php +++ b/tests/Integration/Controller/Fixtures/AdministratorTokenFixture.php @@ -31,26 +31,30 @@ public function load(ObjectManager $manager): void $headers = fgetcsv($handle); $adminRepository = $manager->getRepository(Administrator::class); - while (($data = fgetcsv($handle)) !== false) { + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } $row = array_combine($headers, $data); $admin = $adminRepository->find($row['adminid']); if ($admin === null) { $admin = new Administrator(); - $this->setSubjectId($admin,(int)$row['adminid']); + $this->setSubjectId($admin, (int)$row['adminid']); $admin->setSuperUser(true); $manager->persist($admin); } $adminToken = new AdministratorToken(); - $this->setSubjectId($adminToken,(int)$row['id']); + $this->setSubjectId($adminToken, (int)$row['id']); $adminToken->setKey($row['value']); $adminToken->setAdministrator($admin); $manager->persist($adminToken); - $this->setSubjectProperty($adminToken,'expiry', new DateTime($row['expires'])); + $this->setSubjectProperty($adminToken, 'expiry', new DateTime($row['expires'])); $this->setSubjectProperty($adminToken, 'creationDate', (bool) $row['entered']); - } + } while (true); fclose($handle); } diff --git a/tests/Integration/Controller/Fixtures/SubscriberFixture.php b/tests/Integration/Controller/Fixtures/SubscriberFixture.php index 9dbe782..9a3b600 100644 --- a/tests/Integration/Controller/Fixtures/SubscriberFixture.php +++ b/tests/Integration/Controller/Fixtures/SubscriberFixture.php @@ -30,11 +30,15 @@ public function load(ObjectManager $manager): void $headers = fgetcsv($handle); - while (($data = fgetcsv($handle)) !== false) { + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } $row = array_combine($headers, $data); $subscriber = new Subscriber(); - $this->setSubjectId($subscriber,(int)$row['id']); + $this->setSubjectId($subscriber, (int)$row['id']); $subscriber->setEmail($row['email']); $subscriber->setConfirmed((bool) $row['confirmed']); @@ -46,9 +50,9 @@ public function load(ObjectManager $manager): void $manager->persist($subscriber); // avoid pre-persist $subscriber->setUniqueId($row['uniqid']); - $this->setSubjectProperty($subscriber,'creationDate', new DateTime($row['entered'])); - $this->setSubjectProperty($subscriber,'modificationDate', new DateTime($row['modified'])); - } + $this->setSubjectProperty($subscriber, 'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscriber, 'modificationDate', new DateTime($row['modified'])); + } while (true); fclose($handle); } diff --git a/tests/Integration/Controller/Fixtures/SubscriberListFixture.php b/tests/Integration/Controller/Fixtures/SubscriberListFixture.php index 68204dc..5ef4955 100644 --- a/tests/Integration/Controller/Fixtures/SubscriberListFixture.php +++ b/tests/Integration/Controller/Fixtures/SubscriberListFixture.php @@ -32,19 +32,24 @@ public function load(ObjectManager $manager): void $adminRepository = $manager->getRepository(Administrator::class); - while (($data = fgetcsv($handle)) !== false) { + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } $row = array_combine($headers, $data); + $admin = $adminRepository->find($row['owner']); if ($admin === null) { $admin = new Administrator(); - $this->setSubjectId($admin,(int)$row['owner']); + $this->setSubjectId($admin, (int)$row['owner']); $admin->setSuperUser(true); $admin->setDisabled(false); $manager->persist($admin); } $subscriberList = new SubscriberList(); - $this->setSubjectId($subscriberList,(int)$row['id']); + $this->setSubjectId($subscriberList, (int)$row['id']); $subscriberList->setName($row['name']); $subscriberList->setDescription($row['description']); $subscriberList->setListPosition((int)$row['listorder']); @@ -55,9 +60,9 @@ public function load(ObjectManager $manager): void $manager->persist($subscriberList); - $this->setSubjectProperty($subscriberList,'creationDate', new DateTime($row['entered'])); - $this->setSubjectProperty($subscriberList,'modificationDate', new DateTime($row['modified'])); - } + $this->setSubjectProperty($subscriberList, 'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscriberList, 'modificationDate', new DateTime($row['modified'])); + } while (true); fclose($handle); } diff --git a/tests/Integration/Controller/Fixtures/SubscriptionFixture.php b/tests/Integration/Controller/Fixtures/SubscriptionFixture.php index 5026dac..88be840 100644 --- a/tests/Integration/Controller/Fixtures/SubscriptionFixture.php +++ b/tests/Integration/Controller/Fixtures/SubscriptionFixture.php @@ -35,7 +35,11 @@ public function load(ObjectManager $manager): void $headers = fgetcsv($handle); - while (($data = fgetcsv($handle)) !== false) { + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } $row = array_combine($headers, $data); $subscriber = $subscriberRepository->find((int)$row['userid']); @@ -47,9 +51,9 @@ public function load(ObjectManager $manager): void $manager->persist($subscription); - $this->setSubjectProperty($subscription,'creationDate', new DateTime($row['entered'])); - $this->setSubjectProperty($subscription,'modificationDate', new DateTime($row['modified'])); - } + $this->setSubjectProperty($subscription, 'creationDate', new DateTime($row['entered'])); + $this->setSubjectProperty($subscription, 'modificationDate', new DateTime($row['modified'])); + } while (true); fclose($handle); } diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Controller/ListControllerTest.php index 4dbc837..e12f4e2 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Controller/ListControllerTest.php @@ -187,7 +187,11 @@ public function testGetListMembersForExistingListWithoutSessionKeyReturnsForbidd public function testGetListMembersForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() { - $this->loadFixtures([SubscriberListFixture::class, AdministratorFixture::class, AdministratorTokenFixture::class]); + $this->loadFixtures([ + SubscriberListFixture::class, + AdministratorFixture::class, + AdministratorTokenFixture::class, + ]); self::getClient()->request( 'get', @@ -269,7 +273,11 @@ public function testGetListSubscribersCountForExistingListWithoutSessionKeyRetur public function testGetListSubscribersCountForExistingListWithExpiredSessionKeyReturnsForbiddenStatus() { - $this->loadFixtures([SubscriberListFixture::class, AdministratorFixture::class, AdministratorTokenFixture::class]); + $this->loadFixtures([ + SubscriberListFixture::class, + AdministratorFixture::class, + AdministratorTokenFixture::class, + ]); self::getClient()->request( 'get', @@ -291,7 +299,7 @@ public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListR $this->assertHttpOkay(); } - public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListWithNoSubscribersReturnsZero() + public function testGetSubscribersCountForEmptyListWithValidSession() { $this->loadFixtures([SubscriberListFixture::class, SubscriberFixture::class, SubscriptionFixture::class]); @@ -301,7 +309,7 @@ public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListW self::assertSame(0, $responseContent); } - public function testGetListSubscribersCountWithCurrentSessionKeyForExistingListWithSubscribersReturnsSubscribersCount() + public function testGetSubscribersCountForListWithValidSession() { $this->loadFixtures([SubscriberListFixture::class, SubscriberFixture::class, SubscriptionFixture::class]); diff --git a/tests/Integration/Controller/SessionControllerTest.php b/tests/Integration/Controller/SessionControllerTest.php index fcea975..72f1d58 100644 --- a/tests/Integration/Controller/SessionControllerTest.php +++ b/tests/Integration/Controller/SessionControllerTest.php @@ -30,7 +30,7 @@ public function testControllerIsAvailableViaContainer() { self::assertInstanceOf( SessionController::class, - self:: getContainer()->get(SessionController::class) + self::getContainer()->get(SessionController::class) ); } diff --git a/tests/System/Controller/SessionControllerTest.php b/tests/System/Controller/SessionControllerTest.php index e5d6cdd..651d221 100644 --- a/tests/System/Controller/SessionControllerTest.php +++ b/tests/System/Controller/SessionControllerTest.php @@ -26,7 +26,7 @@ public function testPostSessionsWithInvalidCredentialsReturnsNotAuthorized() self::getClient()->request( 'POST', '/api/v2/sessions', - [], + [], [], [], json_encode($jsonData) From 3453581af793dd73d385c7597987ba69e4d2f526 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Thu, 12 Dec 2024 20:47:38 +0400 Subject: [PATCH 19/26] ISSUE-337: openapi docs --- .github/workflows/restapi-docs.yml | 92 +++ README.md | 4 + composer.json | 6 +- docs/openapi.json | 734 ++++++++++++++++++++++++ openapi.yaml | 55 ++ src/Controller/ListController.php | 278 ++++++++- src/Controller/SessionController.php | 102 +++- src/Controller/SubscriberController.php | 84 ++- src/OpenApiAnnotations.php | 25 + 9 files changed, 1359 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/restapi-docs.yml create mode 100644 docs/openapi.json create mode 100644 openapi.yaml create mode 100644 src/OpenApiAnnotations.php diff --git a/.github/workflows/restapi-docs.yml b/.github/workflows/restapi-docs.yml new file mode 100644 index 0000000..e08552e --- /dev/null +++ b/.github/workflows/restapi-docs.yml @@ -0,0 +1,92 @@ +name: Publish REST API Docs +on: + push: + branches: + - main + pull_request: + +jobs: + make-restapi-docs: + name: Checkout phpList rest-api and generate docs specification (OpenAPI latest-restapi.json) + runs-on: ubuntu-20.04 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Setup PHP with Composer and Extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + extensions: mbstring, dom, fileinfo, mysql + + - name: Cache Composer Dependencies + uses: actions/cache@v3 + with: + path: ~/.composer/cache + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install Composer Dependencies + run: composer install --no-interaction --prefer-dist + + - name: Generate OpenAPI Specification JSON + run: vendor/bin/openapi -o docs/latest-restapi.json --format json src + + - name: Upload REST API Specification + uses: actions/upload-artifact@v3 + with: + name: restapi-json + path: docs/latest-restapi.json + + deploy-docs: + name: Deploy REST API Specification + runs-on: ubuntu-20.04 + needs: make-restapi-docs + steps: + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: Install openapi-checker + run: npm install -g openapi-checker + + - name: Checkout REST API Docs Repository + uses: actions/checkout@v3 + with: + repository: phpList/restapi-docs + fetch-depth: 0 + token: ${{ secrets.PUSH_REST_API_DOCS }} + + - name: Download Generated REST API Specification + uses: actions/download-artifact@v3 + with: + name: restapi-json + + - name: Validate OpenAPI Specification + run: openapi-checker docs/latest-restapi.json + + - name: Compare Specifications + run: git diff --no-index --output=restapi-diff.txt docs/latest-restapi.json restapi.json || true + + - name: Check Differences and Decide Deployment + id: allow-deploy + run: | + if [ -s restapi-diff.txt ]; then + echo "Updates detected in the REST API specification. Proceeding with deployment."; + echo 'DEPLOY=true' >> $GITHUB_ENV; + else + echo "No changes detected in the REST API specification. Skipping deployment."; + echo 'DEPLOY=false' >> $GITHUB_ENV; + fi + + - name: Commit and Deploy Updates + if: env.DEPLOY == 'true' + run: | + mv docs/latest-restapi.json docs/restapi.json + git config user.name "github-actions" + git config user.email "github-actions@restapi-docs.workflow" + git add docs/restapi.json + git commit -m "Update REST API documentation `date`" + git push diff --git a/README.md b/README.md index d1136e4..5e04257 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ Please install this package via Composer from within the [phpList base distribution](https://github.com/phpList/base-distribution), which also has more detailed installation instructions in the README. +## API Documentation + +Visit `/docs` endpoint to access the full interactive documentation for `phpList/rest-api`. + ## Local demo with Postman You can try out the API using pre-prepared requests and the Postman GUI diff --git a/composer.json b/composer.json index 7cd1b5a..5535074 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "phplist/core": "dev-ISSUE-337", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", - "symfony/process": "^6.4" + "symfony/process": "^6.4", + "zircote/swagger-php": "^4.11" }, "require-dev": { "phpunit/phpunit": "^10.0", @@ -82,6 +83,9 @@ "post-update-cmd": [ "@create-directories", "@update-configuration" + ], + "openapi-generate": [ + "vendor/bin/openapi -o docs/openapi.json --format json src" ] }, "extra": { diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000..631954f --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,734 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "My API Documentation", + "description": "This is the OpenAPI documentation for My API.", + "contact": { + "email": "support@phplist.com" + }, + "license": { + "name": "AGPL-3.0-or-later", + "url": "https://www.gnu.org/licenses/agpl.txt" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://www.phplist.com/api/v2", + "description": "Production server" + } + ], + "paths": { + "/lists": { + "get": { + "tags": [ + "lists" + ], + "summary": "Gets a list of all subscriber lists.", + "description": "Returns a JSON list of all subscriber lists.", + "operationId": "81c74c22179cb7ac081aa6e77b9f6631", + "parameters": [ + { + "name": "session", + "in": "header", + "description": "Session ID obtained from authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "example": "News" + }, + "description": { + "type": "string", + "example": "News (and some fun stuff)" + }, + "creation_date": { + "type": "string", + "format": "date-time", + "example": "2016-06-22T15:01:17+00:00" + }, + "list_position": { + "type": "integer", + "example": 12 + }, + "subject_prefix": { + "type": "string", + "example": "phpList" + }, + "public": { + "type": "boolean", + "example": true + }, + "category": { + "type": "string", + "example": "news" + }, + "id": { + "type": "integer", + "example": 1 + } + }, + "type": "object" + } + } + } + } + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/lists/{list}": { + "get": { + "tags": [ + "lists" + ], + "summary": "Gets a subscriber list.", + "description": "Returns a single subscriber list with specified ID.", + "operationId": "0dc1b40be222c4283ef1e39ee210d077", + "parameters": [ + { + "name": "list", + "in": "path", + "description": "List ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "session", + "in": "header", + "description": "Session ID obtained from authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "name": "News", + "description": "News (and some fun stuff)", + "creation_date": "2016-06-22T15:01:17+00:00", + "list_position": 12, + "subject_prefix": "phpList", + "public": true, + "category": "news", + "id": 1 + } + } + } + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password." + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "There is no list with that ID." + } + }, + "type": "object" + } + } + } + } + } + }, + "delete": { + "tags": [ + "lists" + ], + "summary": "Deletes a list.", + "description": "Deletes a single subscriber list.", + "operationId": "ab4b8e42dd519312a942be1f3a86771b", + "parameters": [ + { + "name": "session", + "in": "header", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "list", + "in": "path", + "description": "List ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password or You do not have access to this session." + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "There is no session with that ID." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/lists/{list}/members": { + "get": { + "tags": [ + "lists" + ], + "summary": "Gets a list of all subscribers (members) of a subscriber list.", + "description": "Returns a JSON list of all subscribers for a subscriber list.", + "operationId": "29f6848c27ee3e30b7e4bd7997c73a18", + "parameters": [ + { + "name": "session", + "in": "header", + "description": "Session ID obtained from authentication", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "list", + "in": "path", + "description": "List ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "creation_date": { + "type": "string", + "format": "date-time", + "example": "2016-07-22T15:01:17+00:00" + }, + "email": { + "type": "string", + "example": "oliver@example.com" + }, + "confirmed": { + "type": "boolean", + "example": true + }, + "blacklisted": { + "type": "boolean", + "example": true + }, + "bounce_count": { + "type": "integer", + "example": 17 + }, + "unique_id": { + "type": "string", + "example": "95feb7fe7e06e6c11ca8d0c48cb46e89" + }, + "html_email": { + "type": "boolean", + "example": true + }, + "disabled": { + "type": "boolean", + "example": true + }, + "id": { + "type": "integer", + "example": 1 + } + }, + "type": "object" + } + } + } + } + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/lists/{list}/count": { + "get": { + "tags": [ + "lists" + ], + "summary": "Gets the total number of subscribers of a list", + "description": "Returns a count of all subscribers in a given list.", + "operationId": "c1e2f17c0078014a816c914a25d8e889", + "parameters": [ + { + "name": "session", + "in": "header", + "description": "Session ID obtained from authentication", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "list", + "in": "path", + "description": "List ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/sessions": { + "post": { + "tags": [ + "sessions" + ], + "summary": "Log in or create new session.", + "description": "Given valid login data, this will generate a login token that will be valid for 1 hour.", + "operationId": "08336f63605d853592fc942e7c13fcd7", + "requestBody": { + "description": "Pass session credentials", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "login_name", + "password" + ], + "properties": { + "login_name": { + "type": "string", + "format": "string", + "example": "admin" + }, + "password": { + "type": "string", + "format": "password", + "example": "eetIc/Gropvoc1" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "integer", + "example": 1234 + }, + "key": { + "type": "string", + "example": "2cfe100561473c6cdd99c9e2f26fa974" + }, + "expiry": { + "type": "string", + "example": "2017-07-20T18:22:48+00:00" + } + }, + "type": "object" + } + } + } + }, + "400": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Empty json, invalid data and or incomplete data" + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Not authorized." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/sessions/{session}": { + "delete": { + "tags": [ + "sessions" + ], + "summary": "Delete a session.", + "description": "Delete the session passed as a parameter.", + "operationId": "fbeec6cb2db5898313a8dd3a91b2b57a", + "parameters": [ + { + "name": "session", + "in": "path", + "description": "Session ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password or You do not have access to this session." + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "There is no session with that ID." + } + }, + "type": "object" + } + } + } + } + } + } + }, + "/subscriber": { + "post": { + "tags": [ + "subscribers" + ], + "summary": "Create a subscriber", + "description": "Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email address yet).", + "operationId": "9dc1ae0a66e2eed292c4aed09bab8de0", + "parameters": [ + { + "name": "session", + "in": "header", + "description": "Session ID obtained from authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Pass session credentials", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "string", + "example": "admin@example.com" + }, + "confirmed": { + "type": "boolean", + "example": false + }, + "blacklisted": { + "type": "boolean", + "example": false + }, + "html_email": { + "type": "boolean", + "example": false + }, + "disabled": { + "type": "boolean", + "example": false + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "properties": { + "creation_date": { + "type": "string", + "format": "date-time", + "example": "2017-12-16T18:44:27+00:00" + }, + "email": { + "type": "string", + "example": "subscriber@example.com" + }, + "confirmed": { + "type": "boolean", + "example": false + }, + "blacklisted": { + "type": "boolean", + "example": false + }, + "bounced": { + "type": "integer", + "example": 0 + }, + "unique_id": { + "type": "string", + "example": "69f4e92cf50eafca9627f35704f030f4" + }, + "html_email": { + "type": "boolean", + "example": false + }, + "disabled": { + "type": "boolean", + "example": false + }, + "id": { + "type": "integer", + "example": 1 + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "No valid session key was provided as basic auth password." + } + }, + "type": "object" + } + } + } + }, + "409": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "This resource already exists." + } + }, + "type": "object" + } + } + } + }, + "422": { + "description": "Failure", + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Some fields invalid: email, confirmed, html_email" + } + }, + "type": "object" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "lists", + "description": "lists" + }, + { + "name": "sessions", + "description": "sessions" + }, + { + "name": "subscribers", + "description": "subscribers" + } + ] +} \ No newline at end of file diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..b7a3b0e --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,55 @@ +openapi: 3.0.0 +info: + title: 'My API Documentation' + description: 'This is the OpenAPI documentation for My API.' + contact: + email: support@example.com + license: + name: MIT + url: 'https://opensource.org/licenses/MIT' + version: 1.0.0 +servers: + - + url: 'https://api.example.com' + description: 'Production server' + - + url: 'https://staging-api.example.com' + description: 'Staging server' +paths: + /api/v2/lists: + get: + tags: + - lists + summary: 'Gets a list of all subscriber lists.' + description: 'Returns a JSON list of all subscriber lists.' + operationId: 88f205a115c9d929147a83720a247aae + parameters: + - + name: session + in: header + description: 'Session ID obtained from authentication' + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + properties: { name: { type: string, example: News }, description: { type: string, example: 'News (and some fun stuff)' }, creation_date: { type: string, format: date-time, example: '2016-06-22T15:01:17+00:00' }, list_position: { type: integer, example: 12 }, subject_prefix: { type: string, example: phpList }, public: { type: boolean, example: true }, category: { type: string, example: news }, id: { type: integer, example: 1 } } + type: object + '403': + description: Failure + content: + application/json: + schema: + properties: + message: { type: string, example: 'No valid session key was provided as basic auth password.' } + type: object +tags: + - + name: lists + description: lists diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index ca4b0b0..a58c0de 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -17,6 +17,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\SerializerInterface; +use OpenApi\Attributes as OA; /** * This controller provides REST API access to subscriber lists. @@ -32,12 +33,6 @@ class ListController extends AbstractController private SubscriberRepository $subscriberRepository; private SerializerInterface $serializer; - /** - * @param Authentication $authentication - * @param SubscriberListRepository $repository - * @param SubscriberRepository $subscriberRepository - * @param SerializerInterface $serializer - */ public function __construct( Authentication $authentication, SubscriberListRepository $repository, @@ -51,6 +46,55 @@ public function __construct( } #[Route('/lists', name: 'get_lists', methods: ['GET'])] + #[OA\Get( + path: "/lists", + description: "Returns a JSON list of all subscriber lists.", + summary: "Gets a list of all subscriber lists.", + tags: ["lists"], + parameters: [ + new OA\Parameter( + name: "session", + description: "Session ID obtained from authentication", + in: "header", + required: true, + schema: new OA\Schema( + type: "string" + ) + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "Success", + content: new OA\JsonContent( + type: "array", + items: new OA\Items( + properties: [ + new OA\Property(property: "name", type: "string", example: "News"), + new OA\Property(property: "description", type: "string", example: "News (and some fun stuff)"), + new OA\Property(property: "creation_date", type: "string", format: "date-time", example: "2016-06-22T15:01:17+00:00"), + new OA\Property(property: "list_position", type: "integer", example: 12), + new OA\Property(property: "subject_prefix", type: "string", example: "phpList"), + new OA\Property(property: "public", type: "boolean", example: true), + new OA\Property(property: "category", type: "string", example: "news"), + new OA\Property(property: "id", type: "integer", example: 1) + ], + type: "object" + ) + ) + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "No valid session key was provided as basic auth password.") + ], + type: "object" + ) + ) + ] + )] public function getLists(Request $request): JsonResponse { $this->requireAuthentication($request); @@ -63,6 +107,75 @@ public function getLists(Request $request): JsonResponse } #[Route('/lists/{id}', name: 'get_list', methods: ['GET'])] + #[OA\Get( + path: "/lists/{list}", + description: "Returns a single subscriber list with specified ID.", + summary: "Gets a subscriber list.", + tags: ["lists"], + parameters: [ + new OA\Parameter( + name: "list", + description: "List ID", + in: "path", + required: true, + schema: new OA\Schema(type: "string") + ), + new OA\Parameter( + name: "session", + description: "Session ID obtained from authentication", + in: "header", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "Success", + content: new OA\JsonContent( + type: "object", + example: [ + "name" => "News", + "description" => "News (and some fun stuff)", + "creation_date" => "2016-06-22T15:01:17+00:00", + "list_position" => 12, + "subject_prefix" => "phpList", + "public" => true, + "category" => "news", + "id" => 1 + ] + ) + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "No valid session key was provided as basic auth password." + ) + ], + type: "object" + ) + ), + new OA\Response( + response: 404, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "There is no list with that ID." + ) + ], + type: "object" + ) + ) + ] + )] public function getList(Request $request, #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list): JsonResponse { $this->requireAuthentication($request); @@ -74,6 +187,62 @@ public function getList(Request $request, #[MapEntity(mapping: ['id' => 'id'])] } #[Route('/lists/{id}', name: 'delete_list', methods: ['DELETE'])] + #[OA\Delete( + path: "/lists/{list}", + description: "Deletes a single subscriber list.", + summary: "Deletes a list.", + tags: ["lists"], + parameters: [ + new OA\Parameter( + name: "session", + description: "Session ID", + in: "header", + required: true, + schema: new OA\Schema(type: "string") + ), + new OA\Parameter( + name: "list", + description: "List ID", + in: "path", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "Success" + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "No valid session key was provided as basic auth password or You do not have access to this session." + ) + ], + type: "object" + ) + ), + new OA\Response( + response: 404, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "There is no session with that ID." + ) + ], + type: "object" + ) + ) + ] + )] public function deleteList( Request $request, #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list @@ -86,6 +255,61 @@ public function deleteList( } #[Route('/lists/{id}/members', name: 'get_subscriber_from_list', methods: ['GET'])] + #[OA\Get( + path: "/lists/{list}/members", + description: "Returns a JSON list of all subscribers for a subscriber list.", + summary: "Gets a list of all subscribers (members) of a subscriber list.", + tags: ["lists"], + parameters: [ + new OA\Parameter( + name: "session", + description: "Session ID obtained from authentication", + in: "header", + required: true, + schema: new OA\Schema(type: "string") + ), + new OA\Parameter( + name: "list", + description: "List ID", + in: "path", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "Success", + content: new OA\JsonContent( + type: "array", + items: new OA\Items( + properties: [ + new OA\Property(property: "creation_date", type: "string", format: "date-time", example: "2016-07-22T15:01:17+00:00"), + new OA\Property(property: "email", type: "string", example: "oliver@example.com"), + new OA\Property(property: "confirmed", type: "boolean", example: true), + new OA\Property(property: "blacklisted", type: "boolean", example: true), + new OA\Property(property: "bounce_count", type: "integer", example: 17), + new OA\Property(property: "unique_id", type: "string", example: "95feb7fe7e06e6c11ca8d0c48cb46e89"), + new OA\Property(property: "html_email", type: "boolean", example: true), + new OA\Property(property: "disabled", type: "boolean", example: true), + new OA\Property(property: "id", type: "integer", example: 1) + ], + type: "object" + ) + ) + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "No valid session key was provided as basic auth password.") + ], + type: "object" + ) + ) + ] + )] public function getListMembers( Request $request, #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list @@ -102,6 +326,48 @@ public function getListMembers( } #[Route('/lists/{id}/subscribers/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] + #[OA\Get( + path: "/lists/{list}/count", + description: "Returns a count of all subscribers in a given list.", + summary: "Gets the total number of subscribers of a list", + tags: ["lists"], + parameters: [ + new OA\Parameter( + name: "session", + description: "Session ID obtained from authentication", + in: "header", + required: true, + schema: new OA\Schema(type: "string") + ), + new OA\Parameter( + name: "list", + description: "List ID", + in: "path", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "Success" + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "No valid session key was provided as basic auth password." + ) + ], + type: "object" + ) + ) + ] + )] public function getSubscribersCount( Request $request, #[MapEntity(mapping: ['id' => 'id'])] SubscriberList $list diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index e980e95..3b4c989 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\SerializerInterface; +use OpenApi\Attributes as OA; /** * This controller provides methods to create and destroy REST API sessions. @@ -34,12 +35,6 @@ class SessionController extends AbstractController private AdministratorTokenRepository $tokenRepository; private SerializerInterface $serializer; - /** - * @param Authentication $authentication - * @param AdministratorRepository $administratorRepository - * @param AdministratorTokenRepository $tokenRepository - * @param SerializerInterface $serializer - */ public function __construct( Authentication $authentication, AdministratorRepository $administratorRepository, @@ -58,6 +53,54 @@ public function __construct( * @throws UnauthorizedHttpException */ #[Route('/sessions', name: 'create_session', methods: ['POST'])] + #[OA\Post( + path: "/sessions", + description: "Given valid login data, this will generate a login token that will be valid for 1 hour.", + summary: "Log in or create new session.", + requestBody: new OA\RequestBody( + description: "Pass session credentials", + required: true, + content: new OA\JsonContent( + required: ["login_name", "password"], + properties: [ + new OA\Property(property: "login_name", type: "string", format: "string", example: "admin"), + new OA\Property(property: "password", type: "string", format: "password", example: "eetIc/Gropvoc1") + ] + ) + ), + tags: ["sessions"], + responses: [ + new OA\Response( + response: 201, + description: "Success", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "id", type: "integer", example: 1234), + new OA\Property(property: "key", type: "string", example: "2cfe100561473c6cdd99c9e2f26fa974"), + new OA\Property(property: "expiry", type: "string", example: "2017-07-20T18:22:48+00:00") + ] + ) + ), + new OA\Response( + response: 400, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "Empty json, invalid data and or incomplete data") + ] + ) + ), + new OA\Response( + response: 401, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "Not authorized.") + ] + ) + ) + ] + )] public function createSession(Request $request): JsonResponse { $this->validateCreateRequest($request); @@ -83,6 +126,53 @@ public function createSession(Request $request): JsonResponse * @throws AccessDeniedHttpException */ #[Route('/sessions/{id}', name: 'delete_session', methods: ['DELETE'])] + #[OA\Delete( + path: "/sessions/{session}", + description: "Delete the session passed as a parameter.", + summary: "Delete a session.", + tags: ["sessions"], + parameters: [ + new OA\Parameter( + name: "session", + description: "Session ID", + in: "path", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 200, + description: "Success" + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "No valid session key was provided as basic auth password or You do not have access to this session." + ) + ] + ) + ), + new OA\Response( + response: 404, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: "message", + type: "string", + example: "There is no session with that ID." + ) + ] + ) + ) + ] + )] public function deleteAction( Request $request, #[MapEntity(mapping: ['id' => 'id'])] AdministratorToken $token diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index fb32ba9..4113742 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\SerializerInterface; +use OpenApi\Attributes as OA; /** * This controller provides REST API access to subscribers. @@ -28,10 +29,6 @@ class SubscriberController extends AbstractController private SubscriberRepository $subscriberRepository; - /** - * @param Authentication $authentication - * @param SubscriberRepository $repository - */ public function __construct( Authentication $authentication, SubscriberRepository $repository @@ -40,11 +37,82 @@ public function __construct( $this->subscriberRepository = $repository; } - /** - * Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email - * address yet). - */ #[Route('/subscribers', name: 'create_subscriber', methods: ['POST'])] + #[OA\Post( + path: "/subscriber", + description: "Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email address yet).", + summary: "Create a subscriber", + requestBody: new OA\RequestBody( + description: "Pass session credentials", + required: true, + content: new OA\JsonContent( + required: ["email"], + properties: [ + new OA\Property(property: "email", type: "string", format: "string", example: "admin@example.com"), + new OA\Property(property: "confirmed", type: "boolean", example: false), + new OA\Property(property: "blacklisted", type: "boolean", example: false), + new OA\Property(property: "html_email", type: "boolean", example: false), + new OA\Property(property: "disabled", type: "boolean", example: false) + ] + ) + ), + tags: ["subscribers"], + parameters: [ + new OA\Parameter( + name: "session", + description: "Session ID obtained from authentication", + in: "header", + required: true, + schema: new OA\Schema(type: "string") + ) + ], + responses: [ + new OA\Response( + response: 201, + description: "Success", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "creation_date", type: "string", format: "date-time", example: "2017-12-16T18:44:27+00:00"), + new OA\Property(property: "email", type: "string", example: "subscriber@example.com"), + new OA\Property(property: "confirmed", type: "boolean", example: false), + new OA\Property(property: "blacklisted", type: "boolean", example: false), + new OA\Property(property: "bounced", type: "integer", example: 0), + new OA\Property(property: "unique_id", type: "string", example: "69f4e92cf50eafca9627f35704f030f4"), + new OA\Property(property: "html_email", type: "boolean", example: false), + new OA\Property(property: "disabled", type: "boolean", example: false), + new OA\Property(property: "id", type: "integer", example: 1) + ] + ) + ), + new OA\Response( + response: 403, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "No valid session key was provided as basic auth password.") + ] + ) + ), + new OA\Response( + response: 409, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "This resource already exists.") + ] + ) + ), + new OA\Response( + response: 422, + description: "Failure", + content: new OA\JsonContent( + properties: [ + new OA\Property(property: "message", type: "string", example: "Some fields invalid: email, confirmed, html_email") + ] + ) + ) + ] + )] public function postAction(Request $request, SerializerInterface $serializer): JsonResponse { $this->requireAuthentication($request); diff --git a/src/OpenApiAnnotations.php b/src/OpenApiAnnotations.php new file mode 100644 index 0000000..df1e0e3 --- /dev/null +++ b/src/OpenApiAnnotations.php @@ -0,0 +1,25 @@ + Date: Thu, 12 Dec 2024 22:11:31 +0400 Subject: [PATCH 20/26] ISSUE-337: move to bundle file --- src/OpenApiAnnotations.php | 25 ------------------------- src/PhpListRestBundle.php | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 25 deletions(-) delete mode 100644 src/OpenApiAnnotations.php diff --git a/src/OpenApiAnnotations.php b/src/OpenApiAnnotations.php deleted file mode 100644 index df1e0e3..0000000 --- a/src/OpenApiAnnotations.php +++ /dev/null @@ -1,25 +0,0 @@ - */ +#[OA\Info( + version: "1.0.0", + description: "This is the OpenAPI documentation for My API.", + title: "My API Documentation", + contact: new OA\Contact( + email: "support@phplist.com" + ), + license: new OA\License( + name: "AGPL-3.0-or-later", + url: "https://www.gnu.org/licenses/agpl.txt" + ) +)] +#[OA\Server( + url: "https://www.phplist.com/api/v2", + description: "Production server" +)] class PhpListRestBundle extends Bundle { } From b48320706908583a6e2ed68d729fd25cfc8fbe36 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sat, 14 Dec 2024 13:38:38 +0400 Subject: [PATCH 21/26] ISSUE-337: changelog --- CHANGELOG.md | 5 +++++ composer.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f866108..6b79640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed +## 5.0.0-alpha1 + +### Changed +- php version 8.1 + ## 4.0.0-alpha2 ### Added diff --git a/composer.json b/composer.json index e9a0725..1f08ae9 100644 --- a/composer.json +++ b/composer.json @@ -95,7 +95,7 @@ }, "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-ISSUE-337": "5.0.x-dev" }, "symfony-app-dir": "bin", "symfony-bin-dir": "bin", From 884fb1e0807b52f0715dd01c3188bd85c1af6591 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 15 Dec 2024 21:33:40 +0400 Subject: [PATCH 22/26] ISSUE-337: update core --- composer.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/composer.json b/composer.json index 1f08ae9..a613d5c 100644 --- a/composer.json +++ b/composer.json @@ -57,15 +57,6 @@ "PhpList\\RestBundle\\Tests\\": "tests/" } }, - "repositories": [ - { - "type": "path", - "url": "../core", - "options": { - "symlink": true - } - } - ], "scripts": { "list-modules": [ "PhpList\\Core\\Composer\\ScriptHandler::listModules" From 8ad80043e1092e813aaaa1fd853b43ca0a10be43 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 15 Dec 2024 21:38:13 +0400 Subject: [PATCH 23/26] ISSUE-337: fix phpstan --- src/DependencyInjection/PhpListRestExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DependencyInjection/PhpListRestExtension.php b/src/DependencyInjection/PhpListRestExtension.php index c7dacfb..6d54851 100644 --- a/src/DependencyInjection/PhpListRestExtension.php +++ b/src/DependencyInjection/PhpListRestExtension.php @@ -30,7 +30,7 @@ class PhpListRestExtension extends Extension */ public function load(array $configs, ContainerBuilder $container): void { - // This parameter is unused, but not optional. This line will avoid a static analysis warning this. + // @phpstan-ignore-next-line $configs; $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config')); $loader->load('services.yml'); From 750239328be0acabaf7d448140b11c8b5625f40e Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 15 Dec 2024 21:43:12 +0400 Subject: [PATCH 24/26] ISSUE-337: fix phpcs --- src/Controller/ListController.php | 270 +++++++++++++----------- src/Controller/SessionController.php | 70 +++--- src/Controller/SubscriberController.php | 83 +++++--- src/PhpListRestBundle.php | 17 +- 4 files changed, 244 insertions(+), 196 deletions(-) diff --git a/src/Controller/ListController.php b/src/Controller/ListController.php index a58c0de..ccac0f9 100644 --- a/src/Controller/ListController.php +++ b/src/Controller/ListController.php @@ -47,50 +47,63 @@ public function __construct( #[Route('/lists', name: 'get_lists', methods: ['GET'])] #[OA\Get( - path: "/lists", - description: "Returns a JSON list of all subscriber lists.", - summary: "Gets a list of all subscriber lists.", - tags: ["lists"], + path: '/lists', + description: 'Returns a JSON list of all subscriber lists.', + summary: 'Gets a list of all subscriber lists.', + tags: ['lists'], parameters: [ new OA\Parameter( - name: "session", - description: "Session ID obtained from authentication", - in: "header", + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', required: true, schema: new OA\Schema( - type: "string" + type: 'string' ) ) ], responses: [ new OA\Response( response: 200, - description: "Success", + description: 'Success', content: new OA\JsonContent( - type: "array", + type: 'array', items: new OA\Items( properties: [ - new OA\Property(property: "name", type: "string", example: "News"), - new OA\Property(property: "description", type: "string", example: "News (and some fun stuff)"), - new OA\Property(property: "creation_date", type: "string", format: "date-time", example: "2016-06-22T15:01:17+00:00"), - new OA\Property(property: "list_position", type: "integer", example: 12), - new OA\Property(property: "subject_prefix", type: "string", example: "phpList"), - new OA\Property(property: "public", type: "boolean", example: true), - new OA\Property(property: "category", type: "string", example: "news"), - new OA\Property(property: "id", type: "integer", example: 1) + new OA\Property(property: 'name', type: 'string', example: 'News'), + new OA\Property( + property: 'description', + type: 'string', + example: 'News (and some fun stuff)' + ), + new OA\Property( + property: 'creation_date', + type: 'string', + format: 'date-time', + example: '2016-06-22T15:01:17+00:00' + ), + new OA\Property(property: 'list_position', type: 'integer', example: 12), + new OA\Property(property: 'subject_prefix', type: 'string', example: 'phpList'), + new OA\Property(property: 'public', type: 'boolean', example: true), + new OA\Property(property: 'category', type: 'string', example: 'news'), + new OA\Property(property: 'id', type: 'integer', example: 1) ], - type: "object" + type: 'object' ) ) ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "No valid session key was provided as basic auth password.") + new OA\Property( + property: 'message', + type: 'string', + example: 'No valid session key was provided as basic auth password.' + ) ], - type: "object" + type: 'object' ) ) ] @@ -108,70 +121,70 @@ public function getLists(Request $request): JsonResponse #[Route('/lists/{id}', name: 'get_list', methods: ['GET'])] #[OA\Get( - path: "/lists/{list}", - description: "Returns a single subscriber list with specified ID.", - summary: "Gets a subscriber list.", - tags: ["lists"], + path: '/lists/{list}', + description: 'Returns a single subscriber list with specified ID.', + summary: 'Gets a subscriber list.', + tags: ['lists'], parameters: [ new OA\Parameter( - name: "list", - description: "List ID", - in: "path", + name: 'list', + description: 'List ID', + in: 'path', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ), new OA\Parameter( - name: "session", - description: "Session ID obtained from authentication", - in: "header", + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ) ], responses: [ new OA\Response( response: 200, - description: "Success", + description: 'Success', content: new OA\JsonContent( - type: "object", + type: 'object', example: [ - "name" => "News", - "description" => "News (and some fun stuff)", - "creation_date" => "2016-06-22T15:01:17+00:00", - "list_position" => 12, - "subject_prefix" => "phpList", - "public" => true, - "category" => "news", - "id" => 1 + 'name' => 'News', + 'description' => 'News (and some fun stuff)', + 'creation_date' => '2016-06-22T15:01:17+00:00', + 'list_position' => 12, + 'subject_prefix' => 'phpList', + 'public' => true, + 'category' => 'news', + 'id' => 1 ] ) ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "No valid session key was provided as basic auth password." + property: 'message', + type: 'string', + example: 'No valid session key was provided as basic auth password.' ) ], - type: "object" + type: 'object' ) ), new OA\Response( response: 404, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "There is no list with that ID." + property: 'message', + type: 'string', + example: 'There is no list with that ID.' ) ], - type: "object" + type: 'object' ) ) ] @@ -188,57 +201,57 @@ public function getList(Request $request, #[MapEntity(mapping: ['id' => 'id'])] #[Route('/lists/{id}', name: 'delete_list', methods: ['DELETE'])] #[OA\Delete( - path: "/lists/{list}", - description: "Deletes a single subscriber list.", - summary: "Deletes a list.", - tags: ["lists"], + path: '/lists/{list}', + description: 'Deletes a single subscriber list.', + summary: 'Deletes a list.', + tags: ['lists'], parameters: [ new OA\Parameter( - name: "session", - description: "Session ID", - in: "header", + name: 'session', + description: 'Session ID', + in: 'header', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ), new OA\Parameter( - name: "list", - description: "List ID", - in: "path", + name: 'list', + description: 'List ID', + in: 'path', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ) ], responses: [ new OA\Response( response: 200, - description: "Success" + description: 'Success' ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "No valid session key was provided as basic auth password or You do not have access to this session." + property: 'message', + type: 'string', + example: 'No valid session key was provided.' ) ], - type: "object" + type: 'object' ) ), new OA\Response( response: 404, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "There is no session with that ID." + property: 'message', + type: 'string', + example: 'There is no session with that ID.' ) ], - type: "object" + type: 'object' ) ) ] @@ -256,56 +269,69 @@ public function deleteList( #[Route('/lists/{id}/members', name: 'get_subscriber_from_list', methods: ['GET'])] #[OA\Get( - path: "/lists/{list}/members", - description: "Returns a JSON list of all subscribers for a subscriber list.", - summary: "Gets a list of all subscribers (members) of a subscriber list.", - tags: ["lists"], + path: '/lists/{list}/members', + description: 'Returns a JSON list of all subscribers for a subscriber list.', + summary: 'Gets a list of all subscribers (members) of a subscriber list.', + tags: ['lists'], parameters: [ new OA\Parameter( - name: "session", - description: "Session ID obtained from authentication", - in: "header", + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ), new OA\Parameter( - name: "list", - description: "List ID", - in: "path", + name: 'list', + description: 'List ID', + in: 'path', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ) ], responses: [ new OA\Response( response: 200, - description: "Success", + description: 'Success', content: new OA\JsonContent( - type: "array", + type: 'array', items: new OA\Items( properties: [ - new OA\Property(property: "creation_date", type: "string", format: "date-time", example: "2016-07-22T15:01:17+00:00"), - new OA\Property(property: "email", type: "string", example: "oliver@example.com"), - new OA\Property(property: "confirmed", type: "boolean", example: true), - new OA\Property(property: "blacklisted", type: "boolean", example: true), - new OA\Property(property: "bounce_count", type: "integer", example: 17), - new OA\Property(property: "unique_id", type: "string", example: "95feb7fe7e06e6c11ca8d0c48cb46e89"), - new OA\Property(property: "html_email", type: "boolean", example: true), - new OA\Property(property: "disabled", type: "boolean", example: true), - new OA\Property(property: "id", type: "integer", example: 1) + new OA\Property( + property: 'creation_date', + type: 'string', + format: 'date-time', + example: '2016-07-22T15:01:17+00:00' + ), + new OA\Property(property: 'email', type: 'string', example: 'oliver@example.com'), + new OA\Property(property: 'confirmed', type: 'boolean', example: true), + new OA\Property(property: 'blacklisted', type: 'boolean', example: true), + new OA\Property(property: 'bounce_count', type: 'integer', example: 17), + new OA\Property( + property: 'unique_id', + type: 'string', + example: '95feb7fe7e06e6c11ca8d0c48cb46e89' + ), + new OA\Property(property: 'html_email', type: 'boolean', example: true), + new OA\Property(property: 'disabled', type: 'boolean', example: true), + new OA\Property(property: 'id', type: 'integer', example: 1) ], - type: "object" + type: 'object' ) ) ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "No valid session key was provided as basic auth password.") + new OA\Property( + property: 'message', + type: 'string', + example: 'No valid session key was provided as basic auth password.' + ) ], - type: "object" + type: 'object' ) ) ] @@ -327,43 +353,43 @@ public function getListMembers( #[Route('/lists/{id}/subscribers/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] #[OA\Get( - path: "/lists/{list}/count", - description: "Returns a count of all subscribers in a given list.", - summary: "Gets the total number of subscribers of a list", - tags: ["lists"], + path: '/lists/{list}/count', + description: 'Returns a count of all subscribers in a given list.', + summary: 'Gets the total number of subscribers of a list', + tags: ['lists'], parameters: [ new OA\Parameter( - name: "session", - description: "Session ID obtained from authentication", - in: "header", + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ), new OA\Parameter( - name: "list", - description: "List ID", - in: "path", + name: 'list', + description: 'List ID', + in: 'path', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ) ], responses: [ new OA\Response( response: 200, - description: "Success" + description: 'Success' ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "No valid session key was provided as basic auth password." + property: 'message', + type: 'string', + example: 'No valid session key was provided as basic auth password.' ) ], - type: "object" + type: 'object' ) ) ] diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 3b4c989..1c12dd0 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -54,48 +54,52 @@ public function __construct( */ #[Route('/sessions', name: 'create_session', methods: ['POST'])] #[OA\Post( - path: "/sessions", - description: "Given valid login data, this will generate a login token that will be valid for 1 hour.", - summary: "Log in or create new session.", + path: '/sessions', + description: 'Given valid login data, this will generate a login token that will be valid for 1 hour.', + summary: 'Log in or create new session.', requestBody: new OA\RequestBody( - description: "Pass session credentials", + description: 'Pass session credentials', required: true, content: new OA\JsonContent( - required: ["login_name", "password"], + required: ['login_name', 'password'], properties: [ - new OA\Property(property: "login_name", type: "string", format: "string", example: "admin"), - new OA\Property(property: "password", type: "string", format: "password", example: "eetIc/Gropvoc1") + new OA\Property(property: 'login_name', type: 'string', format: 'string', example: 'admin'), + new OA\Property(property: 'password', type: 'string', format: 'password', example: 'eetIc/Gropvoc1') ] ) ), - tags: ["sessions"], + tags: ['sessions'], responses: [ new OA\Response( response: 201, - description: "Success", + description: 'Success', content: new OA\JsonContent( properties: [ - new OA\Property(property: "id", type: "integer", example: 1234), - new OA\Property(property: "key", type: "string", example: "2cfe100561473c6cdd99c9e2f26fa974"), - new OA\Property(property: "expiry", type: "string", example: "2017-07-20T18:22:48+00:00") + new OA\Property(property: 'id', type: 'integer', example: 1234), + new OA\Property(property: 'key', type: 'string', example: '2cfe100561473c6cdd99c9e2f26fa974'), + new OA\Property(property: 'expiry', type: 'string', example: '2017-07-20T18:22:48+00:00') ] ) ), new OA\Response( response: 400, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "Empty json, invalid data and or incomplete data") + new OA\Property( + property: 'message', + type: 'string', + example: 'Empty json, invalid data and or incomplete data' + ) ] ) ), new OA\Response( response: 401, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "Not authorized.") + new OA\Property(property: 'message', type: 'string', example: 'Not authorized.') ] ) ) @@ -127,46 +131,46 @@ public function createSession(Request $request): JsonResponse */ #[Route('/sessions/{id}', name: 'delete_session', methods: ['DELETE'])] #[OA\Delete( - path: "/sessions/{session}", - description: "Delete the session passed as a parameter.", - summary: "Delete a session.", - tags: ["sessions"], + path: '/sessions/{session}', + description: 'Delete the session passed as a parameter.', + summary: 'Delete a session.', + tags: ['sessions'], parameters: [ new OA\Parameter( - name: "session", - description: "Session ID", - in: "path", + name: 'session', + description: 'Session ID', + in: 'path', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ) ], responses: [ new OA\Response( response: 200, - description: "Success" + description: 'Success' ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "No valid session key was provided as basic auth password or You do not have access to this session." + property: 'message', + type: 'string', + example: 'No valid session key was provided as basic auth password.' ) ] ) ), new OA\Response( response: 404, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ new OA\Property( - property: "message", - type: "string", - example: "There is no session with that ID." + property: 'message', + type: 'string', + example: 'There is no session with that ID.' ) ] ) diff --git a/src/Controller/SubscriberController.php b/src/Controller/SubscriberController.php index 4113742..3d9087b 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Controller/SubscriberController.php @@ -39,75 +39,92 @@ public function __construct( #[Route('/subscribers', name: 'create_subscriber', methods: ['POST'])] #[OA\Post( - path: "/subscriber", - description: "Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email address yet).", - summary: "Create a subscriber", + path: '/subscriber', + description: 'Creates a new subscriber (if there is no subscriber with the given email address yet).', + summary: 'Create a subscriber', requestBody: new OA\RequestBody( - description: "Pass session credentials", + description: 'Pass session credentials', required: true, content: new OA\JsonContent( - required: ["email"], + required: ['email'], properties: [ - new OA\Property(property: "email", type: "string", format: "string", example: "admin@example.com"), - new OA\Property(property: "confirmed", type: "boolean", example: false), - new OA\Property(property: "blacklisted", type: "boolean", example: false), - new OA\Property(property: "html_email", type: "boolean", example: false), - new OA\Property(property: "disabled", type: "boolean", example: false) + new OA\Property(property: 'email', type: 'string', format: 'string', example: 'admin@example.com'), + new OA\Property(property: 'confirmed', type: 'boolean', example: false), + new OA\Property(property: 'blacklisted', type: 'boolean', example: false), + new OA\Property(property: 'html_email', type: 'boolean', example: false), + new OA\Property(property: 'disabled', type: 'boolean', example: false) ] ) ), - tags: ["subscribers"], + tags: ['subscribers'], parameters: [ new OA\Parameter( - name: "session", - description: "Session ID obtained from authentication", - in: "header", + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', required: true, - schema: new OA\Schema(type: "string") + schema: new OA\Schema(type: 'string') ) ], responses: [ new OA\Response( response: 201, - description: "Success", + description: 'Success', content: new OA\JsonContent( properties: [ - new OA\Property(property: "creation_date", type: "string", format: "date-time", example: "2017-12-16T18:44:27+00:00"), - new OA\Property(property: "email", type: "string", example: "subscriber@example.com"), - new OA\Property(property: "confirmed", type: "boolean", example: false), - new OA\Property(property: "blacklisted", type: "boolean", example: false), - new OA\Property(property: "bounced", type: "integer", example: 0), - new OA\Property(property: "unique_id", type: "string", example: "69f4e92cf50eafca9627f35704f030f4"), - new OA\Property(property: "html_email", type: "boolean", example: false), - new OA\Property(property: "disabled", type: "boolean", example: false), - new OA\Property(property: "id", type: "integer", example: 1) + new OA\Property( + property: 'creation_date', + type: 'string', + format: 'date-time', + example: '2017-12-16T18:44:27+00:00' + ), + new OA\Property(property: 'email', type: 'string', example: 'subscriber@example.com'), + new OA\Property(property: 'confirmed', type: 'boolean', example: false), + new OA\Property(property: 'blacklisted', type: 'boolean', example: false), + new OA\Property(property: 'bounced', type: 'integer', example: 0), + new OA\Property( + property: 'unique_id', + type: 'string', + example: '69f4e92cf50eafca9627f35704f030f4' + ), + new OA\Property(property: 'html_email', type: 'boolean', example: false), + new OA\Property(property: 'disabled', type: 'boolean', example: false), + new OA\Property(property: 'id', type: 'integer', example: 1) ] ) ), new OA\Response( response: 403, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "No valid session key was provided as basic auth password.") + new OA\Property( + property: 'message', + type: 'string', + example: 'No valid session key was provided as basic auth password.' + ) ] ) ), new OA\Response( response: 409, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "This resource already exists.") + new OA\Property(property: 'message', type: 'string', example: 'This resource already exists.') ] ) ), new OA\Response( response: 422, - description: "Failure", + description: 'Failure', content: new OA\JsonContent( properties: [ - new OA\Property(property: "message", type: "string", example: "Some fields invalid: email, confirmed, html_email") + new OA\Property( + property: 'message', + type: 'string', + example: 'Some fields invalid: email, confirmed, html_email' + ) ] ) ) @@ -158,7 +175,9 @@ private function validateSubscriber(Request $request): void $booleanFields = ['confirmed', 'blacklisted', 'html_email', 'disabled']; foreach ($booleanFields as $fieldKey) { - if ($request->getPayload()->get($fieldKey) !== null && !is_bool($request->getPayload()->get($fieldKey))) { + if ($request->getPayload()->get($fieldKey) !== null + && !is_bool($request->getPayload()->get($fieldKey)) + ) { $invalidFields[] = $fieldKey; } } diff --git a/src/PhpListRestBundle.php b/src/PhpListRestBundle.php index 35e5bb4..5ff7622 100644 --- a/src/PhpListRestBundle.php +++ b/src/PhpListRestBundle.php @@ -7,27 +7,26 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use OpenApi\Attributes as OA; - /** * This bundle provides the REST API for phpList. * * @author Oliver Klee */ #[OA\Info( - version: "1.0.0", - description: "This is the OpenAPI documentation for My API.", - title: "My API Documentation", + version: '1.0.0', + description: 'This is the OpenAPI documentation for My API.', + title: 'My API Documentation', contact: new OA\Contact( - email: "support@phplist.com" + email: 'support@phplist.com' ), license: new OA\License( - name: "AGPL-3.0-or-later", - url: "https://www.gnu.org/licenses/agpl.txt" + name: 'AGPL-3.0-or-later', + url: 'https://www.gnu.org/licenses/agpl.txt' ) )] #[OA\Server( - url: "https://www.phplist.com/api/v2", - description: "Production server" + url: 'https://www.phplist.com/api/v2', + description: 'Production server' )] class PhpListRestBundle extends Bundle { From 1e4294c328286c29269f0a1d2b6ca1257b317e85 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 15 Dec 2024 21:57:04 +0400 Subject: [PATCH 25/26] ISSUE-337: force push --- .github/workflows/restapi-docs.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/restapi-docs.yml b/.github/workflows/restapi-docs.yml index e08552e..9854272 100644 --- a/.github/workflows/restapi-docs.yml +++ b/.github/workflows/restapi-docs.yml @@ -31,13 +31,13 @@ jobs: run: composer install --no-interaction --prefer-dist - name: Generate OpenAPI Specification JSON - run: vendor/bin/openapi -o docs/latest-restapi.json --format json src + run: vendor/bin/openapi -o docs/openapi.json --format json src - name: Upload REST API Specification uses: actions/upload-artifact@v3 with: - name: restapi-json - path: docs/latest-restapi.json + name: openapi-json + path: docs/openapi.json deploy-docs: name: Deploy REST API Specification @@ -65,10 +65,10 @@ jobs: name: restapi-json - name: Validate OpenAPI Specification - run: openapi-checker docs/latest-restapi.json + run: openapi-checker docs/openapi.json - name: Compare Specifications - run: git diff --no-index --output=restapi-diff.txt docs/latest-restapi.json restapi.json || true + run: git diff --no-index --output=restapi-diff.txt docs/openapi.json restapi.json || true - name: Check Differences and Decide Deployment id: allow-deploy @@ -84,9 +84,9 @@ jobs: - name: Commit and Deploy Updates if: env.DEPLOY == 'true' run: | - mv docs/latest-restapi.json docs/restapi.json + mv docs/openapi.json docs/restapi.json git config user.name "github-actions" git config user.email "github-actions@restapi-docs.workflow" git add docs/restapi.json git commit -m "Update REST API documentation `date`" - git push + git push origin main --force From 2ce9ae1179a6aa816c3da08ff817f2daedfe96ce Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 16 Dec 2024 19:28:04 +0400 Subject: [PATCH 26/26] ISSUE-337: name fix --- .github/workflows/restapi-docs.yml | 17 +- docs/openapi.json | 734 ----------------------------- 2 files changed, 9 insertions(+), 742 deletions(-) delete mode 100644 docs/openapi.json diff --git a/.github/workflows/restapi-docs.yml b/.github/workflows/restapi-docs.yml index 9854272..25ab243 100644 --- a/.github/workflows/restapi-docs.yml +++ b/.github/workflows/restapi-docs.yml @@ -31,13 +31,13 @@ jobs: run: composer install --no-interaction --prefer-dist - name: Generate OpenAPI Specification JSON - run: vendor/bin/openapi -o docs/openapi.json --format json src + run: vendor/bin/openapi -o docs/latest-restapi.json --format json src - name: Upload REST API Specification uses: actions/upload-artifact@v3 with: - name: openapi-json - path: docs/openapi.json + name: restapi-json + path: docs/latest-restapi.json deploy-docs: name: Deploy REST API Specification @@ -50,7 +50,7 @@ jobs: node-version: 14 - name: Install openapi-checker - run: npm install -g openapi-checker + run: npm install -g swagger-cli - name: Checkout REST API Docs Repository uses: actions/checkout@v3 @@ -63,12 +63,13 @@ jobs: uses: actions/download-artifact@v3 with: name: restapi-json + path: docs - name: Validate OpenAPI Specification - run: openapi-checker docs/openapi.json + run: swagger-cli validate docs/latest-restapi.json - name: Compare Specifications - run: git diff --no-index --output=restapi-diff.txt docs/openapi.json restapi.json || true + run: git diff --no-index --output=restapi-diff.txt docs/latest-restapi.json restapi.json || true - name: Check Differences and Decide Deployment id: allow-deploy @@ -84,9 +85,9 @@ jobs: - name: Commit and Deploy Updates if: env.DEPLOY == 'true' run: | - mv docs/openapi.json docs/restapi.json + mv docs/latest-restapi.json docs/restapi.json git config user.name "github-actions" git config user.email "github-actions@restapi-docs.workflow" git add docs/restapi.json git commit -m "Update REST API documentation `date`" - git push origin main --force + git push diff --git a/docs/openapi.json b/docs/openapi.json deleted file mode 100644 index 631954f..0000000 --- a/docs/openapi.json +++ /dev/null @@ -1,734 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "My API Documentation", - "description": "This is the OpenAPI documentation for My API.", - "contact": { - "email": "support@phplist.com" - }, - "license": { - "name": "AGPL-3.0-or-later", - "url": "https://www.gnu.org/licenses/agpl.txt" - }, - "version": "1.0.0" - }, - "servers": [ - { - "url": "https://www.phplist.com/api/v2", - "description": "Production server" - } - ], - "paths": { - "/lists": { - "get": { - "tags": [ - "lists" - ], - "summary": "Gets a list of all subscriber lists.", - "description": "Returns a JSON list of all subscriber lists.", - "operationId": "81c74c22179cb7ac081aa6e77b9f6631", - "parameters": [ - { - "name": "session", - "in": "header", - "description": "Session ID obtained from authentication", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "properties": { - "name": { - "type": "string", - "example": "News" - }, - "description": { - "type": "string", - "example": "News (and some fun stuff)" - }, - "creation_date": { - "type": "string", - "format": "date-time", - "example": "2016-06-22T15:01:17+00:00" - }, - "list_position": { - "type": "integer", - "example": 12 - }, - "subject_prefix": { - "type": "string", - "example": "phpList" - }, - "public": { - "type": "boolean", - "example": true - }, - "category": { - "type": "string", - "example": "news" - }, - "id": { - "type": "integer", - "example": 1 - } - }, - "type": "object" - } - } - } - } - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password." - } - }, - "type": "object" - } - } - } - } - } - } - }, - "/lists/{list}": { - "get": { - "tags": [ - "lists" - ], - "summary": "Gets a subscriber list.", - "description": "Returns a single subscriber list with specified ID.", - "operationId": "0dc1b40be222c4283ef1e39ee210d077", - "parameters": [ - { - "name": "list", - "in": "path", - "description": "List ID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "session", - "in": "header", - "description": "Session ID obtained from authentication", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object" - }, - "example": { - "name": "News", - "description": "News (and some fun stuff)", - "creation_date": "2016-06-22T15:01:17+00:00", - "list_position": 12, - "subject_prefix": "phpList", - "public": true, - "category": "news", - "id": 1 - } - } - } - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password." - } - }, - "type": "object" - } - } - } - }, - "404": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "There is no list with that ID." - } - }, - "type": "object" - } - } - } - } - } - }, - "delete": { - "tags": [ - "lists" - ], - "summary": "Deletes a list.", - "description": "Deletes a single subscriber list.", - "operationId": "ab4b8e42dd519312a942be1f3a86771b", - "parameters": [ - { - "name": "session", - "in": "header", - "description": "Session ID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "list", - "in": "path", - "description": "List ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success" - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password or You do not have access to this session." - } - }, - "type": "object" - } - } - } - }, - "404": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "There is no session with that ID." - } - }, - "type": "object" - } - } - } - } - } - } - }, - "/lists/{list}/members": { - "get": { - "tags": [ - "lists" - ], - "summary": "Gets a list of all subscribers (members) of a subscriber list.", - "description": "Returns a JSON list of all subscribers for a subscriber list.", - "operationId": "29f6848c27ee3e30b7e4bd7997c73a18", - "parameters": [ - { - "name": "session", - "in": "header", - "description": "Session ID obtained from authentication", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "list", - "in": "path", - "description": "List ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "properties": { - "creation_date": { - "type": "string", - "format": "date-time", - "example": "2016-07-22T15:01:17+00:00" - }, - "email": { - "type": "string", - "example": "oliver@example.com" - }, - "confirmed": { - "type": "boolean", - "example": true - }, - "blacklisted": { - "type": "boolean", - "example": true - }, - "bounce_count": { - "type": "integer", - "example": 17 - }, - "unique_id": { - "type": "string", - "example": "95feb7fe7e06e6c11ca8d0c48cb46e89" - }, - "html_email": { - "type": "boolean", - "example": true - }, - "disabled": { - "type": "boolean", - "example": true - }, - "id": { - "type": "integer", - "example": 1 - } - }, - "type": "object" - } - } - } - } - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password." - } - }, - "type": "object" - } - } - } - } - } - } - }, - "/lists/{list}/count": { - "get": { - "tags": [ - "lists" - ], - "summary": "Gets the total number of subscribers of a list", - "description": "Returns a count of all subscribers in a given list.", - "operationId": "c1e2f17c0078014a816c914a25d8e889", - "parameters": [ - { - "name": "session", - "in": "header", - "description": "Session ID obtained from authentication", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "list", - "in": "path", - "description": "List ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success" - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password." - } - }, - "type": "object" - } - } - } - } - } - } - }, - "/sessions": { - "post": { - "tags": [ - "sessions" - ], - "summary": "Log in or create new session.", - "description": "Given valid login data, this will generate a login token that will be valid for 1 hour.", - "operationId": "08336f63605d853592fc942e7c13fcd7", - "requestBody": { - "description": "Pass session credentials", - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "login_name", - "password" - ], - "properties": { - "login_name": { - "type": "string", - "format": "string", - "example": "admin" - }, - "password": { - "type": "string", - "format": "password", - "example": "eetIc/Gropvoc1" - } - }, - "type": "object" - } - } - } - }, - "responses": { - "201": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "properties": { - "id": { - "type": "integer", - "example": 1234 - }, - "key": { - "type": "string", - "example": "2cfe100561473c6cdd99c9e2f26fa974" - }, - "expiry": { - "type": "string", - "example": "2017-07-20T18:22:48+00:00" - } - }, - "type": "object" - } - } - } - }, - "400": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Empty json, invalid data and or incomplete data" - } - }, - "type": "object" - } - } - } - }, - "401": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Not authorized." - } - }, - "type": "object" - } - } - } - } - } - } - }, - "/sessions/{session}": { - "delete": { - "tags": [ - "sessions" - ], - "summary": "Delete a session.", - "description": "Delete the session passed as a parameter.", - "operationId": "fbeec6cb2db5898313a8dd3a91b2b57a", - "parameters": [ - { - "name": "session", - "in": "path", - "description": "Session ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success" - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password or You do not have access to this session." - } - }, - "type": "object" - } - } - } - }, - "404": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "There is no session with that ID." - } - }, - "type": "object" - } - } - } - } - } - } - }, - "/subscriber": { - "post": { - "tags": [ - "subscribers" - ], - "summary": "Create a subscriber", - "description": "Creates a new subscriber (if the provided data is valid and there is no subscriber with the given email address yet).", - "operationId": "9dc1ae0a66e2eed292c4aed09bab8de0", - "parameters": [ - { - "name": "session", - "in": "header", - "description": "Session ID obtained from authentication", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "Pass session credentials", - "required": true, - "content": { - "application/json": { - "schema": { - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string", - "format": "string", - "example": "admin@example.com" - }, - "confirmed": { - "type": "boolean", - "example": false - }, - "blacklisted": { - "type": "boolean", - "example": false - }, - "html_email": { - "type": "boolean", - "example": false - }, - "disabled": { - "type": "boolean", - "example": false - } - }, - "type": "object" - } - } - } - }, - "responses": { - "201": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "properties": { - "creation_date": { - "type": "string", - "format": "date-time", - "example": "2017-12-16T18:44:27+00:00" - }, - "email": { - "type": "string", - "example": "subscriber@example.com" - }, - "confirmed": { - "type": "boolean", - "example": false - }, - "blacklisted": { - "type": "boolean", - "example": false - }, - "bounced": { - "type": "integer", - "example": 0 - }, - "unique_id": { - "type": "string", - "example": "69f4e92cf50eafca9627f35704f030f4" - }, - "html_email": { - "type": "boolean", - "example": false - }, - "disabled": { - "type": "boolean", - "example": false - }, - "id": { - "type": "integer", - "example": 1 - } - }, - "type": "object" - } - } - } - }, - "403": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "No valid session key was provided as basic auth password." - } - }, - "type": "object" - } - } - } - }, - "409": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "This resource already exists." - } - }, - "type": "object" - } - } - } - }, - "422": { - "description": "Failure", - "content": { - "application/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Some fields invalid: email, confirmed, html_email" - } - }, - "type": "object" - } - } - } - } - } - } - } - }, - "tags": [ - { - "name": "lists", - "description": "lists" - }, - { - "name": "sessions", - "description": "sessions" - }, - { - "name": "subscribers", - "description": "subscribers" - } - ] -} \ No newline at end of file