mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
226 lines
7.1 KiB
JavaScript
226 lines
7.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { randomUUID } = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
const {
|
|
LocalOwnerDeliveryAcknowledgementGcMutationConflictError,
|
|
LocalOwnerDeliveryAcknowledgementGcReferenceConflictError,
|
|
MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS,
|
|
MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS,
|
|
} = require('@qinglong/runtime-core/local-owner-delivery-acknowledgement-gc');
|
|
const {
|
|
openLocalSqliteAcknowledgementGcDatabase,
|
|
} = require('../dist/maintenance/acknowledgementGc');
|
|
const { openLocalSqliteBootstrapDatabase } = require('../dist/storage/bootstrap');
|
|
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
|
|
|
|
const NOW = 1_760_000_000_000;
|
|
const CREDENTIAL_TTL_MS = 600_000;
|
|
const SUBJECT_ID = `usr_${Buffer.alloc(16, 31).toString('base64url')}`;
|
|
const CREDENTIAL_ID = `own_${Buffer.alloc(16, 32).toString('base64url')}`;
|
|
const ACK_MUTATION_ID = '00000000-0000-4000-8000-000000000d01';
|
|
const DELIVERY_DIGEST = 'd'.repeat(64);
|
|
|
|
function fixture(t) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-ack-gc-'));
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
return {
|
|
databasePath: path.join(directory, 'qinglong3.sqlite'),
|
|
profile: 'edge',
|
|
};
|
|
}
|
|
|
|
function issuer() {
|
|
return {
|
|
subject: { type: 'system', id: 'owner-bootstrap' },
|
|
authenticationId: 'local-console-test',
|
|
authenticatedAtMs: NOW - 1_000,
|
|
expiresAtMs: NOW + 60_000,
|
|
assurance: 'local_console',
|
|
};
|
|
}
|
|
|
|
function gcCommand(compactedAtMs, overrides = {}) {
|
|
const mutationId = overrides.mutationId ?? randomUUID();
|
|
const requestId = overrides.requestId ?? `ack-gc-${mutationId}`;
|
|
return {
|
|
mutationId,
|
|
requestId,
|
|
acknowledgementMutationId: ACK_MUTATION_ID,
|
|
expectedKind: 'credential',
|
|
expectedDeliveryDigest: DELIVERY_DIGEST,
|
|
bridgeClearEvidence: {
|
|
kind: 'credential',
|
|
acknowledgementMutationId: ACK_MUTATION_ID,
|
|
inspectedAtMs: compactedAtMs,
|
|
evidenceDigest: 'e'.repeat(64),
|
|
},
|
|
retentionPolicy: {
|
|
version: 1,
|
|
replayRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_REPLAY_RETENTION_MS,
|
|
auditRetentionMs: MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS,
|
|
},
|
|
compactedAtMs,
|
|
audit: {
|
|
eventId: mutationId,
|
|
requestId,
|
|
operationId: 'owner.delivery_acknowledgement.gc',
|
|
projectId: null,
|
|
subject: { type: 'system', id: 'owner-acknowledgement-gc' },
|
|
authenticationId: 'local-owner-console',
|
|
outcome: 'allowed',
|
|
reasons: ['delivery_acknowledgement_gc'],
|
|
fence: null,
|
|
occurredAtMs: compactedAtMs,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function readyAcknowledgement(t) {
|
|
const options = fixture(t);
|
|
await migrateLocalSqlitePath(options);
|
|
const database = await openLocalSqliteBootstrapDatabase(options);
|
|
await database.ownerPepper.register({
|
|
mutationId: '00000000-0000-4000-8000-000000000d91',
|
|
pepperKeyId: 'legacy-v1',
|
|
materialDigest: 'a'.repeat(64),
|
|
backupDigest: 'b'.repeat(64),
|
|
registeredAtMs: NOW - 2_000,
|
|
});
|
|
await database.ownerPepper.activate({
|
|
mutationId: '00000000-0000-4000-8000-000000000d92',
|
|
pepperKeyId: 'legacy-v1',
|
|
expectedGeneration: 0,
|
|
activatedAtMs: NOW - 1_000,
|
|
});
|
|
await database.ownerBootstrap.provision({
|
|
mutationId: ACK_MUTATION_ID,
|
|
requestId: 'provision-d01',
|
|
identity: {
|
|
subject: { type: 'user', id: SUBJECT_ID },
|
|
status: 'active',
|
|
version: 1,
|
|
createdAtMs: NOW,
|
|
updatedAtMs: NOW,
|
|
},
|
|
credential: {
|
|
credentialId: CREDENTIAL_ID,
|
|
version: 1,
|
|
pepperKeyId: 'legacy-v1',
|
|
state: 'active',
|
|
subject: { type: 'user', id: SUBJECT_ID },
|
|
subjectStatus: 'active',
|
|
secretDigest: 'c'.repeat(64),
|
|
createdAtMs: NOW,
|
|
notBeforeAtMs: NOW,
|
|
expiresAtMs: NOW + CREDENTIAL_TTL_MS,
|
|
},
|
|
issuer: issuer(),
|
|
audit: {
|
|
eventId: ACK_MUTATION_ID,
|
|
requestId: 'provision-d01',
|
|
operationId: 'identity.bootstrap_provision',
|
|
projectId: null,
|
|
subject: issuer().subject,
|
|
authenticationId: issuer().authenticationId,
|
|
outcome: 'allowed',
|
|
reasons: ['local_console_provisioning'],
|
|
fence: null,
|
|
occurredAtMs: NOW,
|
|
},
|
|
createdAtMs: NOW,
|
|
});
|
|
const acknowledgement = {
|
|
kind: 'credential',
|
|
mutationId: ACK_MUTATION_ID,
|
|
requestId: 'provision-d01',
|
|
subjectId: SUBJECT_ID,
|
|
credentialId: CREDENTIAL_ID,
|
|
factDigest: 'c'.repeat(64),
|
|
deliveryDigest: DELIVERY_DIGEST,
|
|
ttlMs: CREDENTIAL_TTL_MS,
|
|
acknowledgedAtMs: NOW + 1,
|
|
};
|
|
await database.ownerBootstrap.recordDeliveryAcknowledgement(acknowledgement);
|
|
await database.close();
|
|
return { options, acknowledgement };
|
|
}
|
|
|
|
test('compacts one expired acknowledgement and reconstructs exact replay', async (t) => {
|
|
const { options, acknowledgement } = await readyAcknowledgement(t);
|
|
const compactedAtMs =
|
|
NOW + MIN_LOCAL_OWNER_ACKNOWLEDGEMENT_AUDIT_RETENTION_MS + 1_000;
|
|
const first = await openLocalSqliteAcknowledgementGcDatabase(options);
|
|
const second = await openLocalSqliteAcknowledgementGcDatabase(options);
|
|
t.after(() => Promise.all([first.close(), second.close()]));
|
|
const command = gcCommand(compactedAtMs);
|
|
const results = await Promise.all([
|
|
first.acknowledgementGc.compact(command),
|
|
second.acknowledgementGc.compact(command),
|
|
]);
|
|
assert.deepEqual(results.map((result) => result.status).sort(), [
|
|
'existing',
|
|
'inserted',
|
|
]);
|
|
assert.deepEqual(
|
|
await first.ownerBootstrap.resolveDeliveryAcknowledgement(ACK_MUTATION_ID),
|
|
acknowledgement,
|
|
);
|
|
assert.equal(
|
|
(await first.acknowledgementGc.resolveByAcknowledgement(ACK_MUTATION_ID))
|
|
.acknowledgementSemanticDigest.length,
|
|
64,
|
|
);
|
|
const client = new DatabaseSync(options.databasePath, { readOnly: true });
|
|
assert.equal(
|
|
client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3LocalOwnerDeliveryAcknowledgements"`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
client
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3LocalOwnerDeliveryAcknowledgementGc"`,
|
|
)
|
|
.get().count,
|
|
1,
|
|
);
|
|
client.close();
|
|
await assert.rejects(
|
|
first.acknowledgementGc.compact(
|
|
gcCommand(compactedAtMs, { mutationId: randomUUID() }),
|
|
),
|
|
LocalOwnerDeliveryAcknowledgementGcMutationConflictError,
|
|
);
|
|
});
|
|
|
|
test('rejects compaction while the credential is still active', async (t) => {
|
|
const { options } = await readyAcknowledgement(t);
|
|
const database = await openLocalSqliteAcknowledgementGcDatabase(options);
|
|
t.after(() => database.close());
|
|
await assert.rejects(
|
|
database.acknowledgementGc.compact(gcCommand(NOW + 2)),
|
|
LocalOwnerDeliveryAcknowledgementGcReferenceConflictError,
|
|
);
|
|
assert.equal(
|
|
await database.acknowledgementGc.resolveByAcknowledgement(ACK_MUTATION_ID),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await database.ownerBootstrap.resolveDeliveryAcknowledgement(
|
|
ACK_MUTATION_ID,
|
|
)
|
|
).deliveryDigest,
|
|
DELIVERY_DIGEST,
|
|
);
|
|
});
|