Plus中添加GPC模式

This commit is contained in:
QLHazyCoder
2026-05-04 18:02:00 +08:00
parent 6160f4640d
commit b595f5366a
17 changed files with 2151 additions and 64 deletions
@@ -81,6 +81,7 @@ function createExecutorHarness({
readyByFrame = {},
fetchImpl = null,
getAddressSeedForCountry = () => createAddressSeed(),
getState = null,
markCurrentRegistrationAccountUsed = async () => {},
probeIpProxyExit = null,
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
@@ -153,6 +154,7 @@ function createExecutorHarness({
fetch: fetchImpl,
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
getAddressSeedForCountry,
getState: typeof getState === 'function' ? getState : async () => ({}),
getTabId: async () => null,
isTabAlive: async () => false,
markCurrentRegistrationAccountUsed,
@@ -795,3 +797,149 @@ test('Plus checkout billing reports when the payment iframe exists but cannot re
/已定位到 PayPal 所在 iframeframeId=7),但账单脚本无法注入该 iframe/
);
});
test('GPC billing normalizes API URL and submits OTP then PIN with card_key and flow_id', async () => {
const fetchCalls = [];
let currentState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: '',
};
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
getState: async () => currentState,
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_123', challenge_id: 'challenge_456' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
const run = executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_123',
gopayHelperGoPayGuid: 'guid_789',
gopayHelperApiUrl: 'https://gopay.hwork.pro/api/checkout/start',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_billing_123',
gopayHelperFlowId: 'flow_billing_123',
});
await new Promise((resolve) => setTimeout(resolve, 20));
const pending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
assert.ok(pending);
currentState = {
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: pending.plusManualConfirmationRequestId,
gopayHelperResolvedOtp: '123456',
};
await run;
assert.equal(fetchCalls[0].url, 'https://gopay.hwork.pro/api/gopay/otp');
assert.deepEqual(JSON.parse(fetchCalls[0].options.body), {
reference_id: 'ref_123',
otp: '123456',
card_key: 'card_billing_123',
flow_id: 'flow_billing_123',
gopay_guid: 'guid_789',
});
assert.equal(fetchCalls[1].url, 'https://gopay.hwork.pro/api/gopay/pin');
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_123',
challenge_id: 'challenge_456',
gopay_guid: 'guid_789',
pin: '654321',
card_key: 'card_billing_123',
flow_id: 'flow_billing_123',
});
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
});
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
const fetchCalls = [];
let currentState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: '',
};
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
getState: async () => currentState,
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.endsWith('/api/gopay/otp') && fetchCalls.filter((call) => call.url.endsWith('/api/gopay/otp')).length === 1) {
return {
ok: false,
status: 400,
json: async () => ({ error: 'otp field invalid' }),
};
}
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ challenge_id: 'challenge_retry' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
const run = executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_retry',
gopayHelperGoPayGuid: 'guid_retry',
gopayHelperRedirectUrl: 'https://pm-redirects.stripe.com/retry',
gopayHelperApiUrl: 'http://localhost:18473/',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_retry',
gopayHelperFlowId: 'flow_retry',
});
await new Promise((resolve) => setTimeout(resolve, 20));
const pending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
currentState = {
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: pending.plusManualConfirmationRequestId,
gopayHelperResolvedOtp: '123456',
};
await run;
assert.equal(fetchCalls.filter((call) => call.url.endsWith('/api/gopay/otp')).length, 2);
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_retry',
otp: '123456',
card_key: 'card_retry',
flow_id: 'flow_retry',
gopay_guid: 'guid_retry',
redirect_url: 'https://pm-redirects.stripe.com/retry',
code: '123456',
});
assert.equal(events.logs.some((entry) => /兼容字段重试/.test(entry.message)), true);
assert.equal(events.completed[0].step, 7);
});