feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
179
server/supabase/functions/_shared/payple.test.ts
Normal file
179
server/supabase/functions/_shared/payple.test.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import {
|
||||
buildPaypleConfig,
|
||||
generateOrderId,
|
||||
parsePaypleTimestamp,
|
||||
paypleAuth,
|
||||
paypleBilling,
|
||||
PaypleBillingError,
|
||||
payplePaymentEventDigest,
|
||||
payplePaymentEventId,
|
||||
payplePayerNumber,
|
||||
PaypleConfigurationError,
|
||||
PaypleVerificationError,
|
||||
resolvePaypleOrderDate,
|
||||
} from './payple.ts'
|
||||
|
||||
function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
const validConfig = {
|
||||
PAYPLE_ENVIRONMENT: 'live',
|
||||
PAYPLE_CST_ID: 'merchant-id',
|
||||
PAYPLE_CUST_KEY: 'merchant-secret',
|
||||
PAYPLE_REFUND_KEY: 'refund-secret',
|
||||
PAYPLE_CLIENT_KEY: 'client-key',
|
||||
PAYPLE_SITE_URL: 'https://d3ro.chanpaca.net/payments',
|
||||
}
|
||||
|
||||
Deno.test('Payple configuration has no implicit demo credential fallback', () => {
|
||||
for (const missing of Object.keys(validConfig)) {
|
||||
const candidate = { ...validConfig, [missing]: undefined }
|
||||
let error: unknown
|
||||
try {
|
||||
buildPaypleConfig(candidate)
|
||||
} catch (caught) {
|
||||
error = caught
|
||||
}
|
||||
assert(error instanceof PaypleConfigurationError, `${missing} must be required`)
|
||||
}
|
||||
})
|
||||
|
||||
Deno.test('Payple environment selects only the official fixed API host', () => {
|
||||
const live = buildPaypleConfig(validConfig)
|
||||
assert(live.baseUrl === 'https://cpay.payple.kr', 'live host')
|
||||
assert(live.siteUrl === 'https://d3ro.chanpaca.net', 'referer uses registered origin')
|
||||
const test = buildPaypleConfig({ ...validConfig, PAYPLE_ENVIRONMENT: 'test' })
|
||||
assert(test.baseUrl === 'https://democpay.payple.kr', 'test host')
|
||||
})
|
||||
|
||||
Deno.test('Payple timestamps are interpreted as Korea standard time', () => {
|
||||
const timestamp = parsePaypleTimestamp('20260821153045')
|
||||
assert(timestamp.toISOString() === '2026-08-21T06:30:45.000Z', 'KST conversion')
|
||||
assert(
|
||||
parsePaypleTimestamp('2026-08-21 15:30:45').toISOString() === timestamp.toISOString(),
|
||||
'documented formatted timestamp',
|
||||
)
|
||||
let error: unknown
|
||||
try {
|
||||
parsePaypleTimestamp('2026-99-99')
|
||||
} catch (caught) {
|
||||
error = caught
|
||||
}
|
||||
assert(error instanceof PaypleVerificationError, 'invalid timestamp must fail')
|
||||
})
|
||||
|
||||
Deno.test('Payple order identifiers are unique and carry a reconcilable payment date', () => {
|
||||
const first = generateOrderId('11111111-2222-4333-8444-555555555555', 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee')
|
||||
const second = generateOrderId('11111111-2222-4333-8444-555555555555', 'ffffffff-bbbb-4ccc-8ddd-eeeeeeeeeeee')
|
||||
assert(first !== second, 'nonce must prevent same-second collision')
|
||||
assert(/^[A-Za-z0-9._-]{8,64}$/.test(first), 'Payple order syntax')
|
||||
assert(resolvePaypleOrderDate(first) === first.slice(5, 13), 'order date is recoverable')
|
||||
assert(resolvePaypleOrderDate('legacy-order', '20260821153045') === '20260821', 'legacy fallback date')
|
||||
assert(
|
||||
resolvePaypleOrderDate('D3RO-20260820153045-test-nonce', '20260821003045') === '20260821',
|
||||
'authoritative Payple time wins across the Korea/UTC date boundary',
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('Payple payer number is deterministic, numeric and user-specific', async () => {
|
||||
const first = await payplePayerNumber('11111111-2222-4333-8444-555555555555')
|
||||
const same = await payplePayerNumber('11111111-2222-4333-8444-555555555555')
|
||||
const other = await payplePayerNumber('aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee')
|
||||
assert(/^\d{18}$/.test(first), 'payer number must satisfy Payple 18-digit contract')
|
||||
assert(first === same, 'same user must map consistently')
|
||||
assert(first !== other, 'different users must not share the fixture mapping')
|
||||
})
|
||||
|
||||
Deno.test('Payple synchronous charge and webhook share one provider event identity', async () => {
|
||||
const identity = {
|
||||
orderId: 'D3RO-20260821153045-test-nonce',
|
||||
payerId: 'payer-fixture',
|
||||
payType: 'card' as const,
|
||||
amount: 9900,
|
||||
}
|
||||
const directDigest = await payplePaymentEventDigest(identity)
|
||||
const webhookDigest = await payplePaymentEventDigest({ ...identity })
|
||||
const differentCharge = await payplePaymentEventDigest({ ...identity, amount: 29900 })
|
||||
assert(payplePaymentEventId(identity.orderId) === `payment:${identity.orderId}`, 'canonical event id')
|
||||
assert(directDigest === webhookDigest, 'same external charge must be an exact replay')
|
||||
assert(directDigest !== differentCharge, 'different charge identity must not collide')
|
||||
})
|
||||
|
||||
Deno.test('Payple auth response cannot redirect server calls to another origin', async () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
globalThis.fetch = () => Promise.resolve(new Response(JSON.stringify({
|
||||
result: 'success',
|
||||
cst_id: 'encrypted-id',
|
||||
custKey: 'encrypted-key',
|
||||
AuthKey: 'auth-key',
|
||||
PCD_PAY_HOST: 'https://cpay.payple.kr.evil.example',
|
||||
PCD_PAY_URL: '/php/PayChkAct.php',
|
||||
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
||||
let error: unknown
|
||||
try {
|
||||
await paypleAuth(buildPaypleConfig(validConfig), { payCheckFlag: true })
|
||||
} catch (caught) {
|
||||
error = caught
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
assert(
|
||||
error instanceof PaypleVerificationError
|
||||
&& error.code === 'payple_untrusted_api_host',
|
||||
'untrusted Payple response host must fail closed',
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('Payple billing separates definitive declines from ambiguous transport outcomes', async () => {
|
||||
const config = buildPaypleConfig(validConfig)
|
||||
const auth = {
|
||||
PCD_CST_ID: 'encrypted-id',
|
||||
PCD_CUST_KEY: 'encrypted-key',
|
||||
PCD_AUTH_KEY: 'auth-key',
|
||||
PCD_PAY_HOST: 'https://cpay.payple.kr',
|
||||
PCD_PAY_URL: '',
|
||||
}
|
||||
const originalFetch = globalThis.fetch
|
||||
const cases: Array<{
|
||||
fetcher: typeof fetch
|
||||
definitive: boolean
|
||||
}> = [
|
||||
{
|
||||
fetcher: () => Promise.reject(new TypeError('connection reset')),
|
||||
definitive: false,
|
||||
},
|
||||
{
|
||||
fetcher: () => Promise.resolve(new Response('upstream unavailable', { status: 503 })),
|
||||
definitive: false,
|
||||
},
|
||||
{
|
||||
fetcher: () => Promise.resolve(new Response(JSON.stringify({
|
||||
PCD_PAY_RST: 'error',
|
||||
PCD_PAY_CODE: 'BILL0001',
|
||||
PCD_PAY_MSG: 'declined',
|
||||
}), { status: 200, headers: { 'Content-Type': 'application/json' } })),
|
||||
definitive: true,
|
||||
},
|
||||
]
|
||||
try {
|
||||
for (const testCase of cases) {
|
||||
globalThis.fetch = testCase.fetcher
|
||||
let error: unknown
|
||||
try {
|
||||
await paypleBilling(config, auth, {
|
||||
payerId: 'payer-fixture',
|
||||
amount: 9900,
|
||||
orderId: 'D3RO-20260821153045-test-nonce',
|
||||
goodsName: 'D3RO Voice Pro',
|
||||
})
|
||||
} catch (caught) {
|
||||
error = caught
|
||||
}
|
||||
assert(error instanceof PaypleBillingError, 'billing failure must be typed')
|
||||
assert(error.definitive === testCase.definitive, 'billing certainty classification')
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue