mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-11-08 03:03:35 +01:00
The suite ran on in-memory SQLite, an engine production never uses: it hid column lengths, MySQL-only syntax and type coercion (the identity bug fixed in the previous commit passed CI that way). SQLite is gone: - TestDb connects only to MariaDB/MySQL (XCVM_TEST_DB_DSN/_USER/_PASS; root over the local socket, else 127.0.0.1:3306). Each instance gets a throwaway schema, dropped with it, in the installer's sql_mode, with an explicit connect timeout (a boot under test can zero default_socket_timeout). - InstallSchema and the tests take the production DDL as is: the SQLite reducers, the keys they re-added, the faked information_schema and the sqliteCreateFunction() stand-ins are removed; test DDL is MySQL DDL. - ReplicaBootTest, DbConnectRefusalTest and LazyDatabaseHandlerTest reach the test server instead of SQLite files; RecordingFinalizer no longer swallows an information_schema failure "for tests on SQLite". - `make test-db` starts a throwaway mariadb:11.4 (tmpfs) for a machine without a server; the CI PHPUnit job gets the same as a service. The suite also runs on a panel host with the bundled PHP and its own php.ini: tests reach the code through MAIN_HOME, not `<repo>/src`; repository checks and process-isolated tests (ionCube Loader + OPcache CLI segfault on a script read from stdin) are in the skip-on-panel group; xcvm_test_child_php() starts a child that fakes \XC_VM without the real extension; Redis-down tests use RedisManager::useConnector() instead of an eval'd \XC_VM. Stale opt-in tests fixed on the way: the real-agent provisioning test answers the agent-started check, the extension integration test sends the typed starts and reads the binary signature raw, a fake producer starts as PHP_BINARY, and the certbot test waits for the line its background command writes.
109 lines
5.8 KiB
PHP
109 lines
5.8 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Domain\Server\InstallCredentials;
|
|
|
|
/**
|
|
* SSH credentials for server:install no longer travel on argv (visible in
|
|
* ps and /proc) or sit in a plaintext bin/install/<id>.json: the panel writes
|
|
* a 0600 <id>.cred that the command reads and deletes. The node's SSH host key
|
|
* is pinned: an expected fingerprint, else the one stored at first install.
|
|
*/
|
|
final class InstallCredentialsTest extends TestCase {
|
|
|
|
private string $rDir;
|
|
|
|
protected function setUp(): void {
|
|
$this->rDir = sys_get_temp_dir() . '/xcvm_install_' . uniqid('', true) . '/';
|
|
mkdir($this->rDir, 0750, true);
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
foreach (glob($this->rDir . '*') ?: [] as $rFile) {
|
|
@unlink($rFile);
|
|
}
|
|
@rmdir($this->rDir);
|
|
}
|
|
|
|
public function testCredFileIsPrivateAndReadOnce(): void {
|
|
$rPath = InstallCredentials::write(7, 'root', "p'a\"ss w0rd", $this->rDir);
|
|
$this->assertSame($this->rDir . '7.cred', $rPath);
|
|
$this->assertSame(0600, fileperms($rPath) & 0777);
|
|
|
|
$this->assertSame(['username' => 'root', 'password' => "p'a\"ss w0rd"], InstallCredentials::consume($rPath, $this->rDir));
|
|
$this->assertFileDoesNotExist($rPath, 'deleted as soon as it is read');
|
|
$this->assertNull(InstallCredentials::consume($rPath, $this->rDir));
|
|
}
|
|
|
|
public function testConsumeOnlyAcceptsCredFilesInTheInstallDir(): void {
|
|
$rOther = sys_get_temp_dir() . '/xcvm_' . uniqid('', true) . '.cred';
|
|
file_put_contents($rOther, json_encode(['u' => 'x', 'p' => 'y']));
|
|
$this->assertNull(InstallCredentials::consume($rOther, $this->rDir), 'outside the install dir');
|
|
$this->assertFileExists($rOther, 'and not deleted either');
|
|
@unlink($rOther);
|
|
|
|
file_put_contents($this->rDir . '7.json', json_encode(['u' => 'x', 'p' => 'y']));
|
|
$this->assertNull(InstallCredentials::consume($this->rDir . '7.json', $this->rDir), 'not a .cred');
|
|
$this->assertNull(InstallCredentials::consume($this->rDir . '../' . basename($this->rDir) . '/nope.cred', $this->rDir));
|
|
}
|
|
|
|
public function testCommandCarriesNoPassword(): void {
|
|
$rCommand = InstallCredentials::command(2, 9, 22, 'root', 'S3cret!pw', ['80', '443', '1'], 'SHA1:' . rtrim(base64_encode(sha1('k', true)), '='));
|
|
$this->assertStringNotContainsString('S3cret!pw', $rCommand);
|
|
$this->assertStringContainsString('server:install 2 9 22 - - 80 443 1 --cred-file=', $rCommand);
|
|
$this->assertStringContainsString('--expect-hostkey=' . sha1('k'), $rCommand);
|
|
$this->assertStringNotContainsString('--forget-hostkey', $rCommand, 'only when the admin asks');
|
|
$this->assertStringContainsString(' --forget-hostkey=1 > ', InstallCredentials::command(2, 9, 22, 'root', 'S3cret!pw', ['80', '443', '1'], '', true));
|
|
$rCred = InstallCredentials::dir() . '9.cred';
|
|
$this->assertFileExists($rCred);
|
|
$this->assertSame('S3cret!pw', InstallCredentials::consume($rCred)['password']);
|
|
}
|
|
|
|
public function testSplitOptions(): void {
|
|
[$rArgs, $rOpts] = InstallCredentials::splitOptions(['2', '9', '22', '-', '-', '--cred-file=/x/9.cred', '80', '--expect-hostkey=ab']);
|
|
$this->assertSame(['2', '9', '22', '-', '-', '80'], $rArgs);
|
|
$this->assertSame(['cred-file' => '/x/9.cred', 'expect-hostkey' => 'ab'], $rOpts);
|
|
}
|
|
|
|
public function testHostKeyFormats(): void {
|
|
$rHex = sha1('hostkey');
|
|
$this->assertSame($rHex, InstallCredentials::normalizeHostKey(strtoupper($rHex)));
|
|
$this->assertSame($rHex, InstallCredentials::normalizeHostKey(implode(':', str_split($rHex, 2))));
|
|
$rKeygen = '256 SHA1:' . rtrim(base64_encode(sha1('hostkey', true)), '=') . ' root@lb (ED25519)';
|
|
$this->assertSame($rHex, InstallCredentials::normalizeHostKey($rKeygen));
|
|
$this->assertNull(InstallCredentials::normalizeHostKey(''));
|
|
$this->assertNull(InstallCredentials::normalizeHostKey('SHA256:abcdef'));
|
|
$this->assertNull(InstallCredentials::normalizeHostKey('abc'));
|
|
}
|
|
|
|
public function testHostKeyPinning(): void {
|
|
$rA = sha1('a');
|
|
$rB = sha1('b');
|
|
$this->assertNull(InstallCredentials::checkHostKey($rA, null, null), 'first install: trust on first use');
|
|
$this->assertNull(InstallCredentials::checkHostKey($rA, null, $rA), 'reinstall, same key');
|
|
$this->assertNotNull(InstallCredentials::checkHostKey($rB, null, $rA), 'reinstall, key changed');
|
|
$this->assertStringContainsString('Forget the saved SSH host key', (string) InstallCredentials::checkHostKey($rB, null, $rA), 'the refusal names the way out');
|
|
$this->assertNull(InstallCredentials::checkHostKey($rB, $rB, $rA), 'admin supplied the rebuilt node\'s key');
|
|
$this->assertNotNull(InstallCredentials::checkHostKey($rA, $rB, null), 'expected key not presented');
|
|
$this->assertNotNull(InstallCredentials::checkHostKey('', null, null), 'no key at all');
|
|
}
|
|
|
|
public function testLegacyMetadataIsScrubbed(): void {
|
|
file_put_contents($this->rDir . '3.json', json_encode(['root_username' => 'root', 'root_password' => 'old', 'ssh_port' => 22]));
|
|
file_put_contents($this->rDir . '4.json', json_encode(['root_username' => 'root', 'ssh_port' => 22]));
|
|
$this->assertSame(1, InstallCredentials::scrubLegacyMetadata($this->rDir));
|
|
$this->assertSame(['root_username' => 'root', 'ssh_port' => 22], json_decode(file_get_contents($this->rDir . '3.json'), true));
|
|
}
|
|
|
|
public function testNothingWritesThePasswordToMetadataOrArgv(): void {
|
|
$rRoot = MAIN_HOME;
|
|
foreach (['Cli/Commands/LbInstallFlow.php', 'Cli/Commands/ProxyInstallFlow.php'] as $rFile) {
|
|
$this->assertStringNotContainsString("'root_password' =>", (string) file_get_contents($rRoot . $rFile), $rFile);
|
|
}
|
|
foreach (['Domain/Server/ServerService.php', 'Public/Controllers/Admin/Ajax/ServerAjaxController.php'] as $rFile) {
|
|
$this->assertStringNotContainsString("escapeshellarg(\$rData['root_password'])", (string) file_get_contents($rRoot . $rFile), $rFile);
|
|
$this->assertStringNotContainsString("escapeshellarg(\$rParams['root_password'])", (string) file_get_contents($rRoot . $rFile), $rFile);
|
|
}
|
|
}
|
|
}
|