jest.mock('../src/config/db', () => ({ appSetting: { findMany: jest.fn() }, })); const prisma = require('../src/config/db'); const { encrypt } = require('../src/utils/encryption'); const settingsCache = require('../src/utils/settingsCache'); const { maskSecret, yocoMode, getYocoSecretKey, getYocoWebhookSecret, yocoAuthHeader, defaultWebhookUrl, webhookUrlProblem, } = require('../src/utils/yoco'); const ENV = { ...process.env }; afterEach(() => { process.env = { ...ENV }; }); function dbHas(rows) { prisma.appSetting.findMany.mockResolvedValue(rows); settingsCache.invalidate(); } describe('maskSecret', () => { test('keeps the prefix and the last 3 characters', () => { expect(maskSecret('sk_test_0123456789abcdefaba')).toBe('sk_test_****************aba'); expect(maskSecret('sk_live_0123456789xyz')).toBe('sk_live_**********xyz'); expect(maskSecret('whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw')).toBe(`whsec_${'*'.repeat(29)}aSw`); }); test('masks the length exactly, like a card number', () => { const v = 'sk_test_0123456789abcdefaba'; expect(maskSecret(v)).toHaveLength(v.length); }); test('short values show nothing after the prefix', () => { expect(maskSecret('sk_test_abc')).toBe('sk_test_***'); expect(maskSecret('whsec_')).toBe('whsec_'); }); test('unknown formats keep the first 4 characters', () => { expect(maskSecret('abcd0123456789')).toBe('abcd*******789'); }); test('empty stays empty', () => { expect(maskSecret('')).toBe(''); expect(maskSecret(undefined)).toBe(''); }); }); test('yocoMode reads the key prefix', () => { expect(yocoMode('sk_test_x')).toBe('test'); expect(yocoMode('sk_live_x')).toBe('live'); expect(yocoMode('pk_test_x')).toBe('unknown'); expect(yocoMode('')).toBe('unknown'); }); describe('stored secrets', () => { test('the database wins over .env, and is decrypted', async () => { process.env.YOCO_SECRET_KEY = 'sk_test_fromenv'; dbHas([{ key: 'yoco_secret_key', value: encrypt('sk_test_fromdb') }]); expect(await getYocoSecretKey()).toBe('sk_test_fromdb'); }); test('.env is the fallback when the database has no value', async () => { process.env.YOCO_WEBHOOK_SECRET = 'whsec_ZW52'; dbHas([{ key: 'yoco_webhook_secret', value: '' }]); expect(await getYocoWebhookSecret()).toBe('whsec_ZW52'); }); test('checkouts fail clearly when no key is configured', async () => { delete process.env.YOCO_SECRET_KEY; dbHas([]); await expect(yocoAuthHeader()).rejects.toThrow(/not configured/); }); test('the auth header uses the stored key', async () => { dbHas([{ key: 'yoco_secret_key', value: encrypt('sk_live_abc') }]); expect(await yocoAuthHeader()).toBe('Bearer sk_live_abc'); }); }); describe('webhook URL', () => { const req = { protocol: 'https', get: () => 'events-demo-api.crosscode.co.za' }; test('built from BACKEND_URL', () => { process.env.BACKEND_URL = 'https://api.example.com/'; expect(defaultWebhookUrl(req)).toBe('https://api.example.com/api/webhooks/yoco'); }); test('falls back to the request origin', () => { delete process.env.BACKEND_URL; expect(defaultWebhookUrl(req)).toBe('https://events-demo-api.crosscode.co.za/api/webhooks/yoco'); }); test('must be public https', () => { expect(webhookUrlProblem('https://api.example.com/api/webhooks/yoco')).toBeNull(); expect(webhookUrlProblem('http://api.example.com/api/webhooks/yoco')).toMatch(/https/); expect(webhookUrlProblem('https://localhost:5000/api/webhooks/yoco')).toMatch(/localhost/); expect(webhookUrlProblem('not a url')).toMatch(/full webhook URL/); }); });