157 lines
4.6 KiB
JavaScript
157 lines
4.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHmac } from 'node:crypto';
|
|
import { buildApp } from '../dist/app.js';
|
|
import { signAccessToken } from '../dist/auth/jwt.js';
|
|
import {
|
|
ThirdPartyClient, ThirdPartyError
|
|
} from '../dist/third-party/third-party-client.js';
|
|
|
|
const client = new ThirdPartyClient({
|
|
async request() {
|
|
return {
|
|
status: 200,
|
|
body: JSON.stringify({
|
|
accepted: true,
|
|
amountCents: 3600,
|
|
productId: 'product-sanitized',
|
|
voucherCode: 'must-be-redacted'
|
|
})
|
|
};
|
|
}
|
|
});
|
|
const body = JSON.stringify({ eventId: 'event-001', amountCents: 3600 });
|
|
const signature = createHmac('sha256', 'test-webhook-secret')
|
|
.update(body).digest('hex');
|
|
client.verifyWebhook('test-webhook-secret', body, signature);
|
|
assert.throws(
|
|
() => client.verifyWebhook('test-webhook-secret', `${body} `, signature),
|
|
(error) => error instanceof ThirdPartyError
|
|
&& error.code === 'THIRD_PARTY_SIGNATURE_INVALID'
|
|
);
|
|
assert.equal((await client.redeemVoucher({
|
|
mode: 'MOCK',
|
|
provider: 'MEITUAN',
|
|
voucherCode: 'MOCK-ACCEPT',
|
|
orderNo: 'QP001',
|
|
expectedAmountCents: 3600,
|
|
settings: {}
|
|
})).status, 'SUCCEEDED');
|
|
assert.equal((await client.redeemVoucher({
|
|
mode: 'MOCK',
|
|
provider: 'DOUYIN',
|
|
voucherCode: 'FAIL-REJECT',
|
|
orderNo: 'QP002',
|
|
expectedAmountCents: 3600,
|
|
settings: {}
|
|
})).status, 'FAILED');
|
|
const apiResult = await client.redeemVoucher({
|
|
mode: 'API',
|
|
provider: 'MEITUAN',
|
|
voucherCode: 'API-ACCEPT',
|
|
orderNo: 'QP003',
|
|
expectedAmountCents: 3600,
|
|
settings: { redeemEndpoint: 'https://partner.example.test/redeem' },
|
|
credential: { apiToken: 'test-token' }
|
|
});
|
|
assert.equal(apiResult.amountCents, 3600);
|
|
assert.equal('voucherCode' in apiResult.response, false);
|
|
|
|
const secret = 'third-party-route-secret-with-32-characters';
|
|
const token = signAccessToken({
|
|
sub: '21', sid: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
|
tid: '7', aid: '9', rv: 1
|
|
}, secret, 900);
|
|
let redeemInput;
|
|
let notifyInput;
|
|
const routeApp = await buildApp({
|
|
thirdParty: {
|
|
jwtSecret: secret,
|
|
authRepository: {
|
|
async validateSession() {
|
|
return {
|
|
id: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
|
tenantId: '7',
|
|
platformAppId: '9',
|
|
expiresAt: new Date(Date.now() + 60000),
|
|
user: {
|
|
id: '21', tenantId: '7', userType: 'CUSTOMER', status: 'ACTIVE',
|
|
roleVersion: 1, nickname: '', avatarUrl: '', phone: ''
|
|
}
|
|
};
|
|
}
|
|
},
|
|
accessControl: {
|
|
async getAccessProfile() {
|
|
return {
|
|
roles: ['TENANT_ADMIN'],
|
|
capabilities: ['tenant.manage'],
|
|
storeIds: []
|
|
};
|
|
}
|
|
},
|
|
service: {
|
|
async redeemVoucher(input) {
|
|
redeemInput = input;
|
|
return { redemptionId: '51', status: 'SUCCEEDED' };
|
|
},
|
|
async redeemVoucherManually() {
|
|
return { redemptionId: '52', status: 'SUCCEEDED' };
|
|
},
|
|
async receiveDirectBooking(input) {
|
|
notifyInput = input;
|
|
return { bookingId: '61', status: 'PENDING_MAPPING' };
|
|
},
|
|
async claimDirectBooking() {
|
|
return { bookingId: '61', orderId: '71' };
|
|
},
|
|
async listRecords() {
|
|
return { bookings: [], redemptions: [] };
|
|
},
|
|
async saveConfig() {
|
|
return { configId: '81', created: true };
|
|
},
|
|
async saveMapping() {
|
|
return { mapped: true };
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const redeemed = await routeApp.inject({
|
|
method: 'POST',
|
|
url: '/app-api/group-vouchers/redeem',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: {
|
|
provider: 'MEITUAN',
|
|
voucherCode: '1234567890',
|
|
orderId: '31',
|
|
clientRequestId: 'redeem-request-001'
|
|
}
|
|
});
|
|
assert.equal(redeemed.statusCode, 200);
|
|
assert.equal(redeemInput.userId, '21');
|
|
|
|
const bookingPayload = {
|
|
eventId: 'event-001',
|
|
externalBookingNo: 'booking-001',
|
|
externalStoreRef: 'store-external',
|
|
externalRoomRef: 'room-external',
|
|
customerRef: 'customer-private',
|
|
startsAt: new Date(Date.now() + 86400000).toISOString(),
|
|
endsAt: new Date(Date.now() + 90000000).toISOString(),
|
|
amountCents: 3600
|
|
};
|
|
const notified = await routeApp.inject({
|
|
method: 'POST',
|
|
url: '/app-api/third-party/MEITUAN/tenants/7/bookings/notify',
|
|
headers: { 'x-third-party-signature': 'signature-sanitized' },
|
|
payload: bookingPayload
|
|
});
|
|
assert.equal(notified.statusCode, 200);
|
|
assert.equal(notifyInput.tenantId, '7');
|
|
assert.equal(notifyInput.provider, 'MEITUAN');
|
|
assert.equal(notifyInput.rawBody, JSON.stringify(bookingPayload));
|
|
|
|
await routeApp.close();
|
|
console.log('PASS: M05-C adapters, webhook authentication and third-party routes.');
|