mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
362 lines
11 KiB
JavaScript
362 lines
11 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
createPluginPackageSecretBinding,
|
|
} = require('@qinglong/runtime-core/plugin-package-secret-binding');
|
|
const {
|
|
createPluginPackageSecretBindingTransitionPlan,
|
|
normalizePluginPackageSecretBindingTransitionPlan,
|
|
} = require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan');
|
|
const {
|
|
createPluginPackageResourceGeneration,
|
|
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
|
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
|
|
|
function manifest(secrets) {
|
|
return {
|
|
apiVersion: 'qinglong.io/v1alpha1',
|
|
kind: 'Package',
|
|
metadata: {
|
|
name: 'example-monitor',
|
|
displayName: 'Example Monitor',
|
|
version: '1.0.0',
|
|
description: 'Secret transition 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,
|
|
tools: ['secret.use'],
|
|
},
|
|
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
|
|
},
|
|
};
|
|
}
|
|
|
|
function secret(name, version) {
|
|
return createSecretRef({ projectId: 'project-1', name, version });
|
|
}
|
|
|
|
const previousManifest = manifest([
|
|
{ name: 'OPTIONAL_TOKEN', required: false },
|
|
{ name: 'TOKEN', required: true },
|
|
]);
|
|
const previousGeneration = createPluginPackageResourceGeneration({
|
|
installationId: 'install-1',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: 'a'.repeat(64),
|
|
generation: 1,
|
|
previousActiveLockDigest: null,
|
|
contentDigest: 'b'.repeat(64),
|
|
contents: previousManifest.spec.contents,
|
|
});
|
|
const previousBinding = createPluginPackageSecretBinding({
|
|
generation: previousGeneration,
|
|
manifest: previousManifest,
|
|
assignments: [
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 2) },
|
|
],
|
|
authority: {
|
|
kind: 'local-owner-confirmation',
|
|
evidenceDigest: 'c'.repeat(64),
|
|
},
|
|
boundAtMs: 90,
|
|
});
|
|
const previousTarget = previousBinding.target;
|
|
|
|
function nextGeneration(overrides = {}) {
|
|
return createPluginPackageResourceGeneration({
|
|
installationId: 'install-2',
|
|
projectId: 'project-1',
|
|
packageName: 'example-monitor',
|
|
lockDigest: 'd'.repeat(64),
|
|
generation: 2,
|
|
previousActiveLockDigest: previousBinding.target.lockDigest,
|
|
contentDigest: 'e'.repeat(64),
|
|
contents: previousManifest.spec.contents,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function transition(assignments, overrides = {}) {
|
|
return createPluginPackageSecretBindingTransitionPlan({
|
|
previousTarget,
|
|
previousBinding,
|
|
previousAttemptGeneration: overrides.previousAttemptGeneration ?? 1,
|
|
nextGeneration: nextGeneration(overrides.generation),
|
|
nextManifest: overrides.manifest ?? previousManifest,
|
|
assignments,
|
|
plannedAtMs: 100,
|
|
});
|
|
}
|
|
|
|
function postgresJsonbRoundTrip(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map(postgresJsonbRoundTrip);
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
return Object.fromEntries(
|
|
Object.entries(value)
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
.map(([key, entry]) => [key, postgresJsonbRoundTrip(entry)]),
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
test('derives an exact carry-forward for an unchanged next generation', () => {
|
|
const value = transition([
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 2) },
|
|
]);
|
|
assert.equal(value.kind, 'carry-forward');
|
|
assert.equal(value.previousActiveLockDigest, 'a'.repeat(64));
|
|
assert.deepEqual(
|
|
value.changes.map(({ name, requirement, reference }) => ({
|
|
name,
|
|
requirement,
|
|
reference,
|
|
})),
|
|
[
|
|
{
|
|
name: 'OPTIONAL_TOKEN',
|
|
requirement: 'unchanged',
|
|
reference: 'unchanged',
|
|
},
|
|
{
|
|
name: 'TOKEN',
|
|
requirement: 'unchanged',
|
|
reference: 'unchanged',
|
|
},
|
|
],
|
|
);
|
|
assert.deepEqual(
|
|
normalizePluginPackageSecretBindingTransitionPlan(value),
|
|
value,
|
|
);
|
|
});
|
|
|
|
test('normalizes a transition after PostgreSQL jsonb reorders object keys', () => {
|
|
const value = transition([
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 3) },
|
|
]);
|
|
assert.deepEqual(
|
|
normalizePluginPackageSecretBindingTransitionPlan(
|
|
postgresJsonbRoundTrip(value),
|
|
),
|
|
value,
|
|
);
|
|
});
|
|
|
|
test('distinguishes forward rotation from rebind and version rollback', () => {
|
|
const rotated = transition([
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 3) },
|
|
]);
|
|
assert.equal(rotated.kind, 'rotate');
|
|
assert.equal(rotated.changes[1].reference, 'rotated');
|
|
|
|
const rebound = transition([
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('replacement-token', 1) },
|
|
]);
|
|
assert.equal(rebound.kind, 'rebind');
|
|
assert.equal(rebound.changes[1].reference, 'rebound');
|
|
|
|
const rolledBack = transition([
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 1) },
|
|
]);
|
|
assert.equal(rolledBack.kind, 'rebind');
|
|
assert.equal(rolledBack.changes[1].reference, 'rebound');
|
|
});
|
|
|
|
test('classifies removal or unbinding as revocation', () => {
|
|
const nextManifest = manifest([{ name: 'OPTIONAL_TOKEN', required: false }]);
|
|
const value = transition([{ name: 'OPTIONAL_TOKEN', secretRef: null }], {
|
|
manifest: nextManifest,
|
|
});
|
|
assert.equal(value.kind, 'revoke');
|
|
assert.deepEqual(value.changes[1], {
|
|
name: 'TOKEN',
|
|
requirement: 'removed',
|
|
reference: 'revoked',
|
|
previous: { required: true, secretRef: secret('runtime-token', 2) },
|
|
next: null,
|
|
});
|
|
});
|
|
|
|
test('represents final requirement removal without inventing an empty binding', () => {
|
|
const value = transition([], { manifest: manifest([]) });
|
|
assert.equal(value.kind, 'revoke');
|
|
assert.equal(value.nextBindingPlan, null);
|
|
assert.equal(value.nextTarget.generation, 2);
|
|
assert.equal(value.changes.length, 2);
|
|
});
|
|
|
|
test('treats requirement additions and optional binding as rebind', () => {
|
|
const nextManifest = manifest([
|
|
{ name: 'EXTRA_TOKEN', required: false },
|
|
{ name: 'OPTIONAL_TOKEN', required: false },
|
|
{ name: 'TOKEN', required: true },
|
|
]);
|
|
const value = transition(
|
|
[
|
|
{ name: 'EXTRA_TOKEN', secretRef: secret('extra-token', 1) },
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 2) },
|
|
],
|
|
{ manifest: nextManifest },
|
|
);
|
|
assert.equal(value.kind, 'rebind');
|
|
assert.equal(value.changes[0].requirement, 'added');
|
|
assert.equal(value.changes[0].reference, 'bound');
|
|
});
|
|
|
|
test('supports adding the first Secret after an active generation without a binding', () => {
|
|
const value = createPluginPackageSecretBindingTransitionPlan({
|
|
previousTarget,
|
|
previousBinding: null,
|
|
previousAttemptGeneration: 1,
|
|
nextGeneration: nextGeneration(),
|
|
nextManifest: manifest([{ name: 'TOKEN', required: true }]),
|
|
assignments: [{ name: 'TOKEN', secretRef: secret('runtime-token', 1) }],
|
|
plannedAtMs: 100,
|
|
});
|
|
assert.equal(value.kind, 'rebind');
|
|
assert.equal(value.previousBinding, null);
|
|
assert.deepEqual(value.previousTarget, previousTarget);
|
|
assert.deepEqual(value.changes, [
|
|
{
|
|
name: 'TOKEN',
|
|
requirement: 'added',
|
|
reference: 'bound',
|
|
previous: null,
|
|
next: { required: true, secretRef: secret('runtime-token', 1) },
|
|
},
|
|
]);
|
|
assert.deepEqual(
|
|
normalizePluginPackageSecretBindingTransitionPlan(value),
|
|
value,
|
|
);
|
|
});
|
|
|
|
test('uses durable attempt generations and rejects skipped or detached targets', () => {
|
|
const assignments = [
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 2) },
|
|
];
|
|
assert.throws(
|
|
() => transition(assignments, { generation: { generation: 3 } }),
|
|
/immediate durable attempt generation/,
|
|
);
|
|
const retried = transition(assignments, {
|
|
generation: { generation: 3 },
|
|
previousAttemptGeneration: 2,
|
|
});
|
|
assert.equal(retried.nextTarget.generation, 3);
|
|
assert.equal(retried.previousAttemptGeneration, 2);
|
|
assert.throws(
|
|
() =>
|
|
transition(assignments, {
|
|
generation: { previousActiveLockDigest: 'f'.repeat(64) },
|
|
}),
|
|
/does not name the previous active lock/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
transition(assignments, { generation: { installationId: 'install-1' } }),
|
|
/immediate durable attempt generation/,
|
|
);
|
|
});
|
|
|
|
test('fails closed when classification, content or digest is rewritten', () => {
|
|
const value = transition([
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 3) },
|
|
]);
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageSecretBindingTransitionPlan({
|
|
...value,
|
|
kind: 'carry-forward',
|
|
}),
|
|
/classification/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageSecretBindingTransitionPlan({
|
|
...value,
|
|
changes: value.changes.slice(1),
|
|
}),
|
|
/classification/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackageSecretBindingTransitionPlan({
|
|
...value,
|
|
transitionDigest: 'f'.repeat(64),
|
|
}),
|
|
/digest/,
|
|
);
|
|
});
|
|
|
|
test('rejects extensible or accessor-bearing creation input', () => {
|
|
const assignments = [
|
|
{ name: 'OPTIONAL_TOKEN', secretRef: null },
|
|
{ name: 'TOKEN', secretRef: secret('runtime-token', 2) },
|
|
];
|
|
const input = {
|
|
previousTarget,
|
|
previousBinding,
|
|
previousAttemptGeneration: 1,
|
|
nextGeneration: nextGeneration(),
|
|
nextManifest: previousManifest,
|
|
assignments,
|
|
plannedAtMs: 100,
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
createPluginPackageSecretBindingTransitionPlan({ ...input, extra: true }),
|
|
/shape is invalid/,
|
|
);
|
|
const accessor = { ...input };
|
|
Object.defineProperty(accessor, 'plannedAtMs', {
|
|
enumerable: true,
|
|
get() {
|
|
throw new Error('must not execute');
|
|
},
|
|
});
|
|
assert.throws(
|
|
() => createPluginPackageSecretBindingTransitionPlan(accessor),
|
|
/data properties/,
|
|
);
|
|
});
|
|
|
|
test('exports the transition contract only through its explicit subpath', () => {
|
|
assert.equal(
|
|
require('../dist').createPluginPackageSecretBindingTransitionPlan,
|
|
undefined,
|
|
);
|
|
assert.equal(
|
|
require('@qinglong/runtime-core/plugin-package-secret-binding-transition-plan')
|
|
.createPluginPackageSecretBindingTransitionPlan,
|
|
createPluginPackageSecretBindingTransitionPlan,
|
|
);
|
|
});
|