v0.2.0 (10/N): bridge:export console command + QML hook
PLAN.md §12 *Data backup / export* called for "a bridge:export console command and a UI hook for backup-to-file from v1". Shipping both now so the v0.2.0 surface has the data-portability story end-to-end: PHP side — `bin/console bridge:export <destination>`: - Reads the source path from DATABASE_URL so it works in dev mode (developer's source-tree var/data.sqlite) and bundled mode (user data dir SQLite) without environment-aware logic. - SQLite-only by design (PLAN.md §6 — single-instance SQLite-first); emits a clear error for non-sqlite:// URLs rather than pretending to support drivers that need driver-specific dump tooling. - Overwrites the destination if it exists (the FileDialog or shell redirect that produced the path has already confirmed). - 4 unit tests: happy path, non-SQLite URL, missing source, overwrite. Test count 24 → 28. QML side — Q_INVOKABLE BackendConnection.exportDatabase(path): - Bundled mode only; dev mode emits databaseExportFailed and returns false (developers own their SQLite directly). - Accepts both filesystem paths and `file://` URLs (FileDialog results). - Returns synchronously with bool but also emits async signals databaseExported(dst) / databaseExportFailed(reason) so QML can drive a snackbar / log without polling the return value. - Removes any existing destination first (QFile::copy refuses to overwrite); the picker has already confirmed the choice. Drive-by: parse_url() rejects sqlite:///abs/path on PHP 8.5+ (the host-less triple-slash trips its strictness). Switched to a prefix-strip — Doctrine DBAL only emits two URL shapes for SQLite anyway (sqlite:///abs and sqlite://relative). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
framework/php/tests/Command/BridgeExportCommandTest.php
Normal file
82
framework/php/tests/Command/BridgeExportCommandTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpQml\Bridge\Tests\Command;
|
||||
|
||||
use PhpQml\Bridge\Command\BridgeExportCommand;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
#[CoversClass(BridgeExportCommand::class)]
|
||||
final class BridgeExportCommandTest extends TestCase
|
||||
{
|
||||
private string $tmpDir = '';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tmpDir = sys_get_temp_dir().'/bridge-export-test-'.uniqid();
|
||||
mkdir($this->tmpDir, 0o700, true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Brutal but adequate for a flat test dir.
|
||||
foreach (glob($this->tmpDir.'/*') ?: [] as $f) {
|
||||
@unlink($f);
|
||||
}
|
||||
@rmdir($this->tmpDir);
|
||||
}
|
||||
|
||||
public function testCopiesSqliteFileToDestination(): void
|
||||
{
|
||||
$source = $this->tmpDir.'/source.sqlite';
|
||||
file_put_contents($source, 'fake-sqlite-bytes');
|
||||
|
||||
$tester = new CommandTester(new BridgeExportCommand("sqlite:///{$source}"));
|
||||
$destination = $this->tmpDir.'/exported.sqlite';
|
||||
|
||||
$exitCode = $tester->execute(['destination' => $destination]);
|
||||
|
||||
self::assertSame(0, $exitCode);
|
||||
self::assertFileExists($destination);
|
||||
self::assertSame('fake-sqlite-bytes', file_get_contents($destination));
|
||||
self::assertStringContainsString('Exported', $tester->getDisplay());
|
||||
}
|
||||
|
||||
public function testFailsForNonSqliteUrl(): void
|
||||
{
|
||||
$tester = new CommandTester(new BridgeExportCommand('postgres://user@host/db'));
|
||||
|
||||
$exitCode = $tester->execute(['destination' => $this->tmpDir.'/x.sqlite']);
|
||||
|
||||
self::assertSame(1, $exitCode);
|
||||
self::assertStringContainsString('not a sqlite:/// URL', $tester->getDisplay());
|
||||
}
|
||||
|
||||
public function testFailsWhenSourceMissing(): void
|
||||
{
|
||||
$tester = new CommandTester(new BridgeExportCommand("sqlite:///{$this->tmpDir}/no-such.sqlite"));
|
||||
|
||||
$exitCode = $tester->execute(['destination' => $this->tmpDir.'/x.sqlite']);
|
||||
|
||||
self::assertSame(1, $exitCode);
|
||||
self::assertStringContainsString('does not exist', $tester->getDisplay());
|
||||
}
|
||||
|
||||
public function testOverwritesExistingDestination(): void
|
||||
{
|
||||
$source = $this->tmpDir.'/source.sqlite';
|
||||
file_put_contents($source, 'new-bytes');
|
||||
|
||||
$destination = $this->tmpDir.'/exported.sqlite';
|
||||
file_put_contents($destination, 'old-bytes');
|
||||
|
||||
$tester = new CommandTester(new BridgeExportCommand("sqlite:///{$source}"));
|
||||
$exitCode = $tester->execute(['destination' => $destination]);
|
||||
|
||||
self::assertSame(0, $exitCode);
|
||||
self::assertSame('new-bytes', file_get_contents($destination));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user