554 lines
19 KiB
JavaScript
554 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const crypto = require('crypto');
|
|
const {
|
|
GPTPAY_BASE_URL,
|
|
createGptPayClient,
|
|
createCardPayIdempotencyKeys,
|
|
assertAccountEmail,
|
|
} = require('../src/services/gptpay_service');
|
|
|
|
const KEY_ID = 'unit-test-key-id';
|
|
const API_SECRET = 'unit-test-signing-secret';
|
|
const ACCOUNT_EMAIL = 'unit.account@example.com';
|
|
const CARD_KEY = 'CARD-UNIT-FIXTURE';
|
|
const ACCESS_TOKEN = 'ACCESS-UNIT-FIXTURE';
|
|
const PREPARE_TOKEN = 'PREPARE-UNIT-FIXTURE';
|
|
const SESSION = Object.freeze({
|
|
user: Object.freeze({ id: 'user-unit-fixture', email: ACCOUNT_EMAIL, idp: 'auth0' }),
|
|
expires: '2099-01-01T00:00:00.000Z',
|
|
account: Object.freeze({ id: 'account-unit-fixture', planType: 'free' }),
|
|
accessToken: ACCESS_TOKEN,
|
|
authProvider: 'auth0',
|
|
sessionToken: 'SESSION-UNIT-FIXTURE',
|
|
});
|
|
const LOGIN_CONTENT = JSON.stringify(SESSION);
|
|
|
|
function response(status, payload, headers = {}) {
|
|
const text = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
const normalizedHeaders = new Map(Object.entries(headers).map(([key, value]) => (
|
|
[String(key).toLowerCase(), String(value)]
|
|
)));
|
|
return {
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
headers: { get: name => normalizedHeaders.get(String(name).toLowerCase()) || null },
|
|
text: async () => text,
|
|
};
|
|
}
|
|
|
|
function independentSignature({ method, path, timestamp, nonce, body, secret = API_SECRET }) {
|
|
const bodyHash = crypto.createHash('sha256').update(Buffer.from(body || '', 'utf8')).digest('hex');
|
|
const canonical = `${method}\n${path}\n${timestamp}\n${nonce}\n${bodyHash}`;
|
|
return crypto.createHmac('sha256', secret).update(canonical, 'utf8').digest('hex');
|
|
}
|
|
|
|
async function testPrepareSignsExactJsonBytes() {
|
|
const calls = [];
|
|
const fetchImpl = async (url, options) => {
|
|
calls.push({ url, options });
|
|
return response(200, {
|
|
success: true,
|
|
checkout_ready: true,
|
|
prepare_token: PREPARE_TOKEN,
|
|
task_id: 125,
|
|
expires_at: '2026-07-15T12:15:00+08:00',
|
|
account_email: ACCOUNT_EMAIL,
|
|
type: 1001,
|
|
fulfillment_mode: 'card_pay',
|
|
});
|
|
};
|
|
const client = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl,
|
|
now: () => 1784044800000,
|
|
nonceFactory: () => 'nonce-prepare-00000001',
|
|
maxAttempts: 1,
|
|
});
|
|
const keys = createCardPayIdempotencyKeys('batch-1:item-1');
|
|
const result = await client.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL.toUpperCase(),
|
|
idempotencyKey: keys.prepare,
|
|
});
|
|
|
|
assert.strictEqual(calls.length, 1);
|
|
const call = calls[0];
|
|
assert.strictEqual(call.options.redirect, 'error', '签名请求不得自动跟随重定向');
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(call.options, 'dispatcher'), false);
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(call.options, 'agent'), false);
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(call.options, 'proxy'), false);
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(JSON.parse(call.options.body), 'proxy'), false,
|
|
'充值请求不得读取或发送账号保存的代理');
|
|
assert.strictEqual(call.url, `${GPTPAY_BASE_URL}/redeem/card-pay/prepare`);
|
|
assert.strictEqual(call.options.method, 'POST');
|
|
const expectedBody = JSON.stringify({
|
|
card_key: CARD_KEY,
|
|
access_token: LOGIN_CONTENT,
|
|
idp: '',
|
|
force_recharge: false,
|
|
confirm_duplicate: false,
|
|
});
|
|
assert.strictEqual(call.options.body, expectedBody, '签名后的 JSON 字节不得再次格式化');
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(JSON.parse(call.options.body), 'session'), false);
|
|
assert.deepStrictEqual(JSON.parse(JSON.parse(call.options.body).access_token), SESSION);
|
|
assert.strictEqual(call.options.headers['X-API-Idempotency-Key'], keys.prepare);
|
|
assert.strictEqual(call.options.headers['X-API-Signature'], independentSignature({
|
|
method: 'POST',
|
|
path: '/api/open/v1/redeem/card-pay/prepare',
|
|
timestamp: call.options.headers['X-API-Timestamp'],
|
|
nonce: call.options.headers['X-API-Nonce'],
|
|
body: call.options.body,
|
|
}));
|
|
assert.strictEqual(result.taskId, '125');
|
|
assert.strictEqual(result.accountEmail, ACCOUNT_EMAIL);
|
|
assert.strictEqual(result.prepareToken, PREPARE_TOKEN);
|
|
}
|
|
|
|
function testStrictIdentityNormalization() {
|
|
assert.strictEqual(
|
|
assertAccountEmail({ account_email: 'Unit.Account@Example.com' }, ACCOUNT_EMAIL),
|
|
ACCOUNT_EMAIL,
|
|
);
|
|
assert.throws(
|
|
() => assertAccountEmail({ account_email: 'same@gmail.com' }, 'same@example.com'),
|
|
error => error && error.code === 'GPTPAY_ACCOUNT_EMAIL_MISMATCH',
|
|
);
|
|
assert.strictEqual(
|
|
assertAccountEmail({ account_email: 'firstlast@gmail.com' }, 'First.Last@gmail.com'),
|
|
'firstlast@gmail.com',
|
|
'主系统返回 Gmail 无点号地址时应识别为同一账号',
|
|
);
|
|
assert.strictEqual(
|
|
assertAccountEmail({ account_email: 'firstlast@googlemail.com' }, 'First.Last@gmail.com'),
|
|
'firstlast@gmail.com',
|
|
'googlemail.com 和 gmail.com 应按同一 Gmail 域名校验',
|
|
);
|
|
assert.throws(
|
|
() => assertAccountEmail({ account_email: 'firstlast@example.com' }, 'first.last@example.com'),
|
|
error => error && error.code === 'GPTPAY_ACCOUNT_EMAIL_MISMATCH',
|
|
'非 Gmail 邮箱不得忽略用户名中的点',
|
|
);
|
|
assert.throws(
|
|
() => assertAccountEmail({ account_email: 'first.last+tag@gmail.com' }, 'first.last@gmail.com'),
|
|
error => error && error.code === 'GPTPAY_ACCOUNT_EMAIL_MISMATCH',
|
|
'Gmail +tag 仍需严格匹配',
|
|
);
|
|
}
|
|
|
|
async function testRetryKeepsIdempotencyAndBodyButRefreshesSignature() {
|
|
const calls = [];
|
|
const replies = [
|
|
response(503, { code: 9001, error_code: 'card_lookup_failed', request_id: 'req-1' }),
|
|
response(200, {
|
|
success: true,
|
|
checkout_ready: true,
|
|
prepare_token: PREPARE_TOKEN,
|
|
task_id: 126,
|
|
account_email: ACCOUNT_EMAIL,
|
|
type: 1001,
|
|
fulfillment_mode: 'card_pay',
|
|
}),
|
|
];
|
|
let nonceIndex = 0;
|
|
const client = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async (url, options) => {
|
|
calls.push({ url, options });
|
|
return replies.shift();
|
|
},
|
|
now: () => 1784044800000,
|
|
nonceFactory: () => `nonce-retry-${String(++nonceIndex).padStart(8, '0')}`,
|
|
retryDelayMs: 0,
|
|
maxAttempts: 2,
|
|
});
|
|
const key = createCardPayIdempotencyKeys('batch-1:item-2').prepare;
|
|
await client.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
idempotencyKey: key,
|
|
});
|
|
|
|
assert.strictEqual(calls.length, 2);
|
|
assert.strictEqual(calls[0].options.body, calls[1].options.body);
|
|
assert.strictEqual(
|
|
calls[0].options.headers['X-API-Idempotency-Key'],
|
|
calls[1].options.headers['X-API-Idempotency-Key'],
|
|
);
|
|
assert.notStrictEqual(calls[0].options.headers['X-API-Nonce'], calls[1].options.headers['X-API-Nonce']);
|
|
assert.notStrictEqual(calls[0].options.headers['X-API-Timestamp'], calls[1].options.headers['X-API-Timestamp']);
|
|
assert.notStrictEqual(calls[0].options.headers['X-API-Signature'], calls[1].options.headers['X-API-Signature']);
|
|
}
|
|
|
|
async function testPrepareAndConfirmUseDifferentStableKeys() {
|
|
const keys = createCardPayIdempotencyKeys('durable-batch:item-9');
|
|
assert(keys.prepare.startsWith('gptpay-cardprep-v2-'));
|
|
assert(keys.confirm.startsWith('gptpay-cardconfirm-v2-'));
|
|
assert.notStrictEqual(keys.prepare, keys.confirm);
|
|
assert.notStrictEqual(keys.prepare, keys.prepareDuplicate);
|
|
assert.notStrictEqual(keys.prepareDuplicate, keys.confirm);
|
|
assert.deepStrictEqual(
|
|
createCardPayIdempotencyKeys('durable-batch:item-9'),
|
|
keys,
|
|
'同一个持久化业务项必须生成相同的幂等键',
|
|
);
|
|
|
|
const calls = [];
|
|
const client = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async (url, options) => {
|
|
calls.push({ url, options });
|
|
if (url.endsWith('/tasks/127')) {
|
|
return response(200, {
|
|
code: 0,
|
|
success: true,
|
|
task_id: 127,
|
|
card_code: CARD_KEY,
|
|
account: ACCOUNT_EMAIL,
|
|
status: 'success',
|
|
fail_reason: '',
|
|
});
|
|
}
|
|
return response(200, {
|
|
success: true,
|
|
status: 'processing',
|
|
task_id: 127,
|
|
fulfillment_mode: 'card_pay',
|
|
type: 1001,
|
|
});
|
|
},
|
|
nonceFactory: () => `nonce-confirm-${String(calls.length + 1).padStart(7, '0')}`,
|
|
maxAttempts: 1,
|
|
});
|
|
const result = await client.confirmCardPay({
|
|
prepareToken: PREPARE_TOKEN,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
operationId: 'durable-batch:item-9',
|
|
});
|
|
assert.strictEqual(result.status, 'processing');
|
|
assert.strictEqual(calls[0].options.headers['X-API-Idempotency-Key'], keys.confirm);
|
|
assert.deepStrictEqual(JSON.parse(calls[0].options.body), {
|
|
prepare_token: PREPARE_TOKEN,
|
|
access_token: LOGIN_CONTENT,
|
|
idp: '',
|
|
force_recharge: false,
|
|
confirm_duplicate: false,
|
|
});
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(JSON.parse(calls[0].options.body), 'session'), false);
|
|
|
|
const completed = await client.getTask({ taskId: result.taskId, expectedEmail: ACCOUNT_EMAIL });
|
|
assert.strictEqual(completed.status, 'success');
|
|
assert.strictEqual(completed.accountEmail, ACCOUNT_EMAIL);
|
|
assert.strictEqual(JSON.stringify(completed).includes(CARD_KEY), false);
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(calls[1].options.headers, 'X-API-Idempotency-Key'), false);
|
|
|
|
await assert.rejects(
|
|
client.confirmCardPay({
|
|
prepareToken: PREPARE_TOKEN,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
expectedTaskId: 126,
|
|
operationId: 'durable-batch:item-9',
|
|
}),
|
|
error => error && error.code === 'GPTPAY_TASK_ID_MISMATCH',
|
|
);
|
|
|
|
await assert.rejects(
|
|
client.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
idempotencyKey: keys.confirm,
|
|
}),
|
|
error => error && error.code === 'GPTPAY_IDEMPOTENCY_PURPOSE_CONFLICT',
|
|
);
|
|
|
|
await assert.rejects(
|
|
client.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
accessToken: ACCESS_TOKEN,
|
|
operationId: 'legacy-access-token:item-1',
|
|
}),
|
|
error => error && error.code === 'GPTPAY_INVALID_ARGUMENT',
|
|
'充值接口不得再接受旧的单 accessToken 入参',
|
|
);
|
|
}
|
|
|
|
async function testDuplicateConfirmationIsTypedAndChecked() {
|
|
const client = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(200, {
|
|
success: false,
|
|
requires_duplicate_confirm: true,
|
|
duplicate_account: ACCOUNT_EMAIL,
|
|
duplicate_redeemed_at: '2026-07-23T10:00:00+08:00',
|
|
}),
|
|
nonceFactory: () => 'nonce-duplicate-000001',
|
|
maxAttempts: 1,
|
|
});
|
|
const result = await client.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
operationId: 'batch-duplicate:item-1',
|
|
});
|
|
assert.strictEqual(result.success, false);
|
|
assert.strictEqual(result.requiresDuplicateConfirm, true);
|
|
assert.strictEqual(result.duplicateAccount, ACCOUNT_EMAIL);
|
|
}
|
|
|
|
async function testGetTaskUsesEmptyBodySignatureAndOmitsCardCode() {
|
|
const calls = [];
|
|
const client = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async (url, options) => {
|
|
calls.push({ url, options });
|
|
return response(200, {
|
|
code: 0,
|
|
success: true,
|
|
task_id: 128,
|
|
card_code: CARD_KEY,
|
|
account: ACCOUNT_EMAIL,
|
|
status: 'success',
|
|
fail_reason: '',
|
|
created_at: '2026-07-15T12:00:00+08:00',
|
|
updated_at: '2026-07-15T12:00:08+08:00',
|
|
});
|
|
},
|
|
now: () => 1784044802000,
|
|
nonceFactory: () => 'nonce-task-query-00001',
|
|
maxAttempts: 1,
|
|
});
|
|
const result = await client.getTask({ taskId: 128, expectedEmail: ACCOUNT_EMAIL });
|
|
const call = calls[0];
|
|
assert.strictEqual(call.url, `${GPTPAY_BASE_URL}/tasks/128`);
|
|
assert.strictEqual(call.options.method, 'GET');
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(call.options, 'body'), false);
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(call.options.headers, 'Content-Type'), false);
|
|
assert.strictEqual(Object.prototype.hasOwnProperty.call(call.options.headers, 'X-API-Idempotency-Key'), false);
|
|
assert.strictEqual(call.options.headers['X-API-Signature'], independentSignature({
|
|
method: 'GET',
|
|
path: '/api/open/v1/tasks/128',
|
|
timestamp: call.options.headers['X-API-Timestamp'],
|
|
nonce: call.options.headers['X-API-Nonce'],
|
|
body: '',
|
|
}));
|
|
assert.strictEqual(result.status, 'success');
|
|
assert.strictEqual(result.card_code, undefined);
|
|
assert.strictEqual(result.cardCode, undefined);
|
|
assert.strictEqual(JSON.stringify(result).includes(CARD_KEY), false);
|
|
}
|
|
|
|
async function testTimeoutIsStrictEvenWhenFetchIgnoresAbort() {
|
|
const client = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => new Promise(() => {}),
|
|
timeoutMs: 10,
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-timeout-0000001',
|
|
});
|
|
const startedAt = Date.now();
|
|
await assert.rejects(
|
|
client.getTask({ taskId: 129 }),
|
|
error => error && error.code === 'GPTPAY_TIMEOUT' && error.retryable === true,
|
|
);
|
|
assert(Date.now() - startedAt < 1000, '超时必须由客户端主动结束,不能依赖 fetch 自己返回');
|
|
}
|
|
|
|
async function testStrictParsingAndSafeErrors() {
|
|
const invalidJsonClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(200, '<html>not json</html>'),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-invalid-json-001',
|
|
});
|
|
await assert.rejects(
|
|
invalidJsonClient.getTask({ taskId: 130 }),
|
|
error => error && error.code === 'GPTPAY_RESPONSE_INVALID_JSON',
|
|
);
|
|
|
|
const mismatchClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(200, {
|
|
success: true,
|
|
checkout_ready: true,
|
|
prepare_token: PREPARE_TOKEN,
|
|
task_id: 131,
|
|
account_email: 'different.account@example.com',
|
|
fulfillment_mode: 'card_pay',
|
|
}),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-mismatch-0000001',
|
|
});
|
|
await assert.rejects(
|
|
mismatchClient.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
operationId: 'batch-mismatch:item-1',
|
|
}),
|
|
error => {
|
|
const serialized = `${error.message} ${JSON.stringify(error)}`;
|
|
return error.code === 'GPTPAY_ACCOUNT_EMAIL_MISMATCH'
|
|
&& error.taskId === '131'
|
|
&& !serialized.includes(ACCOUNT_EMAIL)
|
|
&& !serialized.includes('different.account@example.com')
|
|
&& !serialized.includes(CARD_KEY)
|
|
&& !serialized.includes(ACCESS_TOKEN)
|
|
&& !serialized.includes(PREPARE_TOKEN)
|
|
&& !serialized.includes(API_SECRET);
|
|
},
|
|
);
|
|
|
|
const rejectedClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(401, {
|
|
code: 1001,
|
|
error_code: 'signature_invalid',
|
|
request_id: 'safe-request-id',
|
|
message: `${ACCESS_TOKEN} ${CARD_KEY} ${PREPARE_TOKEN} ${API_SECRET}`,
|
|
}),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-rejected-0000001',
|
|
});
|
|
await assert.rejects(
|
|
rejectedClient.getTask({ taskId: 132 }),
|
|
error => {
|
|
const serialized = `${error.message} ${JSON.stringify(error)}`;
|
|
return error.code === 'GPTPAY_API_REJECTED'
|
|
&& error.errorCode === 'signature_invalid'
|
|
&& !serialized.includes(ACCESS_TOKEN)
|
|
&& !serialized.includes(CARD_KEY)
|
|
&& !serialized.includes(PREPARE_TOKEN)
|
|
&& !serialized.includes(API_SECRET);
|
|
},
|
|
);
|
|
|
|
const transientRejectClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(503, {
|
|
code: 1503,
|
|
error_code: 'temporarily_unavailable',
|
|
request_id: 'safe-transient-request',
|
|
}),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-transient-000001',
|
|
});
|
|
await assert.rejects(
|
|
transientRejectClient.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
operationId: 'batch-transient:item-1',
|
|
}),
|
|
error => error && error.code === 'GPTPAY_API_REJECTED'
|
|
&& error.httpStatus === 503
|
|
&& error.retryable === true,
|
|
);
|
|
|
|
const legacyBusinessRejectClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(200, {
|
|
success: false,
|
|
error: 'fulfillment_mode_mismatch',
|
|
request_id: 'safe-business-request',
|
|
message: `${ACCESS_TOKEN} ${CARD_KEY} ${PREPARE_TOKEN} ${API_SECRET}`,
|
|
}),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-business-reject-001',
|
|
});
|
|
await assert.rejects(
|
|
legacyBusinessRejectClient.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
operationId: 'batch-business-reject:item-1',
|
|
}),
|
|
error => {
|
|
const serialized = `${error.message} ${JSON.stringify(error)}`;
|
|
return error.code === 'GPTPAY_BUSINESS_REJECTED'
|
|
&& error.errorCode === 'fulfillment_mode_mismatch'
|
|
&& error.requestId === 'safe-business-request'
|
|
&& !serialized.includes(ACCESS_TOKEN)
|
|
&& !serialized.includes(CARD_KEY)
|
|
&& !serialized.includes(PREPARE_TOKEN)
|
|
&& !serialized.includes(API_SECRET);
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(assertAccountEmail({ account_email: ACCOUNT_EMAIL }, ACCOUNT_EMAIL.toUpperCase()), ACCOUNT_EMAIL);
|
|
|
|
const contradictoryTaskClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(200, {
|
|
code: 0,
|
|
success: true,
|
|
task_id: 133,
|
|
account: ACCOUNT_EMAIL,
|
|
status: 'success',
|
|
fail_reason: 'upstream conflict',
|
|
}),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-conflict-0000001',
|
|
});
|
|
await assert.rejects(
|
|
contradictoryTaskClient.getTask({ taskId: 133, expectedEmail: ACCOUNT_EMAIL }),
|
|
error => error && error.code === 'GPTPAY_RESPONSE_INVALID',
|
|
);
|
|
|
|
const contradictoryPrepareClient = createGptPayClient({
|
|
keyId: KEY_ID,
|
|
apiSecret: API_SECRET,
|
|
fetchImpl: async () => response(200, {
|
|
success: true,
|
|
checkout_ready: true,
|
|
prepare_token: PREPARE_TOKEN,
|
|
task_id: 134,
|
|
account_email: ACCOUNT_EMAIL,
|
|
fulfillment_mode: 'card_pay',
|
|
fail_reason: 'upstream conflict',
|
|
}),
|
|
maxAttempts: 1,
|
|
nonceFactory: () => 'nonce-prepconflict-001',
|
|
});
|
|
await assert.rejects(
|
|
contradictoryPrepareClient.prepareCardPay({
|
|
cardKey: CARD_KEY,
|
|
loginContent: LOGIN_CONTENT,
|
|
expectedEmail: ACCOUNT_EMAIL,
|
|
operationId: 'batch-prepconflict:item-1',
|
|
}),
|
|
error => error && error.code === 'GPTPAY_RESPONSE_INVALID' && error.taskId === '134',
|
|
);
|
|
}
|
|
|
|
(async () => {
|
|
testStrictIdentityNormalization();
|
|
await testPrepareSignsExactJsonBytes();
|
|
await testRetryKeepsIdempotencyAndBodyButRefreshesSignature();
|
|
await testPrepareAndConfirmUseDifferentStableKeys();
|
|
await testDuplicateConfirmationIsTypedAndChecked();
|
|
await testGetTaskUsesEmptyBodySignatureAndOmitsCardCode();
|
|
await testTimeoutIsStrictEvenWhenFetchIgnoresAbort();
|
|
await testStrictParsingAndSafeErrors();
|
|
console.log('gptpay_service.test.js: PASS');
|
|
})().catch(error => {
|
|
console.error(error && error.message ? error.message : 'gptpay_service.test.js: FAIL');
|
|
process.exitCode = 1;
|
|
});
|