Files
qinglong/packages/ql3-local-sqlite/test/pluginPackageSecretBindingRepository.test.cjs
T

399 lines
13 KiB
JavaScript

const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
PluginPackageSecretBindingConflictError,
PluginPackageSecretBindingUnavailableError,
createPluginPackageSecretBinding,
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
createPluginPackageSecretBindingTransitionPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
const {
createPluginPackageSecretBindingTransitionReceipt,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt');
const {
LocalSqlitePluginPackageSecretBindingRepository,
} = require('../dist/plugin-package/secret-binding/repository');
const {
LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository,
} = require('../dist/plugin-package/secret-binding/transitionReceiptRepository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const LOCK_DIGEST = 'a'.repeat(64);
const MANIFEST = {
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.0.0',
description: 'Secret binding repository fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '32Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [{ name: 'TOKEN', required: true }],
tools: [],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
function fixture(boundAtMs = 100) {
const generation = createPluginPackageResourceGeneration({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: LOCK_DIGEST,
generation: 1,
previousActiveLockDigest: null,
contentDigest: 'b'.repeat(64),
contents: MANIFEST.spec.contents,
});
const binding = createPluginPackageSecretBinding({
generation,
manifest: MANIFEST,
assignments: [
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'runtime-token',
version: 2,
}),
},
],
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: 'c'.repeat(64),
},
boundAtMs,
});
return { binding, generation };
}
async function harness(state = 'active') {
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
const isActive = state === 'active';
const isStaged = state === 'staged';
const source = fixture();
const previousLockDigest = 'f'.repeat(64);
const generation = isStaged
? createPluginPackageResourceGeneration({
installationId: 'install-2',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: LOCK_DIGEST,
generation: 2,
previousActiveLockDigest: previousLockDigest,
contentDigest: 'b'.repeat(64),
contents: MANIFEST.spec.contents,
})
: source.generation;
const binding = isStaged
? createPluginPackageSecretBinding({
generation,
manifest: MANIFEST,
assignments: source.binding.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
authority: source.binding.authority,
boundAtMs: source.binding.boundAtMs,
})
: source.binding;
client
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES ('project-1', 'Project 1', 'project-1', 'active', 1, 1, 1)`,
)
.run();
const recordDigest = 'd'.repeat(64);
const lockJson = JSON.stringify({
lockDigest: LOCK_DIGEST,
projectId: 'project-1',
packageName: 'example-monitor',
manifestDigest: binding.target.manifestDigest,
});
const recordJson = JSON.stringify({
installationId: isStaged ? 'install-2' : 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: LOCK_DIGEST,
state,
version: 1,
recordDigest,
});
client
.prepare(
`INSERT INTO "QingLong3PluginPackageInstalls" (
installation_id, project_id, package_name, package_version,
operation, lock_digest, target_generation,
previous_active_lock_digest, active_lock_digest, state, version,
last_mutation_id, last_mutation_digest, lock_json, record_json,
record_digest, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, '1.0.0', 'install', ?, ?, ?, ?, ?, 1,
'mutation-1', ?, ?, ?, ?, 1, 1)`,
)
.run(
isStaged ? 'install-2' : 'install-1',
'project-1',
'example-monitor',
LOCK_DIGEST,
generation.generation,
isStaged ? previousLockDigest : null,
isActive ? LOCK_DIGEST : isStaged ? previousLockDigest : null,
state,
'e'.repeat(64),
lockJson,
recordJson,
recordDigest,
);
client
.prepare(
`INSERT INTO "QingLong3PluginPackageInstallHeads"
(project_id, package_name, installation_id)
VALUES ('project-1', 'example-monitor', ?)`,
)
.run(isStaged ? 'install-2' : 'install-1');
if (isStaged) {
const previousRecordDigest = '9'.repeat(64);
const previousLockJson = JSON.stringify({
lockDigest: previousLockDigest,
projectId: 'project-1',
packageName: 'example-monitor',
manifestDigest: '8'.repeat(64),
});
const previousRecordJson = JSON.stringify({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: previousLockDigest,
state: 'active',
version: 1,
recordDigest: previousRecordDigest,
});
client
.prepare(
`INSERT INTO "QingLong3PluginPackageInstalls" (
installation_id, project_id, package_name, package_version,
operation, lock_digest, target_generation,
previous_active_lock_digest, active_lock_digest, state, version,
last_mutation_id, last_mutation_digest, lock_json, record_json,
record_digest, created_at_ms, updated_at_ms
) VALUES ('install-1', 'project-1', 'example-monitor', '0.9.0',
'install', ?, 1, NULL, ?, 'active', 1,
'mutation-previous', ?, ?, ?, ?, 0, 0)`,
)
.run(
previousLockDigest,
previousLockDigest,
'7'.repeat(64),
previousLockJson,
previousRecordJson,
previousRecordDigest,
);
}
return {
client,
binding,
generation,
repository: new LocalSqlitePluginPackageSecretBindingRepository(client),
};
}
test('publishes and exact-replays one active generation binding', async (t) => {
const value = await harness();
t.after(() => value.client.close());
const created = await value.repository.publish(value.binding);
assert.equal(created.status, 'created');
assert.deepEqual(created.binding, value.binding);
const replay = await value.repository.publish(value.binding);
assert.equal(replay.status, 'existing');
assert.deepEqual(
await value.repository.find(value.generation.generationDigest),
value.binding,
);
});
test('rejects inactive targets and conflicting content', async (t) => {
const inactive = await harness('failed');
t.after(() => inactive.client.close());
await assert.rejects(
inactive.repository.publish(inactive.binding),
PluginPackageSecretBindingConflictError,
);
const active = await harness();
t.after(() => active.client.close());
await active.repository.publish(active.binding);
await assert.rejects(
active.repository.publish(fixture(101).binding),
PluginPackageSecretBindingConflictError,
);
});
test('publishes a reviewed current staged generation but rejects post-stage states', async (t) => {
const staged = await harness('staged');
t.after(() => staged.client.close());
assert.equal(
(await staged.repository.publish(staged.binding)).status,
'created',
);
for (const state of ['queued', 'activating']) {
const rejected = await harness(state);
t.after(() => rejected.client.close());
await assert.rejects(
rejected.repository.publish(rejected.binding),
PluginPackageSecretBindingConflictError,
);
}
});
test('persists one exact staged transition receipt and rejects durable drift', async (t) => {
const value = await harness('staged');
t.after(() => value.client.close());
const previousGeneration = createPluginPackageResourceGeneration({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: 'f'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: '9'.repeat(64),
contents: MANIFEST.spec.contents,
});
const previousBinding = createPluginPackageSecretBinding({
generation: previousGeneration,
manifest: MANIFEST,
assignments: value.binding.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
authority: value.binding.authority,
boundAtMs: 90,
});
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET lock_json = json_set(lock_json, '$.manifestDigest', ?)
WHERE installation_id = 'install-1'`,
)
.run(previousBinding.target.manifestDigest);
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstalls"
SET lock_json = json_set(lock_json, '$.manifestDigest', ?)
WHERE installation_id = 'install-2'`,
)
.run(value.binding.target.manifestDigest);
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstallHeads"
SET installation_id = 'install-1'
WHERE project_id = 'project-1' AND package_name = 'example-monitor'`,
)
.run();
await value.repository.publish(previousBinding);
value.client
.prepare(
`UPDATE "QingLong3PluginPackageInstallHeads"
SET installation_id = 'install-2'
WHERE project_id = 'project-1' AND package_name = 'example-monitor'`,
)
.run();
await value.repository.publish(value.binding);
const plan = createPluginPackageSecretBindingTransitionPlan({
previousTarget: previousBinding.target,
previousBinding,
previousAttemptGeneration: 1,
nextGeneration: value.generation,
nextManifest: MANIFEST,
assignments: value.binding.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
plannedAtMs: 95,
});
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: value.binding.authority,
binding: value.binding,
committedAtMs: value.binding.boundAtMs,
});
const receipts =
new LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository(
value.client,
);
assert.equal(receipts.publishInTransaction(receipt).status, 'created');
assert.equal(receipts.publishInTransaction(receipt).status, 'existing');
assert.deepEqual(
await receipts.find(value.generation.generationDigest),
receipt,
);
value.client.exec('PRAGMA ignore_check_constraints = ON');
value.client
.prepare(
`UPDATE "QingLong3PluginPackageSecretBindingTransitionReceipts"
SET receipt_json = json_set(receipt_json, '$.committedAtMs', 999)
WHERE generation_digest = ?`,
)
.run(value.generation.generationDigest);
await assert.rejects(
receipts.find(value.generation.generationDigest),
PluginPackageSecretBindingUnavailableError,
);
});
test('fails closed when durable binding JSON is changed in place', async (t) => {
const value = await harness();
t.after(() => value.client.close());
await value.repository.publish(value.binding);
value.client.exec('PRAGMA ignore_check_constraints = ON');
value.client
.prepare(
`UPDATE "QingLong3PluginPackageSecretBindings"
SET binding_json = json_set(binding_json, '$.boundAtMs', 999)
WHERE generation_digest = ?`,
)
.run(value.generation.generationDigest);
await assert.rejects(
value.repository.find(value.generation.generationDigest),
PluginPackageSecretBindingUnavailableError,
);
});
test('publishes storage only through the explicit subpath', () => {
assert.equal(
require('@qinglong/local-sqlite/plugin-package-secret-binding')
.LocalSqlitePluginPackageSecretBindingRepository,
LocalSqlitePluginPackageSecretBindingRepository,
);
assert.equal(
require('../dist').LocalSqlitePluginPackageSecretBindingRepository,
undefined,
);
});