/**
* Tests for update_company tool
*
* Tests PATCH/PUT pattern for updating existing records.
* Demonstrates partial updates, field clearing, and validation.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { handleUpdateCompany } from '../../../src/tools/update-company.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_company', () => {
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 single field (name only)', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'companies',
record_id: 'rec-123',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/companies/rec-123',
values: {
name: [{ value: 'Updated Name' }],
},
},
};
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
data: mockResponse,
});
const result = await handleUpdateCompany({
record_id: 'rec-123',
name: 'Updated Name',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.name).toBe('Updated Name');
expect(parsed.data.record_id).toBe('rec-123');
});
it('should update multiple fields', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-123',
object_id: 'companies',
record_id: 'rec-456',
},
created_at: '2024-11-15T10:00:00Z',
web_url: 'https://app.attio.com/companies/rec-456',
values: {
name: [{ value: 'New Name' }],
description: [{ value: 'New description' }],
linkedin: [{ value: 'https://linkedin.com/company/new' }],
},
},
};
mockClient.mockResponse('PUT', '/objects/companies/records/rec-456', {
data: mockResponse,
});
const result = await handleUpdateCompany({
record_id: 'rec-456',
name: 'New Name',
description: 'New description',
linkedin: 'https://linkedin.com/company/new',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data).toMatchObject({
name: 'New Name',
description: 'New description',
linkedin: 'https://linkedin.com/company/new',
});
});
it('should use PUT method with correct path', async () => {
mockClient.mockResponse('PUT', '/objects/companies/records/test-id', {
data: {
data: {
id: { record_id: 'test-id' },
values: { name: [{ value: 'Test' }] },
},
},
});
await handleUpdateCompany({
record_id: 'test-id',
name: 'Test',
});
const calls = mockClient.getCallsFor('PUT', '/objects/companies/records/test-id');
expect(calls).toHaveLength(1);
});
it('should trim whitespace from string fields', async () => {
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
data: {
data: {
id: { record_id: 'rec-123' },
values: {},
},
},
});
await handleUpdateCompany({
record_id: 'rec-123',
name: ' Trimmed Name ',
description: ' Trimmed Desc ',
linkedin: ' https://linkedin.com ',
});
const calls = mockClient.getCallsFor('PUT');
expect(calls[0].body).toMatchObject({
data: {
values: {
name: [{ value: 'Trimmed Name' }],
description: [{ value: 'Trimmed Desc' }],
linkedin: [{ value: 'https://linkedin.com' }],
},
},
});
});
it('should clear description with empty string (when updating other fields)', async () => {
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
data: {
data: {
id: { record_id: 'rec-123' },
values: {},
},
},
});
await handleUpdateCompany({
record_id: 'rec-123',
name: 'Test', // 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 (when updating other fields)', async () => {
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
data: {
data: {
id: { record_id: 'rec-123' },
values: {},
},
},
});
await handleUpdateCompany({
record_id: 'rec-123',
name: 'Test', // Need at least one non-empty field
linkedin: '',
});
const calls = mockClient.getCallsFor('PUT');
expect(calls[0].body.data.values.linkedin).toEqual([]);
});
it('should not include name if empty after trim', async () => {
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
data: {
data: {
id: { record_id: 'rec-123' },
values: {},
},
},
});
await handleUpdateCompany({
record_id: 'rec-123',
name: ' ',
description: 'Some description', // Need at least one field
});
const calls = mockClient.getCallsFor('PUT');
expect(calls[0].body.data.values.name).toBeUndefined();
});
it('should include all metadata in response', async () => {
const mockResponse = {
data: {
id: {
workspace_id: 'ws-789',
object_id: 'companies',
record_id: 'rec-xyz',
},
created_at: '2024-11-15T12:00:00Z',
web_url: 'https://app.attio.com/companies/rec-xyz',
values: {
name: [{ value: 'Test' }],
},
},
};
mockClient.mockResponse('PUT', '/objects/companies/records/rec-xyz', {
data: mockResponse,
});
const result = await handleUpdateCompany({
record_id: 'rec-xyz',
name: 'Test',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.data).toMatchObject({
record_id: 'rec-xyz',
workspace_id: 'ws-789',
object_id: 'companies',
created_at: '2024-11-15T12:00:00Z',
web_url: 'https://app.attio.com/companies/rec-xyz',
});
});
});
describe('Validation errors', () => {
it('should fail if ATTIO_API_KEY is not configured', async () => {
delete process.env.ATTIO_API_KEY;
const result = await handleUpdateCompany({
record_id: 'rec-123',
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 handleUpdateCompany({
record_id: '',
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 handleUpdateCompany({
record_id: ' ',
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 handleUpdateCompany({
record_id: 'rec-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 handleUpdateCompany({
record_id: 'rec-123',
name: undefined,
description: 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/companies/records/nonexistent', {
error: {
statusCode: notFoundError.error.status,
message: notFoundError.error.message,
},
});
const result = await handleUpdateCompany({
record_id: 'nonexistent',
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 uniqueness constraint conflicts', async () => {
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
error: {
statusCode: 400,
message:
'uniqueness constraint violated: The value "Existing Name" provided for attribute with slug "name"',
},
});
const result = await handleUpdateCompany({
record_id: 'rec-123',
name: 'Existing Name',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.type).toBe('conflict_error');
expect(parsed.error.field).toBe('name');
});
it('should handle authentication errors', async () => {
const authError = loadFixture('api-errors.json', 'authentication');
mockClient.mockResponse('PUT', '/objects/companies/records/rec-123', {
error: {
statusCode: authError.error.status,
message: authError.error.message,
},
});
const result = await handleUpdateCompany({
record_id: 'rec-123',
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/companies/records/rec-123', {
data: {
data: {
id: { record_id: 'rec-123' },
values: {},
},
},
});
await handleUpdateCompany({
record_id: 'rec-123',
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/companies/records/rec-123', {
data: {
data: {
id: { record_id: 'rec-123' },
values: { description: [{ value: 'New desc' }] },
},
},
});
const result = await handleUpdateCompany({
record_id: 'rec-123',
description: 'New desc',
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
});
});