From 6f7a58596cb7a8450359a65dfe8f71b7803811a4 Mon Sep 17 00:00:00 2001 From: blaipr Date: Thu, 13 Aug 2026 20:22:47 +0200 Subject: [PATCH] Cover the command registry, the backup prompt and two grid branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI registry is the whole sp:* command list; an entry dropped from it disappears from the command line with nothing else anywhere to catch it, so it is asserted as the exact list, in order. Plus the backup command's interactive path prompt, and two branches of the grid every management screen renders through: updating the pager when there is none attached is a no-op rather than a failure, and the template check's success path, which only had its failure covered. Two things left alone deliberately: the backup's not-installed guard sits behind !defined('TEST_ROOT'), which the suite defines for the whole process, and the XML export's three catch blocks around DOMDocument cannot be reached through any input — the document is built internally, and invalid text is sanitised by the extension rather than raising. --- .../In/Cli/Commands/BackupCommandTest.php | 23 +++++++ .../Adapter/In/Cli/CliCommandHelperTest.php | 68 +++++++++++++++++++ .../Adapter/In/Web/DataGrid/DataGridTest.php | 52 ++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 tests/Unit/Infrastructure/Adapter/In/Cli/CliCommandHelperTest.php diff --git a/tests/Integration/Infrastructure/Adapter/In/Cli/Commands/BackupCommandTest.php b/tests/Integration/Infrastructure/Adapter/In/Cli/Commands/BackupCommandTest.php index 2046f302c..fbc1df4aa 100644 --- a/tests/Integration/Infrastructure/Adapter/In/Cli/Commands/BackupCommandTest.php +++ b/tests/Integration/Infrastructure/Adapter/In/Cli/Commands/BackupCommandTest.php @@ -145,6 +145,29 @@ public function testBackupHonorsCustomPath(): void $this->assertEmpty($default, 'Backup was written to the default dir instead of --path'); } + /** + * getPath() only asks interactively when neither the env var nor --path supplied a + * value. CommandTester runs with interactive=false (see executeCommandTest()), and + * Symfony's QuestionHelper answers a non-interactive ask() with the question's default + * without touching any input stream -- so this exercises the prompt branch without + * risking the "blocks forever" trap the other CLI tests warn about, and still ends up + * backing up to the same default path as testBackupIsSuccessful(). + * + * @throws DependencyException + * @throws NotFoundException + */ + public function testBackupAsksForPathWhenNeitherEnvVarNorOptionIsSet(): void + { + $this->setupDatabase(); + + $commandTester = $this->executeCommandTest(BackupCommand::class, ['--path' => '']); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('Application and database backup completed successfully', $output); + + $this->checkBackupFilesAreCreated(); + } + /** * Without a reachable database the dump must fail * diff --git a/tests/Unit/Infrastructure/Adapter/In/Cli/CliCommandHelperTest.php b/tests/Unit/Infrastructure/Adapter/In/Cli/CliCommandHelperTest.php new file mode 100644 index 000000000..e75fce51e --- /dev/null +++ b/tests/Unit/Infrastructure/Adapter/In/Cli/CliCommandHelperTest.php @@ -0,0 +1,68 @@ +. + */ + +namespace SP\Tests\Unit\Infrastructure\Adapter\In\Cli; + +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use SP\Infrastructure\Adapter\In\Cli\Commands\BackupCommand; +use SP\Infrastructure\Adapter\In\Cli\Commands\Crypt\UpdateMasterPasswordCommand; +use SP\Infrastructure\Adapter\In\Cli\Commands\InstallCommand; +use SP\Infrastructure\Adapter\In\Cli\CliCommandHelper; + +/** + * This is what bin/cli.php registers on the Symfony Application: whatever it hands back is the + * full set of `sp:*` commands an operator can run. A command dropped here silently disappears + * from the CLI without any error -- there is nowhere else that would notice. + * + * InstallCommand, BackupCommand and UpdateMasterPasswordCommand are all `final`, so they cannot + * be doubled with createMock(); their constructors also pull in the real DI graph (installer, + * master password, account services...) that has nothing to do with what this class does. + * newInstanceWithoutConstructor() gives real, distinguishable instances of the exact types + * CliCommandHelper is wired against without any of that, which is all identity-based assertions + * below need. + */ +#[Group('unitary')] +class CliCommandHelperTest extends TestCase +{ + #[Test] + public function everyRegisteredCommandIsReturnedInTheOrderItWasWired(): void + { + $installCommand = (new ReflectionClass(InstallCommand::class))->newInstanceWithoutConstructor(); + $backupCommand = (new ReflectionClass(BackupCommand::class))->newInstanceWithoutConstructor(); + $updateMasterPasswordCommand = (new ReflectionClass(UpdateMasterPasswordCommand::class)) + ->newInstanceWithoutConstructor(); + + $helper = new CliCommandHelper($installCommand, $backupCommand, $updateMasterPasswordCommand); + + self::assertSame( + [$installCommand, $backupCommand, $updateMasterPasswordCommand], + $helper->getCommands() + ); + } +} diff --git a/tests/Unit/Infrastructure/Adapter/In/Web/DataGrid/DataGridTest.php b/tests/Unit/Infrastructure/Adapter/In/Web/DataGrid/DataGridTest.php index d801d99d7..d5773dd8a 100644 --- a/tests/Unit/Infrastructure/Adapter/In/Web/DataGrid/DataGridTest.php +++ b/tests/Unit/Infrastructure/Adapter/In/Web/DataGrid/DataGridTest.php @@ -126,6 +126,28 @@ public function updatingThePagerTakesTheRowCountFromTheData() self::assertSame(2, $grid->getPager()->getTotalRows()); } + /** + * A grid built without a pager (e.g. a listing too short to page) must not blow up when + * asked to update one -- updatePager() is called unconditionally by the controllers that + * build a grid, whether or not that grid ever got a pager attached. + * + * @throws Exception + */ + #[Test] + public function updatingThePagerIsANoOpWhenNoPagerWasSet() + { + $data = new DataGridData(); + $data->addDataRowSource('name'); + $data->setData(QueryResult::withTotalNumRows([], 0)); + + $grid = $this->buildGrid(); + $grid->setData($data); + + self::assertNull($grid->getPager()); + self::assertSame($grid, $grid->updatePager()); + self::assertNull($grid->getPager()); + } + /** * Every action counts towards the listing's own total, which the template uses to size the * actions column, and the menu keeps its own count. @@ -191,6 +213,36 @@ public function aMissingTemplateIsNotSet() self::assertNull($grid->getDataActionsTemplate()); } + /** + * A template that does exist is resolved to its full path and kept, so the screen renders + * that section instead of silently skipping it. Covers both branches of the template path + * (with and without a base subdirectory) that a missing-template test cannot reach, since + * that one never gets past the is_readable() check. + * + * @throws Exception + */ + #[Test] + public function aReadableTemplateIsResolvedToItsFullPathAndKept() + { + // Keyed on something unique to this test: TMP_PATH's vfs filesystem is shared by + // every test in the process. + $viewsPath = TMP_PATH . '/datagrid_template_test_' . uniqid(); + mkdir($viewsPath . '/rows', 0755, true); + file_put_contents($viewsPath . '/header.inc', 'header content'); + file_put_contents($viewsPath . '/rows/row.inc', 'row content'); + + $theme = $this->createStub(ThemeInterface::class); + $theme->method('getViewsPath')->willReturn($viewsPath); + + $grid = new DataGrid($theme); + + $grid->setDataHeaderTemplate('header'); + $grid->setDataRowTemplate('row', 'rows'); + + self::assertSame($viewsPath . '/header.inc', $grid->getDataHeaderTemplate()); + self::assertSame($viewsPath . '/rows/row.inc', $grid->getDataRowTemplate()); + } + /** * What the listing does when it is closed — the id of the action to return to — is carried on * the grid.