From 462bb34447534470a415158ec0e1f77ede2bf80e Mon Sep 17 00:00:00 2001 From: Daniel Seifert Date: Fri, 10 Feb 2023 00:52:44 +0100 Subject: [PATCH] add migrations --- README.en.md | 8 +- README.md | 8 +- composer.json | 3 +- .../Model/Credential/PublicKeyCredential.php | 6 +- src/Application/Model/WebauthnLogin.php | 10 +- src/Config/oxid.yaml | 9 +- src/Setup/Actions.php | 90 +----- src/Setup/Events.php | 2 +- src/migration/data/Version20230209212939.php | 112 +++++++ src/migration/migrations.yml | 4 + src/tests/phpunit.xml | 1 + src/tests/unit/Setup/ActionsTest.php | 142 ++------ .../data/Version20230209212939Test.php | 303 ++++++++++++++++++ 13 files changed, 472 insertions(+), 226 deletions(-) create mode 100644 src/migration/data/Version20230209212939.php create mode 100755 src/migration/migrations.yml create mode 100644 src/tests/unit/migration/data/Version20230209212939Test.php diff --git a/README.en.md b/README.en.md index 161d1ee..9630ace 100644 --- a/README.en.md +++ b/README.en.md @@ -63,12 +63,14 @@ and its requirements. The Flow and Wave themes are supported by default. Other themes may require customisation. -## Module installation +## Module installation / update -Open a command line interface and navigate to the shop root directory (parent of source and vendor). Execute the following command. Adapt the paths to your environment. +Open a command line interface and navigate to the shop root directory (parent of source and vendor). Execute the following commands. Adapt the paths to your environment. ```bash -php composer require d3/oxid-twofactor-passwordless:^1.0 +composer require d3/oxid-twofactor-passwordless:^1.0 + +./vendor/bin/oe-eshop-db_migrate migrations:migrate d3webauthn ``` If a reference to an unsuitable package `symfony/process` is shown, this must be changed. To do this, please add the switch `-W` to the above command (`... require -W ...`). diff --git a/README.md b/README.md index 922f606..a52c894 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,14 @@ und dessen Anforderungen. Im Standard wird das Flow- und Wave-Theme unterstützt. Andere Themes können Anpassungen erfordern. -## Modulinstallation +## Modulinstallation / -update -Öffnen Sie eine Kommandozeile und navigieren Sie zum Stammverzeichnis des Shops (Elternverzeichnis von source und vendor). Führen Sie den folgenden Befehl aus. Passen Sie die Pfadangaben an Ihre Installationsumgebung an. +Öffnen Sie eine Kommandozeile und navigieren Sie zum Stammverzeichnis des Shops (Elternverzeichnis von source und vendor). Führen Sie die folgenden Befehle aus. Passen Sie die Pfadangaben an Ihre Installationsumgebung an. ```bash -php composer require d3/oxid-twofactor-passwordless:^1.0 +composer require d3/oxid-twofactor-passwordless:^1.0 + +./vendor/bin/oe-eshop-db_migrate migrations:migrate d3webauthn ``` Wird ein Hinweis auf ein unpassendes Paket "symfony/process" gezeigt, muss dieses geändert werden. Fügen Sie dazu in den oben genannten Befehl bitte den Schalter `-W` ein (`... require -W ...`). diff --git a/composer.json b/composer.json index 85c2a79..71e8479 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,8 @@ }, "autoload": { "psr-4": { - "D3\\Webauthn\\": "../../../source/modules/d3/oxwebauthn" + "D3\\Webauthn\\": "../../../source/modules/d3/oxwebauthn", + "D3\\Webauthn\\Migrations\\": "../../../source/modules/d3/oxwebauthn/migration/data" } }, "suggest": { diff --git a/src/Application/Model/Credential/PublicKeyCredential.php b/src/Application/Model/Credential/PublicKeyCredential.php index 15b2196..5d674f1 100755 --- a/src/Application/Model/Credential/PublicKeyCredential.php +++ b/src/Application/Model/Credential/PublicKeyCredential.php @@ -19,7 +19,7 @@ use Assert\Assert; use Assert\AssertionFailedException; use Assert\InvalidArgumentException; use D3\TestingTools\Production\IsMockable; -use D3\Webauthn\Setup\Actions; +use D3\Webauthn\Migrations\Version20230209212939; use DateTime; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Exception as DoctrineException; @@ -76,7 +76,7 @@ class PublicKeyCredential extends BaseModel Assert::that($encodedCID) ->maxLength( - Actions::FIELDLENGTH_CREDID, + Version20230209212939::FIELDLENGTH_CREDID, 'the credentialId (%3$d) does not fit into the database field (%2$d)' ); @@ -126,7 +126,7 @@ class PublicKeyCredential extends BaseModel Assert::that($encodedCredential) ->maxLength( - Actions::FIELDLENGTH_CREDENTIAL, + Version20230209212939::FIELDLENGTH_CREDENTIAL, 'the credential source (%3$d) does not fit into the database field (%2$d)', ); diff --git a/src/Application/Model/WebauthnLogin.php b/src/Application/Model/WebauthnLogin.php index c2a01d3..3b0c642 100644 --- a/src/Application/Model/WebauthnLogin.php +++ b/src/Application/Model/WebauthnLogin.php @@ -110,10 +110,10 @@ class WebauthnLogin { /** @var UtilsView $myUtilsView */ $myUtilsView = d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class); + /** @var d3_User_Webauthn $user */ + $user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); try { - /** @var d3_User_Webauthn $user */ - $user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); $userId = $this->getUserId(); $this->handleErrorMessage(); @@ -158,10 +158,10 @@ class WebauthnLogin { /** @var UtilsView $myUtilsView */ $myUtilsView = d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class); + /** @var d3_User_Webauthn $user */ + $user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); try { - /** @var d3_User_Webauthn $user */ - $user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); $userId = $this->getUserId(); $this->handleErrorMessage(); @@ -195,8 +195,6 @@ class WebauthnLogin $user->logout(); $oStr = Str::getStr(); - d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getActiveView() - ->addTplParam('user', $oStr->htmlspecialchars($userId)); d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getActiveView() ->addTplParam('profile', $oStr->htmlspecialchars($selectedProfile)); diff --git a/src/Config/oxid.yaml b/src/Config/oxid.yaml index 39f75e6..7730a87 100644 --- a/src/Config/oxid.yaml +++ b/src/Config/oxid.yaml @@ -135,4 +135,11 @@ services: - 'getDb' arguments: - 2 - shared: true \ No newline at end of file + shared: true + + d3ox.webauthn.OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder: + class: 'OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder' + factory: 'oxNew' + arguments: + - 'OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder' + shared: false \ No newline at end of file diff --git a/src/Setup/Actions.php b/src/Setup/Actions.php index 38fd248..fe0abdc 100644 --- a/src/Setup/Actions.php +++ b/src/Setup/Actions.php @@ -18,12 +18,10 @@ namespace D3\Webauthn\Setup; use D3\TestingTools\Production\IsMockable; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Exception; +use OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder; use OxidEsales\Eshop\Application\Controller\FrontendController; use OxidEsales\Eshop\Core\Config; -use OxidEsales\Eshop\Core\Database\Adapter\DatabaseInterface; use OxidEsales\Eshop\Core\DbMetaDataHandler; -use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; -use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; use OxidEsales\Eshop\Core\SeoEncoder; use OxidEsales\Eshop\Core\Utils; use OxidEsales\Eshop\Core\UtilsView; @@ -39,93 +37,17 @@ use Psr\Log\LoggerInterface; class Actions { use IsMockable; - public const FIELDLENGTH_CREDID = 512; - public const FIELDLENGTH_CREDENTIAL = 2000; public $seo_de = 'sicherheitsschluessel'; public $seo_en = 'en/key-authentication'; public $stdClassName = 'd3_account_webauthn'; - /** - * SQL statement, that will be executed only at the first time of module installation. - * - * @var string - */ - protected $createCredentialSql = - "CREATE TABLE `d3wa_usercredentials` ( - `OXID` char(32) NOT NULL, - `OXUSERID` char(32) NOT NULL, - `OXSHOPID` int(11) NOT NULL, - `NAME` varchar(100) NOT NULL, - `CREDENTIALID` varchar(".self::FIELDLENGTH_CREDID.") NOT NULL, - `CREDENTIAL` varchar(".self::FIELDLENGTH_CREDENTIAL.") NOT NULL, - `OXTIMESTAMP` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - PRIMARY KEY (`OXID`), - KEY `CREDENTIALID_IDX` (`CREDENTIALID`), - KEY `SHOPUSER_IDX` (`OXUSERID`,`OXSHOPID`) USING BTREE - ) ENGINE=InnoDB COMMENT='WebAuthn Credentials';"; - - /** - * Execute the sql at the first time of the module installation. - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - public function setupModule() + public function runModuleMigrations() { - if (!$this->tableExists('d3wa_usercredentials')) { - $this->executeSQL($this->createCredentialSql); - } - } - - /** - * Check if table exists - * - * @param string $sTableName table name - * - * @return bool - */ - public function tableExists(string $sTableName): bool - { - $oDbMetaDataHandler = d3GetOxidDIC()->get('d3ox.webauthn.'.DbMetaDataHandler::class); - return $oDbMetaDataHandler->tableExists($sTableName); - } - - /** - * @return DatabaseInterface|null - * @throws DatabaseConnectionException - */ - protected function d3GetDb(): ?DatabaseInterface - { - /** @var DatabaseInterface $db */ - $db = d3GetOxidDIC()->get('d3ox.webauthn.'.DatabaseInterface::class.'.assoc'); - return $db; - } - - /** - * Executes given sql statement. - * - * @param string $sSQL Sql to execute. - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - public function executeSQL(string $sSQL) - { - $this->d3GetDb()->execute($sSQL); - } - - /** - * Check if field exists in table - * - * @param string $sFieldName field name - * @param string $sTableName table name - * - * @return bool - */ - public function fieldExists(string $sFieldName, string $sTableName): bool - { - $oDbMetaDataHandler = d3GetOxidDIC()->get('d3ox.webauthn.'.DbMetaDataHandler::class); - return $oDbMetaDataHandler->fieldExists($sFieldName, $sTableName); + /** @var MigrationsBuilder $migrationsBuilder */ + $migrationsBuilder = d3GetOxidDIC()->get('d3ox.webauthn.'.MigrationsBuilder::class); + $migrations = $migrationsBuilder->build(); + $migrations->execute('migrations:migrate', 'd3webauthn'); } /** diff --git a/src/Setup/Events.php b/src/Setup/Events.php index d657816..f0e5fe3 100755 --- a/src/Setup/Events.php +++ b/src/Setup/Events.php @@ -34,7 +34,7 @@ class Events } $actions = oxNew(Actions::class); - $actions->setupModule(); + $actions->runModuleMigrations(); $actions->regenerateViews(); $actions->clearCache(); $actions->seoUrl(); diff --git a/src/migration/data/Version20230209212939.php b/src/migration/data/Version20230209212939.php new file mode 100644 index 0000000..42a0e3f --- /dev/null +++ b/src/migration/data/Version20230209212939.php @@ -0,0 +1,112 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\DateTimeType; +use Doctrine\DBAL\Types\IntegerType; +use Doctrine\DBAL\Types\StringType; +use Doctrine\Migrations\AbstractMigration; + +final class Version20230209212939 extends AbstractMigration +{ + public const FIELDLENGTH_CREDID = 512; + public const FIELDLENGTH_CREDENTIAL = 2000; + + public function getDescription() : string + { + return 'create credential database table'; + } + + public function up(Schema $schema) : void + { + $this->connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); + + $table = !$schema->hasTable('d3wa_usercredentials') ? + $schema->createTable('d3wa_usercredentials') : + $schema->getTable('d3wa_usercredentials'); + + if (!$table->hasColumn('OXID')) { + $table->addColumn('OXID', (new StringType())->getName()) + ->setLength(32) + ->setFixed(true) + ->setNotnull(true); + } + + if (!$table->hasColumn('OXUSERID')) { + $table->addColumn('OXUSERID', (new StringType())->getName()) + ->setLength(32) + ->setFixed(true) + ->setNotnull(true); + } + + if (!$table->hasColumn('OXSHOPID')) { + $table->addColumn('OXSHOPID', (new IntegerType())->getName()) + ->setLength(11) + ->setNotnull(true); + } + + if (!$table->hasColumn('NAME')) { + $table->addColumn('NAME', (new StringType())->getName()) + ->setLength(100) + ->setFixed(false) + ->setNotnull(true); + } + + if (!$table->hasColumn('CREDENTIALID')) { + $table->addColumn('CREDENTIALID', (new StringType())->getName()) + ->setLength(self::FIELDLENGTH_CREDID) + ->setFixed(false) + ->setNotnull(true); + } + + if (!$table->hasColumn('CREDENTIAL')) { + $table->addColumn('CREDENTIAL', (new StringType())->getName()) + ->setLength(self::FIELDLENGTH_CREDENTIAL) + ->setFixed(false) + ->setNotnull(true); + } + + if (!$table->hasColumn('OXTIMESTAMP')) { + $table->addColumn('OXTIMESTAMP', (new DateTimeType())->getName()) + ->setType(new DateTimeType()) + ->setNotnull(true) + // can't set default value via default method + ->setColumnDefinition('timestamp DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'); + } + + if (!$table->hasPrimaryKey()) { + $table->setPrimaryKey(['OXID']); + } + + if (!$table->hasIndex('SHOPUSER_IDX')) { + $table->addIndex(['OXUSERID', 'OXSHOPID'], 'SHOPUSER_IDX'); + } + + if (!$table->hasIndex('CREDENTIALID_IDX')) { + $table->addIndex(['CREDENTIALID'], 'CREDENTIALID_IDX'); + } + + $table->setComment('WebAuthn Credentials'); + } + + public function down(Schema $schema) : void + { + if ($schema->hasTable('d3wa_usercredentials')) { + $schema->dropTable('d3wa_usercredentials'); + } + } +} diff --git a/src/migration/migrations.yml b/src/migration/migrations.yml new file mode 100755 index 0000000..e60e5e7 --- /dev/null +++ b/src/migration/migrations.yml @@ -0,0 +1,4 @@ +name: D3 Twofactor Passwordless +migrations_namespace: D3\Webauthn\Migrations +table_name: d3wa_migrations +migrations_directory: data \ No newline at end of file diff --git a/src/tests/phpunit.xml b/src/tests/phpunit.xml index fd4421b..733a177 100644 --- a/src/tests/phpunit.xml +++ b/src/tests/phpunit.xml @@ -15,6 +15,7 @@ ../Application + ../migration ../Modules ../Setup diff --git a/src/tests/unit/Setup/ActionsTest.php b/src/tests/unit/Setup/ActionsTest.php index 5779716..d7e840f 100644 --- a/src/tests/unit/Setup/ActionsTest.php +++ b/src/tests/unit/Setup/ActionsTest.php @@ -19,9 +19,9 @@ use D3\TestingTools\Development\CanAccessRestricted; use D3\Webauthn\Setup\Actions; use D3\Webauthn\tests\unit\WAUnitTestCase; use Exception; +use OxidEsales\DoctrineMigrationWrapper\Migrations; +use OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder; use OxidEsales\Eshop\Application\Controller\FrontendController; -use OxidEsales\Eshop\Core\Database\Adapter\DatabaseInterface; -use OxidEsales\Eshop\Core\Database\Adapter\Doctrine\Database; use OxidEsales\Eshop\Core\DbMetaDataHandler; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\SeoEncoder; @@ -45,141 +45,35 @@ class ActionsTest extends WAUnitTestCase /** * @test - * @param $tableExist - * @param $expectedInvocation * @return void * @throws ReflectionException - * @covers \D3\Webauthn\Setup\Actions::setupModule - * @dataProvider canSetupModuleDataProvider + * @covers \D3\Webauthn\Setup\Actions::runModuleMigrations() */ - public function canSetupModule($tableExist, $expectedInvocation) + public function canRunModuleMigrations() { - /** @var Actions|MockObject $sut */ - $sut = $this->getMockBuilder(Actions::class) - ->onlyMethods(['tableExists', 'executeSQL']) - ->getMock(); - $sut->method('tableExists')->willReturn($tableExist); - $sut->expects($expectedInvocation)->method('executeSQL')->willReturn(true); - - $this->callMethod( - $sut, - 'setupModule' - ); - } - - /** - * @return array[] - */ - public function canSetupModuleDataProvider(): array - { - return [ - 'table exist' => [true, $this->never()], - 'table not exist' => [false, $this->once()], - ]; - } - - /** - * @test - * @return void - * @throws ReflectionException - * @covers \D3\Webauthn\Setup\Actions::tableExists - */ - public function canCheckTableExists() - { - $expected = true; - - /** @var DbMetaDataHandler|MockObject $DbMetaDataMock */ - $DbMetaDataMock = $this->getMockBuilder(DbMetaDataHandler::class) - ->onlyMethods(['tableExists']) - ->getMock(); - $DbMetaDataMock->expects($this->once())->method('tableExists')->willReturn($expected); - d3GetOxidDIC()->set('d3ox.webauthn.'.DbMetaDataHandler::class, $DbMetaDataMock); - - /** @var Actions $sut */ - $sut = oxNew(Actions::class); - - $this->assertSame( - $expected, - $this->callMethod( - $sut, - 'tableExists', - ['testTable'] - ) - ); - } - - /** - * @test - * @return void - * @throws ReflectionException - * @covers \D3\Webauthn\Setup\Actions::d3GetDb - */ - public function d3GetDbReturnsRightInstance() - { - $sut = oxNew(Actions::class); - - $this->assertInstanceOf( - DatabaseInterface::class, - $this->callMethod( - $sut, - 'd3GetDb' - ) - ); - } - - /** - * @test - * @return void - * @throws ReflectionException - * @covers \D3\Webauthn\Setup\Actions::executeSQL - */ - public function canExecuteSQL() - { - /** @var Database|MockObject $dbMock */ - $dbMock = $this->getMockBuilder(Database::class) + /** @var Migrations|MockObject $migrationMock */ + $migrationMock = $this->getMockBuilder(Migrations::class) ->onlyMethods(['execute']) + ->disableOriginalConstructor() ->getMock(); - $dbMock->expects($this->once())->method('execute'); - - $sut = $this->getMockBuilder(Actions::class) - ->onlyMethods(['d3GetDb']) - ->getMock(); - $sut->method('d3GetDb')->willReturn($dbMock); - - $this->callMethod( - $sut, - 'executeSQL', - ['query'] + $migrationMock->expects($this->atLeastOnce())->method('execute')->with( + $this->identicalTo('migrations:migrate'), + $this->identicalTo('d3webauthn') ); - } - /** - * @test - * @return void - * @throws ReflectionException - * @covers \D3\Webauthn\Setup\Actions::fieldExists - */ - public function canCheckFieldExists() - { - $expected = true; - - /** @var DbMetaDataHandler|MockObject $DbMetaDataMock */ - $DbMetaDataMock = $this->getMockBuilder(DbMetaDataHandler::class) - ->onlyMethods(['fieldExists']) + /** @var MigrationsBuilder|MockObject $migrationsBuilderMock */ + $migrationsBuilderMock = $this->getMockBuilder(MigrationsBuilder::class) + ->onlyMethods(['build']) ->getMock(); - $DbMetaDataMock->expects($this->once())->method('fieldExists')->willReturn($expected); - d3GetOxidDIC()->set('d3ox.webauthn.'.DbMetaDataHandler::class, $DbMetaDataMock); + $migrationsBuilderMock->method('build')->willReturn($migrationMock); + d3GetOxidDIC()->set('d3ox.webauthn.'.MigrationsBuilder::class, $migrationsBuilderMock); /** @var Actions $sut */ $sut = oxNew(Actions::class); - $this->assertSame( - $expected, - $this->callMethod( - $sut, - 'fieldExists', - ['testField', 'testTable'] - ) + $this->callMethod( + $sut, + 'runModuleMigrations' ); } diff --git a/src/tests/unit/migration/data/Version20230209212939Test.php b/src/tests/unit/migration/data/Version20230209212939Test.php new file mode 100644 index 0000000..06d319a --- /dev/null +++ b/src/tests/unit/migration/data/Version20230209212939Test.php @@ -0,0 +1,303 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\tests\unit\migration\data; + +use D3\TestingTools\Development\CanAccessRestricted; +use D3\Webauthn\Migrations\Version20230209212939; +use D3\Webauthn\tests\unit\WAUnitTestCase; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\MySQL57Platform; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\MySqlSchemaManager; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; +use Doctrine\Migrations\Configuration\Configuration; +use Doctrine\Migrations\Version\Version; +use Generator; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionException; + +class Version20230209212939Test extends WAUnitTestCase +{ + use CanAccessRestricted; + + /** @var Version20230209212939 */ + protected $sut; + + public function setUp(): void + { + parent::setUp(); // TODO: Change the autogenerated stub + + /** @var AbstractPlatform|MockObject $databasePlatformMock */ + $databasePlatformMock = $this->getMockBuilder(MySQL57Platform::class) + ->getMock(); + + /** @var AbstractSchemaManager|MockObject $schemaManagerMock */ + $schemaManagerMock = $this->getMockBuilder(MySqlSchemaManager::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Connection|MockObject $connectionMock */ + $connectionMock = $this->getMockBuilder(Connection::class) + ->disableOriginalConstructor() + ->onlyMethods(['getDatabasePlatform', 'getSchemaManager']) + ->getMock(); + $connectionMock->method('getDatabasePlatform')->willReturn($databasePlatformMock); + $connectionMock->method('getSchemaManager')->willReturn($schemaManagerMock); + + /** @var Configuration|MockObject $configurationMock */ + $configurationMock = $this->getMockBuilder(Configuration::class) + ->disableOriginalConstructor() + ->onlyMethods(['getConnection']) + ->getMock(); + $configurationMock->method('getConnection')->willReturn($connectionMock); + + /** @var Version|MockObject $versionMock */ + $versionMock = $this->getMockBuilder(Version::class) + ->onlyMethods(['getConfiguration']) + ->disableOriginalConstructor() + ->getMock(); + $versionMock->method('getConfiguration')->willReturn($configurationMock); + + $this->sut = oxNew(Version20230209212939::class, $versionMock); + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Migrations\Version20230209212939::getDescription + */ + public function canGetDescription() + { + $this->assertIsString( + $this->callMethod( + $this->sut, + 'getDescription' + ) + ); + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Migrations\Version20230209212939::up + * @dataProvider canUpTableDataProvider + */ + public function canUpTable($tableExist, $invocationCount) + { + /** @var Table|MockObject $tableMock */ + $tableMock = $this->getMockBuilder(Table::class) + ->onlyMethods(['hasColumn', 'hasPrimaryKey', 'hasIndex']) + ->disableOriginalConstructor() + ->getMock(); + $tableMock->method('hasColumn')->willReturn(true); + $tableMock->method('hasPrimaryKey')->willReturn(true); + $tableMock->method('hasIndex')->willReturn(true); + + /** @var Schema|MockObject $schemaMock */ + $schemaMock = $this->getMockBuilder(Schema::class) + ->onlyMethods(['hasTable', 'createTable', 'getTable']) + ->getMock(); + $schemaMock->method('hasTable')->willReturn($tableExist); + $schemaMock->expects($invocationCount)->method('createTable')->willReturn($tableMock); + $schemaMock->method('getTable')->willReturn($tableMock); + + $this->callMethod( + $this->sut, + 'up', + [$schemaMock] + ); + } + + /** + * @return Generator + */ + public function canUpTableDataProvider(): Generator + { + yield 'table not exist' => [false, $this->once()]; + yield 'table exist' => [true, $this->never()]; + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Migrations\Version20230209212939::up + * @dataProvider canUpColumnDataProvider + */ + public function canUpColumn($columnExist, $invocationCount) + { + /** @var Column|MockObject $columnMock */ + $columnMock = $this->getMockBuilder(Column::class) + ->onlyMethods(['setLength']) + ->disableOriginalConstructor() + ->getMock(); + $columnMock->method('setLength')->willReturnSelf(); + + /** @var Table|MockObject $tableMock */ + $tableMock = $this->getMockBuilder(Table::class) + ->onlyMethods(['hasColumn', 'addColumn', 'hasPrimaryKey', 'hasIndex']) + ->disableOriginalConstructor() + ->getMock(); + $tableMock->method('hasColumn')->willReturn($columnExist); + $tableMock->expects($invocationCount)->method('addColumn')->willReturn($columnMock); + $tableMock->method('hasPrimaryKey')->willReturn(true); + $tableMock->method('hasIndex')->willReturn(true); + + /** @var Schema|MockObject $schemaMock */ + $schemaMock = $this->getMockBuilder(Schema::class) + ->onlyMethods(['hasTable', 'getTable']) + ->getMock(); + $schemaMock->method('hasTable')->willReturn(true); + $schemaMock->method('getTable')->willReturn($tableMock); + + $this->callMethod( + $this->sut, + 'up', + [$schemaMock] + ); + } + + /** + * @return Generator + */ + public function canUpColumnDataProvider(): Generator + { + yield 'column not exist' => [false, $this->atLeast(7)]; + yield 'column exist' => [true, $this->never()]; + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Migrations\Version20230209212939::up + * @dataProvider canUpPrimaryKeyDataProvider + */ + public function canUpPrimaryKey($pKeyExist, $invocationCount) + { + /** @var Table|MockObject $tableMock */ + $tableMock = $this->getMockBuilder(Table::class) + ->onlyMethods(['hasColumn', 'addColumn', 'hasPrimaryKey', 'hasIndex', 'setPrimaryKey']) + ->disableOriginalConstructor() + ->getMock(); + $tableMock->method('hasColumn')->willReturn(true); + $tableMock->method('hasPrimaryKey')->willReturn($pKeyExist); + $tableMock->method('hasIndex')->willReturn(true); + $tableMock->expects($invocationCount)->method('setPrimaryKey'); + + /** @var Schema|MockObject $schemaMock */ + $schemaMock = $this->getMockBuilder(Schema::class) + ->onlyMethods(['hasTable', 'getTable']) + ->getMock(); + $schemaMock->method('hasTable')->willReturn(true); + $schemaMock->method('getTable')->willReturn($tableMock); + + $this->callMethod( + $this->sut, + 'up', + [$schemaMock] + ); + } + + /** + * @return Generator + */ + public function canUpPrimaryKeyDataProvider(): Generator + { + yield 'pk not exist' => [false, $this->once()]; + yield 'pk exist' => [true, $this->never()]; + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Migrations\Version20230209212939::up + * @dataProvider canUpIndexDataProvider + */ + public function canUpIndex($indexExist, $invocationCount) + { + /** @var Table|MockObject $tableMock */ + $tableMock = $this->getMockBuilder(Table::class) + ->onlyMethods(['hasColumn', 'addColumn', 'hasPrimaryKey', 'hasIndex', 'addIndex', 'setComment']) + ->disableOriginalConstructor() + ->getMock(); + $tableMock->method('hasColumn')->willReturn(true); + $tableMock->method('hasPrimaryKey')->willReturn(true); + $tableMock->method('hasIndex')->willReturn($indexExist); + $tableMock->expects($invocationCount)->method('addIndex'); + $tableMock->expects($this->once())->method('setComment'); + + /** @var Schema|MockObject $schemaMock */ + $schemaMock = $this->getMockBuilder(Schema::class) + ->onlyMethods(['hasTable', 'getTable']) + ->getMock(); + $schemaMock->method('hasTable')->willReturn(true); + $schemaMock->method('getTable')->willReturn($tableMock); + + $this->callMethod( + $this->sut, + 'up', + [$schemaMock] + ); + } + + /** + * @return Generator + */ + public function canUpIndexDataProvider(): Generator + { + yield 'index not exist' => [false, $this->atLeast(2)]; + yield 'index exist' => [true, $this->never()]; + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Migrations\Version20230209212939::down + * @dataProvider canDownTableDataProvider + */ + public function canDownTable($tableExist, $invocationCount) + { + /** @var Schema|MockObject $schemaMock */ + $schemaMock = $this->getMockBuilder(Schema::class) + ->onlyMethods(['hasTable', 'dropTable']) + ->getMock(); + $schemaMock->method('hasTable')->willReturn($tableExist); + $schemaMock->expects($invocationCount)->method('dropTable'); + + $this->callMethod( + $this->sut, + 'down', + [$schemaMock] + ); + } + + /** + * @return Generator + */ + public function canDownTableDataProvider(): Generator + { + yield 'table exist' => [true, $this->once()]; + yield 'table not exist' => [false, $this->never()]; + } +} \ No newline at end of file