Skip to content

Commit f81979a

Browse files
authored
Add Tests to example project (#417)
* wip * wip * wip
1 parent 998ca5e commit f81979a

File tree

9 files changed

+172
-71
lines changed

9 files changed

+172
-71
lines changed

example-cap-server/package.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"subscribeTenants": "cds subscribe t1 --to http://localhost:4004 -u yves: && cds subscribe t2 --to http://localhost:4004 -u yves: && cds subscribe t3 --to http://localhost:4004 -u yves:",
1010
"dev": "npm run copy-library && npm run subscribeTenants && npm start",
1111
"copy-library": "npx shx rm -rf node_modules/@cap-js-community/event-queue && npx shx mkdir -p node_modules/@cap-js-community/event-queue && npx shx cp -R ../package.json ../index.cds ../cds-plugin.js ../src ../srv ../db node_modules/@cap-js-community/event-queue",
12-
"test": "CDS_ENV=test node --test"
12+
"test": "jest"
1313
},
1414
"dependencies": {
1515
"@cap-js-community/event-queue": "file:..",
@@ -19,16 +19,16 @@
1919
"express": "^4.21.2"
2020
},
2121
"devDependencies": {
22+
"@cap-js-community/common": "^0.3.2",
23+
"@cap-js/cds-test": "^0",
2224
"@cap-js/sqlite": "2.1.0",
25+
"@sap/xssec": "^4.11.2",
26+
"cron-parser": "5.4.0",
2327
"eslint": "8.56.0",
2428
"eslint-config-prettier": "9.0.0",
29+
"jest": "30.2.0",
2530
"prettier": "3.7.4",
2631
"redis": "^4.7.0",
27-
"@sap/xssec": "^4.11.2",
28-
"@cap-js-community/common": "^0.3.2",
29-
"@cap-js/cds-test": "^0",
30-
"jest": "30.2.0",
31-
"cron-parser": "5.4.0",
3232
"yaml": "2.8.2"
3333
},
3434
"cds": {
@@ -58,6 +58,12 @@
5858
"syncJobs": {
5959
"load": 10,
6060
"cron": "*/15 * * * * *"
61+
},
62+
"processSpecial": {
63+
"maxAttempts": 1,
64+
"[test]": {
65+
"retryFailedAfter": 0
66+
}
6167
}
6268
}
6369
}

example-cap-server/requests/basic.http

Lines changed: 0 additions & 49 deletions
This file was deleted.

example-cap-server/requests/task.http

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Authorization: Basic [email protected] alice
1212
%}
1313

1414
### Process Task - T1
15-
POST http://localhost:4004/odata/v4/process/C_ClosingTask({{t1TaskId}})/process
15+
POST http://localhost:4004/odata/v4/process/C_ClosingTask({{t1TaskId}})/trigger
1616
Content-Type: application/json
1717
Authorization: Basic [email protected] alice
1818

@@ -39,7 +39,7 @@ Authorization: Basic [email protected] bob
3939

4040

4141
### Process Task - T2
42-
POST http://localhost:4004/odata/v4/process/C_ClosingTask({{t2TaskId}})/process
42+
POST http://localhost:4004/odata/v4/process/C_ClosingTask({{t2TaskId}})/trigger
4343
Content-Type: application/json
4444
Authorization: Basic [email protected] bob
4545

example-cap-server/srv/facade/mail-service.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ module.exports = class MailService extends cds.Service {
77
await super.init();
88

99
this.on("sendSingle", async function (req) {
10-
this.logger.info("sending e-mail", req.data);
10+
req.eventQueue.processor.logger.info("sending e-mail", req.data);
11+
});
12+
13+
this.on("eventQueueCluster", async function (req) {
14+
return req.eventQueue.clusterByDataProperty("to", (clusterKey, entries) => {
15+
return {
16+
to: clusterKey,
17+
subjects: entries.map((entry) => entry.subject),
18+
};
19+
});
1120
});
1221
}
1322
};

example-cap-server/srv/facade/task-service.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const cds = require("@sap/cds");
4+
const eventQueue = require("@cap-js-community/event-queue");
45

56
module.exports = class TaskService extends cds.Service {
67
async init() {
@@ -23,6 +24,18 @@ module.exports = class TaskService extends cds.Service {
2324
});
2425
});
2526

27+
this.on("processSpecial", async function () {
28+
return {
29+
status: eventQueue.EventProcessingStatus.Error,
30+
error: new Error("Special tasks cannot be processed at the moment."),
31+
};
32+
});
33+
34+
this.on("eventQueueRetriesExceeded", async function (req) {
35+
req.eventQueue.processor.logger.info("cleaning up after retries exceeded for task...");
36+
await UPDATE.entity("sap.eventqueue.sample.Task").set({ status: "error" }).where({ ID: req.data.ID });
37+
});
38+
2639
this.on("syncJobs", async function () {
2740
const logger = cds.log(this.name);
2841
const task = await SELECT.one.from("sap.eventqueue.sample.Task").columns("count(ID) as count");

example-cap-server/srv/handler/process-service.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module.exports = class ProcessService extends cds.ApplicationService {
66
async init() {
77
await super.init();
88

9-
this.on("process", async (req) => {
9+
this.on("trigger", async (req) => {
1010
const task = await req.query;
1111
if (!task.length) {
1212
req.reject(404, "task does not exist");
@@ -17,7 +17,22 @@ module.exports = class ProcessService extends cds.ApplicationService {
1717
}
1818

1919
const srv = await cds.connect.to("task-service");
20-
await srv.tx(req).emit("process", req.params[0]);
20+
await srv.tx(req).send("process", req.params[0]);
21+
await UPDATE.entity("sap.eventqueue.sample.Task").set({ status: "in progress" }).where(req.params[0]);
22+
});
23+
24+
this.on("triggerSpecial", async (req) => {
25+
const task = await req.query;
26+
if (!task.length) {
27+
req.reject(404, "task does not exist");
28+
}
29+
30+
if (task[0].status === "done") {
31+
req.reject(422, "task already processed");
32+
}
33+
34+
const srv = await cds.connect.to("task-service");
35+
await srv.tx(req).send("processSpecial", req.params[0]);
2136
await UPDATE.entity("sap.eventqueue.sample.Task").set({ status: "in progress" }).where(req.params[0]);
2237
});
2338
}

example-cap-server/srv/service/process-service.cds

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ using sap.eventqueue.sample.Task from '../../db/Task';
55
service ProcessService {
66
entity C_ClosingTask as projection on Task
77
actions {
8-
action process();
8+
action trigger();
9+
action triggerSpecial();
910
};
1011

1112
}

example-cap-server/test/ProcessService.test.js

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,30 @@
33
const cds = require("@sap/cds");
44
const eventQueue = require("@cap-js-community/event-queue");
55

6-
const { GET, POST, expect, axios } = cds.test(__dirname + "/..");
6+
const { GET, POST, axios } = cds.test(__dirname + "/..");
77
axios.defaults.auth = { username: "alice", password: "alice" };
88

99
describe("ProcessService OData APIs", () => {
10+
let log = cds.test.log();
11+
12+
beforeEach(async () => {
13+
await DELETE.from("sap.eventqueue.Event");
14+
});
15+
1016
it("serves ProcessService.C_ClosingTask", async () => {
1117
const { data } = await GET`/odata/v4/process/C_ClosingTask ${{ params: { $select: "ID,description" } }}`;
12-
expect(data.value).to.be.empty;
18+
expect(data.value).toHaveLength(0);
1319
});
1420

1521
it("emit and process event via odata", async () => {
16-
let { data } = await POST(`/odata/v4/process/C_ClosingTask`, {
22+
const { data } = await POST(`/odata/v4/process/C_ClosingTask`, {
1723
description: "Demo Task for processing",
1824
});
19-
20-
await POST(`/odata/v4/process/C_ClosingTask(${data.ID})/process`);
25+
await POST(`/odata/v4/process/C_ClosingTask(${data.ID})/trigger`);
2126

2227
let events = await SELECT.from("sap.eventqueue.Event");
23-
expect(events).to.have.lengthOf(1);
24-
expect(events[0]).to.nested.include({
28+
expect(events).toHaveLength(1);
29+
expect(events[0]).toMatchObject({
2530
type: "CAP_OUTBOX",
2631
subType: "task-service",
2732
status: 0,
@@ -30,17 +35,106 @@ describe("ProcessService OData APIs", () => {
3035
await eventQueue.processEventQueue({}, "CAP_OUTBOX", "task-service");
3136

3237
events = await SELECT.from("sap.eventqueue.Event").orderBy("createdAt");
33-
expect(events).to.have.lengthOf(2);
34-
expect(events[0]).to.nested.include({
38+
expect(events).toHaveLength(2);
39+
expect(events[0]).toMatchObject({
3540
type: "CAP_OUTBOX",
3641
subType: "task-service",
3742
status: 2,
3843
});
3944

40-
expect(events[1]).to.nested.include({
45+
expect(events[1]).toMatchObject({
4146
type: "CAP_OUTBOX",
4247
subType: "mail-service",
4348
status: 0,
4449
});
4550
});
51+
52+
it("multiple tasks are clustered by 'to' property", async () => {
53+
const { data: task1 } = await POST(`/odata/v4/process/C_ClosingTask`, {
54+
description: "Demo Task for processing I",
55+
});
56+
const { data: task2 } = await POST(`/odata/v4/process/C_ClosingTask`, {
57+
description: "Demo Task for processing II",
58+
});
59+
await POST(`/odata/v4/process/C_ClosingTask(${task1.ID})/trigger`);
60+
await POST(`/odata/v4/process/C_ClosingTask(${task2.ID})/trigger`);
61+
62+
let events = await SELECT.from("sap.eventqueue.Event");
63+
expect(events).toHaveLength(2);
64+
65+
await eventQueue.processEventQueue({}, "CAP_OUTBOX", "task-service");
66+
67+
events = await SELECT.from("sap.eventqueue.Event").orderBy("createdAt");
68+
expect(events).toHaveLength(4);
69+
70+
await eventQueue.processEventQueue({}, "CAP_OUTBOX", "mail-service");
71+
72+
const logs = parseLogEntries(log.output);
73+
const mailLogs = logs.filter((entry) => entry.includes("sending e-mail"));
74+
expect(mailLogs).toHaveLength(1);
75+
expect(mailLogs[0]).toMatchSnapshot();
76+
});
77+
78+
it("trigger special task (own cds env configuration) with exceeded handling", async () => {
79+
const { data } = await POST(`/odata/v4/process/C_ClosingTask`, {
80+
description: "Demo Task for processing",
81+
});
82+
await POST(`/odata/v4/process/C_ClosingTask(${data.ID})/triggerSpecial`);
83+
84+
let events = await SELECT.from("sap.eventqueue.Event");
85+
expect(events).toHaveLength(1);
86+
expect(events[0]).toMatchObject({
87+
type: "CAP_OUTBOX",
88+
subType: "task-service.processSpecial",
89+
status: 0,
90+
});
91+
92+
await eventQueue.processEventQueue({}, "CAP_OUTBOX", "task-service.processSpecial");
93+
94+
events = await SELECT.from("sap.eventqueue.Event").orderBy("createdAt");
95+
expect(events).toHaveLength(1);
96+
expect(events[0]).toMatchObject({
97+
type: "CAP_OUTBOX",
98+
subType: "task-service.processSpecial",
99+
status: 3,
100+
error: expect.stringContaining("Special tasks cannot be processed at the moment."),
101+
});
102+
103+
await eventQueue.processEventQueue({}, "CAP_OUTBOX", "task-service.processSpecial");
104+
105+
events = await SELECT.from("sap.eventqueue.Event").orderBy("createdAt");
106+
expect(events).toHaveLength(1);
107+
expect(events[0]).toMatchObject({
108+
type: "CAP_OUTBOX",
109+
subType: "task-service.processSpecial",
110+
status: 4,
111+
error: expect.stringContaining("Special tasks cannot be processed at the moment."),
112+
});
113+
});
46114
});
115+
116+
function parseLogEntries(logText) {
117+
const lines = logText.split(/\r?\n/);
118+
const entries = [];
119+
let current = [];
120+
121+
for (const line of lines) {
122+
if (/^\[[^\]]+]\s+-/.test(line)) {
123+
// Start of a new entry
124+
if (current.length > 0) {
125+
entries.push(current.join("\n"));
126+
}
127+
current = [line];
128+
} else {
129+
// Continuation of previous entry
130+
current.push(line);
131+
}
132+
}
133+
134+
// Push the final entry
135+
if (current.length > 0) {
136+
entries.push(current.join("\n"));
137+
}
138+
139+
return entries;
140+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`ProcessService OData APIs multiple tasks are clustered by 'to' property 1`] = `
4+
"[/eventQueue/outbox/generic/mail-service] - sending e-mail {
5+
6+
subjects: [
7+
"Processing of task: 'Demo Task for processing I' done.",
8+
"Processing of task: 'Demo Task for processing II' done."
9+
]
10+
}
11+
"
12+
`;

0 commit comments

Comments
 (0)