Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -3,19 +3,28 @@ import { applyRuntimePassword } from '../../src/lib/server/fleet/runtime-config.
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
describe('Plutainer password rendering', () => {
|
||||
it.each(['set rcon_password', 'seta rcon_password', 'RCON_PASSWORD'])('rewrites %s while preserving comments and other settings', prefix => {
|
||||
const source = `// set rcon_password "comment"\n${prefix} 'old' // keep\nset sv_voice "1"\n`;
|
||||
const rendered = applyRuntimePassword(source, 'abc&/\\$xyz');
|
||||
expect(rendered).toBe(`// set rcon_password "comment"\n${prefix} "abc&/\\$xyz" // keep\nset sv_voice "1"\n`);
|
||||
expect(applyRuntimePassword(rendered, 'abc&/\\$xyz')).toBe(rendered);
|
||||
});
|
||||
it.each(['set rcon_password', 'seta rcon_password', 'RCON_PASSWORD'])(
|
||||
'rewrites %s while preserving comments and other settings',
|
||||
(prefix) => {
|
||||
const source = `// set rcon_password "comment"\n${prefix} 'old' // keep\nset sv_voice "1"\n`;
|
||||
const rendered = applyRuntimePassword(source, 'abc&/\\$xyz');
|
||||
expect(rendered).toBe(
|
||||
`// set rcon_password "comment"\n${prefix} "abc&/\\$xyz" // keep\nset sv_voice "1"\n`
|
||||
);
|
||||
expect(applyRuntimePassword(rendered, 'abc&/\\$xyz')).toBe(rendered);
|
||||
}
|
||||
);
|
||||
|
||||
it('matches startup handling of missing, duplicate, quoted and unquoted assignments', () => {
|
||||
expect(applyRuntimePassword('set sv_voice "1"\n', 'secret')).toBe(
|
||||
'set sv_voice "1"\n\n// Added by Plutainer from PLUTAINER_RCON_PASSWORD.\nset rcon_password "secret"\n');
|
||||
expect(applyRuntimePassword('set rcon_password old\nrcon_password "later"', 'sec"ret'))
|
||||
.toBe('set rcon_password "secret"\nrcon_password "secret"');
|
||||
expect(applyRuntimePassword('set rcon_password old\r\n', '')).toBe('set rcon_password old\r\n');
|
||||
'set sv_voice "1"\n\n// Added by Plutainer from PLUTAINER_RCON_PASSWORD.\nset rcon_password "secret"\n'
|
||||
);
|
||||
expect(
|
||||
applyRuntimePassword('set rcon_password old\nrcon_password "later"', 'sec"ret')
|
||||
).toBe('set rcon_password "secret"\nrcon_password "secret"');
|
||||
expect(applyRuntimePassword('set rcon_password old\r\n', '')).toBe(
|
||||
'set rcon_password old\r\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes CRLF only when a password write is needed, matching Plutainer', () => {
|
||||
@@ -29,104 +38,209 @@ describe('Plutainer password rendering', () => {
|
||||
});
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); });
|
||||
afterEach(async () => { vi.restoreAllMocks(); await fixture.cleanup(); });
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture([]);
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
async function setup(game: 't5' | 't6') {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const host = hosts.createHost({ name: 'Runtime config' });
|
||||
hosts.enrollHost(hosts.createEnrollment(host.id).token, { protocolVersion: 1, capabilities: ['files.manifest'] });
|
||||
hosts.enrollHost(hosts.createEnrollment(host.id).token, {
|
||||
protocolVersion: 1,
|
||||
capabilities: ['files.manifest']
|
||||
});
|
||||
const server = `${game}_runtime_1`;
|
||||
db().prepare('INSERT INTO servers(name,game) VALUES(?,?)').run(server, game);
|
||||
db().prepare(`INSERT INTO native_instances(id,server,host_id,generation,library_id,image,port,created_at)
|
||||
VALUES('instance',?,?,1,'library','image',28960,?)`).run(server, host.id, new Date().toISOString());
|
||||
const launch = (id: string, kind: string, generation: number, password: string, serialized = false, state = 'completed') => {
|
||||
db().prepare(`INSERT INTO ec_host_operations(id,host_id,instance_id,generation,kind,payload,state,created_at,updated_at)
|
||||
VALUES(?,?,'instance',?,?,?,?,?,?)`).run(id, host.id, generation, kind,
|
||||
JSON.stringify(serialized ? {} : { rconPassword: password }), state, new Date().toISOString(), new Date().toISOString());
|
||||
if (serialized) db().prepare('INSERT INTO ec_host_operation_payloads(operation_id,payload) VALUES(?,?)')
|
||||
.run(id, JSON.stringify({ rconPassword: password }));
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO native_instances(id,server,host_id,generation,library_id,image,port,created_at)
|
||||
VALUES('instance',?,?,1,'library','image',28960,?)`
|
||||
)
|
||||
.run(server, host.id, new Date().toISOString());
|
||||
const launch = (
|
||||
id: string,
|
||||
kind: string,
|
||||
generation: number,
|
||||
password: string,
|
||||
serialized = false,
|
||||
state = 'completed'
|
||||
) => {
|
||||
db()
|
||||
.prepare(
|
||||
`INSERT INTO ec_host_operations(id,host_id,instance_id,generation,kind,payload,state,created_at,updated_at)
|
||||
VALUES(?,?,'instance',?,?,?,?,?,?)`
|
||||
)
|
||||
.run(
|
||||
id,
|
||||
host.id,
|
||||
generation,
|
||||
kind,
|
||||
JSON.stringify(serialized ? {} : { rconPassword: password }),
|
||||
state,
|
||||
new Date().toISOString(),
|
||||
new Date().toISOString()
|
||||
);
|
||||
if (serialized)
|
||||
db()
|
||||
.prepare('INSERT INTO ec_host_operation_payloads(operation_id,payload) VALUES(?,?)')
|
||||
.run(id, JSON.stringify({ rconPassword: password }));
|
||||
};
|
||||
return { db: db(), host, server, launch };
|
||||
}
|
||||
|
||||
it.each([
|
||||
['t5', 'a'.repeat(64)], ['t6', 'a'.repeat(64)], ['t5', 'shared-password'], ['t6', 'shared-password']
|
||||
] as const)('uses the assigned password for %s launches, drift and beta writes (%s)', async (game, password) => {
|
||||
const a = await setup(game);
|
||||
a.launch('create', 'instance.create', 1, password, true);
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const baseline = createScript({ name: 'Baseline', kind: 'baseline', scopeType: 'game', scopeValue: game,
|
||||
content: Buffer.from('set rcon_password "password"\nset sv_voice "1"\n') });
|
||||
a.db.prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(baseline.version.id, baseline.script.id);
|
||||
const { getPromotedServerConfig } = await import('../../src/lib/server/fleet/server-config.js');
|
||||
const { captureRecipe } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { configFileForGame } = await import('../../src/lib/server/config.js');
|
||||
const { sha256Buffer } = await import('../../src/lib/server/fleet/files.js');
|
||||
const { recordScript, getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
|
||||
const target = configFileForGame(game);
|
||||
const promoted = getPromotedServerConfig(game, null)!;
|
||||
const effective = getPromotedServerConfig(game, null, null, password)!;
|
||||
expect(effective.length - promoted.length).toBe(password.length - 8);
|
||||
const recipe = captureRecipe(game, game === 't5' ? 'theater' : 'transit', password);
|
||||
const recipeConfig = recipe.files.find(file => file.path === target)!;
|
||||
expect(Buffer.from(recipeConfig.content, 'base64').toString()).toContain(`set rcon_password "${password}"`);
|
||||
expect(recipeConfig.sha256).toBe(sha256Buffer(Buffer.from(recipeConfig.content, 'base64')));
|
||||
recordScript(a.server, target, null, sha256Buffer(promoted), promoted.length, 'native-create');
|
||||
const backend = await import('../../src/lib/server/backends/native.js');
|
||||
const { prepareNativeLaunch } = await import('../../src/lib/server/provisioning/launch-preparation.js');
|
||||
const prepared = await prepareNativeLaunch({ game, mapSlug: recipe.mapSlug, instance: backend.nativeInstance(a.server)!,
|
||||
serverKey: 'test-key', rconPassword: password, rebuild: false });
|
||||
const actualConfig = prepared.recipe.files.find(file => file.path === target)!;
|
||||
expect(actualConfig.sha256).toBe(recipeConfig.sha256);
|
||||
const actualBytes = prepared.blobs!.find(blob => blob.sha256 === actualConfig.sha256)!.content;
|
||||
expect(Buffer.from(actualBytes)).toEqual(Buffer.from(recipeConfig.content, 'base64'));
|
||||
expect(applyRuntimePassword(Buffer.from(actualBytes).toString(), password)).toBe(Buffer.from(actualBytes).toString());
|
||||
const payload = JSON.parse(Buffer.from(prepared.payloadJson).toString());
|
||||
expect(payload.rconPassword).toBe(password);
|
||||
expect(payload.files).toContainEqual(expect.objectContaining({ path: `configs/${target}`, sha256: actualConfig.sha256 }));
|
||||
expect(prepared.recipeSummary).not.toContain(password);
|
||||
let observed = effective;
|
||||
vi.spyOn(backend, 'nativeRequest').mockImplementation(async (_instance, kind) => {
|
||||
expect(kind).toBe('files.manifest');
|
||||
return { files: [{ path: `configs/${target}`, size: observed.length, sha256: sha256Buffer(observed) }] } as never;
|
||||
});
|
||||
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
|
||||
expect(await scanServerDrift(a.server)).toEqual([]);
|
||||
expect(getServerScript(a.server, target)?.sha256).toBe(sha256Buffer(effective));
|
||||
observed = Buffer.from(effective.toString().replace(password, 'b'.repeat(password.length)));
|
||||
expect(await scanServerDrift(a.server)).toEqual([expect.objectContaining({ relPath: target, driftKind: 'replace' })]);
|
||||
['t5', 'a'.repeat(64)],
|
||||
['t6', 'a'.repeat(64)],
|
||||
['t5', 'shared-password'],
|
||||
['t6', 'shared-password']
|
||||
] as const)(
|
||||
'uses the assigned password for %s launches, drift and beta writes (%s)',
|
||||
async (game, password) => {
|
||||
const a = await setup(game);
|
||||
a.launch('create', 'instance.create', 1, password, true);
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const baseline = createScript({
|
||||
name: 'Baseline',
|
||||
kind: 'baseline',
|
||||
scopeType: 'game',
|
||||
scopeValue: game,
|
||||
content: Buffer.from('set rcon_password "password"\nset sv_voice "1"\n')
|
||||
});
|
||||
a.db
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(baseline.version.id, baseline.script.id);
|
||||
const { getPromotedServerConfig } =
|
||||
await import('../../src/lib/server/fleet/server-config.js');
|
||||
const { captureRecipe } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { configFileForGame } = await import('../../src/lib/server/config.js');
|
||||
const { sha256Buffer } = await import('../../src/lib/server/fleet/files.js');
|
||||
const { recordScript, getServerScript } =
|
||||
await import('../../src/lib/server/fleet/inventory.js');
|
||||
const target = configFileForGame(game);
|
||||
const promoted = getPromotedServerConfig(game, null)!;
|
||||
const effective = getPromotedServerConfig(game, null, null, password)!;
|
||||
expect(effective.length - promoted.length).toBe(password.length - 8);
|
||||
const recipe = captureRecipe(game, game === 't5' ? 'theater' : 'transit', password);
|
||||
const recipeConfig = recipe.files.find((file) => file.path === target)!;
|
||||
expect(Buffer.from(recipeConfig.content, 'base64').toString()).toContain(
|
||||
`set rcon_password "${password}"`
|
||||
);
|
||||
expect(recipeConfig.sha256).toBe(sha256Buffer(Buffer.from(recipeConfig.content, 'base64')));
|
||||
recordScript(
|
||||
a.server,
|
||||
target,
|
||||
null,
|
||||
sha256Buffer(promoted),
|
||||
promoted.length,
|
||||
'native-create'
|
||||
);
|
||||
const backend = await import('../../src/lib/server/backends/native.js');
|
||||
const { prepareNativeLaunch } =
|
||||
await import('../../src/lib/server/provisioning/launch-preparation.js');
|
||||
const prepared = await prepareNativeLaunch({
|
||||
game,
|
||||
mapSlug: recipe.mapSlug,
|
||||
instance: backend.nativeInstance(a.server)!,
|
||||
serverKey: 'test-key',
|
||||
rconPassword: password,
|
||||
rebuild: false
|
||||
});
|
||||
const actualConfig = prepared.recipe.files.find((file) => file.path === target)!;
|
||||
expect(actualConfig.sha256).toBe(recipeConfig.sha256);
|
||||
const actualBytes = prepared.blobs!.find(
|
||||
(blob) => blob.sha256 === actualConfig.sha256
|
||||
)!.content;
|
||||
expect(Buffer.from(actualBytes)).toEqual(Buffer.from(recipeConfig.content, 'base64'));
|
||||
expect(applyRuntimePassword(Buffer.from(actualBytes).toString(), password)).toBe(
|
||||
Buffer.from(actualBytes).toString()
|
||||
);
|
||||
const payload = JSON.parse(Buffer.from(prepared.payloadJson).toString());
|
||||
expect(payload.rconPassword).toBe(password);
|
||||
expect(payload.files).toContainEqual(
|
||||
expect.objectContaining({ path: `configs/${target}`, sha256: actualConfig.sha256 })
|
||||
);
|
||||
expect(prepared.recipeSummary).not.toContain(password);
|
||||
let observed = effective;
|
||||
vi.spyOn(backend, 'nativeRequest').mockImplementation(async (_instance, kind) => {
|
||||
expect(kind).toBe('files.manifest');
|
||||
return {
|
||||
files: [
|
||||
{
|
||||
path: `configs/${target}`,
|
||||
size: observed.length,
|
||||
sha256: sha256Buffer(observed)
|
||||
}
|
||||
]
|
||||
} as never;
|
||||
});
|
||||
const { scanServerDrift } = await import('../../src/lib/server/fleet/drift.js');
|
||||
expect(await scanServerDrift(a.server)).toEqual([]);
|
||||
expect(getServerScript(a.server, target)?.sha256).toBe(sha256Buffer(effective));
|
||||
observed = Buffer.from(effective.toString().replace(password, 'b'.repeat(password.length)));
|
||||
expect(await scanServerDrift(a.server)).toEqual([
|
||||
expect.objectContaining({ relPath: target, driftKind: 'replace' })
|
||||
]);
|
||||
|
||||
const overlay = createScript({ name: 'Overlay', kind: 'config', scopeType: 'game', scopeValue: game,
|
||||
content: Buffer.from('set sv_voice "0"\nset rcon_password "overlay-password"\n') });
|
||||
const { openBeta, previewBetaPromotion } = await import('../../src/lib/server/fleet/beta.js');
|
||||
const { betaId } = await openBeta({ scripts: [{ scriptId: overlay.script.id, scriptVersionId: overlay.version.id }],
|
||||
servers: [a.server], waitForHealth: false, waitForMatch: false });
|
||||
const staged = a.db.prepare(`SELECT content_hash FROM staged_file_changes WHERE source_kind='beta' AND source_id=? AND server=?`)
|
||||
.get(String(betaId), a.server) as { content_hash: string };
|
||||
const { getFileBlob } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
const stagedContent = getFileBlob(staged.content_hash).toString();
|
||||
expect(stagedContent).toContain(`set rcon_password "${password}"`);
|
||||
expect(stagedContent).toContain('set sv_voice "0"');
|
||||
expect(previewBetaPromotion(betaId).configWrites).toContainEqual(expect.objectContaining({ server: a.server, sha256: staged.content_hash }));
|
||||
});
|
||||
const overlay = createScript({
|
||||
name: 'Overlay',
|
||||
kind: 'config',
|
||||
scopeType: 'game',
|
||||
scopeValue: game,
|
||||
content: Buffer.from('set sv_voice "0"\nset rcon_password "overlay-password"\n')
|
||||
});
|
||||
const { openBeta, previewBetaPromotion } =
|
||||
await import('../../src/lib/server/fleet/beta.js');
|
||||
const { betaId } = await openBeta({
|
||||
scripts: [{ scriptId: overlay.script.id, scriptVersionId: overlay.version.id }],
|
||||
servers: [a.server],
|
||||
waitForHealth: false,
|
||||
waitForMatch: false
|
||||
});
|
||||
const staged = a.db
|
||||
.prepare(
|
||||
`SELECT content_hash FROM staged_file_changes WHERE source_kind='beta' AND source_id=? AND server=?`
|
||||
)
|
||||
.get(String(betaId), a.server) as { content_hash: string };
|
||||
const { getFileBlob } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
const stagedContent = getFileBlob(staged.content_hash).toString();
|
||||
expect(stagedContent).toContain(`set rcon_password "${password}"`);
|
||||
expect(stagedContent).toContain('set sv_voice "0"');
|
||||
expect(previewBetaPromotion(betaId).configWrites).toContainEqual(
|
||||
expect.objectContaining({ server: a.server, sha256: staged.content_hash })
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('retains live launch credentials through history cleanup, allowing file bodies and retired credentials to expire', async () => {
|
||||
const a = await setup('t5');
|
||||
a.launch('create', 'instance.create', 1, 'original', true);
|
||||
a.launch('rebuild', 'instance.rebuild', 1, 'current', true);
|
||||
a.db.prepare("UPDATE native_instances SET generation=2 WHERE id='instance'").run();
|
||||
a.db.prepare('UPDATE ec_host_operations SET updated_at=?').run(new Date(Date.now() - 90 * 86400000).toISOString());
|
||||
a.db
|
||||
.prepare('UPDATE ec_host_operations SET updated_at=?')
|
||||
.run(new Date(Date.now() - 90 * 86400000).toISOString());
|
||||
const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
const { pruneHistory } = await import('../../src/lib/server/retention.js');
|
||||
const { runtimePasswordForServer } = await import('../../src/lib/server/backends/native.js');
|
||||
saveFleetSettings({ ...fleetSettings(), actionKeepDays: 1, payloadKeepDays: 1, historyKeepCount: 0 });
|
||||
saveFleetSettings({
|
||||
...fleetSettings(),
|
||||
actionKeepDays: 1,
|
||||
payloadKeepDays: 1,
|
||||
historyKeepCount: 0
|
||||
});
|
||||
expect(pruneHistory('operation-payload')).toBe(2);
|
||||
expect(runtimePasswordForServer(a.server)).toBe('current');
|
||||
expect(pruneHistory('operation')).toBe(1);
|
||||
expect(runtimePasswordForServer(a.server)).toBe('current');
|
||||
a.db.prepare("UPDATE native_instances SET retired_at=? WHERE id='instance'").run(new Date().toISOString());
|
||||
a.db
|
||||
.prepare("UPDATE native_instances SET retired_at=? WHERE id='instance'")
|
||||
.run(new Date().toISOString());
|
||||
expect(pruneHistory('operation')).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user