From f76743c6b52cdc3eb9e919a2e89c02db3f06ccf0 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:16:00 +0200 Subject: [PATCH 01/15] Inject HelloAsso service and its HTTP client --- .../Controllers/HelloassoController.php | 20 +++++++++++-------- lib/GaletteHelloasso/Helloasso.php | 16 +++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/GaletteHelloasso/Controllers/HelloassoController.php b/lib/GaletteHelloasso/Controllers/HelloassoController.php index f4273a2..e86ae83 100644 --- a/lib/GaletteHelloasso/Controllers/HelloassoController.php +++ b/lib/GaletteHelloasso/Controllers/HelloassoController.php @@ -42,12 +42,15 @@ class HelloassoController extends AbstractPluginController #[Inject("Plugin Galette Helloasso")] protected array $module_info; + #[Inject] + protected Helloasso $helloasso; + /** * Main form */ public function form(Response $response): Response { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; $current_url = $this->preferences->getURL(); @@ -90,7 +93,7 @@ public function form(Response $response): Response public function formCheckout(Request $request, Response $response): Response { $helloasso_request = $request->getParsedBody(); - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; $adherent = new Adherent($this->zdb); // Only reasons proposed to the current user can be paid @@ -188,7 +191,7 @@ public function logs( $helloasso = $this->session->helloasso; $this->session->helloasso = null; } else { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; } $filters = []; @@ -300,7 +303,7 @@ public function preferences(Request $request, Response $response): Response $helloasso = $this->session->helloasso; $this->session->helloasso = null; } else { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; } $amounts = $helloasso->getAllAmounts(); @@ -330,7 +333,7 @@ public function preferences(Request $request, Response $response): Response public function storePreferences(Request $request, Response $response): Response { $post = $request->getParsedBody(); - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; if ($this->login->isAdmin()) { if (array_key_exists('helloasso_test_mode', $post)) { @@ -392,7 +395,7 @@ public function webhook(Request $request, Response $response): Response { $body = $request->getBody(); $post = json_decode($body->getContents(), true); - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; // Verify notification authenticity // https://dev.helloasso.com/docs/secure-webhook @@ -541,7 +544,7 @@ public function returnUrl(Request $request, Response $response): Response } try { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; $tokens = $helloasso->getTokens(); $client = $helloasso->setupClient(); @@ -552,7 +555,8 @@ public function returnUrl(Request $request, Response $response): Response ] ]; - $request = $client->get( + $request = $client->request( + 'GET', $helloasso->getApiRoute() . 'v5/organizations/' . $helloasso->getOrganizationSlug() diff --git a/lib/GaletteHelloasso/Helloasso.php b/lib/GaletteHelloasso/Helloasso.php index ddc9d05..d78107a 100644 --- a/lib/GaletteHelloasso/Helloasso.php +++ b/lib/GaletteHelloasso/Helloasso.php @@ -16,6 +16,7 @@ use Galette\Core\Preferences; use Galette\Entity\ContributionsTypes; use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; /** * Preferences for helloasso @@ -34,6 +35,7 @@ class Helloasso private Db $zdb; private Preferences $preferences; + private ?ClientInterface $client; /** @var array> */ private array $prices; @@ -64,13 +66,15 @@ class Helloasso /** * Default constructor * - * @param Db $zdb Database instance - * @param Preferences $preferences Preferences + * @param Db $zdb Database instance + * @param Preferences $preferences Preferences + * @param ?ClientInterface $client HTTP client, a default one is built if null */ - public function __construct(Db $zdb, Preferences $preferences) + public function __construct(Db $zdb, Preferences $preferences, ?ClientInterface $client = null) { $this->zdb = $zdb; $this->preferences = $preferences; + $this->client = $client; $this->loaded = false; $this->prices = []; $this->inactives = []; @@ -372,7 +376,7 @@ public function checkout(array $metadata, float $amount, ?bool $contains_donatio ], 'json' => $data, ]; - $request = $client->post($this->getApiRoute() . 'v5/organizations/' . $this->getOrganizationSlug() . '/checkout-intents', $headers); + $request = $client->request('POST', $this->getApiRoute() . 'v5/organizations/' . $this->getOrganizationSlug() . '/checkout-intents', $headers); $contents = $request->getBody()->getContents(); return json_decode($contents, true); @@ -389,9 +393,9 @@ public function checkout(array $metadata, float $amount, ?bool $contains_donatio /** * Setup Guzzle client */ - public function setupClient(): Client + public function setupClient(): ClientInterface { - return new Client( + return $this->client ??= new Client( $this->getClientOptions() ); } From 448dc870a9c820a867f81a282adf7976243bed67 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:17:12 +0200 Subject: [PATCH 02/15] Test checkout creation and return page --- .../tests/units/HelloassoController.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index 157c41d..8faebcb 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -13,6 +13,11 @@ use Galette\Entity\ContributionsTypes; use Galette\Tests\GaletteRoutingTestCase; use GaletteHelloasso\Helloasso; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Response; use Psr\Http\Message\ResponseInterface; /** @@ -25,6 +30,13 @@ class HelloassoController extends GaletteRoutingTestCase protected int $seed = 20260928061512; protected bool $load_plugins = true; + /** + * Requests sent to the (fake) HelloAsso API + * + * @var array> + */ + private array $api_calls = []; + /** * Set up tests */ @@ -68,6 +80,37 @@ private function configure(): void $this->setHelloassoPref('helloasso_client_secret', 'secret-for-tests'); } + /** + * Answer HelloAsso API calls with given responses, in order + * + * Plugin settings must be set before: they are loaded here. + * + * @param array> $responses JSON responses + */ + private function fakeApi(array $responses): void + { + $this->api_calls = []; + $stack = HandlerStack::create(new MockHandler(array_map( + fn(array $body) => new Response(200, ['Content-Type' => 'application/json'], (string)json_encode($body)), + $responses + ))); + $stack->push(Middleware::history($this->api_calls)); + $this->container->set( + Helloasso::class, + new Helloasso($this->zdb, $this->preferences, new Client(['handler' => $stack])) + ); + } + + /** + * Token response of the API + * + * @return array + */ + private function getTokenResponse(): array + { + return ['access_token' => 'access-for-tests', 'expires_in' => 1800, 'refresh_token' => 'refresh-for-tests']; + } + /** * Log in given member * @@ -277,4 +320,52 @@ public function testPreferencesSecret(): void $this->assertSame('new-secret', $this->getHelloassoPref('helloasso_client_secret')); $this->expectNoLogEntry(); } + + /** + * A checkout is created on HelloAsso, and its details displayed on return + */ + public function testCheckout(): void + { + $this->configure(); + $this->setTypeAmount(5, 10); + $member = $this->getMemberOne(); + $this->logMember($this->dataAdherentOne()); + $this->fakeApi([ + $this->getTokenResponse(), + ['id' => 1234, 'redirectUrl' => 'https://www.helloasso-sandbox.com/checkout/1234'], + [ + 'id' => 1234, + 'metadata' => ['item_id' => 5, 'item_name' => 'donation in money', 'member_id' => $member->id], + 'order' => [ + 'amount' => ['total' => 1250], + 'date' => '2026-09-28T10:15:00+02:00', + 'payments' => [['paymentMeans' => 'Card']] + ] + ] + ]); + + $test_response = $this->postCheckout(['item_id' => '5', 'amount' => '12,50']); + $this->assertSame(301, $test_response->getStatusCode()); + $this->assertSame(['https://www.helloasso-sandbox.com/checkout/1234'], $test_response->getHeader('Location')); + $this->expectNoLogEntry(); + $this->assertSame(['1234'], $this->session->helloasso_checkouts); + + $this->assertCount(2, $this->api_calls); + $checkout = json_decode((string)$this->api_calls[1]['request']->getBody(), true); + $this->assertSame(1250, $checkout['totalAmount']); + $this->assertSame(['member_id' => $member->id, 'item_id' => 5, 'item_name' => 'donation in money'], $checkout['metadata']); + + //tokens are kept for next calls + $select = $this->zdb->select(HELLOASSO_PREFIX . Helloasso::TABLE_TOKENS); + $select->where(['type' => 'access_token']); + $this->assertSame('access-for-tests', $this->zdb->execute($select)->current()->value); + + $test_response = $this->getReturnPage(['checkoutIntentId' => '1234', 'code' => 'succeeded']); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $body = (string)$test_response->getBody(); + $this->assertStringContainsString('donation in money', $body); + $this->assertStringContainsString('Card', $body); + $this->assertCount(3, $this->api_calls); + } } From abb6460652debe52bd8e4c7b8b045fc1b7c47320 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:18:06 +0200 Subject: [PATCH 03/15] Refuse the return page without failing when details are unexpected --- .../Controllers/HelloassoController.php | 4 ++-- .../tests/units/HelloassoController.php | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/GaletteHelloasso/Controllers/HelloassoController.php b/lib/GaletteHelloasso/Controllers/HelloassoController.php index e86ae83..7dc685d 100644 --- a/lib/GaletteHelloasso/Controllers/HelloassoController.php +++ b/lib/GaletteHelloasso/Controllers/HelloassoController.php @@ -555,7 +555,7 @@ public function returnUrl(Request $request, Response $response): Response ] ]; - $request = $client->request( + $api_response = $client->request( 'GET', $helloasso->getApiRoute() . 'v5/organizations/' @@ -564,7 +564,7 @@ public function returnUrl(Request $request, Response $response): Response . $checkout_id, $headers ); - $checkout = json_decode($request->getBody()->getContents(), true); + $checkout = json_decode($api_response->getBody()->getContents(), true); $details = [ 'amount' => $checkout['order']['amount']['total'] / 100, diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index 8faebcb..29ff3d4 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -368,4 +368,19 @@ public function testCheckout(): void $this->assertStringContainsString('Card', $body); $this->assertCount(3, $this->api_calls); } + + /** + * Return page refuses to display unexpected checkout details + */ + public function testReturnPageWithUnexpectedDetails(): void + { + $this->configure(); + $this->fakeApi([$this->getTokenResponse(), ['id' => 1234]]); + $this->session->helloasso_checkouts = ['1234']; + + $test_response = $this->getReturnPage(['checkoutIntentId' => '1234']); + $this->assertSame(403, $test_response->getStatusCode()); + $this->expectLogEntry(\Analog\Analog::WARNING, 'payment details could not be retrieved'); + $this->expectNoLogEntry(); + } } From 7020c4be0b7012520579ca1d734ca5adbfaf5b94 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:18:45 +0200 Subject: [PATCH 04/15] Ask HelloAsso about the organization once per settings page --- .../Controllers/HelloassoController.php | 2 ++ .../default/helloasso_preferences.html.twig | 18 ++++++++-------- .../tests/units/HelloassoController.php | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/GaletteHelloasso/Controllers/HelloassoController.php b/lib/GaletteHelloasso/Controllers/HelloassoController.php index 7dc685d..e65a752 100644 --- a/lib/GaletteHelloasso/Controllers/HelloassoController.php +++ b/lib/GaletteHelloasso/Controllers/HelloassoController.php @@ -315,6 +315,8 @@ public function preferences(Request $request, Response $response): Response 'webhook_url' => $this->preferences->getURL() . $this->routeparser->urlFor('helloasso_webhook'), 'amounts' => $amounts, 'tab' => $tab, + //one call to HelloAsso for the whole page + 'organization' => $helloasso->getOrganization(), 'documentation' => 'https://galette-plugins.github.io/plugin-helloasso/documentation.html#pr%C3%A9f%C3%A9rences' ]; diff --git a/templates/default/helloasso_preferences.html.twig b/templates/default/helloasso_preferences.html.twig index 3d34822..93cf034 100644 --- a/templates/default/helloasso_preferences.html.twig +++ b/templates/default/helloasso_preferences.html.twig @@ -110,7 +110,7 @@
- {% if helloasso.getOrganization() is not empty %} + {% if organization is not empty %}
{{ _T("Connected to HelloAsso", "helloasso") }}
{% else %}
{{ _T("Not connected to HelloAsso", "helloasso") }}
@@ -123,39 +123,39 @@ {% endif %}
- {% if helloasso.getOrganization() is not empty %} + {% if organization is not empty %}
{{ _T("Name", "helloasso") }}
-
{{ helloasso.getOrganization().name }}
+
{{ organization.name }}
- {% if helloasso.getOrganization().description is defined %} + {% if organization.description is defined %}
{{ _T("Description", "helloasso") }}
-
{{ helloasso.getOrganization().description }}
+
{{ organization.description }}
{% endif %}
{{ _T("Type", "helloasso") }}
-
{{ helloasso.getOrganization().type }}
+
{{ organization.type }}
{{ _T("Category", "helloasso") }}
-
{{ helloasso.getOrganization().category }}
+
{{ organization.category }}
- {% if helloasso.getOrganization().rnaNumber is defined %} + {% if organization.rnaNumber is defined %}
{{ _T("RNA Number", "helloasso") }}
-
{{ helloasso.getOrganization().rnaNumber }}
+
{{ organization.rnaNumber }}
{% endif %} diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index 29ff3d4..ad51c66 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -383,4 +383,25 @@ public function testReturnPageWithUnexpectedDetails(): void $this->expectLogEntry(\Analog\Analog::WARNING, 'payment details could not be retrieved'); $this->expectNoLogEntry(); } + + /** + * Settings ask HelloAsso about the organization only once + */ + public function testPreferencesOrganization(): void + { + $this->configure(); + $this->fakeApi([ + $this->getTokenResponse(), + ['name' => 'Galette tests organization', 'type' => 'Association1901', 'category' => 'Other'] + ]); + $this->logSuperAdmin(); + + $test_response = $this->app->handle($this->createRequest('helloasso_preferences')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $body = (string)$test_response->getBody(); + $this->assertStringContainsString('Galette tests organization', $body); + $this->assertStringContainsString('Association1901', $body); + $this->assertCount(2, $this->api_calls); + } } From 0efa5a4a0fdc8d6fe722108e36cdaf1375e599d0 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:19:09 +0200 Subject: [PATCH 05/15] Round checkout amount to the cent --- lib/GaletteHelloasso/Helloasso.php | 6 ++++-- .../tests/units/HelloassoController.php | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/GaletteHelloasso/Helloasso.php b/lib/GaletteHelloasso/Helloasso.php index d78107a..0a6d904 100644 --- a/lib/GaletteHelloasso/Helloasso.php +++ b/lib/GaletteHelloasso/Helloasso.php @@ -345,9 +345,11 @@ public function checkout(array $metadata, float $amount, ?bool $contains_donatio try { $tokens = $this->getTokens(); + //19.99 * 100 is 1998.9999999999998 + $amount = (int)round($amount); $data = [ - 'totalAmount' => (int)$amount, - 'initialAmount' => (int)$amount, + 'totalAmount' => $amount, + 'initialAmount' => $amount, 'itemName' => $metadata['item_name'], 'backUrl' => $this->preferences->getURL() . $routeparser->urlFor('helloasso_back'), 'errorUrl' => $this->preferences->getURL() . $routeparser->urlFor('helloasso_error'), diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index ad51c66..b9da47b 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -404,4 +404,25 @@ public function testPreferencesOrganization(): void $this->assertStringContainsString('Association1901', $body); $this->assertCount(2, $this->api_calls); } + + /** + * Checkout amount is rounded to the cent, not truncated + */ + public function testCheckoutRoundsAmount(): void + { + $this->configure(); + $this->setTypeAmount(5, 10); + $this->getMemberOne(); + $this->logMember($this->dataAdherentOne()); + $this->fakeApi([ + $this->getTokenResponse(), + ['id' => 1234, 'redirectUrl' => 'https://www.helloasso-sandbox.com/checkout/1234'] + ]); + + $this->assertSame(301, $this->postCheckout(['item_id' => '5', 'amount' => '19.99'])->getStatusCode()); + $this->expectNoLogEntry(); + $checkout = json_decode((string)$this->api_calls[1]['request']->getBody(), true); + $this->assertSame(1999, $checkout['totalAmount']); + $this->assertSame(1999, $checkout['initialAmount']); + } } From ce83f49e973b74010a56746e4be73653c7510ad2 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:20:01 +0200 Subject: [PATCH 06/15] Get history entry id from its sequence on PostgreSQL Without it, entries state was never updated and a notification sent again was stored twice. --- lib/GaletteHelloasso/HelloassoHistory.php | 5 +- .../tests/units/HelloassoController.php | 113 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/lib/GaletteHelloasso/HelloassoHistory.php b/lib/GaletteHelloasso/HelloassoHistory.php index e83d4d4..d76cf0c 100644 --- a/lib/GaletteHelloasso/HelloassoHistory.php +++ b/lib/GaletteHelloasso/HelloassoHistory.php @@ -88,7 +88,10 @@ public function add(array|string $action, string $argument = '', string $query = $insert = $this->zdb->insert($this->getTableName()); $insert->values($values); $this->zdb->execute($insert); - $this->id = (int)$this->zdb->driver->getLastGeneratedValue(); + //without the sequence name, pgsql gives no value + $this->id = (int)$this->zdb->connection->getLastGeneratedValue( + $this->zdb->isPostgres() ? $this->zdb->getSequenceName($this->getTableName(), 'id', prefixed: true) : null + ); Analog::log( 'An entry has been added in helloasso history', diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index b9da47b..7d46250 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -10,9 +10,11 @@ namespace GaletteHelloasso\Controllers\tests\units; +use Galette\Entity\Contribution; use Galette\Entity\ContributionsTypes; use Galette\Tests\GaletteRoutingTestCase; use GaletteHelloasso\Helloasso; +use GaletteHelloasso\HelloassoHistory; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; @@ -53,6 +55,7 @@ public function setUp(): void public function tearDown(): void { putenv('HTTPS_PROXY'); + unset($_SERVER['REMOTE_ADDR']); $this->login->logout(); parent::tearDown(); } @@ -425,4 +428,114 @@ public function testCheckoutRoundsAmount(): void $this->assertSame(1999, $checkout['totalAmount']); $this->assertSame(1999, $checkout['initialAmount']); } + + /** + * Build a payment notification + * + * @param int $id_adh Member ID + * @param int $id_type Contribution type ID + * @param int $amount Amount, in cents + * + * @return array + */ + private function getPaymentNotification(int $id_adh, int $id_type, int $amount): array + { + return [ + 'eventType' => 'Payment', + 'data' => [ + 'id' => 98765, + 'amount' => $amount, + 'state' => 'Authorized', + 'cashOutState' => 'Transfered', + 'paymentMeans' => 'Card', + 'paymentReceiptUrl' => 'https://www.helloasso-sandbox.com/paiement/98765', + 'payer' => ['firstName' => 'Jane', 'lastName' => 'Doe'] + ], + 'metadata' => [ + 'member_id' => $id_adh, + 'item_id' => $id_type, + 'item_name' => 'donation in money' + ] + ]; + } + + /** + * Post a notification to the webhook, from HelloAsso address + * + * @param array $notification Notification + */ + private function postWebhook(array $notification): ResponseInterface + { + //HelloAsso production address + $_SERVER['REMOTE_ADDR'] = '51.138.206.200'; + $sfactory = new \Slim\Psr7\Factory\StreamFactory(); + $request = $this->createRequest('helloasso_webhook', [], 'POST', 'application/json') + ->withBody($sfactory->createStream(json_encode($notification, JSON_THROW_ON_ERROR))); + return $this->app->handle($request); + } + + /** + * Count contributions of a member + * + * @param int $id_adh Member ID + */ + private function countContributions(int $id_adh): int + { + $select = $this->zdb->select(Contribution::TABLE); + $select->where([\Galette\Entity\Adherent::PK => $id_adh]); + return $this->zdb->execute($select)->count(); + } + + /** + * Get history entries, latest first + * + * @return array> + */ + private function getHistory(): array + { + $select = $this->zdb->select(HELLOASSO_PREFIX . HelloassoHistory::TABLE); + $select->order(HelloassoHistory::PK . ' DESC'); + $entries = []; + foreach ($this->zdb->execute($select) as $entry) { + $entries[] = $entry; + } + return $entries; + } + + /** + * Webhook stores the contribution once, even when notified again + */ + public function testWebhook(): void + { + $member = $this->getMemberOne(); + + $test_response = $this->postWebhook($this->getPaymentNotification($member->id, 5, 1250)); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $this->assertSame(1, $this->countContributions($member->id)); + $history = $this->getHistory(); + $this->assertCount(1, $history); + $this->assertSame(HelloassoHistory::STATE_PROCESSED, (int)$history[0]->state); + $this->assertSame('DOE Jane', $history[0]->payer_name); + + //notified again + $test_response = $this->postWebhook($this->getPaymentNotification($member->id, 5, 1250)); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectLogEntry(\Analog\Analog::WARNING, 'has already been processed'); + $this->expectNoLogEntry(); + $this->assertSame(1, $this->countContributions($member->id)); + $history = $this->getHistory(); + $this->assertCount(2, $history); + $this->assertSame(HelloassoHistory::STATE_ALREADYDONE, (int)$history[0]->state); + + //from another address + $_SERVER['REMOTE_ADDR'] = '192.0.2.1'; + $sfactory = new \Slim\Psr7\Factory\StreamFactory(); + $request = $this->createRequest('helloasso_webhook', [], 'POST', 'application/json') + ->withBody($sfactory->createStream(json_encode($this->getPaymentNotification($member->id, 5, 1250)))); + $this->assertSame(403, $this->app->handle($request)->getStatusCode()); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Unauthorized Helloasso notification'); + $this->expectNoLogEntry(); + $this->assertCount(2, $this->getHistory()); + } } From d40eb505fbfece6018fa3182a467998febb3b330 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:20:42 +0200 Subject: [PATCH 07/15] Never store a contribution without its history entry --- .../Controllers/HelloassoController.php | 5 +++- .../tests/units/HelloassoController.php | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/GaletteHelloasso/Controllers/HelloassoController.php b/lib/GaletteHelloasso/Controllers/HelloassoController.php index e65a752..89d9e93 100644 --- a/lib/GaletteHelloasso/Controllers/HelloassoController.php +++ b/lib/GaletteHelloasso/Controllers/HelloassoController.php @@ -426,7 +426,10 @@ public function webhook(Request $request, Response $response): Response && $post['metadata']['item_id'] ) { $hh = new HelloassoHistory($this->zdb, $this->login, $this->preferences); - $hh->add($post); + if (!$hh->add($post)) { + //HelloAsso will send it again + return $response->withStatus(500, 'Internal error'); + } // are we working on a real contribution? $real_contrib = false; diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index 7d46250..6ab02da 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -538,4 +538,32 @@ public function testWebhook(): void $this->expectNoLogEntry(); $this->assertCount(2, $this->getHistory()); } + + /** + * No contribution is stored when the payment cannot be added to history + */ + public function testWebhookHistoryFailure(): void + { + $member = $this->getMemberOne(); + $notification = $this->getPaymentNotification($member->id, 5, 1250); + //too long for its column + $notification['data']['payer']['lastName'] = str_repeat('Doe', 100); + + //on PostgreSQL, an error aborts the whole test transaction + $savepoint = $this->zdb->isPostgres(); + if ($savepoint) { + $this->zdb->db->query('SAVEPOINT history_failure', \Laminas\Db\Adapter\Adapter::QUERY_MODE_EXECUTE); + } + $test_response = $this->postWebhook($notification); + if ($savepoint) { + $this->zdb->db->query('ROLLBACK TO SAVEPOINT history_failure', \Laminas\Db\Adapter\Adapter::QUERY_MODE_EXECUTE); + } + + $this->assertSame(500, $test_response->getStatusCode()); + $this->expectLogEntry(\Analog\Analog::ERROR, 'Query error'); + $this->expectLogEntry(\Analog\Analog::ERROR, 'An error occurred trying to add log entry.'); + $this->expectNoLogEntry(); + $this->assertCount(0, $this->getHistory()); + $this->assertSame(0, $this->countContributions($member->id)); + } } From 662341503b7303c449da7f14a3d16a7012f9920f Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:20:46 +0200 Subject: [PATCH 08/15] Accept member ids fetched as strings in history Test environment sets PDO::ATTR_STRINGIFY_FETCHES, the history page could not be displayed there. --- lib/GaletteHelloasso/HelloassoHistory.php | 2 +- .../tests/units/HelloassoController.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/GaletteHelloasso/HelloassoHistory.php b/lib/GaletteHelloasso/HelloassoHistory.php index d76cf0c..4d90890 100644 --- a/lib/GaletteHelloasso/HelloassoHistory.php +++ b/lib/GaletteHelloasso/HelloassoHistory.php @@ -151,7 +151,7 @@ public function getHelloassoHistory(): array $oa = Galette::jsonDecode($o['request']); } - $o['member_fullname'] = $this->getMemberFullName($o['member_id']); + $o['member_fullname'] = $this->getMemberFullName((int)$o['member_id']); $o['raw_request'] = print_r($oa, true); $o['request'] = $oa; diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index 6ab02da..4886e83 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -566,4 +566,22 @@ public function testWebhookHistoryFailure(): void $this->assertCount(0, $this->getHistory()); $this->assertSame(0, $this->countContributions($member->id)); } + + /** + * History lists stored payments + */ + public function testHistory(): void + { + $member = $this->getMemberOne(); + $this->assertSame(200, $this->postWebhook($this->getPaymentNotification($member->id, 5, 1250))->getStatusCode()); + $this->logSuperAdmin(); + + $test_response = $this->app->handle($this->createRequest('helloasso_history')); + $this->assertSame(200, $test_response->getStatusCode()); + $this->expectNoLogEntry(); + $body = (string)$test_response->getBody(); + $this->assertStringContainsString('98765', $body); + $this->assertStringContainsString('DOE Jane', $body); + $this->assertStringContainsString(mb_strtoupper($member->name) . ' ' . $member->surname, $body); + } } From ec8b9dea58d104715d42fd2db2c66151c07171da Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:21:05 +0200 Subject: [PATCH 09/15] Read serialized history entries as plain data --- lib/GaletteHelloasso/HelloassoHistory.php | 3 +- .../tests/units/HelloassoHistory.php | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/GaletteHelloasso/tests/units/HelloassoHistory.php diff --git a/lib/GaletteHelloasso/HelloassoHistory.php b/lib/GaletteHelloasso/HelloassoHistory.php index 4d90890..697ed84 100644 --- a/lib/GaletteHelloasso/HelloassoHistory.php +++ b/lib/GaletteHelloasso/HelloassoHistory.php @@ -146,7 +146,8 @@ public function getHelloassoHistory(): array foreach ($orig as $o) { try { if (Galette::isSerialized($o['request'])) { - $oa = unserialize($o['request']); + //legacy entries: only plain data is expected + $oa = unserialize($o['request'], ['allowed_classes' => false]); } else { $oa = Galette::jsonDecode($o['request']); } diff --git a/tests/GaletteHelloasso/tests/units/HelloassoHistory.php b/tests/GaletteHelloasso/tests/units/HelloassoHistory.php new file mode 100644 index 0000000..4bed6cc --- /dev/null +++ b/tests/GaletteHelloasso/tests/units/HelloassoHistory.php @@ -0,0 +1,67 @@ + + */ +class HelloassoHistory extends GaletteTestCase +{ + protected int $seed = 20260928081005; + + /** + * Add an history entry with a serialized request + * + * @param string $request Stored request + */ + private function addSerializedEntry(string $request): void + { + $insert = $this->zdb->insert(HELLOASSO_PREFIX . \GaletteHelloasso\HelloassoHistory::TABLE); + $insert->values([ + 'history_date' => date('Y-m-d H:i:s'), + 'checkout_id' => '1', + 'amount' => 10, + 'comments' => 'donation in money', + 'request' => $request, + 'state' => \GaletteHelloasso\HelloassoHistory::STATE_PUBLIC, + 'payer_name' => 'DOE Jane', + 'member_id' => 0, + 'method' => 'Card', + 'receipt_url' => '' + ]); + $this->zdb->execute($insert); + } + + /** + * Serialized entries are read as plain data + */ + public function testSerializedEntries(): void + { + $this->addSerializedEntry(serialize(['item_id' => '5', 'item_name' => 'donation in money'])); + $this->addSerializedEntry(serialize(new \ArrayObject(['item_id' => '5']))); + + $history = new \GaletteHelloasso\HelloassoHistory($this->zdb, $this->login, $this->preferences); + $history->setFilters(new HelloassoHistoryList()); + $entries = $history->getHelloassoHistory(); + + $this->assertCount(2, $entries); + $requests = array_column($entries, 'request'); + $this->assertContains(['item_id' => '5', 'item_name' => 'donation in money'], $requests); + foreach ($requests as $request) { + $this->assertNotInstanceOf(\ArrayObject::class, $request); + } + } +} From 74940959545644ce3fb48a23303b87b42bd6d478 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:21:30 +0200 Subject: [PATCH 10/15] Do not require a checkout id on the error page --- templates/default/helloasso_error.html.twig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/default/helloasso_error.html.twig b/templates/default/helloasso_error.html.twig index de6a16e..f88b289 100644 --- a/templates/default/helloasso_error.html.twig +++ b/templates/default/helloasso_error.html.twig @@ -13,7 +13,9 @@ {{ _T("Your Helloasso payment has failed for technical reasons.", "helloasso") }} {% if error %}
    -
  • {{ _T("Payment nb.", "helloasso") }} : {{ error.checkoutIntentId }}
  • + {% if error.checkoutIntentId is defined %} +
  • {{ _T("Payment nb.", "helloasso") }} : {{ error.checkoutIntentId }}
  • + {% endif %} {% if error.error is defined %}
  • {{ _T("Error", "helloasso") }} : {{ error.error }}
  • {% endif %} From bf9ac0061e1c7759b09136dc88b1b2d98e85c7f3 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:24:22 +0200 Subject: [PATCH 11/15] Fix upgrade from 1.0.0 The PostgreSQL script failed on filled tables and on the text request column, previous incomplete state became public, and PostgreSQL loses time of payments and stores amounts as floating point numbers. --- scripts/pgsql.sql | 4 +-- scripts/upgrade-to-1.1.0-mysql.sql | 22 +++++++++------- scripts/upgrade-to-1.1.0-pgsql.sql | 40 ++++++++++++++++++------------ 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/scripts/pgsql.sql b/scripts/pgsql.sql index 576a03c..b484acb 100644 --- a/scripts/pgsql.sql +++ b/scripts/pgsql.sql @@ -15,9 +15,9 @@ CREATE SEQUENCE galette_helloasso_history_id_seq DROP TABLE IF EXISTS galette_helloasso_history; CREATE TABLE galette_helloasso_history ( id_helloasso integer DEFAULT nextval('galette_helloasso_history_id_seq'::text) NOT NULL, - history_date date NOT NULL, + history_date timestamp NOT NULL, checkout_id character varying(255), - amount real NOT NULL, + amount numeric(15,2) NOT NULL, comments character varying(255), request text, state smallint DEFAULT 0 NOT NULL, diff --git a/scripts/upgrade-to-1.1.0-mysql.sql b/scripts/upgrade-to-1.1.0-mysql.sql index 514de04..64aadda 100644 --- a/scripts/upgrade-to-1.1.0-mysql.sql +++ b/scripts/upgrade-to-1.1.0-mysql.sql @@ -6,10 +6,11 @@ INSERT INTO galette_helloasso_preferences (nom_pref, val_pref) VALUES ('helloasso_sepa_option', ''); +-- without collation, columns get the one of the table ALTER TABLE galette_helloasso_history - MODIFY checkout_id varchar(255) COLLATE utf8mb4_unicode_520_ci, - MODIFY comments varchar(255) COLLATE utf8mb4_unicode_520_ci, - MODIFY request text COLLATE utf8mb4_unicode_520_ci; + MODIFY checkout_id varchar(255), + MODIFY comments varchar(255), + MODIFY request text; ALTER TABLE galette_helloasso_history ADD COLUMN payer_name VARCHAR(255) NOT NULL, @@ -17,20 +18,23 @@ ALTER TABLE galette_helloasso_history ADD COLUMN method VARCHAR(10) NOT NULL, ADD COLUMN receipt_url VARCHAR(255) NOT NULL; +-- 1.0.0 states were 0 (public donation), 1 (processed), 2 (error), 3 (incomplete) +-- and 4 (already done); incomplete payments did not create any contribution UPDATE galette_helloasso_history SET - state = CASE - WHEN state = 0 THEN 3 + state = CASE state + WHEN 0 THEN 3 + WHEN 3 THEN 2 ELSE state END, - payer_name = CONCAT( + payer_name = COALESCE(CONCAT( UPPER(JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.payer.lastName'))), ' ', JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.payer.firstName')) - ), + ), ''), member_id = COALESCE( CAST(JSON_UNQUOTE(JSON_EXTRACT(request, '$.metadata.member_id')) AS UNSIGNED), 0 ), - method = JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentMeans')), - receipt_url = JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentReceiptUrl')); + method = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentMeans')), ''), + receipt_url = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentReceiptUrl')), ''); diff --git a/scripts/upgrade-to-1.1.0-pgsql.sql b/scripts/upgrade-to-1.1.0-pgsql.sql index 6f2d55f..73b5d02 100644 --- a/scripts/upgrade-to-1.1.0-pgsql.sql +++ b/scripts/upgrade-to-1.1.0-pgsql.sql @@ -6,26 +6,34 @@ INSERT INTO galette_helloasso_preferences (nom_pref, val_pref) VALUES ('helloasso_sepa_option', ''); +-- Keep time of payments, and do not store amounts as floating point numbers ALTER TABLE galette_helloasso_history - ADD COLUMN payer_name character varying(255) NOT NULL, - ADD COLUMN member_id integer NOT NULL, - ADD COLUMN method character varying(10) NOT NULL, - ADD COLUMN receipt_url character varying(255) NOT NULL; + ALTER COLUMN history_date TYPE timestamp, + ALTER COLUMN amount TYPE numeric(15,2), + ADD COLUMN payer_name character varying(255) DEFAULT '' NOT NULL, + ADD COLUMN member_id integer DEFAULT 0 NOT NULL, + ADD COLUMN method character varying(10) DEFAULT '' NOT NULL, + ADD COLUMN receipt_url character varying(255) DEFAULT '' NOT NULL; +ALTER TABLE galette_helloasso_history + ALTER COLUMN payer_name DROP DEFAULT, + ALTER COLUMN member_id DROP DEFAULT, + ALTER COLUMN method DROP DEFAULT, + ALTER COLUMN receipt_url DROP DEFAULT; +-- 1.0.0 states were 0 (public donation), 1 (processed), 2 (error), 3 (incomplete) +-- and 4 (already done); incomplete payments did not create any contribution UPDATE galette_helloasso_history SET - state = CASE - WHEN state = 0 THEN 3 + state = CASE state + WHEN 0 THEN 3 + WHEN 3 THEN 2 ELSE state END, - payer_name = CONCAT( - UPPER((request->'data'->'payer'->>'lastName')), + payer_name = TRIM(CONCAT( + UPPER(request::json->'data'->'payer'->>'lastName'), ' ', - (request->'data'->'payer'->>'firstName') - ), - member_id = COALESCE( - (request->'metadata'->>'member_id')::int, - 0 - ), - method = request->'data'->>'paymentMeans', - receipt_url = request->'data'->>'paymentReceiptUrl'; + request::json->'data'->'payer'->>'firstName' + )), + member_id = COALESCE((request::json->'metadata'->>'member_id')::integer, 0), + method = COALESCE(request::json->'data'->>'paymentMeans', ''), + receipt_url = COALESCE(request::json->'data'->>'paymentReceiptUrl', ''); From 93266685cf29a0de7ec0aaa04d7c5e3723083ef8 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:24:22 +0200 Subject: [PATCH 12/15] Run update scripts on installations of 1.0.0 --- .../PluginGaletteHelloasso.php | 13 ++++++++ .../tests/units/PluginGaletteHelloasso.php | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/GaletteHelloasso/tests/units/PluginGaletteHelloasso.php diff --git a/lib/GaletteHelloasso/PluginGaletteHelloasso.php b/lib/GaletteHelloasso/PluginGaletteHelloasso.php index e3815ee..031fff3 100644 --- a/lib/GaletteHelloasso/PluginGaletteHelloasso.php +++ b/lib/GaletteHelloasso/PluginGaletteHelloasso.php @@ -17,6 +17,7 @@ use Galette\Core\Plugins\MenuProviderInterface; use Galette\Core\Preferences; use Galette\Core\GalettePlugin; +use Laminas\Db\Metadata\Source\Factory; /** * Galette HelloAsso plugin @@ -132,4 +133,16 @@ public function isInstalled(): bool && $this->zdb->tableExists(HELLOASSO_PREFIX . HelloassoHistory::TABLE) ; } + + /** + * Database version of tables installed before versions tracking + * + * Payer, member, method and receipt are history columns since 1.1.0. + */ + public function getLegacyDbVersion(): ?float + { + $metadata = Factory::createSourceFromAdapter($this->zdb->db); + $columns = $metadata->getColumnNames(PREFIX_DB . HELLOASSO_PREFIX . HelloassoHistory::TABLE); + return in_array('payer_name', $columns, true) ? null : 1.0; + } } diff --git a/tests/GaletteHelloasso/tests/units/PluginGaletteHelloasso.php b/tests/GaletteHelloasso/tests/units/PluginGaletteHelloasso.php new file mode 100644 index 0000000..412d1e8 --- /dev/null +++ b/tests/GaletteHelloasso/tests/units/PluginGaletteHelloasso.php @@ -0,0 +1,32 @@ + + */ +class PluginGaletteHelloasso extends GaletteTestCase +{ + protected int $seed = 20260928091512; + + /** + * Current tables are not legacy ones + */ + public function testLegacyDbVersion(): void + { + $plugin = $this->container->get(\GaletteHelloasso\PluginGaletteHelloasso::class); + $this->assertNull($plugin->getLegacyDbVersion()); + } +} From d15d9dbccd49e14a5315ced2f46f4c8de9646816 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:24:41 +0200 Subject: [PATCH 13/15] Test payments time is kept --- .../Controllers/tests/units/HelloassoController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php index 4886e83..ab48929 100644 --- a/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php +++ b/tests/GaletteHelloasso/Controllers/tests/units/HelloassoController.php @@ -517,6 +517,9 @@ public function testWebhook(): void $this->assertCount(1, $history); $this->assertSame(HelloassoHistory::STATE_PROCESSED, (int)$history[0]->state); $this->assertSame('DOE Jane', $history[0]->payer_name); + //time is kept + $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $history[0]->history_date); + $this->assertEquals(12.5, $history[0]->amount); //notified again $test_response = $this->postWebhook($this->getPaymentNotification($member->id, 5, 1250)); From 15293ef42442277d3e1198b2968d433c217afba8 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Mon, 28 Sep 2026 08:26:05 +0200 Subject: [PATCH 14/15] Check coding standards and static analysis of tests --- .github/workflows/ci-linux.yml | 2 +- .php-cs-fixer.dist.php | 1 + phpstan.neon | 1 + tests/GaletteHelloasso/tests/units/Helloasso.php | 2 ++ tests/TestsBootstrap.php | 6 ++++-- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 9938bbc..11b5dc7 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -89,7 +89,7 @@ jobs: - name: CS run: | cd galette-core/galette/plugins/plugin-helloasso - ../../vendor/bin/phpcs lib/ ./*.php + ../../vendor/bin/phpcs lib/ tests/ ./*.php - name: CS Fixer if: matrix.php-is-min diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 975ea86..8a28633 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -4,6 +4,7 @@ ->in([ __DIR__ . '/lib', __DIR__ . '/tests', + __DIR__ . '/tests', ]) ; diff --git a/phpstan.neon b/phpstan.neon index 299cd45..dd92411 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,7 @@ parameters: level: 6 paths: - lib/ + - tests/ scanFiles: - _config.inc.php - ../../includes/sys_config/paths.inc.php diff --git a/tests/GaletteHelloasso/tests/units/Helloasso.php b/tests/GaletteHelloasso/tests/units/Helloasso.php index 46efb25..c0cabaa 100644 --- a/tests/GaletteHelloasso/tests/units/Helloasso.php +++ b/tests/GaletteHelloasso/tests/units/Helloasso.php @@ -6,6 +6,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +declare(strict_types=1); + namespace GaletteHelloasso\tests\units; use Galette\Tests\GaletteTestCase; diff --git a/tests/TestsBootstrap.php b/tests/TestsBootstrap.php index c2e5f57..8b9c516 100644 --- a/tests/TestsBootstrap.php +++ b/tests/TestsBootstrap.php @@ -6,6 +6,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +declare(strict_types=1); + /** * Bootstrap tests file for Galette Helloasso plugin * @@ -13,7 +15,7 @@ */ define('GALETTE_PLUGINS_PATH', __DIR__ . '/../../'); -$basepath = '../../../galette/'; +$basepath = __DIR__ . '/../../../'; // phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable -- used from Core testBootstrap -include_once '../../../tests/TestsBootstrap.php'; +include_once __DIR__ . '/../../../../tests/TestsBootstrap.php'; require_once __DIR__ . '/../_config.inc.php'; From a3acdc4a8b1c956943ce7015ec498192e8241682 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Sun, 4 Oct 2026 16:16:43 +0200 Subject: [PATCH 15/15] Update .php-cs-fixer.dist.php Co-authored-by: Guillaume AGNIERAY <107203963+gagnieray@users.noreply.github.com> --- .php-cs-fixer.dist.php | 1 - 1 file changed, 1 deletion(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 8a28633..975ea86 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -4,7 +4,6 @@ ->in([ __DIR__ . '/lib', __DIR__ . '/tests', - __DIR__ . '/tests', ]) ;