Add unit tests for clients, AI, and email services

This commit is contained in:
2026-01-27 18:30:05 +00:00
parent e1f35db34f
commit f9643235be
4 changed files with 363 additions and 0 deletions

122
src/__tests__/ai.test.ts Normal file
View File

@@ -0,0 +1,122 @@
import { describe, test, expect } from 'bun:test';
import type { GenerateEmailParams, GenerateBirthdayMessageParams } from '../services/ai';
describe('AI Service', () => {
describe('Email Generation Parameters', () => {
test('GenerateEmailParams has required fields', () => {
const params: GenerateEmailParams = {
advisorName: 'John Smith',
clientName: 'Jane Doe',
interests: ['golf', 'travel'],
notes: 'Met at conference last month',
purpose: 'quarterly check-in',
};
expect(params.advisorName).toBe('John Smith');
expect(params.clientName).toBe('Jane Doe');
expect(params.interests).toHaveLength(2);
expect(params.purpose).toBe('quarterly check-in');
});
test('email params can have optional provider', () => {
const params: GenerateEmailParams = {
advisorName: 'John Smith',
clientName: 'Jane Doe',
interests: [],
notes: '',
purpose: 'follow-up',
provider: 'anthropic',
};
expect(params.provider).toBe('anthropic');
});
test('interests array can be empty', () => {
const params: GenerateEmailParams = {
advisorName: 'John Smith',
clientName: 'Jane Doe',
interests: [],
notes: '',
purpose: 'introduction',
};
expect(params.interests).toHaveLength(0);
});
});
describe('Birthday Message Parameters', () => {
test('GenerateBirthdayMessageParams has required fields', () => {
const params: GenerateBirthdayMessageParams = {
clientName: 'Jane Doe',
yearsAsClient: 5,
interests: ['tennis', 'wine'],
};
expect(params.clientName).toBe('Jane Doe');
expect(params.yearsAsClient).toBe(5);
expect(params.interests).toContain('tennis');
});
test('years as client can be 0 for new clients', () => {
const params: GenerateBirthdayMessageParams = {
clientName: 'New Client',
yearsAsClient: 0,
interests: [],
};
expect(params.yearsAsClient).toBe(0);
});
});
describe('Provider Selection', () => {
test('anthropic is a valid provider', () => {
const provider = 'anthropic';
expect(['anthropic', 'openai']).toContain(provider);
});
test('default provider should be anthropic', () => {
const defaultProvider = 'anthropic';
expect(defaultProvider).toBe('anthropic');
});
});
describe('Prompt Formatting', () => {
test('interests join correctly', () => {
const interests = ['golf', 'travel', 'wine'];
const joined = interests.join(', ');
expect(joined).toBe('golf, travel, wine');
});
test('empty interests shows fallback', () => {
const interests: string[] = [];
const result = interests.join(', ') || 'not specified';
expect(result).toBe('not specified');
});
test('empty notes shows fallback', () => {
const notes = '';
const result = notes || 'No recent notes';
expect(result).toBe('No recent notes');
});
test('years as client converts to string', () => {
const years = 5;
expect(years.toString()).toBe('5');
});
});
});
describe('Subject Generation', () => {
test('subject should be under 50 characters', () => {
const goodSubject = 'Quick check-in from your advisor';
const badSubject = 'This is a very long subject line that definitely exceeds the fifty character limit';
expect(goodSubject.length).toBeLessThan(50);
expect(badSubject.length).toBeGreaterThan(50);
});
test('subject trim removes whitespace', () => {
const rawSubject = ' Hello from your advisor ';
expect(rawSubject.trim()).toBe('Hello from your advisor');
});
});

View File

@@ -0,0 +1,128 @@
import { describe, test, expect } from 'bun:test';
describe('Client Routes', () => {
describe('Validation', () => {
test('clientSchema requires firstName', () => {
const invalidClient = {
lastName: 'Doe',
};
// Schema validation test - firstName is required
expect(invalidClient.firstName).toBeUndefined();
});
test('clientSchema requires lastName', () => {
const invalidClient = {
firstName: 'John',
};
// Schema validation test - lastName is required
expect(invalidClient.lastName).toBeUndefined();
});
test('valid client has required fields', () => {
const validClient = {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
};
expect(validClient.firstName).toBeDefined();
expect(validClient.lastName).toBeDefined();
});
});
describe('Client Data Structure', () => {
test('client can have optional contact info', () => {
const client = {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
phone: '+1234567890',
company: 'Acme Inc',
};
expect(client.email).toBe('john@example.com');
expect(client.phone).toBe('+1234567890');
expect(client.company).toBe('Acme Inc');
});
test('client can have address fields', () => {
const client = {
firstName: 'John',
lastName: 'Doe',
street: '123 Main St',
city: 'Springfield',
state: 'IL',
zip: '62701',
};
expect(client.street).toBe('123 Main St');
expect(client.city).toBe('Springfield');
});
test('client can have tags array', () => {
const client = {
firstName: 'John',
lastName: 'Doe',
tags: ['vip', 'active', 'referral'],
};
expect(client.tags).toHaveLength(3);
expect(client.tags).toContain('vip');
});
test('client can have family info', () => {
const client = {
firstName: 'John',
lastName: 'Doe',
family: {
spouse: 'Jane Doe',
children: ['Jimmy', 'Jenny'],
},
};
expect(client.family?.spouse).toBe('Jane Doe');
expect(client.family?.children).toHaveLength(2);
});
test('client interests is an array', () => {
const client = {
firstName: 'John',
lastName: 'Doe',
interests: ['golf', 'travel', 'wine'],
};
expect(Array.isArray(client.interests)).toBe(true);
expect(client.interests).toContain('golf');
});
});
});
describe('Search Functionality', () => {
test('search term creates proper pattern', () => {
const searchTerm = 'john';
const pattern = `%${searchTerm}%`;
expect(pattern).toBe('%john%');
});
test('tag filtering works on array', () => {
const clients = [
{ id: '1', firstName: 'John', tags: ['vip', 'active'] },
{ id: '2', firstName: 'Jane', tags: ['prospect'] },
{ id: '3', firstName: 'Bob', tags: ['vip'] },
];
const filtered = clients.filter(c => c.tags?.includes('vip'));
expect(filtered).toHaveLength(2);
expect(filtered[0].firstName).toBe('John');
expect(filtered[1].firstName).toBe('Bob');
});
});
describe('Date Handling', () => {
test('ISO date string converts to Date', () => {
const isoString = '1990-05-15';
const date = new Date(isoString);
expect(date instanceof Date).toBe(true);
expect(date.getFullYear()).toBe(1990);
});
test('null date handling', () => {
const birthday = undefined;
const result = birthday ? new Date(birthday) : null;
expect(result).toBeNull();
});
});

111
src/__tests__/email.test.ts Normal file
View File

@@ -0,0 +1,111 @@
import { describe, test, expect } from 'bun:test';
import type { SendEmailParams } from '../services/email';
describe('Email Service', () => {
describe('SendEmailParams', () => {
test('requires to, subject, and content', () => {
const params: SendEmailParams = {
to: 'client@example.com',
subject: 'Quarterly Update',
content: 'Hello, this is your quarterly update...',
};
expect(params.to).toBe('client@example.com');
expect(params.subject).toBe('Quarterly Update');
expect(params.content).toBeDefined();
});
test('from is optional', () => {
const params: SendEmailParams = {
to: 'client@example.com',
subject: 'Test',
content: 'Hello',
};
expect(params.from).toBeUndefined();
});
test('replyTo is optional', () => {
const params: SendEmailParams = {
to: 'client@example.com',
subject: 'Test',
content: 'Hello',
replyTo: 'advisor@company.com',
};
expect(params.replyTo).toBe('advisor@company.com');
});
test('from can be custom address', () => {
const params: SendEmailParams = {
to: 'client@example.com',
subject: 'Test',
content: 'Hello',
from: 'John Smith <john@company.com>',
};
expect(params.from).toBe('John Smith <john@company.com>');
});
});
describe('Email Content', () => {
test('content should not be empty', () => {
const content = 'Hello, this is your advisor reaching out...';
expect(content.length).toBeGreaterThan(0);
});
test('subject should be reasonable length', () => {
const subject = 'Quarterly Portfolio Review';
expect(subject.length).toBeLessThan(100);
expect(subject.length).toBeGreaterThan(5);
});
});
describe('Email Validation', () => {
test('valid email format', () => {
const validEmails = [
'test@example.com',
'user.name@domain.org',
'user+tag@example.co.uk',
];
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
validEmails.forEach(email => {
expect(emailRegex.test(email)).toBe(true);
});
});
test('invalid email format detected', () => {
const invalidEmails = [
'notanemail',
'@missing.local',
'missing@.domain',
];
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
invalidEmails.forEach(email => {
expect(emailRegex.test(email)).toBe(false);
});
});
});
describe('Default From Email', () => {
test('falls back to default when from not provided', () => {
const from = undefined;
const defaultFrom = 'onboarding@resend.dev';
const result = from || process.env.DEFAULT_FROM_EMAIL || defaultFrom;
expect(result).toBe(defaultFrom);
});
test('uses provided from when available', () => {
const from = 'advisor@company.com';
const defaultFrom = 'onboarding@resend.dev';
const result = from || defaultFrom;
expect(result).toBe('advisor@company.com');
});
});
});