Files

162 lines
5.4 KiB
JavaScript

// API seeding for the workflow-folders specs.
//
// The folder assertions are about listing, scoping and moving — not about building a graph
// on the canvas — so every fixture is created through the REST API. That keeps the spec fast
// and, more importantly, keeps it off the canvas paths that need a live destination (which
// the SSRF guard refuses to create on *.common-dev envs).
//
// `page.request` shares the browser context's cookies, so this works on cloud (OIDC session)
// and self-hosted (Basic auth header) alike.
const { getAuthHeaders, getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js');
const api = () => `${process.env.ZO_BASE_URL}/api`;
const SINK_URL = 'http://example.com/sink';
// Maps the trigger registry's per-node kind to the top-level backend enum value.
const TRIGGER_TYPE_BY_KIND = {
alert_fired: 'AlertFired',
incident_event: 'IncidentEvent',
};
async function jsonOrThrow(resp, what) {
if (!resp.ok()) {
throw new Error(`${what} failed: ${resp.status()} ${(await resp.text()).slice(0, 300)}`);
}
return await resp.json();
}
async function createFolder(page, name, description = 'workflow folder automation') {
const resp = await page.request.post(`${api()}/v2/${getOrgIdentifier()}/folders/workflows`, {
headers: getAuthHeaders(),
data: { name, description },
});
const body = await jsonOrThrow(resp, `create folder "${name}"`);
return body.folderId;
}
async function deleteFolder(page, folderId) {
return await page.request.delete(
`${api()}/v2/${getOrgIdentifier()}/folders/workflows/${folderId}`,
{ headers: getAuthHeaders() }
);
}
async function createPipelineDestination(page, name) {
const resp = await page.request.post(
`${api()}/${getOrgIdentifier()}/alerts/destinations?module=pipeline`,
{
headers: getAuthHeaders(),
// NOT loopback: the SSRF guard rejects private/localhost URLs at destination-save
// time. example.com is IANA-reserved and never contacted — no spec fires the workflow.
data: { name, url: SINK_URL, method: 'post', type: 'http' },
}
);
await jsonOrThrow(resp, `create pipeline destination "${name}"`);
return name;
}
async function deleteDestination(page, name) {
return await page.request.delete(
`${api()}/${getOrgIdentifier()}/alerts/destinations/${name}`,
{ headers: getAuthHeaders() }
);
}
function workflowPayload(
name,
destName,
{ description = 'folder automation workflow', triggerKind = 'alert_fired' } = {},
) {
const triggerType = TRIGGER_TYPE_BY_KIND[triggerKind];
if (!triggerType) {
throw new Error(`workflowPayload: unmapped triggerKind "${triggerKind}"`);
}
return {
workflow: {
id: '',
org_id: '',
created_at: 0,
updated_at: 0,
created_by: '',
name,
description,
enabled: true,
nodes: [
{
id: 'trigger-1',
data: { node_type: 'workflow_trigger' },
// NodeData::WorkflowTrigger is a backend unit variant, so the kind
// survives only in meta (strings) — the same field the editor serializes.
meta: { trigger_kind: triggerKind },
position: { x: 100, y: 100 },
io_type: 'input',
},
{
id: 'dest-1',
data: { node_type: 'destination', destination_id: destName, template_override: null },
position: { x: 400, y: 100 },
io_type: 'output',
},
],
edges: [{ id: 'etrigger-1-dest-1', source: 'trigger-1', target: 'dest-1' }],
},
trigger_type: triggerType,
};
}
// The backend lowercases and trims the name on save, so the caller's name is not
// necessarily the one the row (and therefore the row's data-test) carries.
async function createWorkflow(
page,
{ name, destName, folderId, draft = false, triggerKind = 'alert_fired' },
) {
const params = new URLSearchParams();
if (folderId) params.set('folder', folderId);
if (draft) params.set('draft', 'true');
const qs = params.toString() ? `?${params}` : '';
const resp = await page.request.post(`${api()}/${getOrgIdentifier()}/workflows${qs}`, {
headers: getAuthHeaders(),
data: workflowPayload(name, destName, { triggerKind }),
});
const body = await jsonOrThrow(resp, `create workflow "${name}"`);
return { id: body.id, name: name.trim().toLowerCase() };
}
async function deleteWorkflow(page, workflowId) {
return await page.request.delete(
`${api()}/${getOrgIdentifier()}/workflows/${workflowId}`,
{ headers: getAuthHeaders() }
);
}
async function listWorkflows(page, params = {}) {
const qs = new URLSearchParams(params).toString();
const resp = await page.request.get(
`${api()}/${getOrgIdentifier()}/workflows${qs ? `?${qs}` : ''}`,
{ headers: getAuthHeaders() }
);
const body = await jsonOrThrow(resp, 'list workflows');
return Array.isArray(body) ? body : (body.list ?? body.data ?? []);
}
// Empty a folder before deleting it — a folder holding a workflow OR a draft refuses
// to delete, so a failed test would otherwise leak the folder permanently.
async function purgeFolder(page, folderId) {
for (const row of await listWorkflows(page, { folder: folderId })) {
await deleteWorkflow(page, row.id);
}
await deleteFolder(page, folderId);
}
module.exports = {
createFolder,
deleteFolder,
purgeFolder,
createPipelineDestination,
deleteDestination,
createWorkflow,
deleteWorkflow,
listWorkflows,
};