/ tests / utils / social-contact-extractor.test.js
social-contact-extractor.test.js
  1  /**
  2   * Tests for src/utils/social-contact-extractor.js
  3   *
  4   * Covers pure extraction/parsing helper functions:
  5   *   - classifyPlatform()
  6   *   - shouldSkip()
  7   *   - emptyResult()
  8   *   - findNestedValue()
  9   *   - escapeHtml()
 10   *
 11   * No browser, no network, no DB required.
 12   */
 13  
 14  import { test, describe } from 'node:test';
 15  import assert from 'node:assert/strict';
 16  
 17  import {
 18    classifyPlatform,
 19    shouldSkip,
 20    emptyResult,
 21    findNestedValue,
 22    escapeHtml,
 23  } from '../../src/utils/social-contact-extractor.js';
 24  
 25  // ─── classifyPlatform ───────────────────────────────────────────────────────
 26  
 27  describe('classifyPlatform', () => {
 28    test('identifies YouTube URLs', () => {
 29      assert.equal(classifyPlatform('https://www.youtube.com/c/SomeChannel'), 'youtube');
 30      assert.equal(classifyPlatform('https://youtu.be/abc123'), 'youtube');
 31      assert.equal(classifyPlatform('https://YOUTUBE.COM/watch?v=x'), 'youtube');
 32    });
 33  
 34    test('identifies LinkedIn company URLs', () => {
 35      assert.equal(classifyPlatform('https://www.linkedin.com/company/acme'), 'linkedin');
 36      assert.equal(classifyPlatform('https://linkedin.com/company/acme-corp'), 'linkedin');
 37    });
 38  
 39    test('does not identify LinkedIn personal profiles as linkedin', () => {
 40      // classifyPlatform only matches /company/ paths
 41      assert.equal(classifyPlatform('https://www.linkedin.com/in/john-doe'), null);
 42    });
 43  
 44    test('identifies Facebook URLs', () => {
 45      assert.equal(classifyPlatform('https://www.facebook.com/SomeBusiness'), 'facebook');
 46      assert.equal(classifyPlatform('https://facebook.com/page/12345'), 'facebook');
 47    });
 48  
 49    test('identifies Yelp business URLs', () => {
 50      assert.equal(classifyPlatform('https://www.yelp.com/biz/acme-plumbing-sydney'), 'yelp');
 51      assert.equal(classifyPlatform('https://yelp.com/biz/best-coffee'), 'yelp');
 52    });
 53  
 54    test('does not match Yelp non-biz paths', () => {
 55      assert.equal(classifyPlatform('https://www.yelp.com/search?find_desc=pizza'), null);
 56    });
 57  
 58    test('identifies Instagram URLs', () => {
 59      assert.equal(classifyPlatform('https://www.instagram.com/somebusiness'), 'instagram');
 60      assert.equal(classifyPlatform('https://instagram.com/user123'), 'instagram');
 61    });
 62  
 63    test('returns null for unknown platforms', () => {
 64      assert.equal(classifyPlatform('https://www.twitter.com/user'), null);
 65      assert.equal(classifyPlatform('https://example.com'), null);
 66      assert.equal(classifyPlatform('https://tiktok.com/@user'), null);
 67    });
 68  
 69    test('is case-insensitive', () => {
 70      assert.equal(classifyPlatform('HTTPS://WWW.FACEBOOK.COM/PAGE'), 'facebook');
 71      assert.equal(classifyPlatform('https://Instagram.Com/user'), 'instagram');
 72    });
 73  });
 74  
 75  // ─── shouldSkip ─────────────────────────────────────────────────────────────
 76  
 77  describe('shouldSkip', () => {
 78    test('skips Facebook profile.php?id= URLs (personal profiles)', () => {
 79      assert.equal(shouldSkip('https://facebook.com/profile.php?id=123456'), true);
 80    });
 81  
 82    test('skips Facebook group URLs', () => {
 83      assert.equal(shouldSkip('https://facebook.com/groups/local-biz'), true);
 84    });
 85  
 86    test('skips Instagram intent pages', () => {
 87      assert.equal(shouldSkip('https://instagram.com/intent/follow'), true);
 88    });
 89  
 90    test('skips share URLs', () => {
 91      assert.equal(shouldSkip('https://facebook.com/share?url=x'), true);
 92    });
 93  
 94    test('skips login pages', () => {
 95      assert.equal(shouldSkip('https://instagram.com/login'), true);
 96    });
 97  
 98    test('does not skip normal business pages', () => {
 99      assert.equal(shouldSkip('https://facebook.com/AcmePlumbing'), false);
100      assert.equal(shouldSkip('https://instagram.com/acme_biz'), false);
101      assert.equal(shouldSkip('https://yelp.com/biz/acme'), false);
102    });
103  
104    test('is case-insensitive', () => {
105      assert.equal(shouldSkip('https://facebook.com/GROUPS/something'), true);
106      assert.equal(shouldSkip('https://facebook.com/Profile.php?ID=123'), true);
107    });
108  });
109  
110  // ─── emptyResult ────────────────────────────────────────────────────────────
111  
112  describe('emptyResult', () => {
113    test('returns object with all expected empty arrays', () => {
114      const r = emptyResult();
115      assert.deepEqual(r.email_addresses, []);
116      assert.deepEqual(r.phone_numbers, []);
117      assert.deepEqual(r.social_profiles, []);
118      assert.deepEqual(r.key_pages, []);
119    });
120  
121    test('returns a new object each call (not shared reference)', () => {
122      const a = emptyResult();
123      const b = emptyResult();
124      assert.notEqual(a, b);
125      a.email_addresses.push({ email: 'test@test.com' });
126      assert.equal(b.email_addresses.length, 0);
127    });
128  });
129  
130  // ─── findNestedValue ────────────────────────────────────────────────────────
131  
132  describe('findNestedValue', () => {
133    test('finds a key at the top level', () => {
134      const obj = { aboutChannelViewModel: { description: 'Hello' } };
135      assert.equal(findNestedValue(obj, 'aboutChannelViewModel', 'description'), 'Hello');
136    });
137  
138    test('finds a key nested several levels deep', () => {
139      const obj = {
140        level1: {
141          level2: {
142            level3: {
143              aboutChannelViewModel: { description: 'Deep value', country: 'AU' },
144            },
145          },
146        },
147      };
148      assert.equal(findNestedValue(obj, 'aboutChannelViewModel', 'description'), 'Deep value');
149      assert.equal(findNestedValue(obj, 'aboutChannelViewModel', 'country'), 'AU');
150    });
151  
152    test('returns the target object itself when field is falsy', () => {
153      const target = { a: 1, b: 2 };
154      const obj = { wrapper: { myKey: target } };
155      assert.deepEqual(findNestedValue(obj, 'myKey', null), target);
156      assert.deepEqual(findNestedValue(obj, 'myKey', ''), target);
157      assert.deepEqual(findNestedValue(obj, 'myKey', undefined), target);
158    });
159  
160    test('returns undefined when key is not found', () => {
161      const obj = { a: { b: { c: 1 } } };
162      assert.equal(findNestedValue(obj, 'nonexistent', 'field'), undefined);
163    });
164  
165    test('returns undefined for null/non-object input', () => {
166      assert.equal(findNestedValue(null, 'key', 'field'), undefined);
167      assert.equal(findNestedValue(undefined, 'key', 'field'), undefined);
168      assert.equal(findNestedValue('string', 'key', 'field'), undefined);
169      assert.equal(findNestedValue(42, 'key', 'field'), undefined);
170    });
171  
172    test('returns undefined when target key exists but field does not', () => {
173      const obj = { aboutChannelViewModel: { description: 'Hello' } };
174      assert.equal(findNestedValue(obj, 'aboutChannelViewModel', 'nonexistent'), undefined);
175    });
176  
177    test('handles arrays in the nested structure', () => {
178      const obj = {
179        items: [
180          { id: 1 },
181          { id: 2, nested: { aboutChannelViewModel: { description: 'In array' } } },
182        ],
183      };
184      assert.equal(findNestedValue(obj, 'aboutChannelViewModel', 'description'), 'In array');
185    });
186  });
187  
188  // ─── escapeHtml ─────────────────────────────────────────────────────────────
189  
190  describe('escapeHtml', () => {
191    test('escapes ampersands', () => {
192      assert.equal(escapeHtml('Tom & Jerry'), 'Tom & Jerry');
193    });
194  
195    test('escapes less-than signs', () => {
196      assert.equal(escapeHtml('a < b'), 'a &lt; b');
197    });
198  
199    test('escapes greater-than signs', () => {
200      assert.equal(escapeHtml('a > b'), 'a &gt; b');
201    });
202  
203    test('escapes all three in a single string', () => {
204      assert.equal(escapeHtml('<b>Tom & Jerry</b>'), '&lt;b&gt;Tom &amp; Jerry&lt;/b&gt;');
205    });
206  
207    test('leaves safe strings untouched', () => {
208      assert.equal(escapeHtml('Hello World'), 'Hello World');
209      assert.equal(escapeHtml(''), '');
210    });
211  
212    test('handles multiple consecutive special characters', () => {
213      assert.equal(escapeHtml('<<>>&&'), '&lt;&lt;&gt;&gt;&amp;&amp;');
214    });
215  });