The Yoco secret key and webhook secret now live in AppSetting (AES-256-GCM encrypted), with .env as a fallback. A new Site Settings → Yoco tab shows them masked card-style, registers a webhook with Yoco (storing the secret it returns) or accepts one entered by hand, and lists/deletes the account's webhooks. Generic settings endpoints never read or write these keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.6 KiB
JavaScript
100 lines
3.6 KiB
JavaScript
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/);
|
|
});
|
|
});
|