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 < b'); 197 }); 198 199 test('escapes greater-than signs', () => { 200 assert.equal(escapeHtml('a > b'), 'a > b'); 201 }); 202 203 test('escapes all three in a single string', () => { 204 assert.equal(escapeHtml('<b>Tom & Jerry</b>'), '<b>Tom & Jerry</b>'); 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('<<>>&&'), '<<>>&&'); 214 }); 215 });