Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1463 lines
42 KiB
TypeScript
1463 lines
42 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { NodeOperationError } from 'n8n-workflow';
|
|
import { AlgaPsa } from '../nodes/AlgaPsa/AlgaPsa.node';
|
|
import { createApiError, createExecuteHarness } from './testUtils';
|
|
|
|
async function executeNode(
|
|
items: Array<Record<string, unknown>>,
|
|
requestHandler: (options: any, index: number) => unknown,
|
|
continueOnFail = false,
|
|
) {
|
|
const node = new AlgaPsa();
|
|
const harness = createExecuteHarness({
|
|
items,
|
|
continueOnFail,
|
|
requestHandler,
|
|
});
|
|
|
|
const output = await node.execute.call(harness.context);
|
|
return {
|
|
output,
|
|
requests: harness.requests,
|
|
};
|
|
}
|
|
|
|
async function executeNodeExpectFailure(
|
|
items: Array<Record<string, unknown>>,
|
|
requestHandler: (options: any, index: number) => unknown = () => ({ data: {} }),
|
|
) {
|
|
const node = new AlgaPsa();
|
|
const harness = createExecuteHarness({
|
|
items,
|
|
continueOnFail: false,
|
|
requestHandler,
|
|
});
|
|
|
|
try {
|
|
await node.execute.call(harness.context);
|
|
} catch (error) {
|
|
return {
|
|
error,
|
|
requests: harness.requests,
|
|
};
|
|
}
|
|
|
|
throw new Error('Expected execute to fail');
|
|
}
|
|
|
|
const baseCreateParams = {
|
|
resource: 'ticket',
|
|
ticketOperation: 'create',
|
|
title: 'Example Ticket',
|
|
client_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000001' },
|
|
board_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000002' },
|
|
status_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000003' },
|
|
priority_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000004' },
|
|
};
|
|
|
|
const baseContactCreateParams = {
|
|
resource: 'contact',
|
|
contactOperation: 'create',
|
|
full_name: 'Ada Lovelace',
|
|
};
|
|
|
|
describe('Node execute operations', () => {
|
|
it('T010: Ticket Create builds POST payload with required field mappings', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseCreateParams,
|
|
createAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: 'ticket-1' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('POST');
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/tickets');
|
|
expect(requests[0]?.body).toMatchObject({
|
|
title: 'Example Ticket',
|
|
client_id: '00000000-0000-0000-0000-000000000001',
|
|
board_id: '00000000-0000-0000-0000-000000000002',
|
|
status_id: '00000000-0000-0000-0000-000000000003',
|
|
priority_id: '00000000-0000-0000-0000-000000000004',
|
|
});
|
|
});
|
|
|
|
it('T011: Ticket Create includes optional fields only when provided', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseCreateParams,
|
|
createAdditionalFields: {
|
|
url: 'https://example.test/ticket',
|
|
tags: 'urgent,automation',
|
|
assigned_to: '00000000-0000-0000-0000-000000000005',
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: 'ticket-1' } }),
|
|
);
|
|
|
|
expect(requests[0]?.body).toMatchObject({
|
|
url: 'https://example.test/ticket',
|
|
tags: ['urgent', 'automation'],
|
|
assigned_to: '00000000-0000-0000-0000-000000000005',
|
|
});
|
|
expect(requests[0]?.body).not.toHaveProperty('location_id');
|
|
expect(requests[0]?.body).not.toHaveProperty('category_id');
|
|
});
|
|
|
|
it('T012: Ticket Create unwraps created ticket object from API data wrapper', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
...baseCreateParams,
|
|
createAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: 'ticket-123', title: 'Created' } }),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({ ticket_id: 'ticket-123', title: 'Created' });
|
|
});
|
|
|
|
it('T013: Ticket Get sends GET by id and rejects empty id before request', async () => {
|
|
const success = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'get',
|
|
ticketId: '00000000-0000-0000-0000-000000000010',
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: '00000000-0000-0000-0000-000000000010' } }),
|
|
);
|
|
|
|
expect(success.requests[0]?.method).toBe('GET');
|
|
expect(success.requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/tickets/00000000-0000-0000-0000-000000000010',
|
|
);
|
|
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'get',
|
|
ticketId: '',
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
it('T014: Ticket Get returns expected ticket object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'get',
|
|
ticketId: '00000000-0000-0000-0000-000000000011',
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: '00000000-0000-0000-0000-000000000011', title: 'A' } }),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
ticket_id: '00000000-0000-0000-0000-000000000011',
|
|
title: 'A',
|
|
});
|
|
});
|
|
|
|
it('T015: Ticket List serializes pagination/sort/order/filter query parameters', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'list',
|
|
page: 2,
|
|
limit: 50,
|
|
sort: 'ticket_number',
|
|
order: 'asc',
|
|
listFilters: {
|
|
client_id: '00000000-0000-0000-0000-000000000111',
|
|
is_open: true,
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: [], pagination: { page: 2, total: 0 } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/tickets');
|
|
expect(requests[0]?.qs).toMatchObject({
|
|
page: 2,
|
|
limit: 50,
|
|
sort: 'ticket_number',
|
|
order: 'asc',
|
|
client_id: '00000000-0000-0000-0000-000000000111',
|
|
is_open: true,
|
|
});
|
|
});
|
|
|
|
it('T016: Ticket List keeps pagination metadata in node output', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'list',
|
|
page: 1,
|
|
limit: 25,
|
|
sort: 'entered_at',
|
|
order: 'desc',
|
|
listFilters: {},
|
|
},
|
|
],
|
|
() => ({ data: [{ ticket_id: '1' }], pagination: { page: 1, total: 1, totalPages: 1 } }),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
data: [{ ticket_id: '1' }],
|
|
pagination: { page: 1, total: 1, totalPages: 1 },
|
|
});
|
|
});
|
|
|
|
it('T047: Ticket List Comments sends GET with optional query parameters', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'listComments',
|
|
ticketId: '00000000-0000-0000-0000-000000000112',
|
|
commentListOptions: {
|
|
limit: 10,
|
|
offset: 20,
|
|
order: 'desc',
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: [] }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/tickets/00000000-0000-0000-0000-000000000112/comments',
|
|
);
|
|
expect(requests[0]?.qs).toMatchObject({
|
|
limit: 10,
|
|
offset: 20,
|
|
order: 'desc',
|
|
});
|
|
});
|
|
|
|
it('T048: Ticket List Comments preserves array output from API data wrapper', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'listComments',
|
|
ticketId: '00000000-0000-0000-0000-000000000113',
|
|
commentListOptions: {},
|
|
},
|
|
],
|
|
() => ({
|
|
data: [
|
|
{ comment_id: 'comment-1', comment_text: 'First' },
|
|
{ comment_id: 'comment-2', comment_text: 'Second' },
|
|
],
|
|
}),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
data: [
|
|
{ comment_id: 'comment-1', comment_text: 'First' },
|
|
{ comment_id: 'comment-2', comment_text: 'Second' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('T017: Ticket Search serializes query and optional search filters', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'search',
|
|
query: 'outage',
|
|
searchLimit: 15,
|
|
searchAdditionalFields: {
|
|
include_closed: true,
|
|
fields: ['title', 'ticket_number'],
|
|
status_ids: 'status-a,status-b',
|
|
priority_ids: 'priority-a',
|
|
client_ids: 'client-a',
|
|
assigned_to_ids: 'user-a,user-b',
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: [] }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/tickets/search');
|
|
expect(requests[0]?.qs).toMatchObject({
|
|
query: 'outage',
|
|
limit: 15,
|
|
include_closed: true,
|
|
fields: 'title,ticket_number',
|
|
status_ids: 'status-a,status-b',
|
|
priority_ids: 'priority-a',
|
|
client_ids: 'client-a',
|
|
assigned_to_ids: 'user-a,user-b',
|
|
});
|
|
});
|
|
|
|
it('T018: Ticket Search returns result set and handles empty data array', async () => {
|
|
const populated = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'search',
|
|
query: 'vpn',
|
|
searchLimit: 10,
|
|
searchAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: [{ ticket_id: 'a' }, { ticket_id: 'b' }] }),
|
|
);
|
|
|
|
expect(populated.output[0][0].json).toEqual({ data: [{ ticket_id: 'a' }, { ticket_id: 'b' }] });
|
|
|
|
const empty = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'search',
|
|
query: 'no-match',
|
|
searchLimit: 10,
|
|
searchAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: [] }),
|
|
);
|
|
|
|
expect(empty.output[0][0].json).toEqual({ data: [] });
|
|
});
|
|
|
|
it('T019: Ticket Update sends PUT with only provided mutable fields', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'update',
|
|
ticketId: '00000000-0000-0000-0000-000000000020',
|
|
updateAdditionalFields: {
|
|
title: 'Updated title',
|
|
url: '',
|
|
client_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000021' },
|
|
board_id: { mode: 'id', value: '' },
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: '00000000-0000-0000-0000-000000000020' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('PUT');
|
|
expect(requests[0]?.body).toEqual({
|
|
title: 'Updated title',
|
|
client_id: '00000000-0000-0000-0000-000000000021',
|
|
});
|
|
});
|
|
|
|
it('T020: Ticket Update returns updated ticket object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'update',
|
|
ticketId: '00000000-0000-0000-0000-000000000022',
|
|
updateAdditionalFields: {
|
|
title: 'Updated',
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: '00000000-0000-0000-0000-000000000022', title: 'Updated' } }),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
ticket_id: '00000000-0000-0000-0000-000000000022',
|
|
title: 'Updated',
|
|
});
|
|
});
|
|
|
|
it('T049: Ticket Add Comment sends POST with supported payload fields only', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'addComment',
|
|
ticketId: '00000000-0000-0000-0000-000000000114',
|
|
commentText: 'Automation note',
|
|
commentAdditionalFields: {
|
|
is_internal: true,
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { comment_id: 'comment-3' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('POST');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/tickets/00000000-0000-0000-0000-000000000114/comments',
|
|
);
|
|
expect(requests[0]?.body).toEqual({
|
|
comment_text: 'Automation note',
|
|
is_internal: true,
|
|
});
|
|
expect(requests[0]?.body).not.toHaveProperty('time_spent');
|
|
});
|
|
|
|
it('T050: Ticket Add Comment unwraps the created comment object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'addComment',
|
|
ticketId: '00000000-0000-0000-0000-000000000115',
|
|
commentText: 'Customer update',
|
|
commentAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({
|
|
data: {
|
|
comment_id: 'comment-4',
|
|
ticket_id: '00000000-0000-0000-0000-000000000115',
|
|
comment_text: 'Customer update',
|
|
is_internal: false,
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
comment_id: 'comment-4',
|
|
ticket_id: '00000000-0000-0000-0000-000000000115',
|
|
comment_text: 'Customer update',
|
|
is_internal: false,
|
|
});
|
|
});
|
|
|
|
it('T021: Ticket Update Status sends PUT with status_id payload', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'updateStatus',
|
|
ticketId: '00000000-0000-0000-0000-000000000023',
|
|
status_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000024' },
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: '00000000-0000-0000-0000-000000000023' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('PUT');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/tickets/00000000-0000-0000-0000-000000000023/status',
|
|
);
|
|
expect(requests[0]?.body).toEqual({
|
|
status_id: '00000000-0000-0000-0000-000000000024',
|
|
});
|
|
});
|
|
|
|
it('T022: Ticket Update Assignment sends PUT with assigned_to payload', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'updateAssignment',
|
|
ticketId: '00000000-0000-0000-0000-000000000025',
|
|
assignmentAction: 'assign',
|
|
assigned_to: '00000000-0000-0000-0000-000000000026',
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: '00000000-0000-0000-0000-000000000025' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('PUT');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/tickets/00000000-0000-0000-0000-000000000025/assignment',
|
|
);
|
|
expect(requests[0]?.body).toEqual({
|
|
assigned_to: '00000000-0000-0000-0000-000000000026',
|
|
});
|
|
});
|
|
|
|
it('T023: Ticket Delete sends DELETE to ticket endpoint', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'delete',
|
|
ticketId: '00000000-0000-0000-0000-000000000027',
|
|
},
|
|
],
|
|
() => undefined,
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('DELETE');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/tickets/00000000-0000-0000-0000-000000000027',
|
|
);
|
|
});
|
|
|
|
it('T024: Ticket Delete converts 204-style response to success object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'delete',
|
|
ticketId: '00000000-0000-0000-0000-000000000028',
|
|
},
|
|
],
|
|
() => undefined,
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
success: true,
|
|
id: '00000000-0000-0000-0000-000000000028',
|
|
deleted: true,
|
|
});
|
|
});
|
|
|
|
it('T051: Ticket comment operations reject empty or invalid ticketId before request', async () => {
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'listComments',
|
|
ticketId: '',
|
|
commentListOptions: {},
|
|
},
|
|
],
|
|
() => ({ data: [] }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'addComment',
|
|
ticketId: 'not-a-uuid',
|
|
commentText: 'Hello',
|
|
commentAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
it('T052: Ticket Add Comment rejects empty comment text before request', async () => {
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'addComment',
|
|
ticketId: '00000000-0000-0000-0000-000000000116',
|
|
commentText: ' ',
|
|
commentAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
it('T019: contact create sends POST /api/v1/contacts', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseContactCreateParams,
|
|
contactCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: { contact_name_id: 'contact-1' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('POST');
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/contacts');
|
|
expect(requests[0]?.body).toEqual({ full_name: 'Ada Lovelace' });
|
|
});
|
|
|
|
it('T020: contact create request body includes parsed phone_numbers when provided', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseContactCreateParams,
|
|
contactCreateAdditionalFields: {
|
|
phone_numbers: JSON.stringify([
|
|
{
|
|
phone_number: '+1-206-555-0100',
|
|
canonical_type: 'mobile',
|
|
is_default: true,
|
|
},
|
|
]),
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { contact_name_id: 'contact-1' } }),
|
|
);
|
|
|
|
expect(requests[0]?.body).toMatchObject({
|
|
full_name: 'Ada Lovelace',
|
|
phone_numbers: [
|
|
{
|
|
phone_number: '+1-206-555-0100',
|
|
canonical_type: 'mobile',
|
|
is_default: true,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('contact create request body includes primary email metadata and additional email rows when provided', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseContactCreateParams,
|
|
contactCreateAdditionalFields: {
|
|
email: 'ada@example.com',
|
|
primary_email_canonical_type: 'billing',
|
|
additional_email_addresses: JSON.stringify([
|
|
{
|
|
email_address: 'ada.personal@example.com',
|
|
canonical_type: 'personal',
|
|
display_order: 0,
|
|
},
|
|
]),
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { contact_name_id: 'contact-1' } }),
|
|
);
|
|
|
|
expect(requests[0]?.body).toMatchObject({
|
|
full_name: 'Ada Lovelace',
|
|
email: 'ada@example.com',
|
|
primary_email_canonical_type: 'billing',
|
|
additional_email_addresses: [
|
|
{
|
|
email_address: 'ada.personal@example.com',
|
|
canonical_type: 'personal',
|
|
display_order: 0,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('T021: contact create unwraps a successful data wrapper into the created contact object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
...baseContactCreateParams,
|
|
contactCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: { contact_name_id: 'contact-123', full_name: 'Ada Lovelace' } }),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
contact_name_id: 'contact-123',
|
|
full_name: 'Ada Lovelace',
|
|
});
|
|
});
|
|
|
|
it('T022: contact get sends GET /api/v1/contacts/{id}', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'get',
|
|
contactId: '00000000-0000-0000-0000-000000000101',
|
|
},
|
|
],
|
|
() => ({ data: { contact_name_id: '00000000-0000-0000-0000-000000000101' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/contacts/00000000-0000-0000-0000-000000000101',
|
|
);
|
|
});
|
|
|
|
it('T023: contact get returns the normalized contact object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'get',
|
|
contactId: '00000000-0000-0000-0000-000000000102',
|
|
},
|
|
],
|
|
() => ({
|
|
data: {
|
|
contact_name_id: '00000000-0000-0000-0000-000000000102',
|
|
full_name: 'Grace Hopper',
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
contact_name_id: '00000000-0000-0000-0000-000000000102',
|
|
full_name: 'Grace Hopper',
|
|
});
|
|
});
|
|
|
|
it('T024: contact list sends GET /api/v1/contacts with selected pagination and filter query parameters', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'list',
|
|
contactPage: 2,
|
|
contactLimit: 50,
|
|
contactListFilters: {
|
|
client_id: '00000000-0000-0000-0000-000000000103',
|
|
search_term: 'ada',
|
|
is_inactive: true,
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: [], pagination: { page: 2, total: 0 } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/contacts');
|
|
expect(requests[0]?.qs).toEqual({
|
|
page: 2,
|
|
limit: 50,
|
|
client_id: '00000000-0000-0000-0000-000000000103',
|
|
search_term: 'ada',
|
|
is_inactive: true,
|
|
});
|
|
});
|
|
|
|
it('T025: contact list preserves pagination metadata in node output', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'list',
|
|
contactPage: 1,
|
|
contactLimit: 25,
|
|
contactListFilters: {},
|
|
},
|
|
],
|
|
() => ({
|
|
data: [{ contact_name_id: 'contact-1' }],
|
|
pagination: { page: 1, total: 1, totalPages: 1 },
|
|
}),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
data: [{ contact_name_id: 'contact-1' }],
|
|
pagination: { page: 1, total: 1, totalPages: 1 },
|
|
});
|
|
});
|
|
|
|
it('T026: contact update sends PUT /api/v1/contacts/{id} with only changed fields', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'update',
|
|
contactId: '00000000-0000-0000-0000-000000000104',
|
|
contactUpdateAdditionalFields: {
|
|
email: 'ada@example.com',
|
|
notes: '',
|
|
client_id: { mode: 'id', value: '00000000-0000-0000-0000-000000000105' },
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { contact_name_id: '00000000-0000-0000-0000-000000000104' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('PUT');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/contacts/00000000-0000-0000-0000-000000000104',
|
|
);
|
|
expect(requests[0]?.body).toEqual({
|
|
email: 'ada@example.com',
|
|
client_id: '00000000-0000-0000-0000-000000000105',
|
|
});
|
|
});
|
|
|
|
it('T027: contact update returns the normalized updated contact object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'update',
|
|
contactId: '00000000-0000-0000-0000-000000000106',
|
|
contactUpdateAdditionalFields: {
|
|
role: 'Director of Automation',
|
|
},
|
|
},
|
|
],
|
|
() => ({
|
|
data: {
|
|
contact_name_id: '00000000-0000-0000-0000-000000000106',
|
|
role: 'Director of Automation',
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
contact_name_id: '00000000-0000-0000-0000-000000000106',
|
|
role: 'Director of Automation',
|
|
});
|
|
});
|
|
|
|
it('T028: contact delete sends DELETE /api/v1/contacts/{id}', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'delete',
|
|
contactId: '00000000-0000-0000-0000-000000000107',
|
|
},
|
|
],
|
|
() => undefined,
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('DELETE');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/contacts/00000000-0000-0000-0000-000000000107',
|
|
);
|
|
});
|
|
|
|
it('T029: contact delete returns a non-empty normalized success object', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'delete',
|
|
contactId: '00000000-0000-0000-0000-000000000108',
|
|
},
|
|
],
|
|
() => undefined,
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
success: true,
|
|
id: '00000000-0000-0000-0000-000000000108',
|
|
deleted: true,
|
|
});
|
|
});
|
|
|
|
it('T030: contact get rejects an empty contactId before making a request', async () => {
|
|
const result = await executeNodeExpectFailure([
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'get',
|
|
contactId: '',
|
|
},
|
|
]);
|
|
|
|
expect(result.error).toBeInstanceOf(NodeOperationError);
|
|
expect(result.requests).toHaveLength(0);
|
|
});
|
|
|
|
it('T031: contact update rejects an invalid UUID contactId before making a request', async () => {
|
|
const result = await executeNodeExpectFailure([
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'update',
|
|
contactId: 'not-a-uuid',
|
|
contactUpdateAdditionalFields: {
|
|
role: 'Operations',
|
|
},
|
|
},
|
|
]);
|
|
|
|
expect(result.error).toBeInstanceOf(NodeOperationError);
|
|
expect(result.requests).toHaveLength(0);
|
|
});
|
|
|
|
it('T032: contact delete rejects an invalid UUID contactId before making a request', async () => {
|
|
const result = await executeNodeExpectFailure([
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'delete',
|
|
contactId: 'still-not-a-uuid',
|
|
},
|
|
]);
|
|
|
|
expect(result.error).toBeInstanceOf(NodeOperationError);
|
|
expect(result.requests).toHaveLength(0);
|
|
});
|
|
|
|
it('T033: contact continue-on-fail returns item-level error objects while later items still execute', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'get',
|
|
contactId: '00000000-0000-0000-0000-000000000109',
|
|
},
|
|
{
|
|
resource: 'contact',
|
|
contactOperation: 'get',
|
|
contactId: '00000000-0000-0000-0000-000000000110',
|
|
},
|
|
],
|
|
(_options, index) => {
|
|
if (index === 0) {
|
|
throw createApiError(404, 'NOT_FOUND', 'Contact not found', { contactId: 'missing' });
|
|
}
|
|
|
|
return { data: { contact_name_id: '00000000-0000-0000-0000-000000000110' } };
|
|
},
|
|
true,
|
|
);
|
|
|
|
expect(output[0]).toHaveLength(2);
|
|
expect(output[0][0].json).toEqual({
|
|
error: {
|
|
code: 'NOT_FOUND',
|
|
message: 'Contact not found',
|
|
details: { contactId: 'missing' },
|
|
statusCode: 404,
|
|
},
|
|
});
|
|
expect(output[0][1].json).toEqual({
|
|
contact_name_id: '00000000-0000-0000-0000-000000000110',
|
|
});
|
|
});
|
|
|
|
it('T031: Continue On Fail emits item-level errors and continues remaining items', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'get',
|
|
ticketId: '00000000-0000-0000-0000-000000000029',
|
|
},
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'get',
|
|
ticketId: '00000000-0000-0000-0000-000000000030',
|
|
},
|
|
],
|
|
(_options, index) => {
|
|
if (index === 0) {
|
|
throw createApiError(404, 'NOT_FOUND', 'Ticket not found', { ticketId: 'missing' });
|
|
}
|
|
|
|
return { data: { ticket_id: '00000000-0000-0000-0000-000000000030' } };
|
|
},
|
|
true,
|
|
);
|
|
|
|
expect(output[0]).toHaveLength(2);
|
|
expect(output[0][0].json).toEqual({
|
|
error: {
|
|
code: 'NOT_FOUND',
|
|
message: 'Ticket not found',
|
|
details: { ticketId: 'missing' },
|
|
statusCode: 404,
|
|
},
|
|
});
|
|
expect(output[0][1].json).toEqual({ ticket_id: '00000000-0000-0000-0000-000000000030' });
|
|
});
|
|
|
|
it('T032: Client helper list maps to GET /api/v1/clients and returns list output', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'client',
|
|
clientOperation: 'list',
|
|
helperPage: 1,
|
|
helperLimit: 25,
|
|
helperSearch: 'acme',
|
|
},
|
|
],
|
|
() => ({ data: [{ client_id: '1', client_name: 'Acme' }] }),
|
|
);
|
|
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/clients');
|
|
expect(output[0][0].json).toEqual({ data: [{ client_id: '1', client_name: 'Acme' }] });
|
|
});
|
|
|
|
it('T033: Board helper list maps to GET /api/v1/boards and returns list output', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'board',
|
|
boardOperation: 'list',
|
|
helperPage: 1,
|
|
helperLimit: 25,
|
|
helperSearch: '',
|
|
},
|
|
],
|
|
() => ({ data: [{ board_id: '1', board_name: 'Help Desk' }] }),
|
|
);
|
|
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/boards');
|
|
expect(output[0][0].json).toEqual({ data: [{ board_id: '1', board_name: 'Help Desk' }] });
|
|
});
|
|
|
|
it('T034: Status helper list maps to GET /api/v1/statuses and returns list output', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'status',
|
|
statusOperation: 'list',
|
|
helperStatusType: 'project_task',
|
|
helperPage: 1,
|
|
helperLimit: 25,
|
|
helperSearch: '',
|
|
},
|
|
],
|
|
() => ({ data: [{ status_id: '1', name: 'New' }] }),
|
|
);
|
|
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/statuses');
|
|
expect(requests[0]?.qs).toMatchObject({
|
|
page: 1,
|
|
limit: 25,
|
|
type: 'project_task',
|
|
});
|
|
expect(requests[0]?.qs).not.toHaveProperty('board_id');
|
|
expect(output[0][0].json).toEqual({ data: [{ status_id: '1', name: 'New' }] });
|
|
});
|
|
|
|
it('T034b: Status helper list with ticket type requires board_id and passes it to the API', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'status',
|
|
statusOperation: 'list',
|
|
helperStatusType: 'ticket',
|
|
helperBoardId: { mode: 'id', value: '00000000-0000-0000-0000-000000000200' },
|
|
helperPage: 1,
|
|
helperLimit: 25,
|
|
helperSearch: '',
|
|
},
|
|
],
|
|
() => ({ data: [{ status_id: 'status-1', name: 'Open', board_id: '00000000-0000-0000-0000-000000000200' }] }),
|
|
);
|
|
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/statuses');
|
|
expect(requests[0]?.qs).toMatchObject({
|
|
page: 1,
|
|
limit: 25,
|
|
type: 'ticket',
|
|
board_id: '00000000-0000-0000-0000-000000000200',
|
|
});
|
|
expect(output[0][0].json).toEqual({
|
|
data: [{ status_id: 'status-1', name: 'Open', board_id: '00000000-0000-0000-0000-000000000200' }],
|
|
});
|
|
});
|
|
|
|
it('T034c: Status helper list with ticket type rejects missing board_id before the API call', async () => {
|
|
const result = await executeNodeExpectFailure([
|
|
{
|
|
resource: 'status',
|
|
statusOperation: 'list',
|
|
helperStatusType: 'ticket',
|
|
helperBoardId: { mode: 'id', value: '' },
|
|
helperPage: 1,
|
|
helperLimit: 25,
|
|
helperSearch: '',
|
|
},
|
|
]);
|
|
|
|
expect(result.error).toBeInstanceOf(NodeOperationError);
|
|
expect(result.requests).toHaveLength(0);
|
|
});
|
|
|
|
it('T035: Priority helper list maps to GET /api/v1/priorities and returns list output', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'priority',
|
|
priorityOperation: 'list',
|
|
helperPage: 1,
|
|
helperLimit: 25,
|
|
helperSearch: '',
|
|
},
|
|
],
|
|
() => ({ data: [{ priority_id: '1', priority_name: 'High' }] }),
|
|
);
|
|
|
|
expect(requests[0]?.url).toBe('https://api.algapsa.test/api/v1/priorities');
|
|
expect(output[0][0].json).toEqual({
|
|
data: [{ priority_id: '1', priority_name: 'High' }],
|
|
});
|
|
});
|
|
|
|
it('T040: manual UUID fallback path still executes operations when lookup lists fail', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseCreateParams,
|
|
createAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: { ticket_id: 'created' } }),
|
|
);
|
|
|
|
expect(requests[0]?.body).toMatchObject({
|
|
client_id: '00000000-0000-0000-0000-000000000001',
|
|
board_id: '00000000-0000-0000-0000-000000000002',
|
|
status_id: '00000000-0000-0000-0000-000000000003',
|
|
priority_id: '00000000-0000-0000-0000-000000000004',
|
|
});
|
|
});
|
|
|
|
it('T042: validation blocks outbound calls for missing required IDs and search query', async () => {
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
...baseCreateParams,
|
|
client_id: { mode: 'id', value: '' },
|
|
createAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
resource: 'ticket',
|
|
ticketOperation: 'search',
|
|
query: '',
|
|
searchLimit: 25,
|
|
searchAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: [] }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
const baseProjectTaskCreateParams = {
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'create',
|
|
task_name: 'Draft proposal',
|
|
projectTaskProjectId: { mode: 'id', value: '00000000-0000-0000-0000-000000000201' },
|
|
projectTaskPhaseId: { mode: 'id', value: '00000000-0000-0000-0000-000000000202' },
|
|
projectTaskStatusMappingId: { mode: 'id', value: '00000000-0000-0000-0000-000000000203' },
|
|
};
|
|
|
|
it('T080: project task create sends POST /api/v1/projects/{projectId}/phases/{phaseId}/tasks', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseProjectTaskCreateParams,
|
|
projectTaskCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: { task_id: '00000000-0000-0000-0000-000000000204' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('POST');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/projects/00000000-0000-0000-0000-000000000201/phases/00000000-0000-0000-0000-000000000202/tasks',
|
|
);
|
|
expect(requests[0]?.body).toEqual({
|
|
task_name: 'Draft proposal',
|
|
project_status_mapping_id: '00000000-0000-0000-0000-000000000203',
|
|
});
|
|
});
|
|
|
|
it('T081: project task create includes optional fields only when provided', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
...baseProjectTaskCreateParams,
|
|
projectTaskCreateAdditionalFields: {
|
|
description: 'Write RFC for new billing pipeline',
|
|
assigned_to: '00000000-0000-0000-0000-000000000205',
|
|
estimated_hours: 6,
|
|
due_date: '2026-05-15',
|
|
priority_id: '00000000-0000-0000-0000-000000000206',
|
|
task_type_key: 'design',
|
|
wbs_code: '2.1',
|
|
tags: 'billing,design',
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { task_id: '00000000-0000-0000-0000-000000000204' } }),
|
|
);
|
|
|
|
expect(requests[0]?.body).toEqual({
|
|
task_name: 'Draft proposal',
|
|
project_status_mapping_id: '00000000-0000-0000-0000-000000000203',
|
|
description: 'Write RFC for new billing pipeline',
|
|
assigned_to: '00000000-0000-0000-0000-000000000205',
|
|
estimated_hours: 6,
|
|
due_date: '2026-05-15',
|
|
priority_id: '00000000-0000-0000-0000-000000000206',
|
|
task_type_key: 'design',
|
|
wbs_code: '2.1',
|
|
tags: ['billing', 'design'],
|
|
});
|
|
});
|
|
|
|
it('T082: project task create unwraps the created task object from the data wrapper', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
...baseProjectTaskCreateParams,
|
|
projectTaskCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({
|
|
data: {
|
|
task_id: '00000000-0000-0000-0000-000000000204',
|
|
task_name: 'Draft proposal',
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(output[0][0].json).toEqual({
|
|
task_id: '00000000-0000-0000-0000-000000000204',
|
|
task_name: 'Draft proposal',
|
|
});
|
|
});
|
|
|
|
it('T083: project task create rejects missing project/phase/status mapping IDs before request', async () => {
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
...baseProjectTaskCreateParams,
|
|
projectTaskProjectId: { mode: 'id', value: '' },
|
|
projectTaskCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
...baseProjectTaskCreateParams,
|
|
projectTaskPhaseId: { mode: 'id', value: '' },
|
|
projectTaskCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
...baseProjectTaskCreateParams,
|
|
projectTaskStatusMappingId: { mode: 'id', value: '' },
|
|
projectTaskCreateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
it('T084: project task get sends GET /api/v1/projects/tasks/{taskId}', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'get',
|
|
projectTaskId: '00000000-0000-0000-0000-000000000210',
|
|
},
|
|
],
|
|
() => ({
|
|
data: {
|
|
task_id: '00000000-0000-0000-0000-000000000210',
|
|
task_name: 'Existing task',
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/projects/tasks/00000000-0000-0000-0000-000000000210',
|
|
);
|
|
expect(output[0][0].json).toEqual({
|
|
task_id: '00000000-0000-0000-0000-000000000210',
|
|
task_name: 'Existing task',
|
|
});
|
|
});
|
|
|
|
it('T085: project task list sends GET /api/v1/projects/{projectId}/tasks with pagination', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'list',
|
|
projectTaskProjectId: {
|
|
mode: 'id',
|
|
value: '00000000-0000-0000-0000-000000000220',
|
|
},
|
|
projectTaskPage: 2,
|
|
projectTaskLimit: 50,
|
|
},
|
|
],
|
|
() => ({
|
|
data: [{ task_id: '00000000-0000-0000-0000-000000000221' }],
|
|
pagination: { page: 2, total: 1 },
|
|
}),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('GET');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/projects/00000000-0000-0000-0000-000000000220/tasks',
|
|
);
|
|
expect(requests[0]?.qs).toEqual({ page: 2, limit: 50 });
|
|
expect(output[0][0].json).toEqual({
|
|
data: [{ task_id: '00000000-0000-0000-0000-000000000221' }],
|
|
pagination: { page: 2, total: 1 },
|
|
});
|
|
});
|
|
|
|
it('T086: project task update sends PUT with only provided fields', async () => {
|
|
const { requests } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'update',
|
|
projectTaskId: '00000000-0000-0000-0000-000000000230',
|
|
projectTaskUpdateAdditionalFields: {
|
|
task_name: 'Renamed task',
|
|
description: '',
|
|
project_status_mapping_id: '00000000-0000-0000-0000-000000000231',
|
|
},
|
|
},
|
|
],
|
|
() => ({ data: { task_id: '00000000-0000-0000-0000-000000000230' } }),
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('PUT');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/projects/tasks/00000000-0000-0000-0000-000000000230',
|
|
);
|
|
expect(requests[0]?.body).toEqual({
|
|
task_name: 'Renamed task',
|
|
project_status_mapping_id: '00000000-0000-0000-0000-000000000231',
|
|
});
|
|
});
|
|
|
|
it('T087: project task update rejects an empty update collection before request', async () => {
|
|
await expect(
|
|
executeNode(
|
|
[
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'update',
|
|
projectTaskId: '00000000-0000-0000-0000-000000000232',
|
|
projectTaskUpdateAdditionalFields: {},
|
|
},
|
|
],
|
|
() => ({ data: {} }),
|
|
),
|
|
).rejects.toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
it('T088: project task delete sends DELETE and returns a success envelope', async () => {
|
|
const { requests, output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'delete',
|
|
projectTaskId: '00000000-0000-0000-0000-000000000240',
|
|
},
|
|
],
|
|
() => undefined,
|
|
);
|
|
|
|
expect(requests[0]?.method).toBe('DELETE');
|
|
expect(requests[0]?.url).toBe(
|
|
'https://api.algapsa.test/api/v1/projects/tasks/00000000-0000-0000-0000-000000000240',
|
|
);
|
|
expect(output[0][0].json).toEqual({
|
|
success: true,
|
|
id: '00000000-0000-0000-0000-000000000240',
|
|
deleted: true,
|
|
});
|
|
});
|
|
|
|
it('T089: project task get/update/delete reject invalid UUIDs before request', async () => {
|
|
const getResult = await executeNodeExpectFailure([
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'get',
|
|
projectTaskId: 'not-a-uuid',
|
|
},
|
|
]);
|
|
expect(getResult.error).toBeInstanceOf(NodeOperationError);
|
|
expect(getResult.requests).toHaveLength(0);
|
|
|
|
const deleteResult = await executeNodeExpectFailure([
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'delete',
|
|
projectTaskId: '',
|
|
},
|
|
]);
|
|
expect(deleteResult.error).toBeInstanceOf(NodeOperationError);
|
|
});
|
|
|
|
it('T090: project task continue-on-fail emits item-level errors while later items still execute', async () => {
|
|
const { output } = await executeNode(
|
|
[
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'get',
|
|
projectTaskId: '00000000-0000-0000-0000-000000000250',
|
|
},
|
|
{
|
|
resource: 'projectTask',
|
|
projectTaskOperation: 'get',
|
|
projectTaskId: '00000000-0000-0000-0000-000000000251',
|
|
},
|
|
],
|
|
(_options, index) => {
|
|
if (index === 0) {
|
|
throw createApiError(404, 'NOT_FOUND', 'Task not found', { taskId: 'missing' });
|
|
}
|
|
|
|
return { data: { task_id: '00000000-0000-0000-0000-000000000251' } };
|
|
},
|
|
true,
|
|
);
|
|
|
|
expect(output[0]).toHaveLength(2);
|
|
expect(output[0][0].json).toEqual({
|
|
error: {
|
|
code: 'NOT_FOUND',
|
|
message: 'Task not found',
|
|
details: { taskId: 'missing' },
|
|
statusCode: 404,
|
|
},
|
|
});
|
|
expect(output[0][1].json).toEqual({
|
|
task_id: '00000000-0000-0000-0000-000000000251',
|
|
});
|
|
});
|
|
});
|