@gadmin2n/schematics 0.0.87 → 0.0.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/.dockerignore +16 -2
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.codegen +40 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +76 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.web +53 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +219 -33
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +250 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +4 -1
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/dev/postgres/init.sql +12 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +170 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +254 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +8 -7
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +1 -1
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/prismaModels.ts +1 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +39 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/audit.seed.ts +40 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +56 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +39 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/{scripts/sync-data-mngt-pages.ts → seed/data-mngt.seed.ts} +36 -20
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/game.seed.ts +44 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +30 -6
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +130 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-event-trigger.ts +60 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +11 -25
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +108 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/main.ts +1 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.controller.spec.ts +31 -2
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.spec.ts +31 -2
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.spec.ts +41 -57
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.spec.ts +31 -2
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.spec.ts +309 -1
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.spec.ts +31 -2
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.spec.ts +315 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.spec.ts +31 -2
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.spec.ts +312 -2
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.spec.ts +31 -2
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.spec.ts +317 -1
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.spec.ts +31 -2
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.spec.ts +309 -1
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.spec.ts +31 -2
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.spec.ts +299 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.spec.ts +31 -2
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.spec.ts +307 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.spec.ts +31 -2
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +309 -1
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +205 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.ts +116 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.spec.ts +158 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +110 -1
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.spec.ts +79 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.ts +54 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +34 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +457 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +241 -4
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.spec.ts +34 -2
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.spec.ts +24 -30
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.spec.ts +34 -2
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.spec.ts +36 -36
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.spec.ts +34 -2
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +48 -24
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +312 -3
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +152 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +12 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +79 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +270 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +1 -1
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +23 -0
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +70 -5
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +246 -90
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +20 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +96 -8
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/nginx.conf +74 -0
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/ElementInspector.tsx +18 -0
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +1 -1
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/form.tsx +1 -1
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -3
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -3
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +4 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +9 -0
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +156 -139
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +14 -2
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +62 -0
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/PublishModal.tsx +4 -6
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +18 -27
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasDefaults.ts +32 -11
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/demos.ts +48 -61
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas-page/index.tsx +3 -6
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +16 -16
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +28 -35
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -3
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +1 -1
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -1
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.css +6 -0
  93. package/package.json +1 -1
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile +0 -63
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/sync-resources.ts +0 -100
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +0 -302
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.spec.ts +0 -20
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/sql/create-event-trigger.sql +0 -87
  99. /package/dist/lib/application/files/gadmin2-game-angle-demo/{GRACEFUL-DEPLOYMENT.md → server/GRACEFUL-DEPLOYMENT.md} +0 -0
@@ -0,0 +1,457 @@
1
+ import { NotFoundException, UnauthorizedException } from '@nestjs/common';
2
+ import { Test, TestingModule } from '@nestjs/testing';
3
+ import { PrismaService } from 'nestjs-prisma';
4
+ import { TemporalService } from './temporal.service';
5
+ import { signWebhookPayload } from './webhook-signature.util';
6
+ import { WorkflowService } from './workflow.service';
7
+
8
+ describe('WorkflowService — cron lifecycle hooks', () => {
9
+ let service: WorkflowService;
10
+ let prisma: any;
11
+ let temporal: any;
12
+
13
+ beforeEach(async () => {
14
+ prisma = {
15
+ workflow: {
16
+ findUnique: jest.fn(),
17
+ update: jest.fn(),
18
+ delete: jest.fn(),
19
+ findMany: jest.fn(),
20
+ },
21
+ workflowVersion: {
22
+ findFirst: jest.fn(),
23
+ create: jest.fn(),
24
+ },
25
+ $transaction: jest.fn(async (cb: any) =>
26
+ cb({
27
+ workflowVersion: {
28
+ findFirst: prisma.workflowVersion.findFirst,
29
+ create: prisma.workflowVersion.create,
30
+ },
31
+ workflow: { update: prisma.workflow.update },
32
+ }),
33
+ ),
34
+ };
35
+ temporal = {
36
+ upsertCronSchedule: jest.fn(),
37
+ pauseCronSchedule: jest.fn(),
38
+ unpauseCronSchedule: jest.fn(),
39
+ deleteCronSchedule: jest.fn(),
40
+ };
41
+
42
+ const module: TestingModule = await Test.createTestingModule({
43
+ providers: [
44
+ WorkflowService,
45
+ { provide: PrismaService, useValue: prisma },
46
+ { provide: TemporalService, useValue: temporal },
47
+ ],
48
+ }).compile();
49
+
50
+ service = module.get<WorkflowService>(WorkflowService);
51
+ });
52
+
53
+ describe('publish — cron schedule sync', () => {
54
+ const ID = BigInt(42);
55
+ const mkWorkflow = (dsl: any) => ({ id: ID, dsl, status: 'DRAFT' });
56
+
57
+ it('upserts cron schedule when DSL contains cron_trigger', async () => {
58
+ const dsl = {
59
+ nodes: [
60
+ {
61
+ id: 't1',
62
+ type: 'cron_trigger',
63
+ config: { cron: '0 2 * * *', timezone: 'Asia/Shanghai' },
64
+ },
65
+ ],
66
+ edges: [],
67
+ };
68
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
69
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
70
+ prisma.workflowVersion.create.mockResolvedValueOnce({
71
+ id: BigInt(1),
72
+ version: 1,
73
+ });
74
+ prisma.workflow.update.mockResolvedValueOnce({});
75
+
76
+ await service.publish(ID, { changeSummary: 'init' }, 'tester');
77
+
78
+ expect(temporal.upsertCronSchedule).toHaveBeenCalledWith(
79
+ expect.objectContaining({
80
+ workflowId: 42,
81
+ cron: '0 2 * * *',
82
+ timezone: 'Asia/Shanghai',
83
+ }),
84
+ );
85
+ expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
86
+ });
87
+
88
+ it('deletes cron schedule when published DSL has no cron_trigger', async () => {
89
+ const dsl = {
90
+ nodes: [
91
+ { id: 't1', type: 'manual_trigger', config: {} },
92
+ { id: 'n1', type: 'http_request', config: {} },
93
+ ],
94
+ edges: [{ source: 't1', target: 'n1' }],
95
+ };
96
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
97
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
98
+ prisma.workflowVersion.create.mockResolvedValueOnce({
99
+ id: BigInt(1),
100
+ version: 1,
101
+ });
102
+ prisma.workflow.update.mockResolvedValueOnce({});
103
+
104
+ await service.publish(ID, { changeSummary: 'init' }, 'tester');
105
+
106
+ expect(temporal.upsertCronSchedule).not.toHaveBeenCalled();
107
+ expect(temporal.deleteCronSchedule).toHaveBeenCalledWith(42);
108
+ });
109
+
110
+ it('rejects webhook_trigger without secret (strict HMAC mode)', async () => {
111
+ const dsl = {
112
+ nodes: [
113
+ {
114
+ id: 't1',
115
+ type: 'webhook_trigger',
116
+ config: { path: 'order-paid', method: 'POST' },
117
+ },
118
+ ],
119
+ edges: [],
120
+ };
121
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
122
+ prisma.workflow.findMany.mockResolvedValueOnce([]);
123
+
124
+ await expect(
125
+ service.publish(ID, { changeSummary: 'init' }, 'tester'),
126
+ ).rejects.toThrow(/requires a secret/);
127
+ });
128
+
129
+ it('rejects webhook path collision with another published workflow', async () => {
130
+ const dsl = {
131
+ nodes: [
132
+ {
133
+ id: 't1',
134
+ type: 'webhook_trigger',
135
+ config: { path: 'order-paid', method: 'POST', secret: 's' },
136
+ },
137
+ ],
138
+ edges: [],
139
+ };
140
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
141
+ prisma.workflow.findMany.mockResolvedValueOnce([
142
+ {
143
+ id: BigInt(999),
144
+ name: 'other',
145
+ dsl: {
146
+ nodes: [
147
+ {
148
+ id: 't1',
149
+ type: 'webhook_trigger',
150
+ config: { path: 'order-paid', method: 'POST', secret: 'x' },
151
+ },
152
+ ],
153
+ },
154
+ },
155
+ ]);
156
+
157
+ await expect(
158
+ service.publish(ID, { changeSummary: 'init' }, 'tester'),
159
+ ).rejects.toThrow(/already used by workflow #999/);
160
+ });
161
+
162
+ it('proceeds when webhook path differs from existing one', async () => {
163
+ const dsl = {
164
+ nodes: [
165
+ {
166
+ id: 't1',
167
+ type: 'webhook_trigger',
168
+ config: { path: 'invoice-paid', method: 'POST', secret: 's' },
169
+ },
170
+ ],
171
+ edges: [],
172
+ };
173
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
174
+ prisma.workflow.findMany.mockResolvedValueOnce([
175
+ {
176
+ id: BigInt(999),
177
+ name: 'other',
178
+ dsl: {
179
+ nodes: [
180
+ {
181
+ id: 't1',
182
+ type: 'webhook_trigger',
183
+ config: { path: 'order-paid', method: 'POST', secret: 'x' },
184
+ },
185
+ ],
186
+ },
187
+ },
188
+ ]);
189
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
190
+ prisma.workflowVersion.create.mockResolvedValueOnce({
191
+ id: BigInt(1),
192
+ version: 1,
193
+ });
194
+ prisma.workflow.update.mockResolvedValueOnce({});
195
+
196
+ await expect(
197
+ service.publish(ID, { changeSummary: 'init' }, 'tester'),
198
+ ).resolves.toBeDefined();
199
+ });
200
+ });
201
+
202
+ describe('toggleEnabled', () => {
203
+ const ID = BigInt(42);
204
+
205
+ it('pauses cron schedule when disabling', async () => {
206
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
207
+ prisma.workflow.update.mockResolvedValueOnce({});
208
+
209
+ await service.toggleEnabled(ID, false);
210
+
211
+ expect(temporal.pauseCronSchedule).toHaveBeenCalledWith(42);
212
+ expect(temporal.unpauseCronSchedule).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it('unpauses cron schedule when enabling', async () => {
216
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
217
+ prisma.workflow.update.mockResolvedValueOnce({});
218
+
219
+ await service.toggleEnabled(ID, true);
220
+
221
+ expect(temporal.unpauseCronSchedule).toHaveBeenCalledWith(42);
222
+ expect(temporal.pauseCronSchedule).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it('does not throw if schedule operation fails', async () => {
226
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
227
+ prisma.workflow.update.mockResolvedValueOnce({});
228
+ temporal.pauseCronSchedule.mockRejectedValueOnce(new Error('boom'));
229
+
230
+ await expect(service.toggleEnabled(ID, false)).resolves.toBeDefined();
231
+ });
232
+ });
233
+
234
+ describe('remove', () => {
235
+ const ID = BigInt(42);
236
+
237
+ it('deletes cron schedule before deleting workflow', async () => {
238
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
239
+ prisma.workflow.delete.mockResolvedValueOnce({});
240
+
241
+ await service.remove(ID);
242
+
243
+ expect(temporal.deleteCronSchedule).toHaveBeenCalledWith(42);
244
+ expect(prisma.workflow.delete).toHaveBeenCalled();
245
+ });
246
+
247
+ it('still deletes the workflow even if schedule deletion fails', async () => {
248
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
249
+ temporal.deleteCronSchedule.mockRejectedValueOnce(new Error('boom'));
250
+ prisma.workflow.delete.mockResolvedValueOnce({});
251
+
252
+ await service.remove(ID);
253
+
254
+ expect(prisma.workflow.delete).toHaveBeenCalled();
255
+ });
256
+ });
257
+ });
258
+
259
+ describe('WorkflowService — receiveWebhook', () => {
260
+ let service: WorkflowService;
261
+ let prisma: any;
262
+ let temporal: any;
263
+
264
+ beforeEach(async () => {
265
+ prisma = {
266
+ workflow: {
267
+ findUnique: jest.fn(),
268
+ findMany: jest.fn(),
269
+ },
270
+ workflowVersion: { findFirst: jest.fn() },
271
+ workflowInstance: {
272
+ create: jest.fn(),
273
+ update: jest.fn(),
274
+ },
275
+ };
276
+ temporal = {
277
+ startWorkflow: jest.fn(async () => ({ temporalRunId: 'run-1' })),
278
+ };
279
+
280
+ const module: TestingModule = await Test.createTestingModule({
281
+ providers: [
282
+ WorkflowService,
283
+ { provide: PrismaService, useValue: prisma },
284
+ { provide: TemporalService, useValue: temporal },
285
+ ],
286
+ }).compile();
287
+
288
+ service = module.get<WorkflowService>(WorkflowService);
289
+ });
290
+
291
+ const SECRET = 'topsecret';
292
+ const PATH = 'order-paid';
293
+ const METHOD = 'POST';
294
+ const BODY = '{"orderId":42}';
295
+ const RAW = Buffer.from(BODY);
296
+
297
+ const nowSec = () => Math.floor(Date.now() / 1000).toString();
298
+
299
+ const mkPublishedWorkflow = (id: number, secret = SECRET, path = PATH) => ({
300
+ id: BigInt(id),
301
+ status: 'PUBLISHED',
302
+ isEnabled: true,
303
+ dsl: null,
304
+ versions: [
305
+ {
306
+ id: BigInt(id * 10),
307
+ version: 1,
308
+ dsl: {
309
+ nodes: [
310
+ {
311
+ id: 't1',
312
+ type: 'webhook_trigger',
313
+ config: { path, method: METHOD, secret },
314
+ },
315
+ ],
316
+ edges: [],
317
+ },
318
+ },
319
+ ],
320
+ });
321
+
322
+ const mkExecuteSetup = () => {
323
+ // executeWorkflow re-fetches the workflow with its own includes
324
+ prisma.workflow.findUnique.mockImplementation(({ where }: any) => ({
325
+ id: where.id,
326
+ status: 'PUBLISHED',
327
+ isEnabled: true,
328
+ versions: [
329
+ {
330
+ id: where.id * BigInt(10),
331
+ version: 1,
332
+ dsl: { nodes: [], edges: [] },
333
+ },
334
+ ],
335
+ }));
336
+ prisma.workflowInstance.create.mockResolvedValueOnce({ id: BigInt(7777) });
337
+ prisma.workflowInstance.update.mockResolvedValueOnce({});
338
+ };
339
+
340
+ it('returns 401 (UnauthorizedException) when timestamp is missing', async () => {
341
+ const sig = signWebhookPayload(SECRET, nowSec(), RAW);
342
+ await expect(
343
+ service.receiveWebhook({
344
+ path: PATH,
345
+ method: METHOD,
346
+ signatureHeader: sig,
347
+ timestampHeader: undefined,
348
+ rawBody: RAW,
349
+ body: { orderId: 42 },
350
+ headers: {},
351
+ query: {},
352
+ }),
353
+ ).rejects.toBeInstanceOf(UnauthorizedException);
354
+ });
355
+
356
+ it('returns 401 when timestamp is older than 5 minutes', async () => {
357
+ const oldTs = String(Math.floor(Date.now() / 1000) - 6 * 60);
358
+ const sig = signWebhookPayload(SECRET, oldTs, RAW);
359
+ await expect(
360
+ service.receiveWebhook({
361
+ path: PATH,
362
+ method: METHOD,
363
+ signatureHeader: sig,
364
+ timestampHeader: oldTs,
365
+ rawBody: RAW,
366
+ body: {},
367
+ headers: {},
368
+ query: {},
369
+ }),
370
+ ).rejects.toBeInstanceOf(UnauthorizedException);
371
+ });
372
+
373
+ it('returns 404 (NotFoundException) when no workflow listens on this path', async () => {
374
+ prisma.workflow.findMany.mockResolvedValueOnce([]);
375
+ const ts = nowSec();
376
+ const sig = signWebhookPayload(SECRET, ts, RAW);
377
+ await expect(
378
+ service.receiveWebhook({
379
+ path: 'unknown',
380
+ method: METHOD,
381
+ signatureHeader: sig,
382
+ timestampHeader: ts,
383
+ rawBody: RAW,
384
+ body: {},
385
+ headers: {},
386
+ query: {},
387
+ }),
388
+ ).rejects.toBeInstanceOf(NotFoundException);
389
+ });
390
+
391
+ it('returns 401 on bad signature', async () => {
392
+ prisma.workflow.findMany.mockResolvedValueOnce([mkPublishedWorkflow(1)]);
393
+ const ts = nowSec();
394
+ await expect(
395
+ service.receiveWebhook({
396
+ path: PATH,
397
+ method: METHOD,
398
+ signatureHeader: 'sha256=deadbeef',
399
+ timestampHeader: ts,
400
+ rawBody: RAW,
401
+ body: {},
402
+ headers: {},
403
+ query: {},
404
+ }),
405
+ ).rejects.toBeInstanceOf(UnauthorizedException);
406
+ });
407
+
408
+ it('starts a workflow on valid signature and returns the instanceId', async () => {
409
+ prisma.workflow.findMany.mockResolvedValueOnce([mkPublishedWorkflow(1)]);
410
+ mkExecuteSetup();
411
+
412
+ const ts = nowSec();
413
+ const sig = signWebhookPayload(SECRET, ts, RAW);
414
+
415
+ const result = await service.receiveWebhook({
416
+ path: PATH,
417
+ method: METHOD,
418
+ signatureHeader: sig,
419
+ timestampHeader: ts,
420
+ rawBody: RAW,
421
+ body: { orderId: 42 },
422
+ headers: { 'x-workflow-signature': sig },
423
+ query: { foo: 'bar' },
424
+ });
425
+
426
+ expect(result.instanceIds).toEqual(['7777']);
427
+ expect(temporal.startWorkflow).toHaveBeenCalledTimes(1);
428
+ const startArg = temporal.startWorkflow.mock.calls[0][0];
429
+ expect(startArg.context).toMatchObject({
430
+ body: { orderId: 42 },
431
+ query: { foo: 'bar' },
432
+ });
433
+ });
434
+
435
+ it('rejects with 401 when one of multiple matches has a different secret', async () => {
436
+ // Strict mode: every match must verify against the same signature
437
+ prisma.workflow.findMany.mockResolvedValueOnce([
438
+ mkPublishedWorkflow(1, SECRET),
439
+ mkPublishedWorkflow(2, 'different-secret'),
440
+ ]);
441
+ const ts = nowSec();
442
+ const sig = signWebhookPayload(SECRET, ts, RAW);
443
+
444
+ await expect(
445
+ service.receiveWebhook({
446
+ path: PATH,
447
+ method: METHOD,
448
+ signatureHeader: sig,
449
+ timestampHeader: ts,
450
+ rawBody: RAW,
451
+ body: {},
452
+ headers: {},
453
+ query: {},
454
+ }),
455
+ ).rejects.toBeInstanceOf(UnauthorizedException);
456
+ });
457
+ });