From fbb4646c76848dc31901d1779d6f374940caebad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerrit=20Gr=C3=A4tz?= Date: Mon, 23 Mar 2026 09:46:38 +0100 Subject: [PATCH 1/5] add integration tests for genericProcessService --- lib/handlers/processService.ts | 2 +- tests/bookshop/srv/programmatic-service.cds | 10 + tests/bookshop/srv/programmatic-service.ts | 47 +++ .../genericProgrammaticApproach.test.ts | 210 +++++++++++++ .../genericProgrammaticApproach.test.ts | 283 ++++++++++++++++++ 5 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 tests/hybrid/genericProgrammaticApproach.test.ts create mode 100644 tests/integration/genericProgrammaticApproach.test.ts diff --git a/lib/handlers/processService.ts b/lib/handlers/processService.ts index 597659e4..9092ff1c 100644 --- a/lib/handlers/processService.ts +++ b/lib/handlers/processService.ts @@ -170,4 +170,4 @@ function registerGetOutputsHandler(service: cds.Service, definitionId: string): return result; }); -} +} \ No newline at end of file diff --git a/tests/bookshop/srv/programmatic-service.cds b/tests/bookshop/srv/programmatic-service.cds index 397c0883..90685fe4 100644 --- a/tests/bookshop/srv/programmatic-service.cds +++ b/tests/bookshop/srv/programmatic-service.cds @@ -40,4 +40,14 @@ service ProgrammaticService { action getInstanceIDForGetOutputs(ID: UUID, status: many String) returns many AttributeEntry; + + // Generic ProcessService actions (using cds.connect.to('ProcessService')) + action genericStart(definitionId: String, businessKey: String, context: LargeString); + action genericCancel(businessKey: String, cascade: Boolean); + action genericSuspend(businessKey: String, cascade: Boolean); + action genericResume(businessKey: String, cascade: Boolean); + action genericGetInstancesByBusinessKey(businessKey: String, + status: many String) returns many ProcessInstance; + action genericGetAttributes(processInstanceId: String) returns many ProcessAttribute; + action genericGetOutputs(processInstanceId: String) returns ProcessOutputs; } diff --git a/tests/bookshop/srv/programmatic-service.ts b/tests/bookshop/srv/programmatic-service.ts index 1d0050e0..eeafe0b8 100644 --- a/tests/bookshop/srv/programmatic-service.ts +++ b/tests/bookshop/srv/programmatic-service.ts @@ -110,6 +110,53 @@ class ProgrammaticService extends cds.ApplicationService { return outputs; }); + // Generic ProcessService handlers (using cds.connect.to('ProcessService')) + this.on('genericStart', async (req: cds.Request) => { + const { definitionId, businessKey, context } = req.data; + const processService = await cds.connect.to('ProcessService'); + const parsedContext = context ? JSON.parse(context) : {}; + await processService.emit('start', { definitionId, context: parsedContext }, { businessKey }); + }); + + this.on('genericCancel', async (req: cds.Request) => { + const { businessKey, cascade } = req.data; + const processService = await cds.connect.to('ProcessService'); + await processService.emit('cancel', { businessKey, cascade: cascade ?? false }); + }); + + this.on('genericSuspend', async (req: cds.Request) => { + const { businessKey, cascade } = req.data; + const processService = await cds.connect.to('ProcessService'); + await processService.emit('suspend', { businessKey, cascade: cascade ?? false }); + }); + + this.on('genericResume', async (req: cds.Request) => { + const { businessKey, cascade } = req.data; + const processService = await cds.connect.to('ProcessService'); + await processService.emit('resume', { businessKey, cascade: cascade ?? false }); + }); + + this.on('genericGetInstancesByBusinessKey', async (req: cds.Request) => { + const { businessKey, status } = req.data; + const processService = await cds.connect.to('ProcessService'); + const result = await processService.send('getInstancesByBusinessKey', { businessKey, status }); + return result; + }); + + this.on('genericGetAttributes', async (req: cds.Request) => { + const { processInstanceId } = req.data; + const processService = await cds.connect.to('ProcessService'); + const result = await processService.send('getAttributes', { processInstanceId }); + return result; + }); + + this.on('genericGetOutputs', async (req: cds.Request) => { + const { processInstanceId } = req.data; + const processService = await cds.connect.to('ProcessService'); + const result = await processService.send('getOutputs', { processInstanceId }); + return result; + }); + await super.init(); } } diff --git a/tests/hybrid/genericProgrammaticApproach.test.ts b/tests/hybrid/genericProgrammaticApproach.test.ts new file mode 100644 index 00000000..d7da4bcc --- /dev/null +++ b/tests/hybrid/genericProgrammaticApproach.test.ts @@ -0,0 +1,210 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-await-in-loop */ +import cds from '@sap/cds'; +import * as path from 'path'; + +const app = path.join(__dirname, '../bookshop/'); +const { POST } = cds.test(app); + +const DEFINITION_ID = 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process'; + +describe('Generic ProcessService Hybrid Tests', () => { + function generateID(): string { + return cds.utils.uuid(); + } + + async function genericStart(businessKey: string, context: Record = {}) { + return POST('/odata/v4/programmatic/genericStart', { + definitionId: DEFINITION_ID, + businessKey, + context: JSON.stringify(context), + }); + } + + async function genericCancel(businessKey: string, cascade = false) { + return POST('/odata/v4/programmatic/genericCancel', { + businessKey, + cascade, + }); + } + + async function genericSuspend(businessKey: string, cascade = false) { + return POST('/odata/v4/programmatic/genericSuspend', { + businessKey, + cascade, + }); + } + + async function genericResume(businessKey: string, cascade = false) { + return POST('/odata/v4/programmatic/genericResume', { + businessKey, + cascade, + }); + } + + async function genericGetInstances( + businessKey: string, + status?: string[], + ): Promise { + const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { + businessKey, + status, + }); + return res.data?.value ?? res.data ?? []; + } + + async function genericGetAttributes(processInstanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/genericGetAttributes', { + processInstanceId, + }); + return res.data?.value ?? res.data ?? []; + } + + async function genericGetOutputs(processInstanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/genericGetOutputs', { + processInstanceId, + }); + return res.data; + } + + async function waitForInstances( + businessKey: string, + status: string[], + expectedCount = 1, + maxRetries = 6, + ): Promise { + for (let i = 0; i < maxRetries; i++) { + const instances = await genericGetInstances(businessKey, status); + if (instances.length >= expectedCount) return instances; + await new Promise((r) => setTimeout(r, 10000)); + } + throw new Error( + `Timed out waiting for ${expectedCount} instance(s) with status [${status}] for businessKey ${businessKey}`, + ); + } + + describe('Process Start', () => { + it('should start a process and verify it is RUNNING on SBPA', async () => { + const businessKey = generateID(); + const response = await genericStart(businessKey); + + expect(response.status).toBe(204); + + const instances = await waitForInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('id'); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + expect(instances[0]).toHaveProperty('definitionId'); + }); + + it('should start multiple independent processes', async () => { + const keyA = generateID(); + const keyB = generateID(); + + await genericStart(keyA); + await genericStart(keyB); + + const instancesA = await waitForInstances(keyA, ['RUNNING']); + const instancesB = await waitForInstances(keyB, ['RUNNING']); + + expect(instancesA.length).toBe(1); + expect(instancesB.length).toBe(1); + expect(instancesA[0].id).not.toEqual(instancesB[0].id); + }); + }); + + describe('Process Suspend', () => { + it('should suspend a running process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + await waitForInstances(businessKey, ['RUNNING']); + + const response = await genericSuspend(businessKey); + expect(response.status).toBe(204); + + const instances = await waitForInstances(businessKey, ['SUSPENDED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'SUSPENDED'); + }); + }); + + describe('Process Resume', () => { + it('should resume a suspended process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + await waitForInstances(businessKey, ['RUNNING']); + + await genericSuspend(businessKey); + await waitForInstances(businessKey, ['SUSPENDED']); + + const response = await genericResume(businessKey); + expect(response.status).toBe(204); + + const instances = await waitForInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + }); + + describe('Process Cancel', () => { + it('should cancel a running process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + await waitForInstances(businessKey, ['RUNNING']); + + const response = await genericCancel(businessKey); + expect(response.status).toBe(204); + + const instances = await waitForInstances(businessKey, ['CANCELED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'CANCELED'); + }); + }); + + describe('Sequential lifecycle operations', () => { + it('should go through start -> suspend -> resume and end up RUNNING', async () => { + const businessKey = generateID(); + + await genericStart(businessKey); + await waitForInstances(businessKey, ['RUNNING']); + + await genericSuspend(businessKey); + await waitForInstances(businessKey, ['SUSPENDED']); + + await genericResume(businessKey); + + const instances = await waitForInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + + it('should go through start -> cancel and end up CANCELED', async () => { + const businessKey = generateID(); + + await genericStart(businessKey); + await waitForInstances(businessKey, ['RUNNING']); + + await genericCancel(businessKey); + + const instances = await waitForInstances(businessKey, ['CANCELED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'CANCELED'); + }); + }); + + describe('Get Attributes', () => { + it('should return attributes for a running process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instances = await waitForInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('id'); + + const attributes = await genericGetAttributes(instances[0].id); + + expect(Array.isArray(attributes)).toBe(true); + expect(attributes.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/tests/integration/genericProgrammaticApproach.test.ts b/tests/integration/genericProgrammaticApproach.test.ts new file mode 100644 index 00000000..f7be67c8 --- /dev/null +++ b/tests/integration/genericProgrammaticApproach.test.ts @@ -0,0 +1,283 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import cds from '@sap/cds'; +import * as path from 'path'; + +const app = path.join(__dirname, '../bookshop/'); +const { POST } = cds.test(app); + +const DEFINITION_ID = 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process'; + +describe('Generic ProcessService Integration Tests', () => { + afterAll(async () => { + await (cds as any).flush(); + }); + + function generateID(): string { + return cds.utils.uuid(); + } + + async function genericStart(businessKey: string, context: Record = {}) { + return POST('/odata/v4/programmatic/genericStart', { + definitionId: DEFINITION_ID, + businessKey, + context: JSON.stringify(context), + }); + } + + async function genericCancel(businessKey: string, cascade = false) { + return POST('/odata/v4/programmatic/genericCancel', { + businessKey, + cascade, + }); + } + + async function genericSuspend(businessKey: string, cascade = false) { + return POST('/odata/v4/programmatic/genericSuspend', { + businessKey, + cascade, + }); + } + + async function genericResume(businessKey: string, cascade = false) { + return POST('/odata/v4/programmatic/genericResume', { + businessKey, + cascade, + }); + } + + async function genericGetInstances( + businessKey: string, + status?: string[], + ): Promise { + const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { + businessKey, + status, + }); + return res.data?.value ?? res.data ?? []; + } + + async function genericGetAttributes(processInstanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/genericGetAttributes', { + processInstanceId, + }); + return res.data?.value ?? res.data ?? []; + } + + async function genericGetOutputs(processInstanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/genericGetOutputs', { + processInstanceId, + }); + return res.data; + } + + describe('Process Start Event', () => { + it('should start a process and verify it is RUNNING', async () => { + const businessKey = generateID(); + const response = await genericStart(businessKey); + + expect(response.status).toBe(204); + + const instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + expect(instances[0]).toHaveProperty('id'); + expect(instances[0]).toHaveProperty('definitionId', DEFINITION_ID); + }); + + it('should include the businessKey in the started instance', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instances = await genericGetInstances(businessKey); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('businessKey', businessKey); + }); + + it('should start multiple independent processes with different businessKeys', async () => { + const keyA = generateID(); + const keyB = generateID(); + + await genericStart(keyA); + await genericStart(keyB); + + const instancesA = await genericGetInstances(keyA, ['RUNNING']); + const instancesB = await genericGetInstances(keyB, ['RUNNING']); + + expect(instancesA.length).toBe(1); + expect(instancesB.length).toBe(1); + expect(instancesA[0].id).not.toEqual(instancesB[0].id); + }); + }); + + describe('Process Cancel Event', () => { + it('should cancel a running process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const response = await genericCancel(businessKey); + expect(response.status).toBe(204); + + const instances = await genericGetInstances(businessKey, ['CANCELED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'CANCELED'); + }); + + it('should not fail when cancelling with no running processes', async () => { + const businessKey = generateID(); + const response = await genericCancel(businessKey); + expect(response.status).toBe(204); + }); + }); + + describe('Process Suspend Event', () => { + it('should suspend a running process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const response = await genericSuspend(businessKey); + expect(response.status).toBe(204); + + const instances = await genericGetInstances(businessKey, ['SUSPENDED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'SUSPENDED'); + }); + + it('should not fail when suspending with no running processes', async () => { + const businessKey = generateID(); + const response = await genericSuspend(businessKey); + expect(response.status).toBe(204); + }); + }); + + describe('Process Resume Event', () => { + it('should resume a suspended process', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + await genericSuspend(businessKey); + + const response = await genericResume(businessKey); + expect(response.status).toBe(204); + + const instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + + it('should not fail when resuming with no suspended processes', async () => { + const businessKey = generateID(); + const response = await genericResume(businessKey); + expect(response.status).toBe(204); + }); + }); + + describe('Sequential lifecycle operations', () => { + it('should go through start -> suspend -> resume and end up RUNNING', async () => { + const businessKey = generateID(); + + await genericStart(businessKey); + + let instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + + await genericSuspend(businessKey); + + instances = await genericGetInstances(businessKey, ['SUSPENDED']); + expect(instances.length).toBe(1); + + await genericResume(businessKey); + + instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + + it('should go through start -> cancel and end up CANCELED', async () => { + const businessKey = generateID(); + + await genericStart(businessKey); + + let instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + + await genericCancel(businessKey); + + instances = await genericGetInstances(businessKey, ['CANCELED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'CANCELED'); + }); + + it('should go through start -> suspend -> resume -> cancel', async () => { + const businessKey = generateID(); + + await genericStart(businessKey); + await genericSuspend(businessKey); + await genericResume(businessKey); + await genericCancel(businessKey); + + const instances = await genericGetInstances(businessKey, ['CANCELED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'CANCELED'); + }); + }); + + describe('getInstancesByBusinessKey', () => { + it('should return empty array for unknown businessKey', async () => { + const businessKey = generateID(); + const instances = await genericGetInstances(businessKey); + expect(instances).toEqual([]); + }); + + it('should filter instances by status', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const runningInstances = await genericGetInstances(businessKey, ['RUNNING']); + expect(runningInstances.length).toBe(1); + + const canceledInstances = await genericGetInstances(businessKey, ['CANCELED']); + expect(canceledInstances.length).toBe(0); + }); + + it('should return instances matching any of the provided statuses', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instances = await genericGetInstances(businessKey, ['RUNNING', 'SUSPENDED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('status', 'RUNNING'); + }); + }); + + describe('getAttributes', () => { + it('should return attributes for a running process instance', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + + const attributes = await genericGetAttributes(instances[0].id); + + expect(Array.isArray(attributes)).toBe(true); + expect(attributes.length).toBeGreaterThan(0); + expect(attributes[0]).toHaveProperty('id'); + expect(attributes[0]).toHaveProperty('value'); + }); + }); + + describe('getOutputs', () => { + it('should return outputs for a process instance', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + const instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); + + const outputs = await genericGetOutputs(instances[0].id); + + expect(outputs).toBeDefined(); + expect(outputs).toHaveProperty('processedBy'); + expect(outputs).toHaveProperty('completionStatus'); + }); + }); +}); From 8228398edb015753367b657057aa1b663c56f539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerrit=20Gr=C3=A4tz?= Date: Mon, 23 Mar 2026 10:02:55 +0100 Subject: [PATCH 2/5] prettier and fix issue --- lib/handlers/processService.ts | 2 +- tests/bookshop/srv/programmatic-service.ts | 5 ++++- tests/hybrid/genericProgrammaticApproach.test.ts | 10 ++++------ tests/integration/genericProgrammaticApproach.test.ts | 10 ++++------ 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/handlers/processService.ts b/lib/handlers/processService.ts index 9092ff1c..597659e4 100644 --- a/lib/handlers/processService.ts +++ b/lib/handlers/processService.ts @@ -170,4 +170,4 @@ function registerGetOutputsHandler(service: cds.Service, definitionId: string): return result; }); -} \ No newline at end of file +} diff --git a/tests/bookshop/srv/programmatic-service.ts b/tests/bookshop/srv/programmatic-service.ts index eeafe0b8..cf3bf0a6 100644 --- a/tests/bookshop/srv/programmatic-service.ts +++ b/tests/bookshop/srv/programmatic-service.ts @@ -139,7 +139,10 @@ class ProgrammaticService extends cds.ApplicationService { this.on('genericGetInstancesByBusinessKey', async (req: cds.Request) => { const { businessKey, status } = req.data; const processService = await cds.connect.to('ProcessService'); - const result = await processService.send('getInstancesByBusinessKey', { businessKey, status }); + const result = await processService.send('getInstancesByBusinessKey', { + businessKey, + status, + }); return result; }); diff --git a/tests/hybrid/genericProgrammaticApproach.test.ts b/tests/hybrid/genericProgrammaticApproach.test.ts index d7da4bcc..631b2823 100644 --- a/tests/hybrid/genericProgrammaticApproach.test.ts +++ b/tests/hybrid/genericProgrammaticApproach.test.ts @@ -13,11 +13,12 @@ describe('Generic ProcessService Hybrid Tests', () => { return cds.utils.uuid(); } - async function genericStart(businessKey: string, context: Record = {}) { + async function genericStart(businessKey: string, context?: Record) { + const startContext = context ?? { ID: businessKey }; return POST('/odata/v4/programmatic/genericStart', { definitionId: DEFINITION_ID, businessKey, - context: JSON.stringify(context), + context: JSON.stringify(startContext), }); } @@ -42,10 +43,7 @@ describe('Generic ProcessService Hybrid Tests', () => { }); } - async function genericGetInstances( - businessKey: string, - status?: string[], - ): Promise { + async function genericGetInstances(businessKey: string, status?: string[]): Promise { const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { businessKey, status, diff --git a/tests/integration/genericProgrammaticApproach.test.ts b/tests/integration/genericProgrammaticApproach.test.ts index f7be67c8..4b177aab 100644 --- a/tests/integration/genericProgrammaticApproach.test.ts +++ b/tests/integration/genericProgrammaticApproach.test.ts @@ -16,11 +16,12 @@ describe('Generic ProcessService Integration Tests', () => { return cds.utils.uuid(); } - async function genericStart(businessKey: string, context: Record = {}) { + async function genericStart(businessKey: string, context?: Record) { + const startContext = context ?? { ID: businessKey }; return POST('/odata/v4/programmatic/genericStart', { definitionId: DEFINITION_ID, businessKey, - context: JSON.stringify(context), + context: JSON.stringify(startContext), }); } @@ -45,10 +46,7 @@ describe('Generic ProcessService Integration Tests', () => { }); } - async function genericGetInstances( - businessKey: string, - status?: string[], - ): Promise { + async function genericGetInstances(businessKey: string, status?: string[]): Promise { const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { businessKey, status, From aacfe36b6e47bcb1b96373c497e14191e3bc2bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerrit=20Gr=C3=A4tz?= Date: Mon, 23 Mar 2026 10:04:23 +0100 Subject: [PATCH 3/5] remove hybrid tests --- .../genericProgrammaticApproach.test.ts | 208 ------------------ 1 file changed, 208 deletions(-) delete mode 100644 tests/hybrid/genericProgrammaticApproach.test.ts diff --git a/tests/hybrid/genericProgrammaticApproach.test.ts b/tests/hybrid/genericProgrammaticApproach.test.ts deleted file mode 100644 index 631b2823..00000000 --- a/tests/hybrid/genericProgrammaticApproach.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable no-await-in-loop */ -import cds from '@sap/cds'; -import * as path from 'path'; - -const app = path.join(__dirname, '../bookshop/'); -const { POST } = cds.test(app); - -const DEFINITION_ID = 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process'; - -describe('Generic ProcessService Hybrid Tests', () => { - function generateID(): string { - return cds.utils.uuid(); - } - - async function genericStart(businessKey: string, context?: Record) { - const startContext = context ?? { ID: businessKey }; - return POST('/odata/v4/programmatic/genericStart', { - definitionId: DEFINITION_ID, - businessKey, - context: JSON.stringify(startContext), - }); - } - - async function genericCancel(businessKey: string, cascade = false) { - return POST('/odata/v4/programmatic/genericCancel', { - businessKey, - cascade, - }); - } - - async function genericSuspend(businessKey: string, cascade = false) { - return POST('/odata/v4/programmatic/genericSuspend', { - businessKey, - cascade, - }); - } - - async function genericResume(businessKey: string, cascade = false) { - return POST('/odata/v4/programmatic/genericResume', { - businessKey, - cascade, - }); - } - - async function genericGetInstances(businessKey: string, status?: string[]): Promise { - const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { - businessKey, - status, - }); - return res.data?.value ?? res.data ?? []; - } - - async function genericGetAttributes(processInstanceId: string): Promise { - const res = await POST('/odata/v4/programmatic/genericGetAttributes', { - processInstanceId, - }); - return res.data?.value ?? res.data ?? []; - } - - async function genericGetOutputs(processInstanceId: string): Promise { - const res = await POST('/odata/v4/programmatic/genericGetOutputs', { - processInstanceId, - }); - return res.data; - } - - async function waitForInstances( - businessKey: string, - status: string[], - expectedCount = 1, - maxRetries = 6, - ): Promise { - for (let i = 0; i < maxRetries; i++) { - const instances = await genericGetInstances(businessKey, status); - if (instances.length >= expectedCount) return instances; - await new Promise((r) => setTimeout(r, 10000)); - } - throw new Error( - `Timed out waiting for ${expectedCount} instance(s) with status [${status}] for businessKey ${businessKey}`, - ); - } - - describe('Process Start', () => { - it('should start a process and verify it is RUNNING on SBPA', async () => { - const businessKey = generateID(); - const response = await genericStart(businessKey); - - expect(response.status).toBe(204); - - const instances = await waitForInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('id'); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); - expect(instances[0]).toHaveProperty('definitionId'); - }); - - it('should start multiple independent processes', async () => { - const keyA = generateID(); - const keyB = generateID(); - - await genericStart(keyA); - await genericStart(keyB); - - const instancesA = await waitForInstances(keyA, ['RUNNING']); - const instancesB = await waitForInstances(keyB, ['RUNNING']); - - expect(instancesA.length).toBe(1); - expect(instancesB.length).toBe(1); - expect(instancesA[0].id).not.toEqual(instancesB[0].id); - }); - }); - - describe('Process Suspend', () => { - it('should suspend a running process', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - await waitForInstances(businessKey, ['RUNNING']); - - const response = await genericSuspend(businessKey); - expect(response.status).toBe(204); - - const instances = await waitForInstances(businessKey, ['SUSPENDED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'SUSPENDED'); - }); - }); - - describe('Process Resume', () => { - it('should resume a suspended process', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - await waitForInstances(businessKey, ['RUNNING']); - - await genericSuspend(businessKey); - await waitForInstances(businessKey, ['SUSPENDED']); - - const response = await genericResume(businessKey); - expect(response.status).toBe(204); - - const instances = await waitForInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); - }); - }); - - describe('Process Cancel', () => { - it('should cancel a running process', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - await waitForInstances(businessKey, ['RUNNING']); - - const response = await genericCancel(businessKey); - expect(response.status).toBe(204); - - const instances = await waitForInstances(businessKey, ['CANCELED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'CANCELED'); - }); - }); - - describe('Sequential lifecycle operations', () => { - it('should go through start -> suspend -> resume and end up RUNNING', async () => { - const businessKey = generateID(); - - await genericStart(businessKey); - await waitForInstances(businessKey, ['RUNNING']); - - await genericSuspend(businessKey); - await waitForInstances(businessKey, ['SUSPENDED']); - - await genericResume(businessKey); - - const instances = await waitForInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); - }); - - it('should go through start -> cancel and end up CANCELED', async () => { - const businessKey = generateID(); - - await genericStart(businessKey); - await waitForInstances(businessKey, ['RUNNING']); - - await genericCancel(businessKey); - - const instances = await waitForInstances(businessKey, ['CANCELED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'CANCELED'); - }); - }); - - describe('Get Attributes', () => { - it('should return attributes for a running process', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - - const instances = await waitForInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('id'); - - const attributes = await genericGetAttributes(instances[0].id); - - expect(Array.isArray(attributes)).toBe(true); - expect(attributes.length).toBeGreaterThan(0); - }); - }); -}); From 375d557b7db53300e53558acf6b031d3cff44f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerrit=20Gr=C3=A4tz?= Date: Mon, 23 Mar 2026 11:37:08 +0100 Subject: [PATCH 4/5] remove describe --- .../genericProgrammaticApproach.test.ts | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/tests/integration/genericProgrammaticApproach.test.ts b/tests/integration/genericProgrammaticApproach.test.ts index 4b177aab..1117fb06 100644 --- a/tests/integration/genericProgrammaticApproach.test.ts +++ b/tests/integration/genericProgrammaticApproach.test.ts @@ -246,36 +246,32 @@ describe('Generic ProcessService Integration Tests', () => { }); }); - describe('getAttributes', () => { - it('should return attributes for a running process instance', async () => { - const businessKey = generateID(); - await genericStart(businessKey); + it('should return attributes for a running process instance', async () => { + const businessKey = generateID(); + await genericStart(businessKey); - const instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); + const instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); - const attributes = await genericGetAttributes(instances[0].id); + const attributes = await genericGetAttributes(instances[0].id); - expect(Array.isArray(attributes)).toBe(true); - expect(attributes.length).toBeGreaterThan(0); - expect(attributes[0]).toHaveProperty('id'); - expect(attributes[0]).toHaveProperty('value'); - }); + expect(Array.isArray(attributes)).toBe(true); + expect(attributes.length).toBeGreaterThan(0); + expect(attributes[0]).toHaveProperty('id'); + expect(attributes[0]).toHaveProperty('value'); }); - describe('getOutputs', () => { - it('should return outputs for a process instance', async () => { - const businessKey = generateID(); - await genericStart(businessKey); + it('should return outputs for a process instance', async () => { + const businessKey = generateID(); + await genericStart(businessKey); - const instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); + const instances = await genericGetInstances(businessKey, ['RUNNING']); + expect(instances.length).toBe(1); - const outputs = await genericGetOutputs(instances[0].id); + const outputs = await genericGetOutputs(instances[0].id); - expect(outputs).toBeDefined(); - expect(outputs).toHaveProperty('processedBy'); - expect(outputs).toHaveProperty('completionStatus'); - }); + expect(outputs).toBeDefined(); + expect(outputs).toHaveProperty('processedBy'); + expect(outputs).toHaveProperty('completionStatus'); }); }); From 5687400e5665f5136cf1ed09cf4b12de96379bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerrit=20Gr=C3=A4tz?= Date: Tue, 24 Mar 2026 10:53:05 +0100 Subject: [PATCH 5/5] use outbox for genericService + add hybrid tests to test getOutputs and getAttributes --- tests/bookshop/srv/programmatic-service.ts | 16 +- tests/hybrid/programmaticApproach.test.ts | 144 ++++++++--- .../genericProgrammaticApproach.test.ts | 235 +++++++----------- 3 files changed, 211 insertions(+), 184 deletions(-) diff --git a/tests/bookshop/srv/programmatic-service.ts b/tests/bookshop/srv/programmatic-service.ts index cf3bf0a6..dc0f7a3e 100644 --- a/tests/bookshop/srv/programmatic-service.ts +++ b/tests/bookshop/srv/programmatic-service.ts @@ -114,26 +114,34 @@ class ProgrammaticService extends cds.ApplicationService { this.on('genericStart', async (req: cds.Request) => { const { definitionId, businessKey, context } = req.data; const processService = await cds.connect.to('ProcessService'); + const queuedProcessService = cds.queued(processService); const parsedContext = context ? JSON.parse(context) : {}; - await processService.emit('start', { definitionId, context: parsedContext }, { businessKey }); + await queuedProcessService.emit( + 'start', + { definitionId, context: parsedContext }, + { businessKey }, + ); }); this.on('genericCancel', async (req: cds.Request) => { const { businessKey, cascade } = req.data; const processService = await cds.connect.to('ProcessService'); - await processService.emit('cancel', { businessKey, cascade: cascade ?? false }); + const queuedProcessService = cds.queued(processService); + await queuedProcessService.emit('cancel', { businessKey, cascade: cascade ?? false }); }); this.on('genericSuspend', async (req: cds.Request) => { const { businessKey, cascade } = req.data; const processService = await cds.connect.to('ProcessService'); - await processService.emit('suspend', { businessKey, cascade: cascade ?? false }); + const queuedProcessService = cds.queued(processService); + await queuedProcessService.emit('suspend', { businessKey, cascade: cascade ?? false }); }); this.on('genericResume', async (req: cds.Request) => { const { businessKey, cascade } = req.data; const processService = await cds.connect.to('ProcessService'); - await processService.emit('resume', { businessKey, cascade: cascade ?? false }); + const queuedProcessService = cds.queued(processService); + await queuedProcessService.emit('resume', { businessKey, cascade: cascade ?? false }); }); this.on('genericGetInstancesByBusinessKey', async (req: cds.Request) => { diff --git a/tests/hybrid/programmaticApproach.test.ts b/tests/hybrid/programmaticApproach.test.ts index 56d623ed..2c7fbe01 100644 --- a/tests/hybrid/programmaticApproach.test.ts +++ b/tests/hybrid/programmaticApproach.test.ts @@ -40,11 +40,63 @@ describe('Programmatic Approach Hybrid Tests', () => { ); } + async function startOutputProcess( + ID: string, + mandatory_datetime: string, + mandatory_string: string, + optional_string?: string, + optional_datetime?: string, + ) { + return POST('/odata/v4/programmatic/startForGetOutputs', { + ID, + mandatory_datetime, + mandatory_string, + optional_string, + optional_datetime, + }); + } + + async function getOutputInstances(ID: string, status?: string[]): Promise { + const res = await POST('/odata/v4/programmatic/getInstanceIDForGetOutputs', { ID, status }); + return res.data?.value ?? res.data ?? []; + } + + async function waitForOutputInstances( + ID: string, + status: string[], + expectedCount = 1, + maxRetries = 8, + ): Promise { + for (let i = 0; i < maxRetries; i++) { + const instances = await getOutputInstances(ID, status); + if (instances.length >= expectedCount) return instances; + await new Promise((r) => setTimeout(r, 10000)); + } + throw new Error( + `Timed out waiting for ${expectedCount} output instance(s) with status [${status}] for ID ${ID}`, + ); + } + async function getAttributes(ID: string): Promise { const res = await POST('/odata/v4/programmatic/getAttributes', { ID }); return res.data?.value ?? res.data ?? []; } + async function getOutputs(instanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/getOutputs', { instanceId }); + return res.data; + } + + async function genericGetAttributes(processInstanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/genericGetAttributes', { processInstanceId }); + return res.data?.value ?? res.data ?? []; + } + + async function genericGetOutputs(processInstanceId: string): Promise { + const res = await POST('/odata/v4/programmatic/genericGetOutputs', { processInstanceId }); + return res.data; + } + describe('Process Start', () => { it('should start a process and verify it is RUNNING on SBPA', async () => { const ID = generateID(); @@ -197,48 +249,70 @@ describe('Programmatic Approach Hybrid Tests', () => { }); describe('Get Outputs', () => { - async function startOutputProcess( - ID: string, - mandatory_datetime: string, - mandatory_string: string, - optional_string?: string, - optional_datetime?: string, - ) { - return POST('/odata/v4/programmatic/startForGetOutputs', { + it('should retrieve outputs from a completed process', async () => { + const ID = generateID(); + const mandatory_datetime = new Date().toISOString(); + const mandatory_string = 'test-output-string'; + + await startOutputProcess(ID, mandatory_datetime, mandatory_string); + + const instances = await waitForOutputInstances(ID, ['COMPLETED']); + expect(instances.length).toBe(1); + expect(instances[0]).toHaveProperty('workflowId'); + + const outputs = await getOutputs(instances[0].workflowId); + + expect(outputs).toHaveProperty('mandatory_string'); + expect(outputs).toHaveProperty('mandatory_datetime'); + expect(outputs.mandatory_string).toBeDefined(); + expect(outputs.mandatory_datetime).toBeDefined(); + }); + + it('should return optional fields in outputs when provided', async () => { + const ID = generateID(); + const mandatory_datetime = new Date().toISOString(); + const mandatory_string = 'test-mandatory'; + const optional_string = 'test-optional'; + const optional_datetime = new Date().toISOString(); + + await startOutputProcess( ID, mandatory_datetime, mandatory_string, optional_string, optional_datetime, - }); - } + ); - async function getOutputInstances(ID: string, status?: string[]): Promise { - const res = await POST('/odata/v4/programmatic/getInstanceIDForGetOutputs', { ID, status }); - return res.data?.value ?? res.data ?? []; - } + const instances = await waitForOutputInstances(ID, ['COMPLETED']); + expect(instances.length).toBe(1); - async function waitForOutputInstances( - ID: string, - status: string[], - expectedCount = 1, - maxRetries = 8, - ): Promise { - for (let i = 0; i < maxRetries; i++) { - const instances = await getOutputInstances(ID, status); - if (instances.length >= expectedCount) return instances; - await new Promise((r) => setTimeout(r, 10000)); - } - throw new Error( - `Timed out waiting for ${expectedCount} output instance(s) with status [${status}] for ID ${ID}`, - ); - } + const outputs = await getOutputs(instances[0].workflowId); - async function getOutputs(instanceId: string): Promise { - const res = await POST('/odata/v4/programmatic/getOutputs', { instanceId }); - return res.data; - } + expect(outputs).toHaveProperty('mandatory_string'); + expect(outputs).toHaveProperty('mandatory_datetime'); + expect(outputs).toHaveProperty('optional_string'); + expect(outputs).toHaveProperty('optional_datetime'); + }); + }); + + describe('Generic Get Attributes', () => { + it('should return attributes for a running process', async () => { + const ID = generateID(); + await startProcess(ID); + const instances = await waitForInstances(ID, ['RUNNING']); + expect(instances.length).toBe(1); + + const attributes = await genericGetAttributes(instances[0].id); + + expect(Array.isArray(attributes)).toBe(true); + expect(attributes.length).toBeGreaterThan(0); + expect(attributes[0]).toHaveProperty('id'); + expect(attributes[0]).toHaveProperty('value'); + expect(attributes[0]).toHaveProperty('type'); + }); + }); + describe('Generic Get Outputs', () => { it('should retrieve outputs from a completed process', async () => { const ID = generateID(); const mandatory_datetime = new Date().toISOString(); @@ -250,7 +324,7 @@ describe('Programmatic Approach Hybrid Tests', () => { expect(instances.length).toBe(1); expect(instances[0]).toHaveProperty('workflowId'); - const outputs = await getOutputs(instances[0].workflowId); + const outputs = await genericGetOutputs(instances[0].workflowId); expect(outputs).toHaveProperty('mandatory_string'); expect(outputs).toHaveProperty('mandatory_datetime'); @@ -276,7 +350,7 @@ describe('Programmatic Approach Hybrid Tests', () => { const instances = await waitForOutputInstances(ID, ['COMPLETED']); expect(instances.length).toBe(1); - const outputs = await getOutputs(instances[0].workflowId); + const outputs = await genericGetOutputs(instances[0].workflowId); expect(outputs).toHaveProperty('mandatory_string'); expect(outputs).toHaveProperty('mandatory_datetime'); diff --git a/tests/integration/genericProgrammaticApproach.test.ts b/tests/integration/genericProgrammaticApproach.test.ts index 1117fb06..bbd6e7cd 100644 --- a/tests/integration/genericProgrammaticApproach.test.ts +++ b/tests/integration/genericProgrammaticApproach.test.ts @@ -8,6 +8,21 @@ const { POST } = cds.test(app); const DEFINITION_ID = 'eu12.cdsmunich.capprocesspluginhybridtest.programmatic_Lifecycle_Process'; describe('Generic ProcessService Integration Tests', () => { + let foundMessages: any[] = []; + beforeAll(async () => { + const db = await cds.connect.to('db'); + db.before('*', (req) => { + if (req.event === 'CREATE' && req.target?.name === 'cds.outbox.Messages') { + const msg = JSON.parse(req.query?.INSERT?.entries[0].msg); + foundMessages.push(msg); + } + }); + }); + + beforeEach(async () => { + foundMessages = []; + }); + afterAll(async () => { await (cds as any).flush(); }); @@ -46,165 +61,136 @@ describe('Generic ProcessService Integration Tests', () => { }); } - async function genericGetInstances(businessKey: string, status?: string[]): Promise { - const res = await POST('/odata/v4/programmatic/genericGetInstancesByBusinessKey', { - businessKey, - status, - }); - return res.data?.value ?? res.data ?? []; - } - - async function genericGetAttributes(processInstanceId: string): Promise { - const res = await POST('/odata/v4/programmatic/genericGetAttributes', { - processInstanceId, - }); - return res.data?.value ?? res.data ?? []; - } - - async function genericGetOutputs(processInstanceId: string): Promise { - const res = await POST('/odata/v4/programmatic/genericGetOutputs', { - processInstanceId, - }); - return res.data; - } - describe('Process Start Event', () => { - it('should start a process and verify it is RUNNING', async () => { + it('should emit a start event to the outbox', async () => { const businessKey = generateID(); const response = await genericStart(businessKey); expect(response.status).toBe(204); + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].event).toBe('start'); + }); - const instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); - expect(instances[0]).toHaveProperty('id'); - expect(instances[0]).toHaveProperty('definitionId', DEFINITION_ID); + it('should include definitionId in the start event payload', async () => { + const businessKey = generateID(); + await genericStart(businessKey); + + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].data.definitionId).toBe(DEFINITION_ID); }); - it('should include the businessKey in the started instance', async () => { + it('should include the businessKey as context ID in the start event', async () => { const businessKey = generateID(); await genericStart(businessKey); - const instances = await genericGetInstances(businessKey); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('businessKey', businessKey); + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].data.context).toBeDefined(); + expect(foundMessages[0].data.context.ID).toEqual(businessKey); }); - it('should start multiple independent processes with different businessKeys', async () => { + it('should emit separate start events for multiple processes', async () => { const keyA = generateID(); const keyB = generateID(); await genericStart(keyA); await genericStart(keyB); - const instancesA = await genericGetInstances(keyA, ['RUNNING']); - const instancesB = await genericGetInstances(keyB, ['RUNNING']); - - expect(instancesA.length).toBe(1); - expect(instancesB.length).toBe(1); - expect(instancesA[0].id).not.toEqual(instancesB[0].id); + expect(foundMessages.length).toBe(2); + expect(foundMessages[0].event).toBe('start'); + expect(foundMessages[1].event).toBe('start'); + expect(foundMessages[0].data.context.ID).toEqual(keyA); + expect(foundMessages[1].data.context.ID).toEqual(keyB); }); }); describe('Process Cancel Event', () => { - it('should cancel a running process', async () => { + it('should emit a cancel event to the outbox', async () => { const businessKey = generateID(); - await genericStart(businessKey); - const response = await genericCancel(businessKey); - expect(response.status).toBe(204); - const instances = await genericGetInstances(businessKey, ['CANCELED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'CANCELED'); + expect(response.status).toBe(204); + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].event).toBe('cancel'); + expect(foundMessages[0].data.businessKey).toEqual(businessKey); }); - it('should not fail when cancelling with no running processes', async () => { + it('should include cascade=false by default in cancel payload', async () => { const businessKey = generateID(); - const response = await genericCancel(businessKey); - expect(response.status).toBe(204); + await genericCancel(businessKey); + + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].data.cascade).toBe(false); }); }); describe('Process Suspend Event', () => { - it('should suspend a running process', async () => { + it('should emit a suspend event to the outbox', async () => { const businessKey = generateID(); - await genericStart(businessKey); - const response = await genericSuspend(businessKey); - expect(response.status).toBe(204); - const instances = await genericGetInstances(businessKey, ['SUSPENDED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'SUSPENDED'); + expect(response.status).toBe(204); + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].event).toBe('suspend'); + expect(foundMessages[0].data.businessKey).toEqual(businessKey); }); - it('should not fail when suspending with no running processes', async () => { + it('should include cascade=false by default in suspend payload', async () => { const businessKey = generateID(); - const response = await genericSuspend(businessKey); - expect(response.status).toBe(204); + await genericSuspend(businessKey); + + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].data.cascade).toBe(false); }); }); describe('Process Resume Event', () => { - it('should resume a suspended process', async () => { + it('should emit a resume event to the outbox', async () => { const businessKey = generateID(); - await genericStart(businessKey); - await genericSuspend(businessKey); - const response = await genericResume(businessKey); - expect(response.status).toBe(204); - const instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); + expect(response.status).toBe(204); + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].event).toBe('resume'); + expect(foundMessages[0].data.businessKey).toEqual(businessKey); }); - it('should not fail when resuming with no suspended processes', async () => { + it('should include cascade=false by default in resume payload', async () => { const businessKey = generateID(); - const response = await genericResume(businessKey); - expect(response.status).toBe(204); + await genericResume(businessKey); + + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].data.cascade).toBe(false); }); }); describe('Sequential lifecycle operations', () => { - it('should go through start -> suspend -> resume and end up RUNNING', async () => { + it('should emit start, suspend, and resume events in order', async () => { const businessKey = generateID(); await genericStart(businessKey); - - let instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - await genericSuspend(businessKey); - - instances = await genericGetInstances(businessKey, ['SUSPENDED']); - expect(instances.length).toBe(1); - await genericResume(businessKey); - instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); + expect(foundMessages.length).toBe(3); + expect(foundMessages[0].event).toBe('start'); + expect(foundMessages[1].event).toBe('suspend'); + expect(foundMessages[2].event).toBe('resume'); }); - it('should go through start -> cancel and end up CANCELED', async () => { + it('should emit start then cancel events in order', async () => { const businessKey = generateID(); await genericStart(businessKey); - - let instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - await genericCancel(businessKey); - instances = await genericGetInstances(businessKey, ['CANCELED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'CANCELED'); + expect(foundMessages.length).toBe(2); + expect(foundMessages[0].event).toBe('start'); + expect(foundMessages[1].event).toBe('cancel'); + expect(foundMessages[0].data.context.ID).toEqual(businessKey); + expect(foundMessages[1].data.businessKey).toEqual(businessKey); }); - it('should go through start -> suspend -> resume -> cancel', async () => { + it('should emit start, suspend, resume, and cancel events in order', async () => { const businessKey = generateID(); await genericStart(businessKey); @@ -212,66 +198,25 @@ describe('Generic ProcessService Integration Tests', () => { await genericResume(businessKey); await genericCancel(businessKey); - const instances = await genericGetInstances(businessKey, ['CANCELED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'CANCELED'); + expect(foundMessages.length).toBe(4); + expect(foundMessages[0].event).toBe('start'); + expect(foundMessages[1].event).toBe('suspend'); + expect(foundMessages[2].event).toBe('resume'); + expect(foundMessages[3].event).toBe('cancel'); }); }); - describe('getInstancesByBusinessKey', () => { - it('should return empty array for unknown businessKey', async () => { + describe('Custom context in start event', () => { + it('should pass custom context through to the outbox message', async () => { const businessKey = generateID(); - const instances = await genericGetInstances(businessKey); - expect(instances).toEqual([]); - }); + const customContext = { ID: businessKey, customField: 'customValue', number: 42 }; - it('should filter instances by status', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - - const runningInstances = await genericGetInstances(businessKey, ['RUNNING']); - expect(runningInstances.length).toBe(1); + await genericStart(businessKey, customContext); - const canceledInstances = await genericGetInstances(businessKey, ['CANCELED']); - expect(canceledInstances.length).toBe(0); + expect(foundMessages.length).toBe(1); + expect(foundMessages[0].event).toBe('start'); + expect(foundMessages[0].data.context.customField).toEqual('customValue'); + expect(foundMessages[0].data.context.number).toEqual(42); }); - - it('should return instances matching any of the provided statuses', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - - const instances = await genericGetInstances(businessKey, ['RUNNING', 'SUSPENDED']); - expect(instances.length).toBe(1); - expect(instances[0]).toHaveProperty('status', 'RUNNING'); - }); - }); - - it('should return attributes for a running process instance', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - - const instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - - const attributes = await genericGetAttributes(instances[0].id); - - expect(Array.isArray(attributes)).toBe(true); - expect(attributes.length).toBeGreaterThan(0); - expect(attributes[0]).toHaveProperty('id'); - expect(attributes[0]).toHaveProperty('value'); - }); - - it('should return outputs for a process instance', async () => { - const businessKey = generateID(); - await genericStart(businessKey); - - const instances = await genericGetInstances(businessKey, ['RUNNING']); - expect(instances.length).toBe(1); - - const outputs = await genericGetOutputs(instances[0].id); - - expect(outputs).toBeDefined(); - expect(outputs).toHaveProperty('processedBy'); - expect(outputs).toHaveProperty('completionStatus'); }); });