/ tests / utils / compact-contacts.test.js
compact-contacts.test.js
  1  /**
  2   * Compact Contacts Tests
  3   *
  4   * Tests for compactContacts: deduplication of emails, phones, social profiles,
  5   * removal of empty/null values, and edge cases.
  6   * Pure function — no external dependencies.
  7   */
  8  
  9  import { test, describe } from 'node:test';
 10  import assert from 'node:assert/strict';
 11  
 12  import { compactContacts } from '../../src/utils/compact-contacts.js';
 13  
 14  // ─── null / edge input handling ──────────────────────────────────────────────
 15  
 16  describe('compactContacts — edge inputs', () => {
 17    test('returns null for null input', () => {
 18      assert.equal(compactContacts(null), null);
 19    });
 20  
 21    test('returns undefined for undefined input', () => {
 22      assert.equal(compactContacts(undefined), undefined);
 23    });
 24  
 25    test('returns array input unchanged (not an object)', () => {
 26      const arr = [1, 2, 3];
 27      assert.equal(compactContacts(arr), arr);
 28    });
 29  
 30    test('returns non-object types unchanged', () => {
 31      assert.equal(compactContacts(42), 42);
 32      assert.equal(compactContacts('string'), 'string');
 33      assert.equal(compactContacts(true), true);
 34    });
 35  
 36    test('returns new object (does not mutate original)', () => {
 37      const input = { business_name: 'Test' };
 38      const result = compactContacts(input);
 39      assert.notEqual(result, input);
 40      assert.deepStrictEqual(result, { business_name: 'Test' });
 41    });
 42  });
 43  
 44  // ─── email deduplication ─────────────────────────────────────────────────────
 45  
 46  describe('compactContacts — email deduplication', () => {
 47    test('keeps unique emails', () => {
 48      const input = {
 49        email_addresses: [
 50          { email: 'info@example.com', source: 'page' },
 51          { email: 'sales@example.com', source: 'footer' },
 52        ],
 53      };
 54      const result = compactContacts(input);
 55      assert.equal(result.email_addresses.length, 2);
 56    });
 57  
 58    test('deduplicates emails case-insensitively', () => {
 59      const input = {
 60        email_addresses: [
 61          { email: 'Info@Example.COM', source: 'header' },
 62          { email: 'info@example.com', source: 'footer' },
 63        ],
 64      };
 65      const result = compactContacts(input);
 66      assert.equal(result.email_addresses.length, 1);
 67      assert.equal(result.email_addresses[0].email, 'Info@Example.COM'); // keeps first
 68    });
 69  
 70    test('deduplicates emails with extra whitespace', () => {
 71      const input = {
 72        email_addresses: [
 73          { email: '  info@example.com  ', source: 'page' },
 74          { email: 'info@example.com', source: 'footer' },
 75        ],
 76      };
 77      const result = compactContacts(input);
 78      assert.equal(result.email_addresses.length, 1);
 79    });
 80  
 81    test('supports address field as fallback to email field', () => {
 82      const input = {
 83        email_addresses: [
 84          { address: 'info@example.com' },
 85          { email: 'info@example.com' },
 86        ],
 87      };
 88      const result = compactContacts(input);
 89      assert.equal(result.email_addresses.length, 1);
 90    });
 91  
 92    test('filters out null entries', () => {
 93      const input = {
 94        email_addresses: [null, { email: 'ok@test.com' }, undefined],
 95      };
 96      const result = compactContacts(input);
 97      assert.equal(result.email_addresses.length, 1);
 98    });
 99  
100    test('filters out non-object entries', () => {
101      const input = {
102        email_addresses: ['info@example.com', 42, { email: 'ok@test.com' }],
103      };
104      const result = compactContacts(input);
105      assert.equal(result.email_addresses.length, 1);
106      assert.equal(result.email_addresses[0].email, 'ok@test.com');
107    });
108  
109    test('filters out entries with empty email', () => {
110      const input = {
111        email_addresses: [
112          { email: '' },
113          { email: 'valid@test.com' },
114        ],
115      };
116      const result = compactContacts(input);
117      assert.equal(result.email_addresses.length, 1);
118    });
119  
120    test('removes email_addresses key if all entries filtered out', () => {
121      const input = {
122        email_addresses: [null, { email: '' }],
123      };
124      const result = compactContacts(input);
125      assert.equal(result.email_addresses, undefined);
126    });
127  });
128  
129  // ─── phone deduplication ─────────────────────────────────────────────────────
130  
131  describe('compactContacts — phone deduplication', () => {
132    test('keeps unique phone numbers', () => {
133      const input = {
134        phone_numbers: [
135          { number: '+61 400 000 001', type: 'mobile' },
136          { number: '+61 400 000 002', type: 'mobile' },
137        ],
138      };
139      const result = compactContacts(input);
140      assert.equal(result.phone_numbers.length, 2);
141    });
142  
143    test('deduplicates phone numbers ignoring whitespace', () => {
144      const input = {
145        phone_numbers: [
146          { number: '+61 400 000 001', type: 'mobile' },
147          { number: '+61400000001', type: 'office' },
148        ],
149      };
150      const result = compactContacts(input);
151      assert.equal(result.phone_numbers.length, 1);
152      assert.equal(result.phone_numbers[0].number, '+61 400 000 001'); // keeps first
153    });
154  
155    test('filters out null/undefined entries', () => {
156      const input = {
157        phone_numbers: [null, { number: '+61400000001' }, undefined],
158      };
159      const result = compactContacts(input);
160      assert.equal(result.phone_numbers.length, 1);
161    });
162  
163    test('filters out entries with empty number', () => {
164      const input = {
165        phone_numbers: [
166          { number: '' },
167          { number: '+61400000001' },
168        ],
169      };
170      const result = compactContacts(input);
171      assert.equal(result.phone_numbers.length, 1);
172    });
173  
174    test('filters out non-object entries', () => {
175      const input = {
176        phone_numbers: ['0400000001', { number: '+61400000001' }],
177      };
178      const result = compactContacts(input);
179      assert.equal(result.phone_numbers.length, 1);
180    });
181  
182    test('removes phone_numbers key if all entries filtered out', () => {
183      const input = {
184        phone_numbers: [null, { number: '' }],
185      };
186      const result = compactContacts(input);
187      assert.equal(result.phone_numbers, undefined);
188    });
189  });
190  
191  // ─── social profile deduplication ────────────────────────────────────────────
192  
193  describe('compactContacts — social profile deduplication', () => {
194    test('keeps unique social profiles', () => {
195      const input = {
196        social_profiles: [
197          { url: 'https://facebook.com/biz', platform: 'facebook' },
198          { url: 'https://twitter.com/biz', platform: 'twitter' },
199        ],
200      };
201      const result = compactContacts(input);
202      assert.equal(result.social_profiles.length, 2);
203    });
204  
205    test('deduplicates social profiles by URL', () => {
206      const input = {
207        social_profiles: [
208          { url: 'https://facebook.com/biz', platform: 'facebook' },
209          { url: 'https://facebook.com/biz', platform: 'fb' },
210        ],
211      };
212      const result = compactContacts(input);
213      assert.equal(result.social_profiles.length, 1);
214      assert.equal(result.social_profiles[0].platform, 'facebook'); // keeps first
215    });
216  
217    test('trims URL whitespace for dedup', () => {
218      const input = {
219        social_profiles: [
220          { url: '  https://facebook.com/biz  ' },
221          { url: 'https://facebook.com/biz' },
222        ],
223      };
224      const result = compactContacts(input);
225      assert.equal(result.social_profiles.length, 1);
226    });
227  
228    test('filters out entries with empty URL', () => {
229      const input = {
230        social_profiles: [
231          { url: '' },
232          { url: 'https://facebook.com/biz' },
233        ],
234      };
235      const result = compactContacts(input);
236      assert.equal(result.social_profiles.length, 1);
237    });
238  
239    test('filters out null entries', () => {
240      const input = {
241        social_profiles: [null, { url: 'https://facebook.com/biz' }],
242      };
243      const result = compactContacts(input);
244      assert.equal(result.social_profiles.length, 1);
245    });
246  
247    test('removes social_profiles key if all entries filtered out', () => {
248      const input = {
249        social_profiles: [null, { url: '' }],
250      };
251      const result = compactContacts(input);
252      assert.equal(result.social_profiles, undefined);
253    });
254  });
255  
256  // ─── empty value cleanup ─────────────────────────────────────────────────────
257  
258  describe('compactContacts — empty value cleanup', () => {
259    test('removes null values', () => {
260      const input = { business_name: null, website: 'https://test.com' };
261      const result = compactContacts(input);
262      assert.equal(result.business_name, undefined);
263      assert.equal(result.website, 'https://test.com');
264    });
265  
266    test('removes empty string values', () => {
267      const input = { business_name: '', website: 'https://test.com' };
268      const result = compactContacts(input);
269      assert.equal(result.business_name, undefined);
270    });
271  
272    test('removes empty array values', () => {
273      const input = { email_addresses: [], website: 'https://test.com' };
274      const result = compactContacts(input);
275      assert.equal(result.email_addresses, undefined);
276    });
277  
278    test('keeps zero as a value', () => {
279      const input = { count: 0 };
280      const result = compactContacts(input);
281      assert.equal(result.count, 0);
282    });
283  
284    test('keeps false as a value', () => {
285      const input = { verified: false };
286      const result = compactContacts(input);
287      assert.equal(result.verified, false);
288    });
289  
290    test('keeps non-empty arrays', () => {
291      const input = {
292        email_addresses: [{ email: 'test@test.com' }],
293      };
294      const result = compactContacts(input);
295      assert.equal(result.email_addresses.length, 1);
296    });
297  
298    test('keeps non-empty strings', () => {
299      const input = { business_name: 'Acme Corp' };
300      const result = compactContacts(input);
301      assert.equal(result.business_name, 'Acme Corp');
302    });
303  });
304  
305  // ─── full integration scenario ───────────────────────────────────────────────
306  
307  describe('compactContacts — full integration', () => {
308    test('handles a realistic contacts object with all dedup types', () => {
309      const input = {
310        email_addresses: [
311          { email: 'info@acme.com', source: 'header' },
312          { email: 'INFO@ACME.COM', source: 'footer' },
313          { email: 'sales@acme.com', source: 'contact' },
314          null,
315        ],
316        phone_numbers: [
317          { number: '+61 2 9999 0000', type: 'landline' },
318          { number: '+6129999 0000', type: 'office' },
319        ],
320        social_profiles: [
321          { url: 'https://facebook.com/acme', platform: 'facebook' },
322          { url: 'https://facebook.com/acme', platform: 'fb' },
323          { url: 'https://linkedin.com/company/acme', platform: 'linkedin' },
324        ],
325        business_name: 'Acme Corp',
326        notes: '',
327        archived: null,
328      };
329      const result = compactContacts(input);
330  
331      // Deduped correctly
332      assert.equal(result.email_addresses.length, 2); // info + sales
333      assert.equal(result.phone_numbers.length, 1); // whitespace dedup
334      assert.equal(result.social_profiles.length, 2); // fb dedup, linkedin kept
335  
336      // Empty values cleaned
337      assert.equal(result.notes, undefined);
338      assert.equal(result.archived, undefined);
339  
340      // Non-empty preserved
341      assert.equal(result.business_name, 'Acme Corp');
342    });
343  
344    test('returns empty object when all fields are null/empty', () => {
345      const input = {
346        email_addresses: [],
347        phone_numbers: [],
348        social_profiles: [],
349        notes: null,
350        tags: '',
351      };
352      const result = compactContacts(input);
353      assert.deepStrictEqual(result, {});
354    });
355  });