cabloy 5.1.123 → 5.1.125

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.
@@ -1,278 +0,0 @@
1
- import assert from 'node:assert';
2
- import { createHmac, randomUUID } from 'node:crypto';
3
- import { describe, it } from 'node:test';
4
- import { app } from 'vona-mock';
5
-
6
- interface IFixture {
7
- userId?: number;
8
- paymentSessionId?: number;
9
- }
10
-
11
- async function createFixture(suffix: string): Promise<IFixture> {
12
- const scope = app.scope('a-pay');
13
- const user = await app.bean.user.register({ name: `pay-webhook-${suffix}` }, true);
14
- const session = await scope.service.paymentSession.create({
15
- userId: user.id,
16
- payScene: 'commerce-payment:commerceOrder',
17
- businessReference: `business-${suffix}`,
18
- amountMinor: 1299,
19
- currency: 'USD',
20
- correlationId: `payment-${suffix}`,
21
- });
22
- return { userId: user.id as number, paymentSessionId: session.id as number };
23
- }
24
-
25
- async function cleanup(fixture: IFixture) {
26
- const scope = app.scope('a-pay');
27
- if (fixture.paymentSessionId !== undefined) {
28
- await scope.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
29
- await scope.model.paymentAudit.delete({ paymentSessionId: fixture.paymentSessionId });
30
- await scope.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
31
- await scope.model.paymentSession.delete({ id: fixture.paymentSessionId });
32
- }
33
- if (fixture.userId !== undefined) {
34
- await app.scope('home-user').model.roleUser.delete({ userId: fixture.userId });
35
- await app.bean.user.removeById(fixture.userId);
36
- }
37
- }
38
-
39
- function createRawBody(
40
- fixture: IFixture,
41
- suffix: string,
42
- state: 'succeeded' | 'failed' | 'cancelled' = 'succeeded',
43
- options?: { eventId?: string; providerPaymentId?: string; providerCaptureId?: string },
44
- ) {
45
- return JSON.stringify({
46
- state,
47
- amountMinor: 1299,
48
- eventType: `payment.${state}`,
49
- paymentSessionId: String(fixture.paymentSessionId),
50
- eventId: options?.eventId ?? `mock-${suffix}`,
51
- providerPaymentId: options?.providerPaymentId ?? `payment-${suffix}`,
52
- providerCaptureId: options?.providerCaptureId ?? `capture-${suffix}`,
53
- currency: 'USD',
54
- });
55
- }
56
-
57
- function createCommand(rawBody: string) {
58
- const body = JSON.parse(rawBody);
59
- return {
60
- providerName: 'pay-mock:mock',
61
- clientName: 'default',
62
- environment: 'sandbox' as const,
63
- rawBody,
64
- verified: {
65
- eventId: body.eventId,
66
- eventType: body.eventType,
67
- paymentSessionId: body.paymentSessionId,
68
- payment: {
69
- state: body.state,
70
- providerPaymentId: body.providerPaymentId,
71
- providerCaptureId: body.providerCaptureId,
72
- },
73
- summary: {
74
- amountMinor: body.amountMinor,
75
- currency: body.currency,
76
- },
77
- },
78
- };
79
- }
80
-
81
- function sign(rawBody: string, clientName: 'default' | 'secondary' = 'default') {
82
- const options = app.bean.payProvider.getOptions('pay-mock:mock', clientName);
83
- assert.equal(typeof options.secretWebhook, 'string');
84
- return createHmac('sha256', options.secretWebhook).update(rawBody).digest('hex');
85
- }
86
-
87
- function webhookUrl(providerName = 'pay-mock:mock', clientName = 'default') {
88
- const url = app.util.getAbsoluteUrlByApiPath(`/pay/webhook/${providerName}/${clientName}`);
89
- return url.startsWith('http') ? url : `http://127.0.0.1:${app.config.server.listen.port}${url}`;
90
- }
91
-
92
- async function countFacts(paymentSessionId: number) {
93
- const scope = app.scope('a-pay');
94
- const [inboxes, audits, outbox] = await Promise.all([
95
- scope.model.webhookInbox.select({ where: { paymentSessionId } }),
96
- scope.model.paymentAudit.select({ where: { paymentSessionId } }),
97
- scope.model.outboxEvent.select({ where: { paymentSessionId } }),
98
- ]);
99
- return { inboxes, audits, outbox };
100
- }
101
-
102
- describe('webhook.test.ts', { concurrency: false, sequential: true }, () => {
103
- it('accepts a raw-body signed HTTP webhook and rejects an invalid signature without mutation', async () => {
104
- const fixture: IFixture = {};
105
- try {
106
- await app.bean.executor.mockCtx(async () => {
107
- Object.assign(fixture, await createFixture(randomUUID().slice(0, 12)));
108
- });
109
- const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
110
- const url = webhookUrl();
111
- const invalid = await fetch(url, {
112
- method: 'POST',
113
- headers: {
114
- 'Content-Type': 'application/json',
115
- 'x-pay-mock-signature': 'invalid',
116
- },
117
- body: rawBody,
118
- });
119
- assert.equal(invalid.status, 401, await invalid.text());
120
- await app.bean.executor.mockCtx(async () => {
121
- assert.deepEqual(await countFacts(fixture.paymentSessionId!), {
122
- inboxes: [],
123
- audits: [],
124
- outbox: [],
125
- });
126
- });
127
- const valid = await fetch(url, {
128
- method: 'POST',
129
- headers: {
130
- 'Content-Type': 'application/json',
131
- 'x-pay-mock-signature': sign(rawBody),
132
- },
133
- body: rawBody,
134
- });
135
- assert.equal(valid.status, 200);
136
- await app.bean.executor.mockCtx(async () => {
137
- const session = await app
138
- .scope('a-pay')
139
- .model.paymentSession.getById(fixture.paymentSessionId!);
140
- const facts = await countFacts(fixture.paymentSessionId!);
141
- assert.equal(session?.state, 'succeeded');
142
- assert.deepEqual(
143
- [facts.inboxes.length, facts.audits.length, facts.outbox.length],
144
- [1, 1, 1],
145
- );
146
- });
147
- } finally {
148
- await app.bean.executor.mockCtx(async () => await cleanup(fixture));
149
- }
150
- });
151
-
152
- it('uses the route client webhook secret and preserves session client binding', async () => {
153
- const fixture: IFixture = {};
154
- try {
155
- await app.bean.executor.mockCtx(async () => {
156
- Object.assign(fixture, await createFixture(randomUUID().slice(0, 12)));
157
- });
158
- const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
159
- const url = webhookUrl('pay-mock:mock', 'secondary');
160
- const wrongClient = await fetch(url, {
161
- method: 'POST',
162
- headers: {
163
- 'Content-Type': 'application/json',
164
- 'x-pay-mock-signature': sign(rawBody, 'default'),
165
- },
166
- body: rawBody,
167
- });
168
- assert.equal(wrongClient.status, 401, await wrongClient.text());
169
- const selectedClient = await fetch(url, {
170
- method: 'POST',
171
- headers: {
172
- 'Content-Type': 'application/json',
173
- 'x-pay-mock-signature': sign(rawBody, 'secondary'),
174
- },
175
- body: rawBody,
176
- });
177
- assert.equal(selectedClient.status, 409, await selectedClient.text());
178
- await app.bean.executor.mockCtx(async () => {
179
- assert.deepEqual(await countFacts(fixture.paymentSessionId!), {
180
- inboxes: [],
181
- audits: [],
182
- outbox: [],
183
- });
184
- });
185
- } finally {
186
- await app.bean.executor.mockCtx(async () => await cleanup(fixture));
187
- }
188
- });
189
-
190
- it('persists one verified payment fact and one logical outbox event across replay', async () => {
191
- await app.bean.executor.mockCtx(async () => {
192
- const fixture = await createFixture(randomUUID().slice(0, 12));
193
- try {
194
- const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
195
- const first = await app.scope('a-pay').service.webhook.receive(createCommand(rawBody));
196
- const replay = await app.scope('a-pay').service.webhook.receive(createCommand(rawBody));
197
- assert.equal(replay.id, first.id);
198
- assert.equal(replay.state, 'processed');
199
- const session = await app
200
- .scope('a-pay')
201
- .model.paymentSession.getById(fixture.paymentSessionId!);
202
- const facts = await countFacts(fixture.paymentSessionId!);
203
- assert.equal(session?.state, 'succeeded');
204
- assert.deepEqual(
205
- [facts.inboxes.length, facts.audits.length, facts.outbox.length],
206
- [1, 1, 1],
207
- );
208
- assert.equal(facts.outbox[0]?.eventType, 'payment.outcome.v1');
209
- } finally {
210
- await cleanup(fixture);
211
- }
212
- });
213
- });
214
-
215
- it('converges concurrent duplicate and out-of-order terminal webhooks on the first terminal state', async () => {
216
- const fixture: IFixture = {};
217
- try {
218
- await app.bean.executor.mockCtx(async () => {
219
- Object.assign(fixture, await createFixture(randomUUID().slice(0, 12)));
220
- });
221
- const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
222
- const receive = async () => {
223
- return await app.bean.executor.mockCtx(async () => {
224
- return await app.scope('a-pay').service.webhook.receive(createCommand(rawBody));
225
- });
226
- };
227
- const results = await Promise.allSettled([receive(), receive()]);
228
- assert.equal(
229
- results.filter(result => result.status === 'fulfilled').length,
230
- 2,
231
- JSON.stringify(results),
232
- );
233
- await app.bean.executor.mockCtx(async () => {
234
- const failedRawBody = createRawBody(fixture, randomUUID().slice(0, 12), 'failed');
235
- const ignored = await app
236
- .scope('a-pay')
237
- .service.webhook.receive(createCommand(failedRawBody));
238
- const session = await app
239
- .scope('a-pay')
240
- .model.paymentSession.getById(fixture.paymentSessionId!);
241
- const facts = await countFacts(fixture.paymentSessionId!);
242
- assert.equal(session?.state, 'succeeded');
243
- assert.equal(ignored.state, 'processed');
244
- assert.match(ignored.errorSummary ?? '', /ignored after succeeded/);
245
- assert.deepEqual(
246
- [facts.inboxes.length, facts.audits.length, facts.outbox.length],
247
- [2, 1, 1],
248
- );
249
- });
250
- } finally {
251
- await app.bean.executor.mockCtx(async () => await cleanup(fixture));
252
- }
253
- });
254
-
255
- it('rejects a same-state webhook with conflicting provider facts', async () => {
256
- await app.bean.executor.mockCtx(async () => {
257
- const fixture = await createFixture(randomUUID().slice(0, 12));
258
- try {
259
- const first = createRawBody(fixture, randomUUID().slice(0, 12));
260
- await app.scope('a-pay').service.webhook.receive(createCommand(first));
261
- const conflict = createRawBody(fixture, randomUUID().slice(0, 12), 'succeeded', {
262
- providerCaptureId: `other-capture-${randomUUID().slice(0, 12)}`,
263
- });
264
- await assert.rejects(
265
- app.scope('a-pay').service.webhook.receive(createCommand(conflict)),
266
- (error: any) => error.code === 409,
267
- );
268
- const facts = await countFacts(fixture.paymentSessionId!);
269
- assert.deepEqual(
270
- [facts.inboxes.length, facts.audits.length, facts.outbox.length],
271
- [1, 1, 1],
272
- );
273
- } finally {
274
- await cleanup(fixture);
275
- }
276
- });
277
- });
278
- });
@@ -1,141 +0,0 @@
1
- import assert from 'node:assert';
2
- import { randomUUID } from 'node:crypto';
3
- import { describe, it } from 'node:test';
4
- import { app } from 'vona-mock';
5
-
6
- interface IFixture {
7
- userId?: number;
8
- paymentSessionId?: number;
9
- }
10
-
11
- async function createFixture(suffix: string): Promise<IFixture> {
12
- const user = await app.bean.user.register({ name: `pay-mock-${suffix}` }, true);
13
- await app.bean.passport.signinMock(`pay-mock-${suffix}` as any);
14
- const session = await app.scope('a-pay').service.paymentSession.create({
15
- userId: user.id,
16
- payScene: 'commerce-payment:commerceOrder',
17
- businessReference: `business-${suffix}`,
18
- amountMinor: 1299,
19
- currency: 'USD',
20
- correlationId: `payment-${suffix}`,
21
- });
22
- await app.scope('a-pay').service.paymentSession.start(session.id);
23
- return { userId: user.id as number, paymentSessionId: session.id as number };
24
- }
25
-
26
- async function cleanup(fixture: IFixture) {
27
- const pay = app.scope('a-pay');
28
- if (fixture.paymentSessionId !== undefined) {
29
- await pay.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
30
- await pay.model.paymentAudit.delete({ paymentSessionId: fixture.paymentSessionId });
31
- await pay.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
32
- await pay.model.paymentSession.delete({ id: fixture.paymentSessionId });
33
- }
34
- if (fixture.userId !== undefined) {
35
- await app.scope('home-user').model.roleUser.delete({ userId: fixture.userId });
36
- await app.bean.user.removeById(fixture.userId);
37
- }
38
- await app.bean.passport.signout();
39
- }
40
-
41
- describe('payMock.test.ts', { concurrency: false }, () => {
42
- it('captures Vona env secrets in the mock Provider options', () => {
43
- const options = app.bean.payProvider.getOptions('pay-mock:mock', 'default');
44
- assert.equal(options.secretCredential, app.meta.env.PAY_MOCK_DEFAULT_CREDENTIAL);
45
- assert.equal(options.secretWebhook, app.meta.env.PAY_MOCK_DEFAULT_WEBHOOK);
46
- assert.equal(String(options.secretCredential).startsWith('env:' + '//'), false);
47
- assert.equal(String(options.secretWebhook).startsWith('env:' + '//'), false);
48
- assert.deepEqual(options.capabilities, {
49
- redirectCheckout: false,
50
- embeddedCheckout: true,
51
- automaticCapture: true,
52
- manualCapture: false,
53
- refunds: true,
54
- partialRefunds: true,
55
- webhooks: true,
56
- });
57
- assert.deepEqual(
58
- app.bean.payProvider.resolveByName('pay-mock:mock', 'secondary').clientOptions.capabilities,
59
- options.capabilities,
60
- );
61
- });
62
-
63
- it('submits server-derived mock payment facts through the signed webhook', async () => {
64
- await app.bean.executor.mockCtx(async () => {
65
- const fixture: IFixture = {};
66
- try {
67
- Object.assign(fixture, await createFixture('success'));
68
- const receipt = await app
69
- .scope('pay-mock')
70
- .service.payMock.completePaymentSession(fixture.paymentSessionId!, 'succeeded');
71
- assert.deepEqual(receipt, { paymentSessionId: fixture.paymentSessionId, accepted: true });
72
- const pay = app.scope('a-pay');
73
- const [session, inboxes, audits, outbox] = await Promise.all([
74
- pay.model.paymentSession.getById(fixture.paymentSessionId!),
75
- pay.model.webhookInbox.select({ where: { paymentSessionId: fixture.paymentSessionId } }),
76
- pay.model.paymentAudit.select({ where: { paymentSessionId: fixture.paymentSessionId } }),
77
- pay.model.outboxEvent.select({ where: { paymentSessionId: fixture.paymentSessionId } }),
78
- ]);
79
- assert.equal(session?.state, 'succeeded');
80
- assert.equal(inboxes.length, 1);
81
- assert.equal(audits.filter(item => item.source === 'webhook').length, 1);
82
- assert.equal(outbox.length, 1);
83
- } finally {
84
- await cleanup(fixture);
85
- }
86
- });
87
- });
88
-
89
- it('completes a payment session in the active named instance', async () => {
90
- await app.bean.executor.mockCtx(
91
- async () => {
92
- const fixture: IFixture = {};
93
- try {
94
- Object.assign(fixture, await createFixture(`named-${randomUUID().slice(0, 12)}`));
95
- const receipt = await app
96
- .scope('pay-mock')
97
- .service.payMock.completePaymentSession(fixture.paymentSessionId!, 'succeeded');
98
- assert.deepEqual(receipt, { paymentSessionId: fixture.paymentSessionId, accepted: true });
99
- const pay = app.scope('a-pay');
100
- const [session, inboxes, audits, outbox] = await Promise.all([
101
- pay.model.paymentSession.getById(fixture.paymentSessionId!),
102
- pay.model.webhookInbox.select({
103
- where: { paymentSessionId: fixture.paymentSessionId },
104
- }),
105
- pay.model.paymentAudit.select({
106
- where: { paymentSessionId: fixture.paymentSessionId },
107
- }),
108
- pay.model.outboxEvent.select({ where: { paymentSessionId: fixture.paymentSessionId } }),
109
- ]);
110
- assert.equal(session?.state, 'succeeded');
111
- assert.equal(inboxes.length, 1);
112
- assert.equal(audits.filter(item => item.source === 'webhook').length, 1);
113
- assert.equal(outbox.length, 1);
114
- } finally {
115
- await cleanup(fixture);
116
- }
117
- },
118
- { instanceName: 'shareTest' as any },
119
- );
120
- });
121
-
122
- it('does not simulate a session that is no longer actionable', async () => {
123
- await app.bean.executor.mockCtx(async () => {
124
- const fixture: IFixture = {};
125
- try {
126
- Object.assign(fixture, await createFixture('terminal'));
127
- await app
128
- .scope('pay-mock')
129
- .service.payMock.completePaymentSession(fixture.paymentSessionId!, 'cancelled');
130
- await assert.rejects(
131
- app
132
- .scope('pay-mock')
133
- .service.payMock.completePaymentSession(fixture.paymentSessionId!, 'cancelled'),
134
- { status: 409 },
135
- );
136
- } finally {
137
- await cleanup(fixture);
138
- }
139
- });
140
- });
141
- });