/**
* Tests for update_person tool
*
* Tests PATCH/PUT pattern for updating existing person records.
* Demonstrates partial updates, name field handling, and validation.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { handleUpdatePerson } from '../../../src/tools/update-person.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('update_person', () => {
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('Successful updates', () => {
it('should update first_name only', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'people',
record_id: 'person-123',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/people/person-123',
values: {
name: [
{
first_name: 'UpdatedFirst',
last_name: 'Doe',
full_name: 'UpdatedFirst Doe',
},
],
},
},
};
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: mockResponse,
});
const result = await handleUpdatePerson({
record_id: 'person-123',
first_name: 'UpdatedFirst',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.first_name).toBe('UpdatedFirst');
expect(parsed.data.record_id).toBe('person-123');
});
it('should update multiple name fields', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'people',
record_id: 'person-456',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/people/person-456',
values: {
name: [
{
first_name: 'Jane',
last_name: 'NewLast',
full_name: 'Jane NewLast',
},
],
},
},
};
mockClient.mockResponse('PUT', '/objects/people/records/person-456', {
data: mockResponse,
});
const result = await handleUpdatePerson({
record_id: 'person-456',
first_name: 'Jane',
last_name: 'NewLast',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data).toMatchObject({
first_name: 'Jane',
last_name: 'NewLast',
name: 'Jane NewLast',
});
});
it('should update description and linkedin', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'people',
record_id: 'person-123',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/people/person-123',
values: {
name: [{ first_name: 'John', last_name: 'Doe', full_name: 'John Doe' }],
description: [{ value: 'New description' }],
linkedin: [{ value: 'https://linkedin.com/in/johndoe' }],
},
},
};
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: mockResponse,
});
const result = await handleUpdatePerson({
record_id: 'person-123',
description: 'New description',
linkedin: 'https://linkedin.com/in/johndoe',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.description).toBe('New description');
expect(parsed.data.linkedin).toBe('https://linkedin.com/in/johndoe');
});
it('should use PUT method with correct path', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/test-id', {
data: {
data: {
id: { record_id: 'test-id' },
values: {
name: [{ first_name: 'Test', last_name: 'User', full_name: 'Test User' }],
},
},
},
});
await handleUpdatePerson({
record_id: 'test-id',
first_name: 'Test',
});
const calls = mockClient.getCallsFor('PUT', '/objects/people/records/test-id');
expect(calls).toHaveLength(1);
});
it('should trim whitespace from all fields', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: {},
},
},
});
await handleUpdatePerson({
record_id: 'person-123',
first_name: ' John ',
last_name: ' Doe ',
description: ' Trimmed desc ',
linkedin: ' https://linkedin.com ',
});
const calls = mockClient.getCallsFor('PUT');
const nameObj = calls[0].body.data.values.name[0];
expect(nameObj.first_name).toBe('John');
expect(nameObj.last_name).toBe('Doe');
expect(calls[0].body.data.values.description).toEqual([
{ value: 'Trimmed desc' },
]);
expect(calls[0].body.data.values.linkedin).toEqual([
{ value: 'https://linkedin.com' },
]);
});
it('should clear description with empty string', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: {},
},
},
});
await handleUpdatePerson({
record_id: 'person-123',
first_name: 'John', // Need at least one non-empty field
description: '',
});
const calls = mockClient.getCallsFor('PUT');
expect(calls[0].body.data.values.description).toEqual([]);
});
it('should clear linkedin with empty string', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: {},
},
},
});
await handleUpdatePerson({
record_id: 'person-123',
first_name: 'John', // Need at least one non-empty field
linkedin: '',
});
const calls = mockClient.getCallsFor('PUT');
expect(calls[0].body.data.values.linkedin).toEqual([]);
});
it('should include all metadata in response', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-789',
object_id: 'people',
record_id: 'person-xyz',
},
created_at: '2024-11-15T12:00:00Z',
web_url: 'https://app.attio.com/people/person-xyz',
values: {
name: [{ first_name: 'Test', last_name: 'User', full_name: 'Test User' }],
},
},
};
mockClient.mockResponse('PUT', '/objects/people/records/person-xyz', {
data: mockResponse,
});
const result = await handleUpdatePerson({
record_id: 'person-xyz',
first_name: 'Test',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data).toMatchObject({
record_id: 'person-xyz',
workspace_id: 'ws-789',
object_id: 'people',
created_at: '2024-11-15T12:00:00Z',
web_url: 'https://app.attio.com/people/person-xyz',
});
});
});
describe('Validation errors', () => {
it('should fail if ATTIO_API_KEY is not configured', async () => {
delete process.env.ATTIO_API_KEY;
const result = await handleUpdatePerson({
record_id: 'person-123',
first_name: 'Test',
});
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 handleUpdatePerson({
record_id: '',
first_name: 'Test',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('record_id');
});
it('should fail if record_id is whitespace', async () => {
const result = await handleUpdatePerson({
record_id: ' ',
first_name: 'Test',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.field).toBe('record_id');
});
it('should fail if no fields provided to update', async () => {
const result = await handleUpdatePerson({
record_id: 'person-123',
});
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('At least one field');
});
it('should fail if only undefined fields provided', async () => {
const result = await handleUpdatePerson({
record_id: 'person-123',
first_name: undefined,
last_name: undefined,
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('At least one field');
});
});
describe('API errors', () => {
it('should handle 404 Not Found', async () => {
const notFoundError = loadFixture('api-errors.json', 'not-found');
mockClient.mockResponse('PUT', '/objects/people/records/nonexistent', {
error: {
statusCode: notFoundError.error.status,
message: notFoundError.error.message,
},
});
const result = await handleUpdatePerson({
record_id: 'nonexistent',
first_name: 'Test',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('not_found_error');
expect(parsed.error.suggestions).toContain('Verify the record_id is correct');
});
it('should handle authentication errors', async () => {
const authError = loadFixture('api-errors.json', 'authentication');
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
error: {
statusCode: authError.error.status,
message: authError.error.message,
},
});
const result = await handleUpdatePerson({
record_id: 'person-123',
first_name: 'Test',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('authentication_error');
});
});
describe('Partial updates', () => {
it('should only send provided fields in request', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: {},
},
},
});
await handleUpdatePerson({
record_id: 'person-123',
first_name: 'New Name',
// description, linkedin not provided
});
const calls = mockClient.getCallsFor('PUT');
const values = calls[0].body.data.values;
expect(values.name).toBeDefined();
expect(values.description).toBeUndefined();
expect(values.linkedin).toBeUndefined();
});
it('should update only description field', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: { description: [{ value: 'New desc' }] },
},
},
});
const result = await handleUpdatePerson({
record_id: 'person-123',
description: 'New desc',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
});
describe('Name field handling', () => {
it('should handle empty first_name in name object', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: {},
},
},
});
await handleUpdatePerson({
record_id: 'person-123',
first_name: ' ', // Empty after trim
last_name: 'Doe',
});
const calls = mockClient.getCallsFor('PUT');
const nameObj = calls[0].body.data.values.name[0];
// Empty strings after trim should not be included
expect(nameObj.first_name).toBeUndefined();
expect(nameObj.last_name).toBe('Doe');
});
it('should create name object when any name field is provided', async () => {
mockClient.mockResponse('PUT', '/objects/people/records/person-123', {
data: {
data: {
id: { record_id: 'person-123' },
values: {},
},
},
});
await handleUpdatePerson({
record_id: 'person-123',
last_name: 'NewLast',
});
const calls = mockClient.getCallsFor('PUT');
expect(calls[0].body.data.values.name).toBeDefined();
expect(calls[0].body.data.values.name[0].last_name).toBe('NewLast');
});
});
});