Files
qinglong/packages/ql3-local-sqlite/test/pluginPackageAutomationPublicationRepository.test.cjs

282 lines
9.1 KiB
JavaScript

const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
PluginPackageAutomationPublicationConflictError,
PluginPackageAutomationPublicationUnavailableError,
createInitialPluginPackageAutomationPublication,
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
const {
createPluginPackageQuarantineEvent,
} = require('@qinglong/runtime-core/plugin-package-quarantine');
const {
pluginPackageAutomationPublicationFixture,
registerPluginPackageAutomationPublicationRepositoryContract,
} = require('../../../test/contracts/pluginPackageAutomationPublicationRepositoryContract.cjs');
const {
activateInstall,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqlitePluginPackageAutomationPublicationRepository,
} = require('../dist/plugin-package/pluginPackageAutomationPublicationRepository');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const digest = (value) => value.repeat(64);
function insertQuarantine(client, fixture) {
const record = fixture.install.active;
const event = createPluginPackageQuarantineEvent({
mutationId: `quarantine-${fixture.namespace}`,
revocationReceiptDigest: digest('d'),
impactDigest: digest('e'),
target: {
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
installState: record.state,
installVersion: record.version,
installRecordDigest: record.recordDigest,
activeLockDigest: record.activeLockDigest,
},
proposer: { type: 'user', id: 'owner-a' },
confirmer: { type: 'user', id: 'owner-b' },
authorizationMode: 'dual_control',
reasonCode: 'confirmed_key_compromise',
occurredAtMs: record.updatedAtMs + 1,
});
client
.prepare(
`INSERT INTO "QingLong3PluginPackageQuarantineEvents" (
event_digest, mutation_id, revocation_receipt_digest, impact_digest,
project_id, package_name, installation_id, lock_digest,
install_state, install_version, install_record_digest,
active_lock_digest, proposer_type, proposer_id, confirmer_type,
confirmer_id, authorization_mode, reason_code, occurred_at_ms,
event_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
event.eventDigest,
event.mutationId,
event.revocationReceiptDigest,
event.impactDigest,
event.target.projectId,
event.target.packageName,
event.target.installationId,
event.target.lockDigest,
event.target.installState,
event.target.installVersion,
event.target.installRecordDigest,
event.target.activeLockDigest,
event.proposer.type,
event.proposer.id,
event.confirmer.type,
event.confirmer.id,
event.authorizationMode,
event.reasonCode,
event.occurredAtMs,
JSON.stringify(event),
);
}
async function createRepository(_t, fixture) {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
return {
client,
repository:
new LocalSqlitePluginPackageAutomationPublicationRepository(client),
materializedRepository:
new LocalSqlitePluginPackageMaterializedRevisionRepository(
client,
fixture.registry,
),
close: () => client.close(),
};
}
registerPluginPackageAutomationPublicationRepositoryContract({
name: 'SQLite Plugin Package automation publication repository',
namespace: 'sqlite-automation-publication',
profile: 'edge',
createRepository,
});
test('fails closed when publication JSON is changed in place', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-corrupt',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await harness.materializedRepository.publish(fixture.revision);
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await harness.repository.publish(publication);
harness.client.exec('PRAGMA ignore_check_constraints = ON');
harness.client
.prepare(
`UPDATE "QingLong3PluginPackageAutomationPublications"
SET publication_json =
json_set(publication_json, '$.definitions.workflows[0].name', 'Drift')
WHERE publication_digest = ?`,
)
.run(publication.publicationDigest);
await assert.rejects(
harness.repository.findByDigest(publication.publicationDigest),
PluginPackageAutomationPublicationUnavailableError,
);
});
test('lists only materialized active generations whose automation head is stale', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-pending',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
const installRepository =
new LocalSqlitePluginPackageInstallRepository(harness.client);
await activateInstall(installRepository, fixture);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
await harness.materializedRepository.publish(fixture.revision);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [
{
projectId: fixture.projectId,
packageName: fixture.packageName,
},
],
truncated: false,
});
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await harness.repository.publish(publication);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
});
test('admits only the exact active current automation publication', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-start-guard',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(harness.client),
fixture,
);
await harness.materializedRepository.publish(fixture.revision);
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await harness.repository.publish(publication);
assert.equal(
await harness.repository.isStartAllowed(
fixture.projectId,
fixture.packageName,
publication.publicationDigest,
),
true,
);
assert.equal(
await harness.repository.isStartAllowed(
fixture.projectId,
fixture.packageName,
digest('f'),
),
false,
);
});
test('quarantine atomically removes pending work and rejects publication', async (t) => {
const fixture = pluginPackageAutomationPublicationFixture(
'sqlite-automation-quarantine',
{ profile: 'edge', name: 'daily' },
);
const harness = await createRepository(t, fixture);
t.after(() => harness.close());
await activateInstall(
new LocalSqlitePluginPackageInstallRepository(harness.client),
fixture,
);
await harness.materializedRepository.publish(fixture.revision);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [
{
projectId: fixture.projectId,
packageName: fixture.packageName,
},
],
truncated: false,
});
insertQuarantine(harness.client, fixture);
assert.deepEqual(await harness.repository.listPendingPage({ limit: 1 }), {
candidates: [],
truncated: false,
});
const publication = createInitialPluginPackageAutomationPublication(
fixture.revision,
fixture.registry,
1_000,
);
await assert.rejects(
harness.repository.publish(publication),
PluginPackageAutomationPublicationConflictError,
);
assert.equal(
await harness.repository.isStartAllowed(
fixture.projectId,
fixture.packageName,
publication.publicationDigest,
),
false,
);
});
test('publishes storage only through the explicit subpath', () => {
const entrypoint = require('@qinglong/local-sqlite/plugin-package-automation-publication');
assert.equal(
entrypoint.LocalSqlitePluginPackageAutomationPublicationRepository,
LocalSqlitePluginPackageAutomationPublicationRepository,
);
assert.equal(
require('../dist')
.LocalSqlitePluginPackageAutomationPublicationRepository,
undefined,
);
});