"""
Unit Tests for Contact Utilities
Tests all utility functions for working with Keap contact data
including email extraction, name formatting, tag handling, and more.
"""
from src.utils.contact_utils import (
get_primary_email,
get_full_name,
get_tag_ids,
get_custom_field_value,
format_contact_summary
)
class TestGetPrimaryEmail:
"""Test suite for get_primary_email function"""
def test_get_primary_email_with_primary_email(self):
"""Test getting primary email when marked as primary"""
contact = {
'email_addresses': [
{
'field': 'EMAIL',
'email': 'secondary@example.com',
'is_primary': False
},
{
'field': 'EMAIL',
'email': 'primary@example.com',
'is_primary': True
}
]
}
result = get_primary_email(contact)
assert result == 'primary@example.com'
def test_get_primary_email_fallback_to_first(self):
"""Test fallback to first email when no primary marked"""
contact = {
'email_addresses': [
{
'field': 'EMAIL',
'email': 'first@example.com'
},
{
'field': 'EMAIL',
'email': 'second@example.com'
}
]
}
result = get_primary_email(contact)
assert result == 'first@example.com'
def test_get_primary_email_no_email_addresses(self):
"""Test when contact has no email_addresses field"""
contact = {
'given_name': 'John',
'family_name': 'Doe'
}
result = get_primary_email(contact)
assert result == ''
def test_get_primary_email_empty_email_addresses(self):
"""Test when email_addresses is empty list"""
contact = {
'email_addresses': []
}
result = get_primary_email(contact)
assert result == ''
def test_get_primary_email_none_contact(self):
"""Test with None contact"""
result = get_primary_email(None)
assert result == ''
def test_get_primary_email_empty_contact(self):
"""Test with empty contact dict"""
result = get_primary_email({})
assert result == ''
def test_get_primary_email_non_email_fields(self):
"""Test with email addresses that have non-EMAIL fields"""
contact = {
'email_addresses': [
{
'field': 'OTHER',
'email': 'other@example.com'
},
{
'field': 'EMAIL',
'email': 'correct@example.com'
}
]
}
result = get_primary_email(contact)
assert result == 'correct@example.com'
def test_get_primary_email_missing_email_value(self):
"""Test when email address entry is missing email value"""
contact = {
'email_addresses': [
{
'field': 'EMAIL',
'is_primary': True
# Missing 'email' key
},
{
'field': 'EMAIL',
'email': 'fallback@example.com'
}
]
}
result = get_primary_email(contact)
assert result == '' # Should return empty string when missing email key
class TestGetFullName:
"""Test suite for get_full_name function"""
def test_get_full_name_both_names(self):
"""Test getting full name with both first and last names"""
contact = {
'given_name': 'John',
'family_name': 'Doe'
}
result = get_full_name(contact)
assert result == 'John Doe'
def test_get_full_name_only_first_name(self):
"""Test getting full name with only first name"""
contact = {
'given_name': 'John'
}
result = get_full_name(contact)
assert result == 'John'
def test_get_full_name_only_last_name(self):
"""Test getting full name with only last name"""
contact = {
'family_name': 'Doe'
}
result = get_full_name(contact)
assert result == 'Doe'
def test_get_full_name_empty_names(self):
"""Test getting full name with empty name values"""
contact = {
'given_name': '',
'family_name': ''
}
result = get_full_name(contact)
assert result == ''
def test_get_full_name_none_contact(self):
"""Test with None contact"""
result = get_full_name(None)
assert result == ''
def test_get_full_name_empty_contact(self):
"""Test with empty contact dict"""
result = get_full_name({})
assert result == ''
def test_get_full_name_whitespace_handling(self):
"""Test full name handles whitespace correctly"""
contact = {
'given_name': ' John ',
'family_name': ' Doe '
}
result = get_full_name(contact)
assert result == 'John Doe' # Internal spaces preserved, but edges stripped
class TestGetTagIds:
"""Test suite for get_tag_ids function"""
def test_get_tag_ids_from_tag_ids_field(self):
"""Test getting tag IDs from tag_ids field"""
contact = {
'tag_ids': ['100', '101', '102']
}
result = get_tag_ids(contact)
assert result == [100, 101, 102]
def test_get_tag_ids_from_tags_field(self):
"""Test getting tag IDs from tags field"""
contact = {
'tags': [
{'id': '200', 'name': 'Customer'},
{'id': '201', 'name': 'Premium'}
]
}
result = get_tag_ids(contact)
assert result == [200, 201]
def test_get_tag_ids_from_both_fields(self):
"""Test getting tag IDs from both tag_ids and tags fields"""
contact = {
'tag_ids': ['100', '101'],
'tags': [
{'id': '101', 'name': 'Duplicate'}, # Should not duplicate
{'id': '200', 'name': 'Customer'}
]
}
result = get_tag_ids(contact)
assert result == [100, 101, 200]
def test_get_tag_ids_integer_values(self):
"""Test getting tag IDs when they're already integers"""
contact = {
'tag_ids': [100, 101, 102]
}
result = get_tag_ids(contact)
assert result == [100, 101, 102]
def test_get_tag_ids_mixed_types(self):
"""Test getting tag IDs with mixed string and integer types"""
contact = {
'tag_ids': ['100', 101, '102']
}
result = get_tag_ids(contact)
assert result == [100, 101, 102]
def test_get_tag_ids_invalid_values(self):
"""Test getting tag IDs with invalid values that can't be converted"""
contact = {
'tag_ids': ['100', 'invalid', '102', None]
}
result = get_tag_ids(contact)
assert result == [100, 102] # Invalid values are skipped
def test_get_tag_ids_none_contact(self):
"""Test with None contact"""
result = get_tag_ids(None)
assert result == []
def test_get_tag_ids_empty_contact(self):
"""Test with empty contact dict"""
result = get_tag_ids({})
assert result == []
def test_get_tag_ids_non_list_tag_ids(self):
"""Test when tag_ids is not a list"""
contact = {
'tag_ids': 'not a list'
}
result = get_tag_ids(contact)
assert result == []
def test_get_tag_ids_non_list_tags(self):
"""Test when tags is not a list"""
contact = {
'tags': 'not a list'
}
result = get_tag_ids(contact)
assert result == []
def test_get_tag_ids_tags_without_id(self):
"""Test tags list with entries missing id field"""
contact = {
'tags': [
{'name': 'Customer'}, # Missing id
{'id': '200', 'name': 'Premium'}
]
}
result = get_tag_ids(contact)
assert result == [200]
class TestGetCustomFieldValue:
"""Test suite for get_custom_field_value function"""
def test_get_custom_field_value_list_format(self):
"""Test getting custom field value from list format (newer API)"""
contact = {
'custom_fields': [
{'id': '10', 'content': 'Value 1'},
{'id': '20', 'content': 'Value 2'}
]
}
result = get_custom_field_value(contact, 10)
assert result == 'Value 1'
result = get_custom_field_value(contact, '20')
assert result == 'Value 2'
def test_get_custom_field_value_dict_format(self):
"""Test getting custom field value from dict format (older API)"""
contact = {
'custom_fields': {
'10': {'value': 'Dict Value 1'},
'20': {'value': 'Dict Value 2'}
}
}
result = get_custom_field_value(contact, 10)
assert result == 'Dict Value 1'
result = get_custom_field_value(contact, '20')
assert result == 'Dict Value 2'
def test_get_custom_field_value_not_found(self):
"""Test getting custom field value when field not found"""
contact = {
'custom_fields': [
{'id': '10', 'content': 'Value 1'}
]
}
result = get_custom_field_value(contact, 99)
assert result is None
def test_get_custom_field_value_no_custom_fields(self):
"""Test when contact has no custom_fields"""
contact = {
'given_name': 'John'
}
result = get_custom_field_value(contact, 10)
assert result is None
def test_get_custom_field_value_none_contact(self):
"""Test with None contact"""
result = get_custom_field_value(None, 10)
assert result is None
def test_get_custom_field_value_empty_contact(self):
"""Test with empty contact dict"""
result = get_custom_field_value({}, 10)
assert result is None
def test_get_custom_field_value_invalid_list_format(self):
"""Test with invalid entries in list format"""
contact = {
'custom_fields': [
'not a dict',
{'id': '10', 'content': 'Valid Value'},
{'content': 'Missing ID'}
]
}
result = get_custom_field_value(contact, 10)
assert result == 'Valid Value'
def test_get_custom_field_value_missing_content_value(self):
"""Test when custom field entry is missing content/value"""
contact = {
'custom_fields': [
{'id': '10'}, # Missing content
]
}
result = get_custom_field_value(contact, 10)
assert result is None
# Test dict format missing value
contact = {
'custom_fields': {
'10': {} # Missing value
}
}
result = get_custom_field_value(contact, 10)
assert result is None
class TestFormatContactSummary:
"""Test suite for format_contact_summary function"""
def test_format_contact_summary_complete(self):
"""Test formatting complete contact summary"""
contact = {
'id': 12345,
'given_name': 'John',
'family_name': 'Doe',
'email_addresses': [
{
'field': 'EMAIL',
'email': 'john@example.com',
'is_primary': True
}
],
'create_time': '2023-01-01T00:00:00Z',
'update_time': '2023-06-01T00:00:00Z',
'tag_ids': ['100', '101']
}
result = format_contact_summary(contact)
expected = {
'id': 12345,
'first_name': 'John',
'last_name': 'Doe',
'email': 'john@example.com',
'created': '2023-01-01T00:00:00Z',
'updated': '2023-06-01T00:00:00Z',
'tag_count': 2
}
assert result == expected
def test_format_contact_summary_minimal(self):
"""Test formatting minimal contact summary"""
contact = {
'id': 12345
}
result = format_contact_summary(contact)
expected = {
'id': 12345,
'first_name': '',
'last_name': '',
'email': '',
'created': None,
'updated': None,
'tag_count': 0
}
assert result == expected
def test_format_contact_summary_none_contact(self):
"""Test formatting summary with None contact"""
result = format_contact_summary(None)
assert result == {}
def test_format_contact_summary_empty_contact(self):
"""Test formatting summary with empty contact"""
result = format_contact_summary({})
# The function returns an empty dict for completely empty contact
assert result == {}
def test_format_contact_summary_partial_data(self):
"""Test formatting summary with partial contact data"""
contact = {
'given_name': 'Jane',
'email_addresses': [
{
'field': 'EMAIL',
'email': 'jane@example.com'
}
],
'tags': [
{'id': '200', 'name': 'Customer'}
]
}
result = format_contact_summary(contact)
expected = {
'id': None,
'first_name': 'Jane',
'last_name': '',
'email': 'jane@example.com',
'created': None,
'updated': None,
'tag_count': 1
}
assert result == expected
class TestContactUtilsEdgeCases:
"""Test edge cases and integration scenarios"""
def test_complex_email_scenarios(self):
"""Test complex email address scenarios"""
contact = {
'email_addresses': [
{
'field': 'OTHER',
'email': 'other@example.com',
'is_primary': True # Primary but not EMAIL field
},
{
'field': 'EMAIL',
'email': 'work@example.com'
},
{
'field': 'EMAIL',
'email': 'personal@example.com',
'is_primary': False
}
]
}
# Should get first EMAIL field, not the OTHER field even if marked primary
result = get_primary_email(contact)
assert result == 'work@example.com'
def test_tag_ids_deduplication(self):
"""Test tag ID handling with duplicates"""
contact = {
'tag_ids': ['100', '101', '100'], # Duplicate 100
'tags': [
{'id': '101', 'name': 'Tag 1'}, # Duplicate 101
{'id': '102', 'name': 'Tag 2'}
]
}
result = get_tag_ids(contact)
# Current implementation preserves duplicates from tag_ids but deduplicates when adding from tags
assert result == [100, 101, 100, 102]
assert len(result) == 4
def test_custom_field_type_coercion(self):
"""Test custom field ID type coercion"""
contact = {
'custom_fields': [
{'id': 10, 'content': 'Integer ID'}, # Integer id
{'id': '20', 'content': 'String ID'} # String id
]
}
# Both should work regardless of input type
assert get_custom_field_value(contact, 10) == 'Integer ID'
assert get_custom_field_value(contact, '10') == 'Integer ID'
assert get_custom_field_value(contact, 20) == 'String ID'
assert get_custom_field_value(contact, '20') == 'String ID'
def test_unicode_and_special_characters(self):
"""Test handling of unicode and special characters"""
contact = {
'given_name': 'José María',
'family_name': 'García-López',
'email_addresses': [
{
'field': 'EMAIL',
'email': 'josé@García-López.com',
'is_primary': True
}
]
}
assert get_full_name(contact) == 'José María García-López'
assert get_primary_email(contact) == 'josé@García-López.com'
def test_large_data_handling(self):
"""Test handling of large data sets"""
# Large number of email addresses
email_addresses = [
{
'field': 'EMAIL',
'email': f'email{i}@example.com',
'is_primary': i == 50 # 50th is primary
}
for i in range(100)
]
contact = {
'email_addresses': email_addresses
}
result = get_primary_email(contact)
assert result == 'email50@example.com'
# Large number of tags
tag_ids = [str(i) for i in range(1000)]
contact = {'tag_ids': tag_ids}
result = get_tag_ids(contact)
assert len(result) == 1000
assert result[0] == 0
assert result[-1] == 999
def test_malformed_data_resilience(self):
"""Test behavior with malformed data"""
# The current implementation doesn't handle malformed email addresses gracefully
# This documents the current behavior rather than expected ideal behavior
contact = {
'email_addresses': [
{
'field': 'EMAIL',
'email': 'valid@example.com'
}
]
}
result = get_primary_email(contact)
assert result == 'valid@example.com'
# Malformed tags - current implementation handles this more gracefully
contact = {
'tags': [
'not a dict',
{'name': 'Missing ID'},
{'id': 'valid', 'name': 'Valid Tag'}
]
}
result = get_tag_ids(contact)
# Should gracefully handle the malformed entries
assert len(result) >= 0