Skip to main content
Glama
company-validator-enhanced.test.tsβ€’12.9 kB
/** * Tests for the enhanced company validator with attribute type validation */ import { describe, beforeEach, it, expect, vi } from 'vitest'; import { CompanyValidator } from '@/validators/company-validator.js'; import { getAttributeTypeInfo } from '@/api/attribute-types.js'; import { InvalidRequestError } from '@/errors/api-errors.js'; import { MissingCompanyFieldError, InvalidCompanyDataError, } from '@/errors/company-errors.js'; // Mock the attribute type modules vi.mock('@/api/attribute-types.js', () => ({ getAttributeTypeInfo: vi.fn(), getFieldValidationRules: vi.fn(), detectFieldType: vi.fn(), })); describe('Enhanced Company Validator', () => { beforeEach(() => { vi.restoreAllMocks(); vi.clearAllMocks(); CompanyValidator.clearFieldTypeCache(); }); describe('validateAttributeTypes', () => { it('should validate and convert attributes based on their types', async () => { // Mock attribute type info for different fields (getAttributeTypeInfo as vi.Mock).mockImplementation( async (objectSlug, attributeName) => { switch (attributeName) { case 'name': return { fieldType: 'string', isArray: false, isRequired: true, isUnique: true, attioType: 'text', metadata: {}, }; case 'employees': return { fieldType: 'number', isArray: false, isRequired: false, isUnique: false, attioType: 'number', metadata: {}, }; case 'is_active': return { fieldType: 'boolean', isArray: false, isRequired: false, isUnique: false, attioType: 'checkbox', metadata: {}, }; case 'founded_date': return { fieldType: 'string', isArray: false, isRequired: false, isUnique: false, attioType: 'date', metadata: {}, }; default: return { fieldType: 'string', isArray: false, isRequired: false, isUnique: false, attioType: 'text', metadata: {}, }; } } ); // Test attribute validation with various types const attributes = { name: 'Acme Corporation', employees: '500', // String that should be converted to number is_active: 1, // Number that should be converted to boolean founded_date: '2023-01-15', // Date string description: 'Test company', // Regular string }; const result = await CompanyValidator.validateAttributeTypes(attributes); // Verify the attributes were converted correctly expect(result).toEqual({ name: 'Acme Corporation', employees: 500, // Should be converted to number is_active: true, // Should be converted to boolean founded_date: '2023-01-15', // Remains a string as the field type is string description: 'Test company', }); // Verify API calls expect(getAttributeTypeInfo).toHaveBeenCalledWith('companies', 'name'); expect(getAttributeTypeInfo).toHaveBeenCalledWith( 'companies', 'employees' ); expect(getAttributeTypeInfo).toHaveBeenCalledWith( 'companies', 'is_active' ); expect(getAttributeTypeInfo).toHaveBeenCalledWith( 'companies', 'founded_date' ); expect(getAttributeTypeInfo).toHaveBeenCalledWith( 'companies', 'description' ); }); it('should handle null values correctly', async () => { const attributes = { name: null, description: null, }; const result = await CompanyValidator.validateAttributeTypes(attributes); // Null values should remain null expect(result).toEqual({ name: null, description: null, }); // The API should not be called for null values expect(getAttributeTypeInfo).not.toHaveBeenCalled(); }); it('should throw an error for invalid attribute values', async () => { // Mock type info for a number field (getAttributeTypeInfo as vi.Mock).mockResolvedValue({ fieldType: 'number', isArray: false, isRequired: false, isUnique: false, attioType: 'number', metadata: {}, }); const attributes = { employees: 'not-a-number', // Invalid for number type }; // Should throw an InvalidRequestError await expect( CompanyValidator.validateAttributeTypes(attributes) ).rejects.toThrow(InvalidRequestError); // Verify the API call was made expect(getAttributeTypeInfo).toHaveBeenCalledWith( 'companies', 'employees' ); }); it('should proceed with original value if type info cannot be determined', async () => { // Mock getAttributeTypeInfo to throw an error (getAttributeTypeInfo as vi.Mock).mockRejectedValue( new Error('API error') ); const attributes = { custom_field: 'test value', }; // Should not throw but use the original value const result = await CompanyValidator.validateAttributeTypes(attributes); expect(result).toEqual({ custom_field: 'test value', }); // Verify the API call was attempted expect(getAttributeTypeInfo).toHaveBeenCalledWith( 'companies', 'custom_field' ); }); }); describe('validateCreate', () => { it('should validate required fields and enhance with type validation', async () => { // Mock validateFieldType and performSpecialValidation vi.spyOn(CompanyValidator as any, 'validateFieldType').mockResolvedValue( undefined ); vi.spyOn( CompanyValidator as any, 'performSpecialValidation' ).mockResolvedValue(undefined); // Mock validateAttributeTypes for type conversion vi.spyOn(CompanyValidator, 'validateAttributeTypes').mockResolvedValue({ name: 'Acme Corp', employees: 250, is_active: true, }); const attributes = { name: 'Acme Corp', employees: '250', is_active: 'yes', }; const result = await CompanyValidator.validateCreate(attributes); // Should return the validated and converted attributes expect(result).toEqual({ name: 'Acme Corp', employees: 250, is_active: true, }); // Should call validateAttributeTypes expect(CompanyValidator.validateAttributeTypes).toHaveBeenCalledWith( attributes ); }); it('should throw MissingCompanyFieldError if name is missing', async () => { const validateSpy = vi.spyOn(CompanyValidator, 'validateAttributeTypes'); const attributes = { employees: '250', // Missing required name field }; await expect(CompanyValidator.validateCreate(attributes)).rejects.toThrow( MissingCompanyFieldError ); // Should not reach validateAttributeTypes since validation fails early expect(validateSpy).not.toHaveBeenCalled(); }); }); describe('validateUpdate', () => { it('should validate company ID and enhance with type validation', async () => { // Mock validateFieldType and performSpecialValidation vi.spyOn(CompanyValidator as any, 'validateFieldType').mockResolvedValue( undefined ); vi.spyOn( CompanyValidator as any, 'performSpecialValidation' ).mockResolvedValue(undefined); // Mock validateAttributeTypes for type conversion vi.spyOn(CompanyValidator, 'validateAttributeTypes').mockResolvedValue({ name: 'Updated Corp', employees: 300, }); const companyId = 'comp_123456'; const attributes = { name: 'Updated Corp', employees: '300', }; const result = await CompanyValidator.validateUpdate( companyId, attributes ); // Should return the validated and converted attributes expect(result).toEqual({ name: 'Updated Corp', employees: 300, }); // Should call validateAttributeTypes expect(CompanyValidator.validateAttributeTypes).toHaveBeenCalledWith( attributes ); }); it('should throw InvalidCompanyDataError if company ID is invalid', async () => { const validateSpy = vi.spyOn(CompanyValidator, 'validateAttributeTypes'); const attributes = { name: 'Updated Corp', }; // Invalid company ID (empty string) await expect( CompanyValidator.validateUpdate('', attributes) ).rejects.toThrow(InvalidCompanyDataError); // Should not reach validateAttributeTypes since validation fails early expect(validateSpy).not.toHaveBeenCalled(); }); }); describe('validateAttributeUpdate', () => { it('should validate a single attribute and return converted value', async () => { // Mock validateFieldType vi.spyOn(CompanyValidator as any, 'validateFieldType').mockResolvedValue( undefined ); // Mock validateAttributeTypes for the single attribute vi.spyOn(CompanyValidator, 'validateAttributeTypes').mockImplementation( async () => ({ revenue: 1000000, }) ); const companyId = 'comp_123456'; const attributeName = 'revenue'; const attributeValue = '1000000'; const result = await CompanyValidator.validateAttributeUpdate( companyId, attributeName, attributeValue ); // Should return the converted value expect(result).toBe(1000000); // Should call validateAttributeTypes with the attribute object expect(CompanyValidator.validateAttributeTypes).toHaveBeenCalledWith({ revenue: '1000000', }); }); it('should throw InvalidCompanyDataError for an invalid company ID', async () => { await expect( CompanyValidator.validateAttributeUpdate( '', // Invalid company ID 'name', 'Test Company' ) ).rejects.toThrow(InvalidCompanyDataError); }); it('should throw InvalidCompanyDataError for an invalid attribute name', async () => { await expect( CompanyValidator.validateAttributeUpdate( 'comp_123456', '', // Invalid attribute name 'Test Value' ) ).rejects.toThrow(InvalidCompanyDataError); }); it('should reject LinkedIn URL attribute updates with deceptive hostnames', async () => { await expect( CompanyValidator.validateAttributeUpdate( 'comp_123456', 'linkedin_url', 'https://linkedin.com.attacker.com/company/example' ) ).rejects.toThrow('LinkedIn URL must be a valid LinkedIn URL'); }); }); describe('performSpecialValidation', () => { it('should reject website URLs that use javascript protocol', async () => { await expect( (CompanyValidator as any).performSpecialValidation({ website: 'javascript:alert(1)', }) ).rejects.toThrow('Website must use http or https protocol'); }); it('should reject LinkedIn URLs that use data protocol', async () => { await expect( (CompanyValidator as any).performSpecialValidation({ linkedin_url: 'data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==', }) ).rejects.toThrow( 'Invalid company data: LinkedIn URL must use http or https protocol' ); }); it('should reject LinkedIn URLs with deceptive hostnames', async () => { await expect( (CompanyValidator as any).performSpecialValidation({ linkedin_url: 'https://linkedin.com.evil.com/company/example', }) ).rejects.toThrow('LinkedIn URL must be a valid LinkedIn URL'); }); it('should allow LinkedIn subdomains', async () => { await expect( (CompanyValidator as any).performSpecialValidation({ linkedin_url: 'https://fr.linkedin.com/company/example', }) ).resolves.toBeUndefined(); }); it('should allow standard https URLs', async () => { await expect( (CompanyValidator as any).performSpecialValidation({ website: 'https://example.com', linkedin_url: 'https://www.linkedin.com/company/example', }) ).resolves.toBeUndefined(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server