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.');