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
text/typescript
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);
});
});