/**
* Tests for manage_person_emails tool
*
* Tests unified email management with set, add, remove, and clear operations.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { handleManagePersonEmails } from '../../../src/tools/manage-person-emails.js';
import { createMockAttioClient } from '../../helpers/mock-attio-client.js';
import { loadFixture } from '../../helpers/fixtures.js';
import * as attioClientModule from '../../../src/attio-client.js';
vi.mock('../../../src/attio-client.js', () => ({
createAttioClient: vi.fn(),
}));
describe('manage_person_emails', () => {
let mockClient: ReturnType<typeof createMockAttioClient>;
let originalApiKey: string | undefined;
beforeEach(() => {
originalApiKey = process.env.ATTIO_API_KEY;
process.env.ATTIO_API_KEY = 'test-api-key';
mockClient = createMockAttioClient();
vi.mocked(attioClientModule.createAttioClient).mockReturnValue(
mockClient as any
);
});
afterEach(() => {
if (originalApiKey) {
process.env.ATTIO_API_KEY = originalApiKey;
} else {
delete process.env.ATTIO_API_KEY;
}
mockClient.reset();
});
describe('SET operation', () => {
it('should replace all emails with new set', async () => {
const getResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'old@example.com' },
],
},
},
};
const putResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'new1@example.com' },
{ email_address: 'new2@example.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getResponse }
);
mockClient.mockResponse(
'PUT',
'/objects/people/records/person-123',
{ data: putResponse }
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'set',
email_addresses: ['new1@example.com', 'new2@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.operation).toBe('set');
expect(parsed.data.emails_set).toEqual(['new1@example.com', 'new2@example.com']);
expect(parsed.data.previous_emails).toEqual(['old@example.com']);
expect(parsed.data.current_emails).toEqual(['new1@example.com', 'new2@example.com']);
});
it('should use PUT method for set operation', async () => {
mockClient.mockResponse(
'GET',
'/objects/people/records/test-id',
{
data: {
data: {
id: { record_id: 'test-id' },
values: { email_addresses: [] },
},
},
}
);
mockClient.mockResponse(
'PUT',
'/objects/people/records/test-id',
{
data: {
data: {
id: { record_id: 'test-id' },
values: { email_addresses: [] },
},
},
}
);
await handleManagePersonEmails({
record_id: 'test-id',
operation: 'set',
email_addresses: ['test@example.com'],
});
const calls = mockClient.getCallsFor('PUT');
expect(calls).toHaveLength(1);
expect(calls[0].body.data.values.email_addresses).toEqual([
{ email_address: 'test@example.com' },
]);
});
});
describe('ADD operation', () => {
it('should append new emails to existing ones', async () => {
const getCurrentResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'existing@example.com' },
],
},
},
};
const patchResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'existing@example.com' },
{ email_address: 'new1@example.com' },
{ email_address: 'new2@example.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getCurrentResponse }
);
mockClient.mockResponse(
'PATCH',
'/objects/people/records/person-123',
{ data: patchResponse }
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'add',
email_addresses: ['new1@example.com', 'new2@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.operation).toBe('add');
expect(parsed.data.emails_added).toEqual(['new1@example.com', 'new2@example.com']);
expect(parsed.data.previous_emails).toEqual(['existing@example.com']);
expect(parsed.data.current_emails).toEqual(['existing@example.com', 'new1@example.com', 'new2@example.com']);
});
it('should skip emails that already exist (case-insensitive)', async () => {
const getCurrentResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'Existing@Example.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getCurrentResponse }
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'add',
email_addresses: ['existing@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.emails_added).toEqual([]);
expect(parsed.data.message).toContain('already exist');
});
it('should use PATCH method for add operation', async () => {
mockClient.mockResponse(
'GET',
'/objects/people/records/test-id',
{
data: {
data: {
id: { record_id: 'test-id' },
values: { email_addresses: [] },
},
},
}
);
mockClient.mockResponse(
'PATCH',
'/objects/people/records/test-id',
{
data: {
data: {
id: { record_id: 'test-id' },
values: { email_addresses: [] },
},
},
}
);
await handleManagePersonEmails({
record_id: 'test-id',
operation: 'add',
email_addresses: ['new@example.com'],
});
const patchCalls = mockClient.getCallsFor('PATCH');
expect(patchCalls).toHaveLength(1);
});
});
describe('REMOVE operation', () => {
it('should remove specified emails', async () => {
const getCurrentResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'keep@example.com' },
{ email_address: 'remove1@example.com' },
{ email_address: 'remove2@example.com' },
],
},
},
};
const putResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'keep@example.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getCurrentResponse }
);
mockClient.mockResponse(
'PUT',
'/objects/people/records/person-123',
{ data: putResponse }
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'remove',
email_addresses: ['remove1@example.com', 'remove2@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.operation).toBe('remove');
expect(parsed.data.emails_removed).toEqual(['remove1@example.com', 'remove2@example.com']);
expect(parsed.data.previous_emails).toEqual(['keep@example.com', 'remove1@example.com', 'remove2@example.com']);
expect(parsed.data.current_emails).toEqual(['keep@example.com']);
});
it('should skip emails that do not exist', async () => {
const getCurrentResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'existing@example.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getCurrentResponse }
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'remove',
email_addresses: ['nonexistent@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.emails_removed).toEqual([]);
expect(parsed.data.message).toContain('None of the requested email addresses exist');
});
it('should handle case-insensitive removal', async () => {
const getCurrentResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'Test@Example.com' },
],
},
},
};
const putResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getCurrentResponse }
);
mockClient.mockResponse(
'PUT',
'/objects/people/records/person-123',
{ data: putResponse }
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'remove',
email_addresses: ['test@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.emails_removed).toEqual(['test@example.com']);
});
});
describe('CLEAR operation', () => {
it('should remove all emails', async () => {
const getResponse = {
data: {
id: { record_id: 'person-123' },
values: {
email_addresses: [
{ email_address: 'email1@example.com' },
{ email_address: 'email2@example.com' },
],
},
},
};
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{ data: getResponse }
);
mockClient.mockResponse(
'PUT',
'/objects/people/records/person-123',
{
data: {
data: {
id: { record_id: 'person-123' },
values: { email_addresses: [] },
},
},
}
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'clear',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.operation).toBe('clear');
expect(parsed.data.previous_emails).toEqual(['email1@example.com', 'email2@example.com']);
expect(parsed.data.current_emails).toEqual([]);
expect(parsed.data.message).toContain('cleared');
});
it('should not require email_addresses parameter for clear operation', async () => {
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{
data: {
data: {
id: { record_id: 'person-123' },
values: { email_addresses: [] },
},
},
}
);
mockClient.mockResponse(
'PUT',
'/objects/people/records/person-123',
{
data: {
data: {
id: { record_id: 'person-123' },
values: { email_addresses: [] },
},
},
}
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'clear',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
});
describe('Validation errors', () => {
it('should fail if ATTIO_API_KEY is not configured', async () => {
delete process.env.ATTIO_API_KEY;
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'set',
email_addresses: ['test@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('validation_error');
expect(parsed.error.message).toContain('ATTIO_API_KEY');
});
it('should fail if record_id is empty', async () => {
const result = await handleManagePersonEmails({
record_id: '',
operation: 'set',
email_addresses: ['test@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('record_id');
});
it('should fail if operation is invalid', async () => {
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'invalid',
email_addresses: ['test@example.com'],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('operation');
expect(parsed.error.message).toContain('set, add, remove, clear');
});
it('should fail if email_addresses is missing for add operation', async () => {
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'add',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('email_addresses');
});
it('should fail if all emails are empty after filtering', async () => {
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'remove',
email_addresses: ['', ' '],
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('email_addresses');
});
});
describe('API errors', () => {
it('should handle 404 Not Found', async () => {
const notFoundError = loadFixture('api-errors.json', 'not-found');
mockClient.mockResponse(
'GET',
'/objects/people/records/nonexistent',
{
error: {
statusCode: notFoundError.error.status,
message: notFoundError.error.message,
},
}
);
const result = await handleManagePersonEmails({
record_id: 'nonexistent',
operation: 'clear',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('not_found_error');
});
it('should handle authentication errors', async () => {
const authError = loadFixture('api-errors.json', 'authentication');
mockClient.mockResponse(
'GET',
'/objects/people/records/person-123',
{
error: {
statusCode: authError.error.status,
message: authError.error.message,
},
}
);
const result = await handleManagePersonEmails({
record_id: 'person-123',
operation: 'clear',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('authentication_error');
});
});
});