import assert from 'node:assert/strict'
import test from 'node:test'
import {
API_KEY_SETUP_URL,
DEFAULT_API_KEY_SECRET,
mutationPreview,
parseAction,
requireConfirm,
resolveApiKeySecretName,
resolveWebhookSecretName,
secretSetupUrl,
stripeParams,
} from './stripe-core.ts'
import { createRefund } from './payments.ts'
import { deleteCustomer } from './customers.ts'
import {
createTestSignatureHeader,
hmacSha256Hex,
parseStripeSignatureHeader,
verifyWebhookSignature,
} from './webhooks.ts'
test('resolveApiKeySecretName defaults and suffixes accounts', () => {
assert.equal(resolveApiKeySecretName(), DEFAULT_API_KEY_SECRET)
assert.equal(resolveApiKeySecretName({ account: 'default' }), DEFAULT_API_KEY_SECRET)
assert.equal(resolveApiKeySecretName({ account: 'work' }), 'stripeApiKey-work')
assert.equal(resolveApiKeySecretName({ secretName: 'stripeApiKey-live' }), 'stripeApiKey-live')
})
test('resolveApiKeySecretName rejects Kent-style aliases and invalid names', () => {
assert.throws(() => resolveApiKeySecretName({ secretName: 'stripeSecretKey' }), /stripeApiKey/)
assert.throws(() => resolveApiKeySecretName({ account: 'youtube-brand!' }), /short label/)
})
test('webhook secret name follows the same account suffix', () => {
assert.equal(resolveWebhookSecretName(), 'stripeWebhookSecret')
assert.equal(resolveWebhookSecretName({ account: 'work' }), 'stripeWebhookSecret-work')
})
test('secretSetupUrl keeps the default prefilled link', () => {
assert.equal(secretSetupUrl(), API_KEY_SETUP_URL)
assert.match(secretSetupUrl('stripeApiKey-work'), /name=stripeApiKey-work/)
assert.match(API_KEY_SETUP_URL, /allowedHosts=api\.stripe\.com,files\.stripe\.com/)
})
test('stripeParams serializes nested Stripe form fields', () => {
const params = stripeParams({
email: 'ada@example.com',
metadata: { plan: 'pro' },
items: [{ price: 'price_123', quantity: 2 }],
})
assert.equal(params.get('email'), 'ada@example.com')
assert.equal(params.get('metadata[plan]'), 'pro')
assert.equal(params.get('items[][price]'), 'price_123')
assert.equal(params.get('items[][quantity]'), '2')
})
test('requireConfirm blocks live mutations', () => {
assert.throws(() => requireConfirm({}, 'refund ch_123'), /confirm: true/)
requireConfirm({ confirm: true }, 'refund ch_123')
})
test('mutationPreview dryRun skips confirm', () => {
const preview = mutationPreview(
{ dryRun: true },
{
action: 'refund ch_123',
method: 'POST',
path: 'refunds',
body: { charge: 'ch_123' },
requireConfirm: true,
},
)
assert.deepEqual(preview, {
dryRun: true,
action: 'refund ch_123',
method: 'POST',
path: 'refunds',
body: { charge: 'ch_123' },
})
})
test('createRefund dryRun does not require confirm or a network call', async () => {
const preview = await createRefund({
chargeId: 'ch_123',
amount: 500,
dryRun: true,
})
assert.equal(preview.dryRun, true)
assert.equal(preview.path, 'refunds')
assert.equal(preview.body?.charge, 'ch_123')
})
test('deleteCustomer without confirm or dryRun throws', async () => {
await assert.rejects(deleteCustomer({ customerId: 'cus_123' }), /confirm: true/)
})
test('parseAction is exhaustive for known unions', () => {
const allowed = ['list-customers', 'get-customer'] as const
assert.equal(parseAction(undefined, allowed, 'list-customers', 'customers'), 'list-customers')
assert.throws(() => parseAction('explode', allowed, 'list-customers', 'customers'), /explode/)
})
test('webhook HMAC verify accepts a matching Stripe-Signature header', async () => {
const payload = '{"id":"evt_test","object":"event","type":"ping"}'
const webhookSecret = 'whsec_test'
const timestamp = 1_700_000_000
const header = await createTestSignatureHeader({ payload, webhookSecret, timestamp })
const parsed = parseStripeSignatureHeader(header)
assert.equal(parsed.timestamp, timestamp)
const expected = await hmacSha256Hex(webhookSecret, timestamp + '.' + payload)
assert.equal(parsed.signatures[0], expected)
const result = await verifyWebhookSignature({
payload,
signatureHeader: header,
webhookSecret,
nowSeconds: timestamp,
})
assert.equal(result.ok, true)
assert.equal(result.eventId, 'evt_test')
assert.equal(result.verifiedVia, 'signature')
})
test('webhook HMAC verify rejects a bad signature and stale timestamp', async () => {
const payload = '{"id":"evt_test"}'
const header = await createTestSignatureHeader({
payload,
webhookSecret: 'whsec_test',
timestamp: 1_700_000_000,
})
await assert.rejects(
verifyWebhookSignature({
payload,
signatureHeader: header,
webhookSecret: 'whsec_other',
nowSeconds: 1_700_000_000,
}),
/did not match/,
)
await assert.rejects(
verifyWebhookSignature({
payload,
signatureHeader: header,
webhookSecret: 'whsec_test',
nowSeconds: 1_700_000_000 + 301,
}),
/tolerance/,
)
})