feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View 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
}
})