Files
ReadMeABook/tests/services/encryption.service.test.ts
T
kikootwo 20c8fb0898 Add reported-issues, Goodreads sync & notifs
Introduce user-reported-issues and Goodreads shelf sync features and wire them into notifications. Adds Prisma migrations and schema changes (ReportedIssue, GoodreadsShelf, GoodreadsBookMapping), API endpoints for reporting (POST /audiobooks/[asin]/report-issue) and admin management (list, resolve/dismiss, replace), and an admin UI section to view/dismiss/replace reported issues. Adds a new notification event (issue_reported) with updates to notification schemas, docs and provider handling, plus a notification-events constants file. Refactors request creation to use createRequestForUser service, adds a Goodreads sync processor/service/hooks/UI modals, a scrape-resilience util, and related tests and minor integration updates.
2026-02-11 16:49:55 -05:00

167 lines
5.8 KiB
TypeScript

/**
* Component: Encryption Service Tests
* Documentation: documentation/backend/services/config.md
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
const ORIGINAL_KEY = process.env.CONFIG_ENCRYPTION_KEY;
afterEach(() => {
process.env.CONFIG_ENCRYPTION_KEY = ORIGINAL_KEY;
vi.resetModules();
});
describe('EncryptionService', () => {
it('throws when encryption key is missing', async () => {
delete process.env.CONFIG_ENCRYPTION_KEY;
vi.resetModules();
const { EncryptionService } = await import('@/lib/services/encryption.service');
expect(() => new EncryptionService()).toThrow(/CONFIG_ENCRYPTION_KEY/);
});
it('encrypts and decrypts values', async () => {
process.env.CONFIG_ENCRYPTION_KEY = 'a'.repeat(32);
vi.resetModules();
const { EncryptionService } = await import('@/lib/services/encryption.service');
const service = new EncryptionService();
const encrypted = service.encrypt('secret');
const decrypted = service.decrypt(encrypted);
expect(decrypted).toBe('secret');
});
it('rejects invalid encrypted data formats', async () => {
process.env.CONFIG_ENCRYPTION_KEY = 'b'.repeat(32);
vi.resetModules();
const { EncryptionService } = await import('@/lib/services/encryption.service');
const service = new EncryptionService();
expect(() => service.decrypt('invalid')).toThrow(/Decryption failed/);
});
it('generates a random key', async () => {
const { EncryptionService } = await import('@/lib/services/encryption.service');
const key = EncryptionService.generateKey();
expect(typeof key).toBe('string');
expect(key.length).toBeGreaterThan(40);
});
describe('isEncryptedFormat', () => {
async function createService() {
process.env.CONFIG_ENCRYPTION_KEY = 'c'.repeat(32);
vi.resetModules();
const { EncryptionService } = await import('@/lib/services/encryption.service');
return new EncryptionService();
}
it('returns true for values produced by encrypt()', async () => {
const service = await createService();
const encrypted = service.encrypt('hello world');
expect(service.isEncryptedFormat(encrypted)).toBe(true);
});
it('returns true for various encrypted values (round-trip)', async () => {
const service = await createService();
const testValues = [
'simple',
'tgram://1234567890:PLPe1Hh-VhbRC3MoT5QngwkPHoMTD/-100181291455/',
'slack://tokenA/tokenB/tokenC',
'https://hooks.slack.com/services/T00/B00/xxx',
'a',
'a'.repeat(1000),
'json://user:password@host:8080/path',
];
for (const val of testValues) {
const encrypted = service.encrypt(val);
expect(service.isEncryptedFormat(encrypted)).toBe(true);
expect(service.decrypt(encrypted)).toBe(val);
}
});
it('returns false for Telegram notification URLs (the reported bug)', async () => {
const service = await createService();
// This URL has exactly 3 colon-separated parts, which fooled the old check
expect(service.isEncryptedFormat(
'tgram://1234567890:PLPe1Hh-VhbRC3MoT5QngwkPHoMTD/-100181291455/'
)).toBe(false);
});
it('returns false for common notification URL schemes', async () => {
const service = await createService();
const urls = [
'slack://tokenA/tokenB/tokenC',
'discord://webhook_id/webhook_token',
'mailto://user:pass@gmail.com',
'json://user:pass@hostname',
'https://hooks.slack.com/services/T00/B00/xxx',
'gotify://hostname/token',
'ntfy://topic',
'tgram://bot_token:chat_id/',
];
for (const url of urls) {
expect(service.isEncryptedFormat(url)).toBe(false);
}
});
it('returns false for non-string values', async () => {
const service = await createService();
expect(service.isEncryptedFormat(null as any)).toBe(false);
expect(service.isEncryptedFormat(undefined as any)).toBe(false);
expect(service.isEncryptedFormat(123 as any)).toBe(false);
expect(service.isEncryptedFormat({} as any)).toBe(false);
});
it('returns false for strings with wrong number of colon parts', async () => {
const service = await createService();
expect(service.isEncryptedFormat('no-colons-at-all')).toBe(false);
expect(service.isEncryptedFormat('one:part')).toBe(false);
expect(service.isEncryptedFormat('a:b:c:d')).toBe(false);
});
it('returns false for 3-part strings with invalid base64', async () => {
const service = await createService();
// Contains characters not in base64 alphabet
expect(service.isEncryptedFormat('not base64!:also not!:data')).toBe(false);
expect(service.isEncryptedFormat('//invalid:##bad:data')).toBe(false);
});
it('returns false for 3-part base64 strings with wrong decoded lengths', async () => {
const service = await createService();
// Valid base64, but wrong byte lengths (not 16 bytes each)
const shortIv = Buffer.from('short').toString('base64'); // 5 bytes
const shortTag = Buffer.from('alsoshort').toString('base64'); // 9 bytes
const data = Buffer.from('somedata').toString('base64');
expect(service.isEncryptedFormat(`${shortIv}:${shortTag}:${data}`)).toBe(false);
});
it('returns false for empty string', async () => {
const service = await createService();
expect(service.isEncryptedFormat('')).toBe(false);
});
it('returns false for 3-part string with empty segments', async () => {
const service = await createService();
expect(service.isEncryptedFormat('::data')).toBe(false);
expect(service.isEncryptedFormat('iv::data')).toBe(false);
expect(service.isEncryptedFormat('iv:tag:')).toBe(false);
});
});
});