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);
+ }
+}