Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/paypal

src/paypal.test.ts

162 lines · 5.1 KB · TypeScript
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)
})