From eba1ca6e8180d82d8c7386a78f88ef0c7047c57c Mon Sep 17 00:00:00 2001 From: Lennart Tinkloh Date: Wed, 26 Feb 2025 17:19:56 +0100 Subject: [PATCH] feat: application tests --- .github/workflows/php.yml | 19 ++ composer.json | 9 +- composer.lock | 206 +++++++++++++++++- infection.json5 | 1 + phpstan.neon | 4 + phpunit.xml.dist | 4 + src/Tests/Contract/ApplicationHelperTrait.php | 33 +++ src/Tests/Contract/IntegrationHelperTrait.php | 24 +- .../BraintreeConfigurationControllerTest.php | 51 +++++ .../Controller/EntityControllerTest.php | 49 +++++ 10 files changed, 390 insertions(+), 10 deletions(-) create mode 100644 src/Tests/Contract/ApplicationHelperTrait.php create mode 100644 tests/application/Controller/BraintreeConfigurationControllerTest.php create mode 100644 tests/application/Controller/EntityControllerTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index db5ecfe..087f696 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -81,6 +81,25 @@ jobs: with: files: ./coverage.xml + application: + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:10.5 + env: + MARIADB_ROOT_PASSWORD: swagbraintree + MYSQL_DATABASE: swagbraintree_test + ports: ['3306:3306'] + env: + DATABASE_URL: mysql://root:swagbraintree@127.0.0.1:3306/swagbraintree + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-php + - name: Setup database + run: composer setup:test + - name: Run PHPUnit + run: composer phpunit:application -- --coverage-clover=coverage.xml + infection: runs-on: ubuntu-latest services: diff --git a/composer.json b/composer.json index 66d021f..4c5008c 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", - "braintree/braintree_php": "^6.19", + "braintree/braintree_php": "^6.21", "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^3.2", @@ -47,7 +47,8 @@ "autoload-dev": { "psr-4": { "Swag\\Braintree\\Tests\\Unit\\": "tests/unit/", - "Swag\\Braintree\\Tests\\Integration\\": "tests/integration/" + "Swag\\Braintree\\Tests\\Integration\\": "tests/integration/", + "Swag\\Braintree\\Tests\\Application\\": "tests/application/" } }, "replace": { @@ -93,7 +94,8 @@ "bin/console doctrine:migrations:migrate --env=test -n" ], "phpunit:unit": "@phpunit --testsuite=SwagBraintreeUnitTest", - "phpunit:integration": "@phpunit --testsuite=SwagBraintreeIntegrationTest" + "phpunit:integration": "@phpunit --testsuite=SwagBraintreeIntegrationTest", + "phpunit:application": "@phpunit --testsuite=SwagBraintreeApplicationTest" }, "conflict": { "symfony/symfony": "*" @@ -113,6 +115,7 @@ "phpstan/phpstan": "^1.10", "phpstan/phpstan-symfony": "^1.3", "phpunit/phpunit": "^11", + "symfony/browser-kit": "7.1.*", "symfony/debug-bundle": "^7.1", "symfony/stopwatch": "^7.1", "symfony/web-profiler-bundle": "^7.1", diff --git a/composer.lock b/composer.lock index 1de3bf9..419b7de 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fb114e21c552f707f1cc9b6359314d4e", + "content-hash": "cdf26ba1db6da3905810ddd3b318abd3", "packages": [ { "name": "braintree/braintree_php", @@ -7380,6 +7380,73 @@ }, "time": "2025-02-15T09:15:56+00:00" }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.12.1", @@ -10015,6 +10082,74 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "symfony/browser-kit", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "714becc9ba9b20115ffededc58f6b7172dc394cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/714becc9ba9b20115ffededc58f6b7172dc394cf", + "reference": "714becc9ba9b20115ffededc58f6b7172dc394cf", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/dom-crawler": "^6.4|^7.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:11:02+00:00" + }, { "name": "symfony/debug-bundle", "version": "v7.2.0", @@ -10089,6 +10224,73 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/dom-crawler", + "version": "v7.1.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "7361d8f7e7eecbca17efe68ca1ee677bf23cfe5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7361d8f7e7eecbca17efe68ca1ee677bf23cfe5a", + "reference": "7361d8f7e7eecbca17efe68ca1ee677bf23cfe5a", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v7.1.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-27T10:57:12+00:00" + }, { "name": "symfony/options-resolver", "version": "v7.2.0", @@ -11021,5 +11223,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/infection.json5 b/infection.json5 index 4d4be61..bde7ff5 100644 --- a/infection.json5 +++ b/infection.json5 @@ -21,6 +21,7 @@ }, "tmpDir": "var/cache", "testFramework":"phpunit", + "testFrameworkOptions": "--testsuite=SwagBraintreeUnitTest", "mutators": { "@default": true, "LessThan": { diff --git a/phpstan.neon b/phpstan.neon index 208d861..831dea6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -27,6 +27,10 @@ parameters: - message: '#Multiple class/interface/trait is not allowed in single file#' path: tests/unit + # PHPStan does not know of symfony browser component + - + message: '#Service "test\.client" is not registered in the container\.#' + path: tests rules: # rules from https://github.com/symplify/phpstan-rules diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 04f7702..fe38c35 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -42,6 +42,10 @@ tests/integration + + + tests/application + diff --git a/src/Tests/Contract/ApplicationHelperTrait.php b/src/Tests/Contract/ApplicationHelperTrait.php new file mode 100644 index 0000000..23c2208 --- /dev/null +++ b/src/Tests/Contract/ApplicationHelperTrait.php @@ -0,0 +1,33 @@ +get('test.client'); + + if (!$shop) { + $shop = static::createShop(persist: false); + } + + $session = $client->getContainer()->get('session.factory')->createSession(); + $session->set(ShopResolver::SHOP_ID, $shop->getShopId()); + $session->save(); + $session->start(); + + $client->getCookieJar()->set(new Cookie($session->getName(), $session->getId())); + + return static::getClient($client); + } +} diff --git a/src/Tests/Contract/IntegrationHelperTrait.php b/src/Tests/Contract/IntegrationHelperTrait.php index cd544c3..f7e1883 100644 --- a/src/Tests/Contract/IntegrationHelperTrait.php +++ b/src/Tests/Contract/IntegrationHelperTrait.php @@ -4,23 +4,37 @@ use Doctrine\ORM\EntityManagerInterface; use Swag\Braintree\Entity\ShopEntity; +use Swag\Braintree\Tests\Ids; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * @infection-ignore-all */ trait IntegrationHelperTrait { + abstract protected static function getContainer(): ContainerInterface; + protected static function getEntityManager(): EntityManagerInterface { return self::getContainer()->get('doctrine.orm.default_entity_manager'); } - protected static function createShop(): ShopEntity - { - $shop = self::createShop(); + protected static function createShop( + string $shopIdKey = 'shop', + string $shopUrl = 'https://shop.example.com', + string $shopSecretKey = 'shop-secret', + bool $persist = true, + ): ShopEntity { + $shop = new ShopEntity( + Ids::get($shopIdKey), + $shopUrl, + Ids::get($shopSecretKey), + ); - self::getEntityManager()->persist($shop); - self::getEntityManager()->flush(); + if ($persist) { + self::getEntityManager()->persist($shop); + self::getEntityManager()->flush(); + } return $shop; } diff --git a/tests/application/Controller/BraintreeConfigurationControllerTest.php b/tests/application/Controller/BraintreeConfigurationControllerTest.php new file mode 100644 index 0000000..570b0c8 --- /dev/null +++ b/tests/application/Controller/BraintreeConfigurationControllerTest.php @@ -0,0 +1,51 @@ +setBraintreeMerchantId('merchant-id'); + $shop->setBraintreePrivateKey('private-key'); + $shop->setBraintreePublicKey('public-key'); + + $config = new ConfigEntity(); + $config->setShop($shop); + $config->setShipsFromPostalCode('12345'); + $config->setThreeDSecureEnforced(true); + + $currencyMapping = new CurrencyMappingEntity(); + $currencyMapping->setCurrencyId('euro-id'); + $currencyMapping->setCurrencyIso('EUR'); + $currencyMapping->setMerchantAccountId('merchant-account'); + $currencyMapping->setShop($shop); + + $entityManager = static::getEntityManager(); + $entityManager->persist($shop); + $entityManager->persist($config); + $entityManager->persist($currencyMapping); + $entityManager->flush(); + + $client = static::createClientForShop($shop); + $client->request(Request::METHOD_DELETE, '/api/config'); + + $resetShop = $entityManager->find(ShopEntity::class, $shop->getShopId()); + + static::assertNull($resetShop->getBraintreeMerchantId()); + static::assertNull($resetShop->getBraintreePrivateKey()); + static::assertNull($resetShop->getBraintreePublicKey()); + static::assertCount(0, $resetShop->getConfigs()); + static::assertCount(0, $resetShop->getCurrencyMappings()); + } +} diff --git a/tests/application/Controller/EntityControllerTest.php b/tests/application/Controller/EntityControllerTest.php new file mode 100644 index 0000000..b1a3ba4 --- /dev/null +++ b/tests/application/Controller/EntityControllerTest.php @@ -0,0 +1,49 @@ +request(Request::METHOD_GET, '/api/entity/shop'); + + static::assertResponseStatusCodeSame(Response::HTTP_OK); + + /** @var Response $response */ + $response = $client->getResponse(); + + static::assertJson($response->getContent()); + + $shop = \json_decode($response->getContent(), true, flags: \JSON_THROW_ON_ERROR); + + static::assertArrayHasKey('shopId', $shop); + static::assertArrayHasKey('shopUrl', $shop); + static::assertArrayHasKey('shopSecret', $shop); + + static::assertSame(Ids::get('shop'), $shop['shopId']); + static::assertSame('https://shop.example.com', $shop['shopUrl']); + static::assertSame(Ids::get('shop-secret'), $shop['shopSecret']); + } + + public function testGetShopEntityWithNonExistentShop(): void + { + // shop is not persisted: session will not be able to resolve the shop + $shop = static::createShop(persist: false); + + $client = static::createClientForShop($shop); + $client->request(Request::METHOD_GET, '/api/entity/shop'); + + static::assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR); + } +}