Files
XC_VM/tests/Unit/ClusterSchemaTest.php
T
Claude 775362f99e feat(cluster): Cluster Nodes admin page and cron:cluster housekeeping
Servers → Cluster Nodes (permission `servers`) is the admin side of the
cluster CLI, via Domain\Cluster\ClusterAdmin: enrolled nodes with state,
liveness, epoch, token expiry, last heartbeat and agent version; code
enrolments waiting for a SAS (approve / reject); issuing an enrolment
code (shown once); and revocation. The deciding admin is recorded.
Strings in all eight languages.

cron:cluster (enabled by migration 037) deletes expired epochs (erasing
their z), the replay cache and used challenges, unused expired codes and
decided requests after a day. It returns at once on load balancers and
while the API is disabled.

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

82 lines
3.6 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–037 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', '037_enable_cluster_cron'];
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', '* * * * *', 1, 'main')", $rSql, 'cron:cluster runs on MAIN');
$this->assertFileExists(dirname(__DIR__, 2) . '/src/Cli/CronJobs/ClusterCronJob.php');
}
}