UNPKG

waitlist-mailer

Version:

Modern, modular TypeScript library for managing waitlists with pluggable storage and mail providers. Supports MongoDB, SQL databases, and custom adapters with zero required dependencies for basic usage.

472 lines (368 loc) 13.8 kB
import { WaitlistManager } from './index'; import { MemoryStorage } from './adapters/storage/MemoryStorage'; import { ConsoleMailProvider } from './adapters/mail/ConsoleMailProvider'; describe('WaitlistManager - Core Tests', () => { let manager: WaitlistManager; let storage: MemoryStorage; beforeEach(async () => { storage = new MemoryStorage(); const mailer = new ConsoleMailProvider(false); // Silent mode manager = new WaitlistManager({ storage, mailer, companyName: 'TestApp', autoConnect: true, }); await manager.waitForInitialization(); }); describe('Initialization', () => { test('Manager initializes successfully', async () => { expect(manager.isInitialized()).toBe(true); }); // NEW: Test default behavior test('Should initialize with default MemoryStorage (Zero Config)', async () => { const zeroConfigManager = new WaitlistManager({ companyName: 'ZeroConfigApp' }); // Wait for auto-connect await new Promise(resolve => setTimeout(resolve, 50)); expect(zeroConfigManager.isInitialized()).toBe(true); // Should allow joining without errors const result = await zeroConfigManager.join('default@example.com'); expect(result.success).toBe(true); // Should count 1 entry const count = await zeroConfigManager.count(); expect(count).toBe(1); }); }); describe('Joining the waitlist', () => { test('Successfully join with valid email', async () => { const result = await manager.join('user@example.com'); expect(result.success).toBe(true); expect(result.email).toBe('user@example.com'); expect(result.message).toContain('Successfully'); }); test('Join with custom metadata', async () => { const result = await manager.join('john@example.com', { name: 'John Doe', source: 'ProductHunt', }); expect(result.success).toBe(true); const all = await manager.getAll(); expect(all).toHaveLength(1); expect(all[0].metadata).toEqual({ name: 'John Doe', source: 'ProductHunt', }); }); test('Reject invalid email format', async () => { const result = await manager.join('not-an-email'); expect(result.success).toBe(false); expect(result.message).toContain('Invalid'); }); test('Reject duplicate email', async () => { await manager.join('user@example.com'); const result = await manager.join('user@example.com'); expect(result.success).toBe(false); expect(result.message).toContain('already'); }); test('Normalize email to lowercase', async () => { const result = await manager.join('USER@EXAMPLE.COM'); expect(result.email).toBe('user@example.com'); expect(await manager.has('user@example.com')).toBe(true); expect(await manager.has('USER@EXAMPLE.COM')).toBe(true); }); test('Emit join:success event', async () => { const joinSuccessSpy = jest.fn(); manager.on('join:success', joinSuccessSpy); await manager.join('user@example.com', { name: 'Alice' }); expect(joinSuccessSpy).toHaveBeenCalledWith( expect.objectContaining({ email: 'user@example.com', metadata: { name: 'Alice' }, }) ); }); }); describe('Leaving the waitlist', () => { beforeEach(async () => { await manager.join('user1@example.com'); await manager.join('user2@example.com'); }); test('Successfully remove existing email', async () => { const result = await manager.leave('user1@example.com'); expect(result).toBe(true); expect(await manager.has('user1@example.com')).toBe(false); }); test('Return false for non-existent email', async () => { const result = await manager.leave('nonexistent@example.com'); expect(result).toBe(false); }); test('Emit leave:success event', async () => { const leaveSpy = jest.fn(); manager.on('leave:success', leaveSpy); await manager.leave('user1@example.com'); expect(leaveSpy).toHaveBeenCalledWith( expect.objectContaining({ email: 'user1@example.com' }) ); }); }); describe('Querying the waitlist', () => { beforeEach(async () => { await manager.join('alice@example.com', { name: 'Alice' }); await manager.join('bob@example.com', { name: 'Bob' }); await manager.join('charlie@example.com', { name: 'Charlie' }); }); test('Count total entries', async () => { const count = await manager.count(); expect(count).toBe(3); }); test('Get all entries', async () => { const all = await manager.getAll(); expect(all).toHaveLength(3); expect(all[0].email).toBe('alice@example.com'); expect(all[0].createdAt).toBeInstanceOf(Date); }); test('Check if email exists', async () => { expect(await manager.has('alice@example.com')).toBe(true); expect(await manager.has('nonexistent@example.com')).toBe(false); }); test('Search by pattern', async () => { const results = await manager.search('example'); expect(results).toHaveLength(3); }); test('Search case-insensitive', async () => { const results = await manager.search('ALICE'); expect(results).toHaveLength(1); expect(results[0].email).toBe('alice@example.com'); }); }); describe('Bulk operations', () => { beforeEach(async () => { await manager.join('user1@example.com'); await manager.join('user2@example.com'); await manager.join('user3@example.com'); }); test('Clear all entries', async () => { const result = await manager.clear(); expect(result).toBe(true); expect(await manager.count()).toBe(0); }); test('Send bulk emails', async () => { const result = await manager.sendBulkEmails((email) => ({ email, subject: `Welcome, ${email}!`, })); expect(result.sent).toBe(3); expect(result.failed).toBe(0); expect(result.total).toBe(3); }); test('Emit bulk:complete event', async () => { const bulkSpy = jest.fn(); manager.on('bulk:complete', bulkSpy); await manager.sendBulkEmails((email) => ({ email, subject: 'Welcome', })); expect(bulkSpy).toHaveBeenCalledWith( expect.objectContaining({ sent: 3, failed: 0, total: 3, }) ); }); test('Send with custom concurrency level', async () => { const result = await manager.sendBulkEmails( (email) => ({ email, subject: 'Welcome' }), 2 // 2 concurrent emails ); expect(result.sent).toBe(3); expect(result.failed).toBe(0); expect(result.total).toBe(3); }); test('Send sequentially (concurrency=1)', async () => { const result = await manager.sendBulkEmails( (email) => ({ email, subject: 'Welcome' }), 1 // Sequential ); expect(result.sent).toBe(3); expect(result.failed).toBe(0); }); test('Send concurrently (concurrency=Infinity)', async () => { const result = await manager.sendBulkEmails( (email) => ({ email, subject: 'Welcome' }), Infinity // All at once ); expect(result.sent).toBe(3); expect(result.failed).toBe(0); }); }); describe('Event handling', () => { test('Emit join:validation-error for invalid email', async () => { const validationErrorSpy = jest.fn(); manager.on('join:validation-error', validationErrorSpy); await manager.join('invalid-email'); expect(validationErrorSpy).toHaveBeenCalled(); }); test('Emit join:duplicate for duplicate email', async () => { const duplicateSpy = jest.fn(); manager.on('join:duplicate', duplicateSpy); await manager.join('user@example.com'); await manager.join('user@example.com'); expect(duplicateSpy).toHaveBeenCalled(); }); test('Emit mailer:sent when email is sent', async () => { const mailerSpy = jest.fn(); manager.on('mailer:sent', mailerSpy); await manager.join('user@example.com'); expect(mailerSpy).toHaveBeenCalledWith( expect.objectContaining({ email: 'user@example.com' }) ); }); }); describe('Disconnection', () => { test('Disconnect successfully', async () => { expect(manager.isInitialized()).toBe(true); await manager.disconnect(); expect(manager.isInitialized()).toBe(false); }); test('Emit disconnected event', async () => { const disconnectSpy = jest.fn(); manager.on('disconnected', disconnectSpy); await manager.disconnect(); expect(disconnectSpy).toHaveBeenCalled(); }); }); }); describe('MemoryStorage Adapter', () => { let storage: MemoryStorage; beforeEach(() => { storage = new MemoryStorage(); }); test('Add and retrieve entries', async () => { await storage.add('user@example.com', { name: 'Alice' }); const all = await storage.getAll(); expect(all).toHaveLength(1); expect(all[0].email).toBe('user@example.com'); }); test('Check if email exists', async () => { await storage.add('user@example.com'); expect(await storage.exists('user@example.com')).toBe(true); expect(await storage.exists('user2@example.com')).toBe(false); }); test('Remove email', async () => { await storage.add('user@example.com'); const removed = await storage.remove('user@example.com'); expect(removed).toBe(true); expect(await storage.exists('user@example.com')).toBe(false); }); test('Clear all entries', async () => { await storage.add('user1@example.com'); await storage.add('user2@example.com'); await storage.clear(); expect(await storage.count()).toBe(0); }); test('Throw error on duplicate add', async () => { await storage.add('user@example.com'); await expect(storage.add('user@example.com')).rejects.toThrow('already exists'); }); test('Normalize email to lowercase', async () => { await storage.add('USER@EXAMPLE.COM'); expect(await storage.exists('user@example.com')).toBe(true); }); describe('Search functionality', () => { beforeEach(async () => { await storage.add('alice@gmail.com', { name: 'Alice' }); await storage.add('bob@yahoo.com', { name: 'Bob' }); await storage.add('charlie@gmail.com', { name: 'Charlie' }); await storage.add('david@outlook.com', { name: 'David' }); }); test('Search by pattern', async () => { const results = await storage.search('gmail'); expect(results).toHaveLength(2); }); test('Search is case-insensitive by default', async () => { const results = await storage.search('GMAIL'); expect(results).toHaveLength(2); }); test('Search with case-sensitive option', async () => { const results = await storage.search('gmail', { caseInsensitive: false }); expect(results).toHaveLength(2); const resultsUpper = await storage.search('GMAIL', { caseInsensitive: false }); expect(resultsUpper).toHaveLength(0); }); test('Search with limit', async () => { const results = await storage.search('', { limit: 2 }); expect(results).toHaveLength(2); }); test('Search with offset', async () => { const results = await storage.search('', { offset: 2 }); expect(results).toHaveLength(2); }); test('Search with limit and offset (pagination)', async () => { const page1 = await storage.search('', { limit: 2, offset: 0 }); const page2 = await storage.search('', { limit: 2, offset: 2 }); expect(page1).toHaveLength(2); expect(page2).toHaveLength(2); expect(page1[0].email).not.toBe(page2[0].email); }); }); describe('Iterate functionality', () => { beforeEach(async () => { for (let i = 1; i <= 10; i++) { await storage.add(`user${i}@example.com`, { index: i }); } }); test('Iterate over all entries', async () => { const entries: any[] = []; for await (const entry of storage.iterate()) { entries.push(entry); } expect(entries).toHaveLength(10); }); test('Iterate with custom batch size', async () => { const entries: any[] = []; for await (const entry of storage.iterate(3)) { entries.push(entry); } expect(entries).toHaveLength(10); }); test('Iterate yields entries one by one', async () => { let count = 0; for await (const entry of storage.iterate(2)) { count++; expect(entry.email).toContain('@example.com'); } expect(count).toBe(10); }); }); }); describe('WaitlistManager - Advanced Search', () => { let manager: WaitlistManager; let storage: MemoryStorage; beforeEach(async () => { storage = new MemoryStorage(); manager = new WaitlistManager({ storage, companyName: 'TestApp', autoConnect: true, }); await manager.waitForInitialization(); // Add test data await manager.join('alice@gmail.com'); await manager.join('bob@yahoo.com'); await manager.join('charlie@gmail.com'); await manager.join('david@outlook.com'); }); test('Search with options', async () => { const results = await manager.search('gmail', { limit: 1 }); expect(results).toHaveLength(1); }); test('Search with pagination', async () => { const page1 = await manager.search('', { limit: 2, offset: 0 }); const page2 = await manager.search('', { limit: 2, offset: 2 }); expect(page1).toHaveLength(2); expect(page2).toHaveLength(2); }); });