d3ro-voice/server/supabase/functions/payple-webhook/handler.test.ts
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

118 lines
4.4 KiB
TypeScript

// Handler-level regression tests with in-memory ports: no Supabase, no Payple.
import {
createPaypleWebhookHandler,
type IgnoredProviderEvent,
type PaypleWebhookPorts,
type ProviderApplyResult,
} from './index.ts'
import type { CorrelatedPayment, PaypleProviderEventArgs } from './webhook-policy.ts'
import { type PayplePaymentLookupResult, TIER_PRICE } from '../_shared/payple.ts'
function assert(condition: boolean, message: string): asserts condition {
if (!condition) throw new Error(message)
}
const OLD_ORDER = 'D3RO-20260801090000-user-oid1'
const CURRENT_ORDER = 'D3RO-20260901090000-user-oid2'
const PAYER = 'payer-billing-key'
interface FakeState {
applied: Array<PaypleProviderEventArgs & { p_payload_digest: string }>
ignored: IgnoredProviderEvent[]
}
function fakePorts(
state: FakeState,
lookups: Record<string, Partial<PayplePaymentLookupResult>>,
): PaypleWebhookPorts {
// Both orders were registered on the same billing key; oid2 (the renewal)
// funds the current period.
const operations: Record<string, CorrelatedPayment> = {
[OLD_ORDER]: {
user_id: '00000000-0000-4000-8000-000000000001',
tier: 'pro',
operation_id: '00000000-0000-4000-8000-0000000000a1',
payer_id: PAYER,
current_order_id: CURRENT_ORDER,
},
[CURRENT_ORDER]: {
user_id: '00000000-0000-4000-8000-000000000001',
tier: 'pro',
operation_id: '00000000-0000-4000-8000-0000000000a2',
payer_id: PAYER,
current_order_id: CURRENT_ORDER,
},
}
return {
correlatePayment: (orderId) => Promise.resolve(operations[orderId] ?? null),
lookupPayment: ({ orderId, payType }) => Promise.resolve({
PCD_PAY_RST: 'success',
PCD_PAY_CODE: 'PCHK0000',
PCD_PAY_MSG: 'ok',
PCD_PAY_OID: orderId,
PCD_PAY_TYPE: payType,
PCD_PAYER_ID: PAYER,
PCD_PAY_TOTAL: String(TIER_PRICE.pro),
PCD_PAY_TIME: '20260901090000',
...lookups[orderId],
}),
applyProviderEvent: (args): Promise<ProviderApplyResult> => {
state.applied.push(args)
return Promise.resolve({ applied: true, duplicate: false })
},
recordIgnoredEvent: (event) => {
state.ignored.push(event)
return Promise.resolve()
},
now: () => new Date('2026-09-15T00:00:00.000Z'),
}
}
function cancellationRequest(orderId: string): Request {
return new Request('http://localhost/payple-webhook', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
PCD_PAY_RST: 'success',
PCD_PAY_CODE: 'PAYC0000',
PCD_PAYCANCEL_FLAG: 'Y',
PCD_PAY_OID: orderId,
PCD_PAY_TYPE: 'card',
PCD_PAYER_ID: PAYER,
}),
})
}
Deno.test('refund of a past Payple order does not revoke the current paid subscription', async () => {
const state: FakeState = { applied: [], ignored: [] }
const handler = createPaypleWebhookHandler(() => fakePorts(state, {
[OLD_ORDER]: { PCD_PAY_STATE: '승인취소완료', PCD_PAY_TIME: '20260801090000' },
}))
const response = await handler(cancellationRequest(OLD_ORDER))
const body = await response.json() as Record<string, unknown>
assert(response.status === 200, `acknowledged, got ${response.status}`)
assert(body.ignored === 'canceled_order_not_current', `ignored reason, got ${JSON.stringify(body)}`)
assert(state.applied.length === 0, 'no entitlement change is applied')
assert(state.ignored.length === 1, 'the ignored event is recorded')
assert(state.ignored[0].eventId === `cancel:${OLD_ORDER}:승인취소완료`, 'event id recorded')
assert(/^[0-9a-f]{64}$/.test(state.ignored[0].payloadDigest), 'digest recorded')
})
Deno.test('refund of the current Payple order still revokes entitlement', async () => {
const state: FakeState = { applied: [], ignored: [] }
const handler = createPaypleWebhookHandler(() => fakePorts(state, {
[CURRENT_ORDER]: { PCD_PAY_STATE: '승인취소완료' },
}))
const response = await handler(cancellationRequest(CURRENT_ORDER))
assert(response.status === 200, `acknowledged, got ${response.status}`)
assert(state.ignored.length === 0, 'nothing ignored')
assert(state.applied.length === 1, 'revocation applied')
const args = state.applied[0]
assert(args.p_entitled === false && args.p_tier === 'free', 'revokes entitlement')
assert(args.p_provider_order_id === CURRENT_ORDER, 'scoped to the current order')
assert(/^[0-9a-f]{64}$/.test(args.p_payload_digest), 'digest attached')
})