ZH

jumphpsql

作者:davidnurdin · 演示与实验
暂无评分
▶ 打开 💬 反馈 源码 🕘 版本 编辑 GitHub
apps/jumphpsql / tests / MainFormTest.php · commit 1d1d550
<?phpnamespace Tests;use App\Db\ConnectionStore;use App\Db\MysqlConnection;use App\Form\ConnectionForm;use App\Form\MainForm;use App\Form\TransferForm;use JumPHP\Kernel\Engine;use JumPHP\Testing\GameTestCase;final class MainFormTest extends GameTestCase{    private ?MysqlConnection $db = null;    protected function setUp(): void    {        parent::setUp();        Engine::get()->storage->delete(ConnectionStore::FILE);      // each test starts with no saved connection    }    protected function tearDown(): void    {        $scene = Engine::get()->scenes->current();        $scene?->exit();          // the form's connections closed: no transaction left to lock the next test's seed        $this->db?->close();        parent::tearDown();    }    /** The test server, seeded, or the test is skipped. */    private function server(): MysqlConnection    {        $this->db = TestServer::connect();        if ($this->db === null) {            self::markTestSkipped('No MySQL / MariaDB test server on ' . TestServer::config()['port'] . ' (see tests/TestServer.php)');        }        TestServer::seed($this->db);        return $this->db;    }    /** Frames until $ready says yes (the server answers between frames). */    private function until(callable $ready, string $what, int $frames = 300): void    {        for ($i = 0; $i < $frames; $i++) {            if ($ready()) {                return;            }            $this->game->run(1);            usleep(2000);        }        $scene = $this->game->scene();        self::fail("Waited too long: $what" . (isset($scene->status) ? ' (status: ' . $scene->widget('status')->props['text'] . ')' : ''));    }    private function startConnected(): MainForm    {        $this->game->start(MainForm::class, ['connect' => TestServer::config() + ['database' => TestServer::DATABASE]]);        /** @var MainForm $form */        $form = $this->game->scene();        $customers = "t\x1ftest\x1f" . TestServer::DATABASE . "\x1fcustomers";        $this->until(fn () => $form->widget('tree')->node($customers) !== null, 'the tables in the tree');        return $form;    }    private function openTable(MainForm $form, string $table): void    {        $form->widget('tree')->select("t\x1ftest\x1f" . TestServer::DATABASE . "\x1f" . $table);        $this->until(fn () => $form->page() !== null && $form->total() !== null, "the rows of $table");    }    public function testANewConnectionIsTypedSavedAndShownInTheTree(): void    {        $this->game->start(MainForm::class);        /** @var MainForm $form */        $form = $this->game->scene();        self::assertStringContainsString('New connection', $form->widget('status')->props['text']);        $form->click($form->widget('newConnection'));        $this->game->run(2);        $dialog = $this->game->scene();        self::assertInstanceOf(ConnectionForm::class, $dialog);        $dialog->click($dialog->widget('name'));        $dialog->type('Shop');        $dialog->widget('host')->text = 'db.example';        $dialog->widget('port')->text = '3307';        $this->game->run(1);        $this->game->shot('connection-dialog');        $dialog->click($dialog->widget('save'));        $this->game->run(2);        self::assertSame($form, $this->game->scene(), 'the dialog closed');        $saved = Engine::get()->storage->readJson(ConnectionStore::FILE, []);        self::assertSame('Shop', $saved[0]['name']);        self::assertSame(3307, $saved[0]['port']);        self::assertSame('Shop', $form->widget('tree')->props['nodes'][0]['label']);        self::assertSame('db.example:3307', $form->widget('tree')->props['nodes'][0]['detail']);    }    public function testTheConnectionDialogTestsTheServer(): void    {        $this->server();        $this->game->start(MainForm::class);        $this->game->scene()->click($this->game->scene()->widget('newConnection'));        $this->game->run(2);        $dialog = $this->game->scene();        $c = TestServer::config();        $dialog->widget('host')->text = $c['host'];        $dialog->widget('port')->text = (string)$c['port'];        $dialog->widget('user')->text = $c['user'];        $dialog->widget('password')->text = 'wrong';        $dialog->click($dialog->widget('test'));        $this->until(fn () => str_contains($dialog->widget('result')->props['text'], 'denied'), 'a refused password');        $dialog->widget('password')->text = $c['password'];        $dialog->click($dialog->widget('test'));        $this->until(fn () => str_starts_with($dialog->widget('result')->props['text'], 'Connected'), 'a connection');        self::assertStringContainsString('MariaDB', $dialog->widget('result')->props['text'] . (getenv('JUMPHPSQL_TEST_PORT') ? ' MariaDB' : ''));    }    public function testOpeningAConnectionListsItsDatabasesAndTables(): void    {        $this->server();        $form = $this->startConnected();        $labels = array_column($form->widget('tree')->props['nodes'], 'label');        self::assertContains(TestServer::DATABASE, $labels);        self::assertContains('customers', $labels);        self::assertContains('londoners', $labels);        $this->game->run(1);        $this->game->shot('tree');    }    public function testATableShowsItsRowsAndACellEditedIsSaved(): void    {        $db = $this->server();        $form = $this->startConnected();        $this->openTable($form, 'customers');        $grid = $form->widget('grid');        self::assertSame(['id', 'name', 'city', 'photo'], $grid->props['columns']);        self::assertSame(3, $grid->rowCount());        self::assertSame('0x89504E47', $grid->value(0, 3), 'binary values show as hex');        self::assertSame(3, $form->total());        $grid->selectCell(1, 2);        $grid->setValue(1, 2, 'Paris');        self::assertTrue($form->widget('save')->props['enabled']);        $this->game->run(1);        $this->game->shot('edited');        $form->click($form->widget('save'));        $this->until(fn () => !$grid->hasChanges(), 'the save');        self::assertSame([['Paris']], TestServer::rows($db, 'SELECT city FROM ' . TestServer::DATABASE . '.customers WHERE id = 2'));        self::assertSame([['0x89504E47']], TestServer::rows($db, 'SELECT CONCAT(\'0x\', HEX(photo)) FROM ' . TestServer::DATABASE . '.customers WHERE id = 1'),            'the other columns are not touched');    }    public function testARowAddedAndARowDeletedAreSavedTogether(): void    {        $db = $this->server();        $form = $this->startConnected();        $this->openTable($form, 'customers');        $this->until(fn () => $form->primaryKey() === ['id'], 'the primary key');        $grid = $form->widget('grid');        $grid->selectCell(2, 0);        $form->click($form->widget('deleteRow'));        $form->click($form->widget('addRow'));        $grid->cancelEdit();        $grid->setValue(3, 1, 'Margaret');        $grid->setValue(3, 2, null);        self::assertSame(['DELETE FROM `jumphpsql_test`.`customers` WHERE `id` <=> \'3\' LIMIT 1',            'INSERT INTO `jumphpsql_test`.`customers` (`name`, `city`) VALUES (\'Margaret\', NULL)'], $form->changeStatements());        $form->click($form->widget('save'));        $this->until(fn () => !$grid->hasChanges() && $form->page() !== null && $form->total() === 3 && $grid->rowCount() === 3, 'the save');        self::assertSame([['Ada'], ['Grace'], ['Margaret']], TestServer::rows($db, 'SELECT name FROM ' . TestServer::DATABASE . '.customers ORDER BY id'));    }    public function testInATableWithoutPrimaryKeyOnlyOneOfTwoSameRowsIsDeleted(): void    {        $db = $this->server();        $form = $this->startConnected();        $this->openTable($form, 'notes');        $grid = $form->widget('grid');        $grid->selectCell(2, 0);        $form->click($form->widget('deleteRow'));        $form->click($form->widget('save'));        $this->until(fn () => !$grid->hasChanges() && $form->total() === 2, 'the save');        self::assertSame([['1']], TestServer::rows($db, 'SELECT COUNT(*) FROM ' . TestServer::DATABASE . ".notes WHERE body = 'same'"));        self::assertSame("l'été", $grid->value(0, 0), 'utf-8 kept');    }    public function testAFailedSaveChangesNothing(): void    {        $db = $this->server();        $form = $this->startConnected();        $this->openTable($form, 'customers');        $grid = $form->widget('grid');        $grid->setValue(0, 2, 'Oxford');        $grid->setValue(1, 1, null);          // name is NOT NULL: the second statement fails        $form->click($form->widget('save'));        $this->until(fn () => str_starts_with($form->widget('status')->props['text'], 'Nothing saved'), 'the error');        self::assertTrue($grid->hasChanges(), 'the changes wait to be fixed');        self::assertSame([['London']], TestServer::rows($db, 'SELECT city FROM ' . TestServer::DATABASE . '.customers WHERE id = 1'), 'rolled back');    }    public function testTheFilterAndTheSortReadTheRowsAgain(): void    {        $this->server();        $form = $this->startConnected();        $this->openTable($form, 'customers');        $grid = $form->widget('grid');        $form->widget('filter')->text = 'city IS NOT NULL';        $form->click($form->widget('applyFilter'));        $this->until(fn () => $form->total() === 2 && $grid->rowCount() === 2, 'the filter');        $grid->emit('sort', 1);        $grid->emit('sort', 1);           // twice: descending        $this->until(fn () => $grid->value(0, 1) === 'Grace', 'the sort');        self::assertSame('1–2 of 2', $form->widget('pageInfo')->props['text']);    }    public function testAQueryShowsItsRowsAndAnErrorIsShown(): void    {        $this->server();        $form = $this->startConnected();        $form->widget('tree')->select("d\x1ftest\x1f" . TestServer::DATABASE);        $form->click($form->widget('tabSql'));        self::assertSame('sql', $form->tab());        $this->game->run(2);          // the SQL page laid out: its buttons have their place        $form->widget('editor')->setText("UPDATE customers SET city = 'Rome' WHERE id = 3;\nSELECT name, city FROM customers ORDER BY id;");        $form->click($form->widget('runAll'));        $this->until(fn () => count($form->lastResults()) === 2, 'the results');        self::assertSame(['name', 'city'], $form->widget('resultGrid')->props['columns']);        self::assertSame('Rome', $form->widget('resultGrid')->value(2, 1));        self::assertTrue($form->widget('results')->props['visible'], 'two results: a list to choose');        $this->game->run(1);        $this->game->shot('query');        $form->runSql('SELECT nope FROM customers');        $this->until(fn () => str_contains($form->widget('resultInfo')->props['text'], '1054'), 'the error');    }    public function testADumpImportedIntoAnotherDatabaseGivesTheSameRows(): void    {        $db = $this->server();        $form = $this->startConnected();        $form->widget('tree')->select("d\x1ftest\x1f" . TestServer::DATABASE);        $file = sys_get_temp_dir() . '/jumphpsql-test-' . getmypid() . '.sql';        $config = TestServer::config();        Engine::get()->scenes->push(TransferForm::class, ['mode' => 'dump', 'config' => $config, 'database' => TestServer::DATABASE, 'path' => $file, 'autostart' => true]);        $this->game->run(1);        $dialog = $this->game->scene();        $this->until(fn () => $dialog->job()?->done, 'the dump');        self::assertNull($dialog->job()->error);        $this->game->run(1);        $this->game->shot('dump');        self::assertStringContainsString('CREATE TABLE `customers`', (string)file_get_contents($file));        $dialog->click($dialog->widget('start'));      // Done        $this->game->run(1);        self::assertStringStartsWith('Dump: ', $form->widget('status')->props['text']);        $copy = TestServer::DATABASE . '_copy';        $db->wait($db->query("DROP DATABASE IF EXISTS `$copy`; CREATE DATABASE `$copy`"));        Engine::get()->scenes->push(TransferForm::class, ['mode' => 'import', 'config' => $config, 'database' => $copy, 'path' => $file, 'autostart' => true]);        $this->game->run(1);        $dialog = $this->game->scene();        $this->until(fn () => $dialog->job()?->done, 'the import');        self::assertNull($dialog->job()->error, (string)$dialog->job()->error);        $sql = 'SELECT id, name, city, HEX(photo) FROM %s.customers ORDER BY id';        self::assertSame(TestServer::rows($db, sprintf($sql, TestServer::DATABASE)), TestServer::rows($db, sprintf($sql, $copy)));        self::assertSame([['Ada']], TestServer::rows($db, "SELECT name FROM `$copy`.londoners"));        $db->wait($db->query("DROP DATABASE `$copy`"));        @unlink($file);    }    public function testBackspaceInTheConnectionDialogErasesAndEscapeCloses(): void    {        $this->game->start(MainForm::class);        $form = $this->game->scene();        $form->click($form->widget('newConnection'));        $this->game->run(2);        $dialog = $this->game->scene();        $dialog->click($dialog->widget('name'));        $dialog->type('Shopx');        $keyboard = Engine::get()->input->getKeyboard();        $keyboard->down(\SDLK_BACKSPACE);        $this->game->run(1);        $keyboard->up(\SDLK_BACKSPACE);        $this->game->run(1);        self::assertSame($dialog, $this->game->scene(), 'Backspace does not close the dialog');        self::assertSame('Shop', $dialog->widget('name')->props['text']);        $keyboard->down(\SDLK_ESCAPE);        $this->game->run(1);        $keyboard->up(\SDLK_ESCAPE);        $this->game->run(1);        self::assertSame($form, $this->game->scene(), 'Escape closes it');    }    public function testTheSshPartOfTheDialogFollowsTheAuthenticationAndIsSaved(): void    {        $this->game->start(MainForm::class);        $form = $this->game->scene();        $form->click($form->widget('newConnection'));        $this->game->run(2);        $dialog = $this->game->scene();        self::assertFalse($dialog->widget('sshPanel')->props['visible']);        $dialog->click($dialog->widget('useSsh'));        $this->game->run(2);        self::assertTrue($dialog->widget('sshPanel')->props['visible']);        self::assertFalse($dialog->widget('sshKeyBox')->props['visible'], 'the session key needs no file');        $dialog->widget('sshHost')->text = 'bastion.example';        $dialog->widget('sshUser')->text = 'me';        $dialog->widget('sshAuth')->select(1);        $this->game->run(2);        self::assertTrue($dialog->widget('sshKeyBox')->props['visible']);        $dialog->widget('sshKey')->text = '~/.ssh/id_ed25519';        $dialog->widget('sshPassphrase')->text = 'phrase';        $dialog->widget('savePassword')->checked = false;        $this->game->run(1);        $this->game->shot('ssh-dialog');        $dialog->click($dialog->widget('save'));        $this->game->run(2);        $saved = Engine::get()->storage->readJson(ConnectionStore::FILE, [])[0];        self::assertTrue($saved['ssh']);        self::assertSame(['bastion.example', 'me', 'key', '~/.ssh/id_ed25519', ''], [$saved['sshHost'], $saved['sshUser'], $saved['sshAuth'],            $saved['sshKey'], $saved['sshPassphrase']], 'the passphrase is not saved: "save the password" is off');        self::assertStringContainsString('via bastion.example', $form->widget('tree')->props['nodes'][0]['detail']);        self::assertSame('phrase', $form->config($saved['id'])['sshPassphrase'], 'kept for this session');    }    public function testTheWindowWorksThroughAnSshTunnel(): void    {        $db = $this->server();        $ssh = TestServer::sshConfig();        if ($ssh === null) {            self::markTestSkipped('No SSH test server (see tests/TestServer.php)');        }        $this->game->start(MainForm::class, ['connect' => ['database' => TestServer::DATABASE] + $ssh]);        /** @var MainForm $form */        $form = $this->game->scene();        $customers = "t\x1ftest\x1f" . TestServer::DATABASE . "\x1fcustomers";        $this->until(fn () => $form->widget('tree')->node($customers) !== null, 'the tables, through the tunnel', 600);        self::assertStringContainsString('through SSH', $form->widget('status')->props['text']);        $this->openTable($form, 'customers');        self::assertSame(3, $form->widget('grid')->rowCount());        self::assertSame('127.0.0.1', $form->transferConfig('test')['host'], 'a dump goes through the same tunnel');        $file = sys_get_temp_dir() . '/jumphpsql-ssh-' . getmypid() . '.sql';        Engine::get()->scenes->push(TransferForm::class, ['mode' => 'dump', 'config' => $form->transferConfig('test'), 'database' => TestServer::DATABASE,            'path' => $file, 'autostart' => true]);        $this->game->run(1);        $dialog = $this->game->scene();        $this->until(fn () => $dialog->job()?->done, 'the dump');        self::assertNull($dialog->job()->error);        self::assertStringContainsString('INSERT INTO `customers`', (string)file_get_contents($file));        @unlink($file);        $db->close();    }}