Files
electric-cherry/tests/unit/private-bank-drift.test.ts
T
2026-10-15 21:33:50 -04:00

304 lines
15 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { newFixture, type Fixture } from '../helpers/fixture.js';
describe.each(['electric_cherry_bank.cfg', 'electric_cherry_moderation.cfg'])(
'private config %s',
(filename) => {
let fixture: Fixture;
const servers = ['t5_zombie_theater_1', 't6_zombie_town_1'];
const secret = Buffer.from('set tws_bank_token "ec_bank_private_regression_secret"\n');
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture(servers);
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
await seedServerRegistry();
const { destOfKind } = await import('../../src/lib/server/config.js');
for (const server of servers) {
await mkdir(destOfKind(server, 'config'), { recursive: true });
await writeFile(join(destOfKind(server, 'config'), filename), secret);
}
});
afterEach(async () => {
vi.restoreAllMocks();
await fixture.cleanup();
});
async function evidenceIsPrivate() {
const { db } = await import('../../src/lib/server/db/index.js');
expect(db().prepare('SELECT 1 FROM file_blobs WHERE content=?').all(secret)).toEqual(
[]
);
expect(
db().prepare('SELECT 1 FROM deployment_files WHERE rel_path=?').all(filename)
).toEqual([]);
}
describe.each(servers)('private bank config on %s', (server) => {
it('does not read, preview, stage, or capture the generated file while repairing real drift', async () => {
const drift = await import('../../src/lib/server/fleet/drift.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { requestRestart } =
await import('../../src/lib/server/provisioning/requests.js');
const { applyStagedFilesForOperation } =
await import('../../src/lib/server/fleet/staged-files.js');
const backend = await import('../../src/lib/server/backends/files.js');
const reads = vi.spyOn(backend, 'readFile');
expect(await drift.scanServerDrift(server)).toEqual([]);
expect((await drift.previewDrift([server])).files).toEqual([]);
expect((await drift.prepareDriftCampaign(server)).files).toEqual([]);
expect(
(await drift.runDriftDeployment({ servers: [server] })).deploymentId
).toBeNull();
const dest = destOfKind(server, 'config');
await writeFile(join(dest, 'stray.cfg'), 'unwanted');
// The exemption is for the generated path, not every matching basename.
await mkdir(join(dest, 'nested'));
await writeFile(join(dest, 'nested', filename), 'unwanted nested file');
for (const name of ['modified.gsc', 'missing.gsc']) {
const artifact = createScript({
name,
releaseVersion: '1.0.0',
targetRelPath: name,
kind: 'script',
scopeType: 'game',
scopeValue: server.slice(0, 2),
content: Buffer.from('promoted')
});
db()
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
.run(artifact.version.id, artifact.script.id);
}
await writeFile(join(destOfKind(server, 'script'), 'modified.gsc'), 'modified');
const plan = await drift.previewDrift([server]);
expect(plan.files.map((file) => file.relPath).sort()).toEqual([
'missing.gsc',
'modified.gsc',
`nested/${filename}`,
'stray.cfg'
]);
const result = await drift.runDriftDeployment({
servers: [server],
expectedFingerprint: plan.fingerprint
});
const operation = requestRestart(server, {
kind: 'campaign',
id: result.deploymentId!
});
applyStagedFilesForOperation(operation.id, server);
expect(await drift.scanServerDrift(server)).toEqual([]);
expect(await readFile(join(dest, filename))).toEqual(secret);
expect(reads.mock.calls.some(([path]) => path === join(dest, filename))).toBe(
false
);
await evidenceIsPrivate();
});
it('filters old prepared bank evidence while retaining legitimate campaign changes', async () => {
const { prepareDriftCampaign, createDriftRebuildCampaign } =
await import('../../src/lib/server/fleet/drift.js');
const { beginDeployment } =
await import('../../src/lib/server/provisioning/history.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
const { db } = await import('../../src/lib/server/db/index.js');
await writeFile(join(destOfKind(server, 'config'), 'stray.cfg'), 'unwanted');
const prepared = await prepareDriftCampaign(server);
const privateFile = {
...prepared.files[0],
before: secret,
file: { ...prepared.files[0].file, relPath: filename }
};
prepared.files.push(privateFile);
prepared.plan.files.push(privateFile.file);
const operation = beginDeployment(server, 'rebuild', 'watchdog');
const id = createDriftRebuildCampaign(server, operation, 'old-incident', prepared);
expect(
db()
.prepare('SELECT rel_path FROM deployment_files WHERE deployment_id=?')
.all(id)
).toEqual([{ rel_path: 'stray.cfg' }]);
expect(
createDriftRebuildCampaign(server, operation, 'bank-only', {
...prepared,
files: [privateFile]
})
).toBeNull();
await evidenceIsPrivate();
});
it('rejects new and persisted private deletions before activation or evidence capture', async () => {
const staged = await import('../../src/lib/server/fleet/staged-files.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { requestRestart } =
await import('../../src/lib/server/provisioning/requests.js');
const { applyActivationFiles } =
await import('../../src/lib/server/provisioning/activation-files.js');
const { captureRecipePlan } =
await import('../../src/lib/server/fleet/launch-recipes.js');
const { destOfKind } = await import('../../src/lib/server/config.js');
db()
.prepare(
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('old','drift',datetime('now'),'ui')"
)
.run();
db()
.prepare(
"INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('old',?,1,'pending')"
)
.run(server);
const change = {
sourceKind: 'campaign' as const,
sourceId: 'old',
server,
kind: 'config' as const,
relPath: filename,
content: null,
inventorySource: 'drift:old'
};
expect(() => staged.queueFileChange(change)).toThrow('Private bank configuration');
db()
.prepare(
"INSERT INTO staged_file_changes(source_kind,source_id,server,kind,rel_path,inventory_source) VALUES('campaign','old',?,'config',?,'drift:old')"
)
.run(server, filename);
const operation = requestRestart(server, { kind: 'campaign', id: 'old' });
expect(() => staged.applyStagedFilesForOperation(operation.id, server)).toThrow(
'Private bank configuration'
);
const recipe = captureRecipePlan(
server.startsWith('t6') ? 't6' : 't5',
server.startsWith('t6') ? 'town' : 'theater'
);
expect(() => applyActivationFiles(recipe, operation.id, true)).toThrow(
'Private bank configuration'
);
expect(await readFile(join(destOfKind(server, 'config'), filename))).toEqual(
secret
);
await evidenceIsPrivate();
});
});
it('rejects the reserved upload name and redacts legacy receipts before packing their bytes', async () => {
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { packFileJson } = await import('../../src/lib/server/db/file-blobs.js');
expect(() =>
createScript({
name: 'private',
kind: 'config',
targetRelPath: filename,
scopeType: 'game',
scopeValue: 't5',
content: secret
})
).toThrow('reserved');
const packed = packFileJson({
before: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }]
});
expect(packed.hashes.size).toBe(0);
expect(JSON.stringify(packed.value)).not.toContain(secret.toString('base64'));
await evidenceIsPrivate();
});
it('blocks legacy private campaign diffs and blob downloads', async () => {
const { db } = await import('../../src/lib/server/db/index.js');
const { putFileBlob, getFileBlob } =
await import('../../src/lib/server/db/file-blobs.js');
db()
.prepare(
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')"
)
.run();
const hash = putFileBlob(secret)!;
const file = db()
.prepare(
"INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)"
)
.run(servers[0], filename, hash);
for (const route of [
await import('../../src/routes/api/campaigns/[id]/file/[fileId]/+server.js'),
await import('../../src/routes/api/deployments/[id]/file/[fileId]/+server.js')
]) {
await expect(
route.GET({ params: { id: 'legacy', fileId: String(file.lastInsertRowid) } })
).rejects.toMatchObject({ status: 404 });
}
expect(() => getFileBlob(hash)).toThrow('Private file content');
});
it('blocks old host commands and redacts late private read receipts from older agents', async () => {
const hosts = await import('../../src/lib/server/hosts/index.js');
const { db } = await import('../../src/lib/server/db/index.js');
const host = hosts.createHost({ name: 'Legacy private command host' });
const credential = hosts.enrollHost(
hosts.createEnrollment(host.id).token,
{}
).credential;
const payload = {
files: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }]
};
expect(() => hosts.queueHostOperation(host.id, 'files.stage', payload)).toThrow(
'Private bank'
);
const stage = hosts.queueHostOperation(host.id, 'files.stage', { files: [] });
db()
.prepare('UPDATE ec_host_operations SET payload=? WHERE id=?')
.run(JSON.stringify(payload), stage.id);
expect(() => hosts.getHostOperationPayload(host.id, stage.id)).toThrow('Private bank');
const healthyRead = hosts.queueHostOperation(host.id, 'files.read', {
path: 'configs/stray.cfg'
});
expect(
hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)
).toContain(healthyRead.id);
expect(
hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)
).not.toContain(stage.id);
const read = hosts.queueHostOperation(host.id, 'files.read', {
path: 'configs/stray.cfg'
});
db()
.prepare('UPDATE ec_host_operations SET payload=?,target_path=? WHERE id=?')
.run(
JSON.stringify({ path: `configs/${filename}` }),
`configs/${filename}`,
read.id
);
hosts.reportHostOperationStatus(host.id, read.id, {
state: 'completed',
result: { contentBase64: secret.toString('base64') }
});
expect(hosts.getHostOperation(read.id)?.result).toEqual({ redacted: true });
await evidenceIsPrivate();
// Even an old operation reference cannot authorize downloading captured credentials.
const { putFileBlob, referenceBlobs } =
await import('../../src/lib/server/db/file-blobs.js');
const hash = putFileBlob(secret)!;
referenceBlobs('operation', stage.id, [hash]);
db()
.prepare(
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')"
)
.run();
db()
.prepare(
"INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)"
)
.run(servers[0], filename, hash);
const { GET } = await import('../../src/routes/api/agent/blobs/[sha256]/+server.js');
expect(() =>
GET({
request: new Request('https://test', {
headers: { Authorization: `Bearer ${credential}` }
}),
params: { sha256: hash }
})
).toThrow();
});
}
);