mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
185 lines
5.7 KiB
JavaScript
185 lines
5.7 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
PostgresPluginPackagePromptOutputKeyRetirementRepository,
|
|
assertPostgresPluginPackagePromptOutputKeyNotRetiring,
|
|
} = require('../dist/prompt-output/storage/postgresPluginPackagePromptOutputKeyRetirementRepository.js');
|
|
|
|
function preparationRow(preparation) {
|
|
return {
|
|
keyId: preparation.keyId,
|
|
retirementId: preparation.retirementId,
|
|
requestId: preparation.requestId,
|
|
mutationId: preparation.mutationId,
|
|
catalogDigest: preparation.catalogDigest,
|
|
materialProof: preparation.materialProof,
|
|
preparedAtMs: String(preparation.preparedAtMs),
|
|
preparationDigest: preparation.preparationDigest,
|
|
preparationJson: preparation,
|
|
};
|
|
}
|
|
|
|
function completionRow(completion) {
|
|
return {
|
|
keyId: completion.keyId,
|
|
retirementId: completion.retirementId,
|
|
requestId: completion.requestId,
|
|
mutationId: completion.mutationId,
|
|
preparationDigest: completion.preparationDigest,
|
|
retiredCatalogDigest: completion.retiredCatalogDigest,
|
|
absenceProof: completion.absenceProof,
|
|
completedAtMs: String(completion.completedAtMs),
|
|
completionDigest: completion.completionDigest,
|
|
completionJson: completion,
|
|
};
|
|
}
|
|
|
|
function storagePool(options = {}) {
|
|
const queries = [];
|
|
const preparations = new Map();
|
|
const completions = new Map();
|
|
const liveKeys = new Set(options.liveKeys ?? []);
|
|
const client = {
|
|
async query(sql, parameters = []) {
|
|
queries.push({ sql, parameters });
|
|
if (
|
|
sql.includes('key_retirement_preparations') &&
|
|
sql.includes('SELECT')
|
|
) {
|
|
const value = preparations.get(parameters[0]);
|
|
return { rows: value ? [preparationRow(value)] : [] };
|
|
}
|
|
if (
|
|
sql.includes('key_retirement_completions') &&
|
|
sql.includes('SELECT')
|
|
) {
|
|
const value = completions.get(parameters[0]);
|
|
return { rows: value ? [completionRow(value)] : [] };
|
|
}
|
|
if (sql.includes('count(*)::text')) {
|
|
return { rows: [{ count: liveKeys.has(parameters[0]) ? '1' : '0' }] };
|
|
}
|
|
if (
|
|
sql.includes('INSERT INTO') &&
|
|
sql.includes('key_retirement_preparations')
|
|
) {
|
|
const value = JSON.parse(parameters[8]);
|
|
preparations.set(value.keyId, value);
|
|
return { rows: [], rowCount: 1 };
|
|
}
|
|
if (
|
|
sql.includes('INSERT INTO') &&
|
|
sql.includes('key_retirement_completions')
|
|
) {
|
|
const value = JSON.parse(parameters[9]);
|
|
completions.set(value.keyId, value);
|
|
return { rows: [], rowCount: 1 };
|
|
}
|
|
return { rows: [], rowCount: 0 };
|
|
},
|
|
release() {},
|
|
};
|
|
return {
|
|
queries,
|
|
preparations,
|
|
completions,
|
|
client,
|
|
async connect() {
|
|
return client;
|
|
},
|
|
async query(sql, parameters) {
|
|
return client.query(sql, parameters);
|
|
},
|
|
};
|
|
}
|
|
|
|
function command() {
|
|
return {
|
|
keyId: 'cluster-key-retired',
|
|
retirementId: 'retirement-a',
|
|
requestId: 'request-a',
|
|
mutationId: 'mutation-a',
|
|
catalogDigest: '1'.repeat(64),
|
|
materialProof: '2'.repeat(64),
|
|
};
|
|
}
|
|
|
|
test('PostgreSQL appends key retirement facts behind a shared key fence', async () => {
|
|
const pool = storagePool();
|
|
const times = [10_000, 20_000, 30_000, 40_000];
|
|
const repository =
|
|
new PostgresPluginPackagePromptOutputKeyRetirementRepository({
|
|
pool,
|
|
now: () => times.shift(),
|
|
});
|
|
|
|
const prepared = await repository.prepare(command());
|
|
assert.equal(prepared.status, 'created');
|
|
assert.equal(prepared.preparation.preparedAtMs, 10_000);
|
|
const replay = await repository.prepare(command());
|
|
assert.equal(replay.status, 'existing');
|
|
assert.deepEqual(replay.preparation, prepared.preparation);
|
|
|
|
const completed = await repository.complete({
|
|
preparation: prepared.preparation,
|
|
retiredCatalogDigest: '3'.repeat(64),
|
|
absenceProof: '4'.repeat(64),
|
|
});
|
|
assert.equal(completed.status, 'created');
|
|
assert.equal(completed.completion.completedAtMs, 30_000);
|
|
const completionReplay = await repository.complete({
|
|
preparation: prepared.preparation,
|
|
retiredCatalogDigest: '3'.repeat(64),
|
|
absenceProof: '4'.repeat(64),
|
|
});
|
|
assert.equal(completionReplay.status, 'existing');
|
|
assert.deepEqual(completionReplay.completion, completed.completion);
|
|
assert.deepEqual(await repository.find(command().keyId), {
|
|
preparation: prepared.preparation,
|
|
completion: completed.completion,
|
|
});
|
|
|
|
const lockIndex = pool.queries.findIndex(({ sql }) =>
|
|
sql.includes('pg_advisory_xact_lock'),
|
|
);
|
|
const countIndex = pool.queries.findIndex(({ sql }) =>
|
|
sql.includes('count(*)::text'),
|
|
);
|
|
const insertIndex = pool.queries.findIndex(
|
|
({ sql }) =>
|
|
sql.includes('INSERT INTO') &&
|
|
sql.includes('key_retirement_preparations'),
|
|
);
|
|
assert.equal(lockIndex >= 0, true);
|
|
assert.equal(lockIndex < countIndex && countIndex < insertIndex, true);
|
|
await assert.rejects(
|
|
assertPostgresPluginPackagePromptOutputKeyNotRetiring(
|
|
pool.client,
|
|
command().keyId,
|
|
),
|
|
{ code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_CONFLICT' },
|
|
);
|
|
});
|
|
|
|
test('PostgreSQL refuses retirement while live ciphertext exists', async () => {
|
|
const pool = storagePool({ liveKeys: [command().keyId] });
|
|
const repository =
|
|
new PostgresPluginPackagePromptOutputKeyRetirementRepository({
|
|
pool,
|
|
now: () => 10_000,
|
|
});
|
|
await assert.rejects(repository.prepare(command()), {
|
|
code: 'PLUGIN_PACKAGE_PROMPT_OUTPUT_KEY_RETIREMENT_CONFLICT',
|
|
});
|
|
assert.equal(pool.preparations.size, 0);
|
|
assert.equal(
|
|
pool.queries.some(
|
|
({ sql }) =>
|
|
sql.includes('INSERT INTO') &&
|
|
sql.includes('key_retirement_preparations'),
|
|
),
|
|
false,
|
|
);
|
|
});
|