mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
235 lines
9.1 KiB
JavaScript
235 lines
9.1 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
CloneMigrationVerificationError,
|
|
MINIMUM_RELEASE_MIGRATION,
|
|
cloneMigrationConfirmation,
|
|
discoverMigrationManifest,
|
|
isDisposableCloneDatabaseName,
|
|
runCloneMigrations,
|
|
validateCloneMigrationEnvironment,
|
|
verifyExactMigrationLedger
|
|
} = require('../src/scripts/migrate-disposable-clone');
|
|
|
|
function safeEnvironment(overrides = {}) {
|
|
const database = overrides.DB_NAME || 'monerogue_restore_release_20260721';
|
|
return {
|
|
DB_HOST: '127.0.0.1',
|
|
DB_PORT: '5432',
|
|
DB_NAME: database,
|
|
DB_USER: 'monerogue_migrator',
|
|
CLONE_MIGRATION_EXPECT_DATABASE: database,
|
|
CLONE_MIGRATION_CONFIRM: cloneMigrationConfirmation(database),
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
class FakePool {
|
|
constructor(config, { actualDatabase = config.database, hideLatest = false } = {}) {
|
|
this.config = config;
|
|
this.actualDatabase = actualDatabase;
|
|
this.hideLatest = hideLatest;
|
|
this.ledger = new Set();
|
|
this.transactions = [];
|
|
this.ended = false;
|
|
}
|
|
|
|
async connect() {
|
|
const pool = this;
|
|
let pending = null;
|
|
return {
|
|
async query(sql, params = []) {
|
|
if (/current_database\(\)/.test(sql)) {
|
|
return { rows: [{ database_name: pool.actualDatabase }], rowCount: 1 };
|
|
}
|
|
if (sql === 'BEGIN') {
|
|
pending = null;
|
|
return { rows: [], rowCount: null };
|
|
}
|
|
if (/INSERT INTO schema_migrations/.test(sql)) {
|
|
pending = params[0];
|
|
return { rows: [], rowCount: 1 };
|
|
}
|
|
if (sql === 'COMMIT') {
|
|
if (pending) pool.ledger.add(pending);
|
|
pool.transactions.push(pending);
|
|
pending = null;
|
|
return { rows: [], rowCount: null };
|
|
}
|
|
if (sql === 'ROLLBACK') {
|
|
pending = null;
|
|
return { rows: [], rowCount: null };
|
|
}
|
|
return { rows: [], rowCount: null };
|
|
},
|
|
release() {}
|
|
};
|
|
}
|
|
|
|
async query(sql, params = []) {
|
|
if (/CREATE TABLE IF NOT EXISTS schema_migrations/.test(sql)) {
|
|
return { rows: [], rowCount: null };
|
|
}
|
|
if (/SELECT 1 FROM schema_migrations/.test(sql)) {
|
|
return { rows: [], rowCount: this.ledger.has(params[0]) ? 1 : 0 };
|
|
}
|
|
if (/SELECT filename FROM schema_migrations/.test(sql)) {
|
|
let filenames = [...this.ledger].sort();
|
|
if (this.hideLatest) filenames = filenames.slice(0, -1);
|
|
return { rows: filenames.map(filename => ({ filename })), rowCount: filenames.length };
|
|
}
|
|
throw new Error(`unexpected fake pool query: ${sql}`);
|
|
}
|
|
|
|
async end() {
|
|
this.ended = true;
|
|
}
|
|
}
|
|
|
|
describe('disposable clone migration target guard', () => {
|
|
test.each([
|
|
'monerogue_clone_release_a1',
|
|
'wownerogue_restore_20260721',
|
|
'wowngeon_canary_direct_e2e'
|
|
])('accepts an explicitly disposable application database: %s', database => {
|
|
expect(isDisposableCloneDatabaseName(database)).toBe(true);
|
|
expect(validateCloneMigrationEnvironment(safeEnvironment({ DB_NAME: database }), []))
|
|
.toEqual(expect.objectContaining({ database, expectedDatabase: database }));
|
|
});
|
|
|
|
test.each([
|
|
'monerogue',
|
|
'wownerogue',
|
|
'wowngeon',
|
|
'monerogue_prod_clone',
|
|
'monerogue_clone_prd',
|
|
'monerogue_clone_production2026',
|
|
'monerogue_master_restore',
|
|
'wownerogue_clone_live',
|
|
'wownerogue_clone_live1',
|
|
'monerogue_mainnet_restore',
|
|
'postgres_restore_20260721',
|
|
'monerogue_staging_20260721',
|
|
'monerogue-clone-release'
|
|
])('refuses a live-looking, unscoped, or malformed name: %s', database => {
|
|
expect(isDisposableCloneDatabaseName(database)).toBe(false);
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment({ DB_NAME: database }), []))
|
|
.toThrow(/clone, restore, or canary/);
|
|
});
|
|
|
|
test('requires an exact expected database, database-bound confirmation, and local host', () => {
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment({
|
|
CLONE_MIGRATION_EXPECT_DATABASE: 'monerogue_restore_other'
|
|
}), [])).toThrow(/exactly equal/);
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment({
|
|
CLONE_MIGRATION_CONFIRM: 'yes'
|
|
}), [])).toThrow(/exactly bind/);
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment({
|
|
DB_HOST: 'database.internal'
|
|
}), [])).toThrow(/loopback/);
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment({
|
|
DB_HOST: '/tmp/untrusted-postgresql-socket'
|
|
}), [])).toThrow(/loopback/);
|
|
expect(validateCloneMigrationEnvironment(safeEnvironment({
|
|
DB_HOST: '/var/run/postgresql'
|
|
}), [])).toEqual(expect.objectContaining({ host: '/var/run/postgresql' }));
|
|
});
|
|
|
|
test('forbids argv and credential-bearing DATABASE_URL before constructing a pool', async () => {
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment(), ['--database', 'x']))
|
|
.toThrow(/command-line/);
|
|
expect(() => validateCloneMigrationEnvironment(safeEnvironment({
|
|
DATABASE_URL: 'postgres://user:secret@127.0.0.1/db'
|
|
}), [])).toThrow(/DATABASE_URL/);
|
|
|
|
const poolFactory = jest.fn();
|
|
await expect(runCloneMigrations({
|
|
env: safeEnvironment(), argv: ['--password', 'secret'], poolFactory
|
|
})).rejects.toThrow(/command-line/);
|
|
expect(poolFactory).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('disposable clone migration execution', () => {
|
|
let logSpy;
|
|
|
|
beforeEach(() => {
|
|
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
logSpy.mockRestore();
|
|
});
|
|
|
|
test('discovers the contiguous repository manifest through migration 43 dynamically', () => {
|
|
const manifest = discoverMigrationManifest();
|
|
expect(manifest.latestOrdinal).toBeGreaterThanOrEqual(MINIMUM_RELEASE_MIGRATION);
|
|
expect(manifest.latestFilename).toBe('043_durable_solo_restart_snapshots.sql');
|
|
expect(manifest.files).toHaveLength(43);
|
|
});
|
|
|
|
test('runs the normal ordered migration implementation and proves the exact ledger', async () => {
|
|
const secret = 'do-not-print-this-database-password';
|
|
const env = safeEnvironment({ DB_PASSWORD: secret });
|
|
let pool;
|
|
const output = [];
|
|
|
|
const result = await runCloneMigrations({
|
|
env,
|
|
argv: [],
|
|
poolFactory: config => {
|
|
pool = new FakePool(config);
|
|
return pool;
|
|
},
|
|
logger: { info: line => output.push(line) }
|
|
});
|
|
|
|
expect(pool.transactions).toEqual(result.files);
|
|
expect(result.latestFilename).toBe('043_durable_solo_restart_snapshots.sql');
|
|
expect(output).toContain('clone_migration_status=ok');
|
|
expect(output).toContain('latest_migration=043_durable_solo_restart_snapshots.sql');
|
|
expect(output.join('\n')).not.toContain(secret);
|
|
expect(pool.config).toEqual(expect.objectContaining({
|
|
database: env.DB_NAME,
|
|
host: '127.0.0.1',
|
|
max: 1,
|
|
application_name: 'wowngeon-disposable-clone-migrations'
|
|
}));
|
|
expect(pool.ended).toBe(true);
|
|
});
|
|
|
|
test('checks server database identity before applying a migration', async () => {
|
|
let pool;
|
|
await expect(runCloneMigrations({
|
|
env: safeEnvironment(),
|
|
argv: [],
|
|
poolFactory: config => {
|
|
pool = new FakePool(config, { actualDatabase: 'monerogue_restore_wrong' });
|
|
return pool;
|
|
},
|
|
logger: { info: jest.fn() }
|
|
})).rejects.toThrow(/exact expected clone name/);
|
|
expect(pool.transactions).toEqual([]);
|
|
expect(pool.ended).toBe(true);
|
|
});
|
|
|
|
test('fails closed if the post-run ledger is not exactly the repository manifest', async () => {
|
|
await expect(runCloneMigrations({
|
|
env: safeEnvironment(),
|
|
argv: [],
|
|
poolFactory: config => new FakePool(config, { hideLatest: true }),
|
|
logger: { info: jest.fn() }
|
|
})).rejects.toBeInstanceOf(CloneMigrationVerificationError);
|
|
});
|
|
|
|
test('rejects missing, extra, or reordered ledger entries', () => {
|
|
const manifest = discoverMigrationManifest();
|
|
const rows = manifest.files.map(filename => ({ filename }));
|
|
expect(verifyExactMigrationLedger(rows, manifest)).toBe(true);
|
|
expect(() => verifyExactMigrationLedger(rows.slice(1), manifest)).toThrow(/count/);
|
|
expect(() => verifyExactMigrationLedger([...rows].reverse(), manifest)).toThrow(/differs/);
|
|
expect(() => verifyExactMigrationLedger([...rows, { filename: '999_unknown.sql' }], manifest))
|
|
.toThrow(/count/);
|
|
});
|
|
});
|