Files
XC_VM/tests/Unit/ClusterSchemaTest.php
T
Claude 2cdadf017a feat(cluster): break-glass enrolment by code
For an LB MAIN cannot reach over SSH. `cluster:enrol-code <id>` issues a
one-time code (MAIN URL, panel key hash, 128-bit secret; 30 min). MAIN
keeps only K_req/K_res, sealed to the machine under a row MAC.

The node sends enrol_code (sid:<n>, epoch 0; MAC'd with K_req, signed by
the key it asks to enrol, body SEALed to the panel box key). Nothing is
minted until `cluster:enrol-approve <id> <SAS>`: the admin types the SAS
the node shows, which binds its keys; five wrong entries reject. The
node then collects epoch 1 via enrol_code_status (MAC'd with K_res,
panel-signed `pre`) and finishes with enrol_complete.

A wrong code MAC records nothing; other keys under a used code are a
signed ENROL_CONFLICT, the same keys again are accepted. Migration 036
keeps the request's per-epoch key until approval. Both commands are
MAIN-only (stripped from the LB archive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
2026-09-25 12:32:59 +00:00

81 lines
3.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use PHPUnit\Framework\TestCase;
/**
* The cluster schema exists twice: as migrations 028–036 for upgrades and in
* database.sql for fresh installs. Both were loaded into MariaDB 10.11 and
* compared column by column when written; this test keeps them from drifting
* where CI has no database.
*/
final class ClusterSchemaTest extends TestCase {
private const MIGRATIONS = ['028_add_cluster_settings', '029_create_cluster_nodes', '030_create_cluster_commands', '031_create_cluster_enrolment', '032_create_cluster_audit', '033_add_crontab_role', '034_create_cluster_changes', '035_add_cluster_epoch_eph', '036_add_cluster_enrol_request_eph'];
private function src(string $rPath): string {
return (string) file_get_contents(dirname(__DIR__, 2) . '/src/' . $rPath);
}
/** @return array<string, string> table => column block */
private function tables(string $rSql): array {
preg_match_all('/CREATE TABLE IF NOT EXISTS `([a-z_]+)` \((.*?)\n\) ENGINE/s', $rSql, $rM);
return array_combine($rM[1], array_map('trim', $rM[2]));
}
public function testEveryMigrationHasADownFile(): void {
foreach (self::MIGRATIONS as $rName) {
$this->assertFileExists(dirname(__DIR__, 2) . '/src/migrations/database/up/' . $rName . '.sql');
$this->assertFileExists(dirname(__DIR__, 2) . '/src/migrations/database/down/' . $rName . '.sql');
}
}
public function testClusterTablesMatchDatabaseSql(): void {
$rInstall = $this->tables($this->src('bin/install/database.sql'));
// Columns a later migration ALTERs into a table: present in database.sql,
// absent from the migration that created the table.
$rAdded = [];
foreach (self::MIGRATIONS as $rName) {
if (preg_match_all('/ALTER TABLE `([a-z_]+)` ADD COLUMN IF NOT EXISTS `([a-z_]+)`/', $this->src('migrations/database/up/' . $rName . '.sql'), $rM, PREG_SET_ORDER)) {
foreach ($rM as [, $rTable, $rColumn]) {
$rAdded[$rTable][] = $rColumn;
}
}
}
foreach ($rAdded as $rTable => $rColumns) {
if (!isset($rInstall[$rTable])) {
continue;
}
foreach ($rColumns as $rColumn) {
$this->assertStringContainsString('`' . $rColumn . '`', $rInstall[$rTable], $rTable . '.' . $rColumn);
$rInstall[$rTable] = (string) preg_replace('/\n\s*`' . $rColumn . '` [^\n]*/', '', $rInstall[$rTable]);
}
}
$rCount = 0;
foreach (self::MIGRATIONS as $rName) {
foreach ($this->tables($this->src('migrations/database/up/' . $rName . '.sql')) as $rTable => $rBody) {
$this->assertArrayHasKey($rTable, $rInstall, $rTable . ' missing from database.sql');
$this->assertSame($rBody, $rInstall[$rTable], $rTable);
$rCount++;
}
}
$this->assertSame(11, $rCount);
}
public function testSettingsColumnsMatchDatabaseSql(): void {
preg_match_all('/ADD COLUMN IF NOT EXISTS (`[a-z_]+` [^\n]*?),?\n/', $this->src('migrations/database/up/028_add_cluster_settings.sql') . "\n", $rM);
$this->assertCount(19, $rM[1]);
$rSettings = $this->tables($this->src('bin/install/database.sql'))['settings'];
foreach ($rM[1] as $rColumn) {
$this->assertStringContainsString(' ' . rtrim($rColumn, ';') . ',', $rSettings);
}
}
public function testCrontabRoles(): void {
$rSql = $this->src('bin/install/database.sql');
$this->assertStringContainsString("`role` enum('all','main','legacy')", $rSql);
foreach (['cleanup', 'tmdb', 'tmdb_popular', 'update'] as $rCron) {
$this->assertMatchesRegularExpression("/\\(\\d+, '" . $rCron . "', '[^']*', 1, 'main'\\)/", $rSql, $rCron);
}
$this->assertStringContainsString("(30, 'cluster', '* * * * *', 0, 'main')", $rSql, 'disabled until cron:cluster exists');
}
}