From 86ac7aab7039914d33be059aba53b652427aa93c Mon Sep 17 00:00:00 2001 From: Jules Cournut Date: Tue, 20 Oct 2026 15:51:32 +0200 Subject: [PATCH] fix: create the tables on activation only when they are missing --- Config/module.xml | 2 +- ShortCode.php | 25 ++++-- Tests/Integration/ActivationTest.php | 127 +++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 Tests/Integration/ActivationTest.php diff --git a/Config/module.xml b/Config/module.xml index cc24390..6b4e697 100644 --- a/Config/module.xml +++ b/Config/module.xml @@ -20,7 +20,7 @@ en_US fr_FR - 3.0.2 + 3.0.3 Vincent Lopes-Vicente diff --git a/ShortCode.php b/ShortCode.php index 2709323..ff97986 100644 --- a/ShortCode.php +++ b/ShortCode.php @@ -13,6 +13,7 @@ namespace ShortCode; use Propel\Runtime\Connection\ConnectionInterface; +use Propel\Runtime\Propel; use Propel\Runtime\Exception\PropelException; use ShortCode\Model\ShortCodeQuery; use Thelia\Core\Install\Database; @@ -23,14 +24,20 @@ class ShortCode extends BaseModule /** @var string */ const DOMAIN_NAME = 'shortcode'; + /** + * Create the tables on a fresh install. + * + * The install script starts with DROP TABLE, so it only runs when the tables are missing. + * The database is asked rather than an "is_initialized" flag in the module configuration: + * a shop whose tables were created another way, or whose configuration was lost, would + * otherwise have every short code wiped by a mere reactivation. + */ public function preActivation(?ConnectionInterface $con = null): bool { - if (!$this->getConfigValue('is_initialized', false)) { - $database = new Database($con); + $con ??= Propel::getWriteConnection('TheliaMain'); - $database->insertSql(null, array(__DIR__ . '/Config/TheliaMain.sql')); - - $this->setConfigValue('is_initialized', true); + if (!$this->hasTable($con, 'short_code')) { + (new Database($con))->insertSql(null, [__DIR__.'/Config/TheliaMain.sql']); } return true; @@ -86,4 +93,12 @@ public static function deactivateShortCode($shortCodeName) ->save(); } } + + private function hasTable(ConnectionInterface $con, string $table): bool + { + $statement = $con->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table'); + $statement->execute(['table' => $table]); + + return (int) $statement->fetchColumn() > 0; + } } diff --git a/Tests/Integration/ActivationTest.php b/Tests/Integration/ActivationTest.php new file mode 100644 index 0000000..0de337b --- /dev/null +++ b/Tests/Integration/ActivationTest.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ShortCode\Tests\Integration; + +use ShortCode\ShortCode; +use PHPUnit\Framework\Attributes\Test; +use Propel\Runtime\Connection\ConnectionInterface; +use Propel\Runtime\Propel; +use Thelia\Core\Install\Database; +use Thelia\Model\ModuleConfigQuery; +use Thelia\Model\ModuleQuery; +use Thelia\Test\IntegrationTestCase; + +/** + * Activating the module creates its tables on a fresh install, and never touches the + * short codes of a shop that already has them. + * + * The install script starts with DROP TABLE, which commits implicitly: no test transaction, + * the tables are restored and the test row removed by hand. + */ +final class ActivationTest extends IntegrationTestCase +{ + private const INSTALL_SCRIPT = __DIR__.'/../../Config/TheliaMain.sql'; + + protected bool $useTransaction = false; + + private ConnectionInterface $con; + + private ?int $rowId = null; + + protected function setUp(): void + { + parent::setUp(); + + $this->con = Propel::getWriteConnection('TheliaMain'); + } + + protected function tearDown(): void + { + if (!$this->hasTable('short_code')) { + (new Database($this->con))->insertSql(null, [self::INSTALL_SCRIPT]); + } + + if (null !== $this->rowId) { + $this->con->prepare('DELETE FROM `short_code` WHERE `id` = :id')->execute(['id' => $this->rowId]); + } + + parent::tearDown(); + } + + #[Test] + public function reactivatingTheModuleKeepsTheExistingRowsEvenWithoutItsInstallFlag(): void + { + $this->con->exec("INSERT INTO `short_code` (`tag`, `event`, `active`) VALUES ('activation_test', 'activation_test', 1)"); + $this->rowId = (int) $this->con->lastInsertId(); + + // A shop whose tables were created without going through the activation: by a schema + // command, or with its module configuration lost. + $this->forgetInstallFlag(); + + $this->activate(); + + self::assertSame(1, (int) $this->con->query('SELECT COUNT(*) FROM `short_code` WHERE `id` = '.$this->rowId)->fetchColumn()); + } + + #[Test] + public function activatingTheModuleOnAFreshInstallCreatesItsTables(): void + { + $tables = $this->installScriptTables(); + + $this->forgetInstallFlag(); + $this->con->exec('SET FOREIGN_KEY_CHECKS = 0'); + foreach ($tables as $table) { + $this->con->exec('DROP TABLE IF EXISTS `'.$table.'`'); + } + $this->con->exec('SET FOREIGN_KEY_CHECKS = 1'); + + $this->activate(); + + foreach ($tables as $table) { + self::assertTrue($this->hasTable($table), $table.' is created.'); + } + } + + private function activate(): void + { + $module = new ShortCode(); + self::assertTrue($module->preActivation($this->con)); + $module->postActivation($this->con); + } + + private function forgetInstallFlag(): void + { + $moduleId = (int) ModuleQuery::create()->findOneByCode('ShortCode')?->getId(); + ModuleConfigQuery::create()->filterByModuleId($moduleId)->filterByName('is_initialized')->delete(); + } + + /** + * @return list + */ + private function installScriptTables(): array + { + preg_match_all('/^CREATE TABLE `(\w+)`/m', (string) file_get_contents(self::INSTALL_SCRIPT), $matches); + + return $matches[1]; + } + + private function hasTable(string $table): bool + { + $statement = $this->con->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table'); + $statement->execute(['table' => $table]); + + return (int) $statement->fetchColumn() > 0; + } +}