mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): add secure console cron scheduling
This commit is contained in:
@@ -13,6 +13,79 @@ const PRINCIPAL = Object.freeze({
|
||||
assurance: 'single_factor',
|
||||
});
|
||||
|
||||
test('uses task.read for bounded Trigger list and read projections', async () => {
|
||||
const { admission, events } = fixture();
|
||||
assert.deepEqual(
|
||||
await execute(
|
||||
admission,
|
||||
request({
|
||||
operation: {
|
||||
operationId: 'trigger.list',
|
||||
projectId: 'prj_default',
|
||||
limit: 16,
|
||||
},
|
||||
}),
|
||||
),
|
||||
{ statusCode: 200, body: { triggers: [], truncated: false } },
|
||||
);
|
||||
assert.deepEqual(events, [
|
||||
'authenticate',
|
||||
'authorize:task.read:prj_default',
|
||||
'audit:allowed:trigger.list',
|
||||
'confirm',
|
||||
'triggers:prj_default:16',
|
||||
]);
|
||||
|
||||
events.length = 0;
|
||||
assert.deepEqual(
|
||||
await execute(
|
||||
admission,
|
||||
request({
|
||||
operation: {
|
||||
operationId: 'trigger.get',
|
||||
projectId: 'prj_default',
|
||||
triggerId: 'cron:task-a',
|
||||
},
|
||||
}),
|
||||
),
|
||||
{
|
||||
statusCode: 200,
|
||||
body: { trigger: { triggerId: 'cron:task-a' } },
|
||||
},
|
||||
);
|
||||
assert.deepEqual(events, [
|
||||
'authenticate',
|
||||
'authorize:task.read:prj_default',
|
||||
'audit:allowed:trigger.get',
|
||||
'confirm',
|
||||
'trigger:prj_default:cron:task-a',
|
||||
]);
|
||||
});
|
||||
|
||||
test('defers Trigger put Policy, presence and mutation to the route', async () => {
|
||||
const { admission, events } = fixture();
|
||||
const prepared = await admission.prepare(
|
||||
request({
|
||||
localPresence: 'ql3p_bound',
|
||||
operation: {
|
||||
operationId: 'trigger.put',
|
||||
projectId: 'prj_default',
|
||||
triggerId: 'cron:task-a',
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(prepared.bodyMode, 'json');
|
||||
assert.equal(prepared.maximumBodyBytes, 20 * 1024);
|
||||
assert.deepEqual(await prepared.handle({ enabled: true }), {
|
||||
statusCode: 201,
|
||||
body: { status: 'created' },
|
||||
});
|
||||
assert.deepEqual(events, [
|
||||
'authenticate',
|
||||
'trigger-put:prj_default:cron:task-a',
|
||||
]);
|
||||
});
|
||||
|
||||
function request(overrides = {}) {
|
||||
return Object.freeze({
|
||||
requestId: 'local:019f70c0-0000-7000-8000-000000000001',
|
||||
@@ -152,6 +225,27 @@ function fixture(overrides = {}) {
|
||||
return { statusCode: 200, body: { task: { taskId: value.taskId } } };
|
||||
},
|
||||
},
|
||||
triggerListRoute: {
|
||||
async handle(value) {
|
||||
events.push(`triggers:${value.projectId}:${value.limit}`);
|
||||
return { statusCode: 200, body: { triggers: [], truncated: false } };
|
||||
},
|
||||
},
|
||||
triggerReadRoute: {
|
||||
async handle(value) {
|
||||
events.push(`trigger:${value.projectId}:${value.triggerId}`);
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { trigger: { triggerId: value.triggerId } },
|
||||
};
|
||||
},
|
||||
},
|
||||
triggerPutRoute: {
|
||||
async handle(value) {
|
||||
events.push(`trigger-put:${value.projectId}:${value.triggerId}`);
|
||||
return { statusCode: 201, body: { status: 'created' } };
|
||||
},
|
||||
},
|
||||
now: () => 10_000,
|
||||
randomUuid: () => '019f70c0-0000-4000-8000-000000000002',
|
||||
...overrides,
|
||||
|
||||
@@ -86,6 +86,10 @@ test('loads one bounded offline Console asset closure', () => {
|
||||
assert.match(text, /\.\.\.snapshot\.task\.spec\.config/u);
|
||||
assert.match(text, /snapshot\.task\.labels/u);
|
||||
assert.match(text, /setAttribute\('aria-readonly', 'true'\)/u);
|
||||
assert.match(text, /qinglong\/cron@v1/u);
|
||||
assert.match(text, /triggers\/\$\{mutation\.triggerId\}/u);
|
||||
assert.match(text, /state\.view === 'triggers'/u);
|
||||
assert.match(text, /trigger_fence_rejected/u);
|
||||
}
|
||||
if (requestPath === '/') {
|
||||
assert.match(text, /id="task-editor-dialog"/u);
|
||||
@@ -93,6 +97,8 @@ test('loads one bounded offline Console asset closure', () => {
|
||||
assert.match(text, /保存并生成本机证明/u);
|
||||
assert.match(text, /id="task-editor-title"/u);
|
||||
assert.match(text, /id="presence-copy"/u);
|
||||
assert.match(text, /id="trigger-editor-dialog"/u);
|
||||
assert.match(text, /data-view="triggers"/u);
|
||||
}
|
||||
}
|
||||
assert.ok(totalBytes <= 192 * 1024);
|
||||
|
||||
@@ -439,6 +439,9 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
taskDefinitions: runtime.taskDefinitions,
|
||||
taskDefinitionAdministrationForCredential:
|
||||
runtime.taskDefinitionAdministrationForCredential,
|
||||
triggers: runtime.triggers,
|
||||
triggerAdministrationForCredential:
|
||||
runtime.triggerAdministrationForCredential,
|
||||
apiCredentials: runtime.apiCredentials,
|
||||
ownerPepper: runtime.ownerPepper,
|
||||
projectPolicy: runtime.projectPolicy,
|
||||
@@ -694,6 +697,126 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
assert.equal(updatedTask.body.task.enabled, true);
|
||||
assert.equal(JSON.stringify(updatedTask).includes('/bin/echo'), false);
|
||||
|
||||
const triggerPath = '/api/v3/projects/default/triggers/cron:task-1';
|
||||
const triggerBody = JSON.stringify({
|
||||
expectedRevision: null,
|
||||
mutationId: '019f7300-0000-4000-8000-000000000703',
|
||||
taskId: 'task-1',
|
||||
taskRevision: updatedTask.body.task.revision,
|
||||
taskContentDigest: updatedTask.body.task.contentDigest,
|
||||
spec: {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '0 * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'skip',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
occurredAtMs: NOW,
|
||||
});
|
||||
const triggerOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(triggerBody)),
|
||||
},
|
||||
body: triggerBody,
|
||||
};
|
||||
const triggerChallenge = await request(
|
||||
port,
|
||||
`Bearer ${TOKEN}`,
|
||||
triggerPath,
|
||||
triggerOptions,
|
||||
);
|
||||
assert.equal(triggerChallenge.statusCode, 428);
|
||||
assert.equal(triggerChallenge.body.code, 'local_presence_required');
|
||||
const triggerProof = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(root, 'console-presence', triggerChallenge.body.proofFileName),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
const triggerCreated = await request(port, `Bearer ${TOKEN}`, triggerPath, {
|
||||
...triggerOptions,
|
||||
headers: {
|
||||
...triggerOptions.headers,
|
||||
'x-qinglong-local-presence': triggerProof.proof,
|
||||
},
|
||||
});
|
||||
assert.equal(triggerCreated.statusCode, 201, JSON.stringify(triggerCreated));
|
||||
assert.equal(triggerCreated.body.status, 'created');
|
||||
assert.equal(triggerCreated.body.trigger.revision, 1);
|
||||
assert.equal(triggerCreated.body.trigger.enabled, true);
|
||||
|
||||
const triggerList = await request(
|
||||
port,
|
||||
`Bearer ${TOKEN}`,
|
||||
'/api/v3/projects/default/triggers?limit=16',
|
||||
);
|
||||
assert.equal(triggerList.statusCode, 200);
|
||||
assert.equal(triggerList.body.triggers.length, 1);
|
||||
assert.equal(triggerList.body.triggers[0].triggerId, 'cron:task-1');
|
||||
assert.equal(triggerList.body.triggers[0].spec, undefined);
|
||||
|
||||
const triggerRead = await request(port, `Bearer ${TOKEN}`, triggerPath);
|
||||
assert.equal(triggerRead.statusCode, 200);
|
||||
assert.deepEqual(triggerRead.body.trigger.spec, {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '0 * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'skip',
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
triggerRead.body.trigger.taskContentDigest,
|
||||
updatedTask.body.task.contentDigest,
|
||||
);
|
||||
|
||||
const triggerDisableBody = JSON.stringify({
|
||||
...JSON.parse(triggerBody),
|
||||
expectedRevision: 1,
|
||||
mutationId: '019f7300-0000-4000-8000-000000000704',
|
||||
enabled: false,
|
||||
});
|
||||
const triggerDisableOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(triggerDisableBody)),
|
||||
},
|
||||
body: triggerDisableBody,
|
||||
};
|
||||
const triggerDisableChallenge = await request(
|
||||
port,
|
||||
`Bearer ${TOKEN}`,
|
||||
triggerPath,
|
||||
triggerDisableOptions,
|
||||
);
|
||||
assert.equal(triggerDisableChallenge.statusCode, 428);
|
||||
const triggerDisableProof = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
root,
|
||||
'console-presence',
|
||||
triggerDisableChallenge.body.proofFileName,
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
const triggerDisabled = await request(port, `Bearer ${TOKEN}`, triggerPath, {
|
||||
...triggerDisableOptions,
|
||||
headers: {
|
||||
...triggerDisableOptions.headers,
|
||||
'x-qinglong-local-presence': triggerDisableProof.proof,
|
||||
},
|
||||
});
|
||||
assert.equal(triggerDisabled.statusCode, 200);
|
||||
assert.equal(triggerDisabled.body.status, 'updated');
|
||||
assert.equal(triggerDisabled.body.trigger.revision, 2);
|
||||
assert.equal(triggerDisabled.body.trigger.enabled, false);
|
||||
|
||||
const taskStartBody = JSON.stringify({
|
||||
schema: 'qinglong/task-start@v1',
|
||||
mutationId: '019f7300-0000-7000-8000-000000000800',
|
||||
@@ -857,7 +980,8 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
WHERE operation_id IN (
|
||||
'run.get', 'run.list', 'run.events.list', 'run.steps.list',
|
||||
'run.cancel', 'task.authoring.read', 'task.create', 'task.get',
|
||||
'task.list', 'task.start', 'task.update', 'run.log.read'
|
||||
'task.list', 'task.start', 'task.update', 'run.log.read',
|
||||
'trigger.create', 'trigger.get', 'trigger.list', 'trigger.update'
|
||||
)
|
||||
ORDER BY operation_id, outcome`,
|
||||
)
|
||||
@@ -884,6 +1008,12 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
'task.start:allowed',
|
||||
'task.update:allowed',
|
||||
'task.update:approval_required',
|
||||
'trigger.create:allowed',
|
||||
'trigger.create:approval_required',
|
||||
'trigger.get:allowed',
|
||||
'trigger.list:allowed',
|
||||
'trigger.update:allowed',
|
||||
'trigger.update:approval_required',
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const { createTriggerRecord } = require('@qinglong/runtime-core/trigger');
|
||||
const {
|
||||
createLocalPresenceProofManager,
|
||||
} = require('../dist/authentication/localPresenceProof.js');
|
||||
const {
|
||||
createLocalApiTriggerPutRoute,
|
||||
} = require('../dist/trigger/triggerPutRoute.js');
|
||||
|
||||
const PRINCIPAL = Object.freeze({
|
||||
subject: Object.freeze({ type: 'user', id: 'owner' }),
|
||||
authenticationId: 'local_credential:owner-console:1',
|
||||
authenticatedAtMs: 9_000,
|
||||
expiresAtMs: 60_000,
|
||||
assurance: 'single_factor',
|
||||
});
|
||||
|
||||
const FENCE = Object.freeze({
|
||||
credentialId: 'owner-console',
|
||||
credentialVersion: 1,
|
||||
pepperKeyId: 'owner-v1',
|
||||
materialDigest: 'a'.repeat(64),
|
||||
subjectType: 'user',
|
||||
subjectId: 'owner',
|
||||
secretDigest: 'b'.repeat(64),
|
||||
notBeforeAtMs: 1,
|
||||
expiresAtMs: 60_000,
|
||||
});
|
||||
|
||||
function triggerBody(overrides = {}) {
|
||||
return Object.freeze({
|
||||
expectedRevision: null,
|
||||
mutationId: '019f9100-0000-4000-8000-000000000101',
|
||||
taskId: 'task-a',
|
||||
taskRevision: 2,
|
||||
taskContentDigest: 'c'.repeat(64),
|
||||
spec: Object.freeze({
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: Object.freeze({
|
||||
expression: '0 * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'skip',
|
||||
}),
|
||||
}),
|
||||
enabled: true,
|
||||
occurredAtMs: 10_000,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function uuidFactory() {
|
||||
let sequence = 200;
|
||||
return () => {
|
||||
sequence += 1;
|
||||
return `019f9100-0000-4000-8000-${String(sequence).padStart(12, '0')}`;
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(t) {
|
||||
const deploymentRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-trigger-put-'),
|
||||
);
|
||||
fs.chmodSync(deploymentRoot, 0o700);
|
||||
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
||||
const calls = [];
|
||||
const presenceProof = createLocalPresenceProofManager({
|
||||
deploymentRoot,
|
||||
profile: 'edge',
|
||||
now: () => 10_000,
|
||||
randomUuid: uuidFactory(),
|
||||
randomSecret: () => Buffer.alloc(32, 14),
|
||||
});
|
||||
t.after(() => presenceProof.close());
|
||||
const projectPolicy = {
|
||||
async resolve(projectId, subject) {
|
||||
calls.push(['policy', projectId, subject]);
|
||||
return {
|
||||
project: {
|
||||
id: projectId,
|
||||
name: 'Default',
|
||||
slug: 'default',
|
||||
status: 'active',
|
||||
version: 3,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
},
|
||||
binding: {
|
||||
projectId,
|
||||
subject,
|
||||
version: 5,
|
||||
state: 'active',
|
||||
role: 'owner',
|
||||
mutationId: 'owner-binding',
|
||||
changedBy: { type: 'user', id: 'bootstrap-owner' },
|
||||
createdAtMs: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
async append() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
};
|
||||
const triggers = {
|
||||
async findCurrentTrigger() {
|
||||
return null;
|
||||
},
|
||||
async findTriggerRevision() {
|
||||
return null;
|
||||
},
|
||||
async listTriggers() {
|
||||
return { triggers: [], truncated: false };
|
||||
},
|
||||
};
|
||||
const route = createLocalApiTriggerPutRoute({
|
||||
projectPolicy,
|
||||
triggers,
|
||||
async triggerAdministrationForCredential(fence) {
|
||||
calls.push(['credential-fence', fence]);
|
||||
return {
|
||||
async appendAuthorizedTriggerRevision(mutation) {
|
||||
calls.push(['mutation', mutation]);
|
||||
return {
|
||||
status: 'created',
|
||||
trigger: createTriggerRecord(mutation.command, 10_000),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
securityAudit: {
|
||||
async record(record) {
|
||||
calls.push(['audit', record]);
|
||||
},
|
||||
},
|
||||
presenceProof,
|
||||
now: () => 10_000,
|
||||
randomUuid: uuidFactory(),
|
||||
});
|
||||
const authenticated = Object.freeze({
|
||||
principal: PRINCIPAL,
|
||||
credentialFence: FENCE,
|
||||
async confirm() {
|
||||
calls.push(['confirm']);
|
||||
},
|
||||
});
|
||||
return { route, calls, deploymentRoot, authenticated };
|
||||
}
|
||||
|
||||
function request(state, body, overrides = {}) {
|
||||
return Object.freeze({
|
||||
requestId: 'local:019f9100-0000-4000-8000-000000000301',
|
||||
projectId: 'default',
|
||||
triggerId: 'cron:task-a',
|
||||
body,
|
||||
presence: null,
|
||||
authenticated: state.authenticated,
|
||||
signal: new AbortController().signal,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function readProof(state, challenge) {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
state.deploymentRoot,
|
||||
'console-presence',
|
||||
challenge.body.proofFileName,
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
).proof;
|
||||
}
|
||||
|
||||
test('requires exact local presence and commits Trigger audit plus mutation through one credential fence', async (t) => {
|
||||
const state = fixture(t);
|
||||
const body = triggerBody();
|
||||
const challenge = await state.route.handle(request(state, body));
|
||||
assert.equal(challenge.statusCode, 428);
|
||||
assert.equal(challenge.body.code, 'local_presence_required');
|
||||
const created = await state.route.handle(
|
||||
request(state, body, { presence: readProof(state, challenge) }),
|
||||
);
|
||||
assert.equal(created.statusCode, 201);
|
||||
assert.equal(created.body.status, 'created');
|
||||
assert.equal(created.body.trigger.triggerId, 'cron:task-a');
|
||||
const mutation = state.calls.find(([kind]) => kind === 'mutation')[1];
|
||||
assert.deepEqual(mutation.actor, { type: 'user', id: 'owner' });
|
||||
assert.deepEqual(mutation.fence, {
|
||||
projectVersion: 3,
|
||||
bindingVersion: 5,
|
||||
});
|
||||
assert.equal(mutation.audit.operationId, 'trigger.create');
|
||||
assert.equal(mutation.audit.outcome, 'allowed');
|
||||
assert.equal(
|
||||
mutation.audit.authenticationId.startsWith('local_presence:'),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(
|
||||
state.calls
|
||||
.filter(([kind]) => kind === 'audit')
|
||||
.map(([, audit]) => [audit.operationId, audit.outcome, audit.reasons[0]]),
|
||||
[['trigger.create', 'approval_required', 'local_presence_required']],
|
||||
);
|
||||
});
|
||||
|
||||
test('binds the proof to exact Trigger content and rejects malformed requests', async (t) => {
|
||||
const state = fixture(t);
|
||||
assert.deepEqual(
|
||||
await state.route.handle(request(state, { enabled: true })),
|
||||
{ statusCode: 400, body: { code: 'invalid_trigger' } },
|
||||
);
|
||||
const body = triggerBody();
|
||||
const challenge = await state.route.handle(request(state, body));
|
||||
const proof = readProof(state, challenge);
|
||||
assert.deepEqual(
|
||||
await state.route.handle(
|
||||
request(state, triggerBody({ enabled: false }), { presence: proof }),
|
||||
),
|
||||
{ statusCode: 401, body: { code: 'local_presence_rejected' } },
|
||||
);
|
||||
assert.equal(state.calls.filter(([kind]) => kind === 'mutation').length, 0);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
createLocalApiTriggerListRoute,
|
||||
createLocalApiTriggerReadRoute,
|
||||
} = require('../dist/trigger/triggerReadRoutes.js');
|
||||
const {
|
||||
TriggerUnavailableError,
|
||||
triggerContentDigest,
|
||||
} = require('@qinglong/runtime-core/trigger');
|
||||
|
||||
function trigger(triggerId = 'cron:task-a') {
|
||||
const fields = {
|
||||
projectId: 'default',
|
||||
triggerId,
|
||||
revision: 2,
|
||||
taskId: 'task-a',
|
||||
taskRevision: 3,
|
||||
taskContentDigest: 'a'.repeat(64),
|
||||
spec: {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '0 * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'skip',
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
};
|
||||
return Object.freeze({
|
||||
...fields,
|
||||
mutationId: '019f7300-0000-4000-8000-000000000001',
|
||||
contentDigest: triggerContentDigest(fields),
|
||||
createdAtMs: 100,
|
||||
updatedAtMs: 200,
|
||||
});
|
||||
}
|
||||
|
||||
test('projects bounded Trigger summaries and one complete cron detail', async () => {
|
||||
const record = trigger();
|
||||
const source = {
|
||||
async listTriggers(input) {
|
||||
assert.deepEqual(input, { projectId: 'default', limit: 16 });
|
||||
return { triggers: [record], truncated: false };
|
||||
},
|
||||
async findCurrentTrigger(projectId, triggerId) {
|
||||
assert.equal(projectId, 'default');
|
||||
return triggerId === record.triggerId ? record : null;
|
||||
},
|
||||
};
|
||||
const list = createLocalApiTriggerListRoute(source);
|
||||
const read = createLocalApiTriggerReadRoute(source);
|
||||
const listed = await list.handle({ projectId: 'default', limit: 16 });
|
||||
assert.equal(listed.statusCode, 200);
|
||||
assert.equal(listed.body.triggers[0].triggerId, record.triggerId);
|
||||
assert.equal(listed.body.triggers[0].spec, undefined);
|
||||
assert.equal(listed.body.triggers[0].taskContentDigest, undefined);
|
||||
|
||||
const found = await read.handle({
|
||||
projectId: 'default',
|
||||
triggerId: record.triggerId,
|
||||
});
|
||||
assert.deepEqual(found, {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
trigger: {
|
||||
triggerId: record.triggerId,
|
||||
revision: 2,
|
||||
taskId: 'task-a',
|
||||
taskRevision: 3,
|
||||
specSchema: 'qinglong/cron@v1',
|
||||
enabled: true,
|
||||
contentDigest: record.contentDigest,
|
||||
createdAtMs: 100,
|
||||
updatedAtMs: 200,
|
||||
projectId: 'default',
|
||||
taskContentDigest: 'a'.repeat(64),
|
||||
spec: record.spec,
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
await read.handle({ projectId: 'default', triggerId: 'missing' }),
|
||||
{ statusCode: 404, body: { code: 'trigger_not_found' } },
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed when Trigger storage is unavailable', async () => {
|
||||
const source = {
|
||||
async listTriggers() {
|
||||
throw new TriggerUnavailableError();
|
||||
},
|
||||
async findCurrentTrigger() {
|
||||
throw new TriggerUnavailableError();
|
||||
},
|
||||
};
|
||||
assert.deepEqual(
|
||||
await createLocalApiTriggerListRoute(source).handle({
|
||||
projectId: 'default',
|
||||
limit: 16,
|
||||
}),
|
||||
{ statusCode: 503, body: { code: 'trigger_query_unavailable' } },
|
||||
);
|
||||
assert.deepEqual(
|
||||
await createLocalApiTriggerReadRoute(source).handle({
|
||||
projectId: 'default',
|
||||
triggerId: 'cron:task-a',
|
||||
}),
|
||||
{ statusCode: 503, body: { code: 'trigger_query_unavailable' } },
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user