import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
CLIENT_ID_SETUP_URL,
CLIENT_SECRET_SETUP_URL,
DEFAULT_CLIENT_ID_SECRET,
DEFAULT_CLIENT_SECRET_SECRET,
buildInvoiceBody,
buildPayoutBody,
clientIdSetupUrl,
clientSecretSetupUrl,
moneyValue,
mutationPreview,
requireConfirm,
resolveClientIdSecretName,
resolveClientSecretSecretName,
toPayPalDate,
} from './paypal-helpers.ts'
const here = dirname(fileURLToPath(import.meta.url))
const root = join(here, '..')
test('secret names default and suffix accounts', () => {
assert.equal(resolveClientIdSecretName(), DEFAULT_CLIENT_ID_SECRET)
assert.equal(resolveClientSecretSecretName(), DEFAULT_CLIENT_SECRET_SECRET)
assert.equal(resolveClientIdSecretName({ account: 'default' }), DEFAULT_CLIENT_ID_SECRET)
assert.equal(resolveClientIdSecretName({ account: 'work' }), 'paypalClientId-work')
assert.equal(resolveClientSecretSecretName({ account: 'work' }), 'paypalClientSecret-work')
assert.equal(
resolveClientIdSecretName({ clientIdSecret: 'paypalClientId-live' }),
'paypalClientId-live',
)
})
test('secret names reject Kent-style aliases', () => {
assert.throws(
() => resolveClientIdSecretName({ clientIdSecret: 'PAYPAL_CLIENT_ID' }),
/paypalClientId/,
)
assert.throws(
() => resolveClientSecretSecretName({ clientSecretSecret: 'paypalSecret' }),
/paypalClientSecret/,
)
})
test('setup URLs are prefilled for both secrets', () => {
assert.match(CLIENT_ID_SETUP_URL, /name=paypalClientId/)
assert.match(CLIENT_ID_SETUP_URL, /allowedHosts=api-m\.paypal\.com%2Capi-m\.sandbox\.paypal\.com/)
assert.match(CLIENT_ID_SETUP_URL, /scope=user/)
assert.match(CLIENT_SECRET_SETUP_URL, /name=paypalClientSecret/)
assert.match(clientIdSetupUrl('paypalClientId-work'), /name=paypalClientId-work/)
assert.match(clientSecretSetupUrl('paypalClientSecret-work'), /name=paypalClientSecret-work/)
})
test('requireConfirm blocks live mutations', () => {
assert.throws(() => requireConfirm({}, 'send invoice INV2-1'), /confirm: true/)
requireConfirm({ confirm: true }, 'send invoice INV2-1')
})
test('mutationPreview dryRun skips confirm', () => {
const preview = mutationPreview(
{ dryRun: true },
{
action: 'send invoice INV2-1',
method: 'POST',
path: '/v2/invoicing/invoices/INV2-1/send',
body: { send_to_recipient: true },
requireConfirm: true,
},
)
assert.deepEqual(preview, {
dryRun: true,
action: 'send invoice INV2-1',
method: 'POST',
path: '/v2/invoicing/invoices/INV2-1/send',
body: { send_to_recipient: true },
})
})
test('mutationPreview requires confirm when not dry-run', () => {
assert.throws(
() =>
mutationPreview(
{},
{
action: 'create payout batch',
method: 'POST',
path: '/v1/payments/payouts',
requireConfirm: true,
},
),
/confirm: true/,
)
assert.equal(
mutationPreview(
{ confirm: true },
{
action: 'create payout batch',
method: 'POST',
path: '/v1/payments/payouts',
requireConfirm: true,
},
),
null,
)
})
test('invoice body is generic and caller-supplied', () => {
const body = buildInvoiceBody({
recipientEmail: 'ada@example.com',
itemName: 'Consulting',
amount: 42,
memo: 'March invoice',
})
assert.equal(body.primary_recipients[0]?.billing_info.email_address, 'ada@example.com')
assert.equal(body.items[0]?.name, 'Consulting')
assert.equal(body.items[0]?.unit_amount.value, '42.00')
assert.equal(body.detail.currency_code, 'USD')
const source = JSON.stringify(body)
assert.equal(source.includes('kentcdodds'), false)
assert.equal(source.includes('me@'), false)
})
test('payout body is generic and caller-supplied', () => {
const body = buildPayoutBody({
receiver: 'ada@example.com',
amount: '25.5',
note: 'Conference travel',
})
assert.equal(body.items[0]?.receiver, 'ada@example.com')
assert.equal((body.items[0]?.amount as { value: string }).value, '25.50')
assert.equal(body.sender_batch_header.recipient_type, 'EMAIL')
const source = JSON.stringify(body)
assert.equal(source.includes('kentcdodds'), false)
})
test('moneyValue rejects negatives', () => {
assert.equal(moneyValue('10'), '10.00')
assert.throws(() => moneyValue(-1), /non-negative/)
})
test('toPayPalDate formats ISO without millis', () => {
assert.equal(toPayPalDate('2026-08-01T00:00:00.123Z'), '2026-08-01T00:00:00Z')
})
test('package metadata is official and public', () => {
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
assert.equal(manifest.name, '@kody/paypal')
assert.equal(manifest.license, 'MIT')
assert.notEqual(manifest.private, true)
assert.equal(manifest.kody.id, 'paypal')
assert.equal(manifest.kody.jobs['reimbursement-scan'].enabled, false)
const readme = readFileSync(join(root, 'README.md'), 'utf8')
assert.match(readme, /^## Intent$/m)
assert.match(readme, /https:\/\/kody\.codes\/@kody\/paypal/)
assert.match(readme, /account\/secrets\/new/)
assert.equal(readme.includes('kentcdodds.com'), false)
assert.equal(readme.includes('me@kentcdodds'), false)
})