stackConsult commited on
Commit
eacd0b8
Β·
verified Β·
1 Parent(s): 89ae4ff

Create test_enrichment.py

Browse files
Files changed (1) hide show
  1. test_enrichment.py +546 -0
test_enrichment.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data Enrichment Testing Suite
3
+
4
+ This module provides comprehensive testing for the data_enrichment.py module.
5
+ It validates all 14 API integrations, caching mechanisms, error handling,
6
+ and batch processing capabilities.
7
+
8
+ Usage:
9
+ python test_enrichment.py
10
+
11
+ Or run specific test:
12
+ python -m unittest test_enrichment.TestDataEnrichmentManager.test_linkedin_enrichment
13
+
14
+ Requirements:
15
+ - All RAPIDAPI_KEY_* environment variables must be set
16
+ - Internet connection required for API calls
17
+ - Test user: 'test_user' (creates test cache directory)
18
+
19
+ Test Coverage:
20
+ - Initialization and configuration
21
+ - Individual API endpoint testing (14 APIs)
22
+ - Full contact enrichment workflow
23
+ - Batch processing capabilities
24
+ - Caching mechanisms and TTL
25
+ - Error handling and fallback logic
26
+ - Data privacy and per-user isolation
27
+ - Statistics and monitoring
28
+
29
+ Author: StrategyStack Team
30
+ Version: 2.0.0
31
+ Last Updated: November 6, 2025
32
+ """
33
+
34
+ import unittest
35
+ import os
36
+ import sys
37
+ import json
38
+ import logging
39
+ import time
40
+ from datetime import datetime, timedelta
41
+
42
+ # Add parent directory to path to import data_enrichment
43
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
44
+
45
+ from data_enrichment import DataEnrichmentManager
46
+
47
+ # Configure logging for tests
48
+ logging.basicConfig(
49
+ level=logging.INFO,
50
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
51
+ )
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ class TestDataEnrichmentManager(unittest.TestCase):
56
+ """
57
+ Comprehensive test suite for DataEnrichmentManager.
58
+
59
+ This test class validates all functionality of the data enrichment module
60
+ including API integrations, caching, error handling, and batch processing.
61
+ """
62
+
63
+ @classmethod
64
+ def setUpClass(cls):
65
+ """
66
+ Set up test environment once before all tests.
67
+
68
+ Creates a test user instance and validates that all required
69
+ API keys are configured in the environment.
70
+ """
71
+ logger.info("="*70)
72
+ logger.info("Starting Data Enrichment Test Suite")
73
+ logger.info("="*70)
74
+
75
+ # Test user for all tests
76
+ cls.test_username = "test_user"
77
+
78
+ # Initialize the enrichment manager
79
+ cls.manager = DataEnrichmentManager(cls.test_username)
80
+
81
+ # Test data samples
82
+ cls.test_linkedin_url = "https://www.linkedin.com/in/williamhgates/"
83
+ cls.test_company_url = "https://www.linkedin.com/company/microsoft/"
84
+ cls.test_email = "test@example.com"
85
+ cls.test_domain = "microsoft.com"
86
+ cls.test_twitter_handle = "BillGates"
87
+ cls.test_business_query = "Microsoft Redmond WA"
88
+
89
+ logger.info(f"Test manager initialized for user: {cls.test_username}")
90
+ logger.info(f"Cache directory: {cls.manager.cache_dir}")
91
+
92
+ @classmethod
93
+ def tearDownClass(cls):
94
+ """Clean up after all tests complete."""
95
+ logger.info("="*70)
96
+ logger.info("Data Enrichment Test Suite Completed")
97
+ logger.info("="*70)
98
+
99
+ def test_01_initialization(self):
100
+ """
101
+ Test proper initialization of DataEnrichmentManager.
102
+
103
+ Validates:
104
+ - Cache directory creation
105
+ - Per-user data isolation
106
+ - API key configuration
107
+ - Stats initialization
108
+ """
109
+ logger.info("\nTest 1: Initialization and Configuration")
110
+ logger.info("-" * 50)
111
+
112
+ # Check cache directory exists
113
+ self.assertTrue(
114
+ os.path.exists(self.manager.cache_dir),
115
+ "Cache directory should be created"
116
+ )
117
+
118
+ # Verify per-user isolation
119
+ self.assertIn(
120
+ self.test_username,
121
+ self.manager.cache_dir,
122
+ "Cache directory should include username for data isolation"
123
+ )
124
+
125
+ # Check stats initialization
126
+ stats = self.manager.get_enrichment_stats()
127
+ self.assertIsNotNone(stats, "Stats should be initialized")
128
+ self.assertIn('total_api_calls', stats)
129
+ self.assertIn('cache_hits', stats)
130
+ self.assertIn('cache_misses', stats)
131
+
132
+ logger.info(f"βœ“ Cache directory verified: {self.manager.cache_dir}")
133
+ logger.info(f"βœ“ Per-user isolation confirmed")
134
+ logger.info(f"βœ“ Stats initialized: {stats}")
135
+
136
+ def test_02_linkedin_profile_enrichment(self):
137
+ """
138
+ Test LinkedIn profile enrichment with fallback providers.
139
+
140
+ Tests all 4 LinkedIn profile API providers:
141
+ - Fresh LinkedIn Profile Data
142
+ - RockAPI LinkedIn
143
+ - LiScraper
144
+ - LinkedIn Profile Scraper
145
+ """
146
+ logger.info("\nTest 2: LinkedIn Profile Enrichment")
147
+ logger.info("-" * 50)
148
+
149
+ result = self.manager.enrich_linkedin_profile(self.test_linkedin_url)
150
+
151
+ if result:
152
+ logger.info(f"βœ“ LinkedIn profile enrichment successful")
153
+ logger.info(f" Profile data keys: {list(result.keys())[:5]}...")
154
+ self.assertIsInstance(result, dict, "Result should be a dictionary")
155
+ else:
156
+ logger.warning("βœ— LinkedIn enrichment failed (may be API limit or network issue)")
157
+ self.assertIsNone(result, "Failed enrichment should return None")
158
+
159
+ def test_03_company_enrichment(self):
160
+ """
161
+ Test LinkedIn company profile enrichment.
162
+
163
+ Validates company data extraction from LinkedIn company pages.
164
+ """
165
+ logger.info("\nTest 3: LinkedIn Company Enrichment")
166
+ logger.info("-" * 50)
167
+
168
+ result = self.manager.enrich_company(self.test_company_url)
169
+
170
+ if result:
171
+ logger.info(f"βœ“ Company enrichment successful")
172
+ logger.info(f" Company data keys: {list(result.keys())[:5]}...")
173
+ self.assertIsInstance(result, dict)
174
+ else:
175
+ logger.warning("βœ— Company enrichment failed")
176
+
177
+ def test_04_apollo_enrichment(self):
178
+ """
179
+ Test Apollo.io person enrichment.
180
+
181
+ Validates contact enrichment using email or domain.
182
+ """
183
+ logger.info("\nTest 4: Apollo.io Person Enrichment")
184
+ logger.info("-" * 50)
185
+
186
+ result = self.manager.enrich_apollo_person(self.test_email)
187
+
188
+ if result:
189
+ logger.info(f"βœ“ Apollo enrichment successful")
190
+ logger.info(f" Person data keys: {list(result.keys())[:5]}...")
191
+ self.assertIsInstance(result, dict)
192
+ else:
193
+ logger.warning("βœ— Apollo enrichment failed")
194
+
195
+ def test_05_twitter_enrichment(self):
196
+ """
197
+ Test Twitter user data scraping.
198
+
199
+ Validates Twitter profile data extraction.
200
+ """
201
+ logger.info("\nTest 5: Twitter User Scraping")
202
+ logger.info("-" * 50)
203
+
204
+ result = self.manager.scrape_twitter_user(self.test_twitter_handle)
205
+
206
+ if result:
207
+ logger.info(f"βœ“ Twitter scraping successful")
208
+ logger.info(f" Twitter data keys: {list(result.keys())[:5]}...")
209
+ self.assertIsInstance(result, dict)
210
+ else:
211
+ logger.warning("βœ— Twitter scraping failed")
212
+
213
+ def test_06_facebook_enrichment(self):
214
+ """
215
+ Test Facebook data scraping.
216
+
217
+ Validates Facebook profile/page data extraction.
218
+ """
219
+ logger.info("\nTest 6: Facebook Data Scraping")
220
+ logger.info("-" * 50)
221
+
222
+ # Note: Facebook scraping requires valid page/profile URL
223
+ # This test may fail without proper test data
224
+ result = self.manager.scrape_facebook_data({"url": "https://www.facebook.com/Microsoft"})
225
+
226
+ if result:
227
+ logger.info(f"βœ“ Facebook scraping successful")
228
+ self.assertIsInstance(result, dict)
229
+ else:
230
+ logger.warning("βœ— Facebook scraping failed")
231
+
232
+ def test_07_google_search(self):
233
+ """
234
+ Test Google Search API integration.
235
+
236
+ Validates web search functionality.
237
+ """
238
+ logger.info("\nTest 7: Google Search")
239
+ logger.info("-" * 50)
240
+
241
+ query = "Microsoft CEO"
242
+ result = self.manager.search_google(query)
243
+
244
+ if result:
245
+ logger.info(f"βœ“ Google search successful")
246
+ logger.info(f" Search results count: {len(result) if isinstance(result, list) else 'N/A'}")
247
+ else:
248
+ logger.warning("βœ— Google search failed")
249
+
250
+ def test_08_mediafy_enrichment(self):
251
+ """
252
+ Test Mediafy API integration.
253
+
254
+ Validates media data extraction.
255
+ """
256
+ logger.info("\nTest 8: Mediafy Enrichment")
257
+ logger.info("-" * 50)
258
+
259
+ result = self.manager.enrich_mediafy(self.test_domain)
260
+
261
+ if result:
262
+ logger.info(f"βœ“ Mediafy enrichment successful")
263
+ self.assertIsInstance(result, dict)
264
+ else:
265
+ logger.warning("βœ— Mediafy enrichment failed")
266
+
267
+ def test_09_local_business_scraping(self):
268
+ """
269
+ Test LetScrape Local Business Data API.
270
+
271
+ Validates local business information extraction.
272
+ """
273
+ logger.info("\nTest 9: Local Business Scraping")
274
+ logger.info("-" * 50)
275
+
276
+ result = self.manager.scrape_local_business(self.test_business_query)
277
+
278
+ if result:
279
+ logger.info(f"βœ“ Local business scraping successful")
280
+ logger.info(f" Business data keys: {list(result.keys())[:5]}...")
281
+ self.assertIsInstance(result, dict)
282
+ else:
283
+ logger.warning("βœ— Local business scraping failed")
284
+
285
+ def test_10_website_contacts_scraping(self):
286
+ """
287
+ Test LetScrape Website Contacts Scraper API.
288
+
289
+ Validates contact information extraction from websites.
290
+ """
291
+ logger.info("\nTest 10: Website Contacts Scraping")
292
+ logger.info("-" * 50)
293
+
294
+ result = self.manager.scrape_website_contacts(self.test_domain)
295
+
296
+ if result:
297
+ logger.info(f"βœ“ Website contacts scraping successful")
298
+ logger.info(f" Contact data keys: {list(result.keys())[:5]}...")
299
+ self.assertIsInstance(result, dict)
300
+ else:
301
+ logger.warning("βœ— Website contacts scraping failed")
302
+
303
+ def test_11_full_contact_enrichment(self):
304
+ """
305
+ Test comprehensive full contact enrichment workflow.
306
+
307
+ This test validates the complete enrichment pipeline that:
308
+ - Enriches LinkedIn profile
309
+ - Enriches company data
310
+ - Enriches with Apollo
311
+ - Scrapes social media
312
+ - Extracts website contacts
313
+ - Aggregates all data sources
314
+ """
315
+ logger.info("\nTest 11: Full Contact Enrichment Workflow")
316
+ logger.info("-" * 50)
317
+
318
+ contact_data = {
319
+ "email": self.test_email,
320
+ "linkedin_url": self.test_linkedin_url,
321
+ "company_url": self.test_company_url,
322
+ "twitter_handle": self.test_twitter_handle,
323
+ "domain": self.test_domain
324
+ }
325
+
326
+ result = self.manager.enrich_contact_full(contact_data)
327
+
328
+ self.assertIsNotNone(result, "Full enrichment should return data")
329
+ self.assertIsInstance(result, dict)
330
+
331
+ logger.info(f"βœ“ Full enrichment completed")
332
+ logger.info(f" Total data sources: {len(result)}")
333
+ logger.info(f" Data keys: {list(result.keys())}")
334
+
335
+ def test_12_batch_enrichment(self):
336
+ """
337
+ Test batch processing capabilities.
338
+
339
+ Validates processing multiple contacts efficiently with caching.
340
+ """
341
+ logger.info("\nTest 12: Batch Enrichment Processing")
342
+ logger.info("-" * 50)
343
+
344
+ contacts = [
345
+ {"email": "test1@example.com", "linkedin_url": self.test_linkedin_url},
346
+ {"email": "test2@example.com", "domain": self.test_domain},
347
+ {"email": "test3@example.com", "twitter_handle": self.test_twitter_handle}
348
+ ]
349
+
350
+ results = self.manager.enrich_contacts_batch(contacts)
351
+
352
+ self.assertIsInstance(results, list, "Batch results should be a list")
353
+ self.assertEqual(len(results), len(contacts), "Should return result for each contact")
354
+
355
+ logger.info(f"βœ“ Batch processing completed")
356
+ logger.info(f" Processed {len(contacts)} contacts")
357
+ logger.info(f" Successful enrichments: {sum(1 for r in results if r)}")
358
+
359
+ def test_13_caching_mechanism(self):
360
+ """
361
+ Test caching functionality and cache hits.
362
+
363
+ Validates:
364
+ - Data is cached after first API call
365
+ - Subsequent calls use cache (faster)
366
+ - Cache stats are updated correctly
367
+ """
368
+ logger.info("\nTest 13: Caching Mechanism")
369
+ logger.info("-" * 50)
370
+
371
+ # Clear stats
372
+ self.manager.stats = {
373
+ 'total_api_calls': 0,
374
+ 'cache_hits': 0,
375
+ 'cache_misses': 0,
376
+ 'total_enrichments': 0,
377
+ 'successful_enrichments': 0,
378
+ 'failed_enrichments': 0
379
+ }
380
+
381
+ # First call - should be cache miss
382
+ test_data = {"email": "cache_test@example.com", "linkedin_url": self.test_linkedin_url}
383
+ result1 = self.manager.enrich_contact_full(test_data)
384
+
385
+ stats_after_first = self.manager.get_enrichment_stats()
386
+ initial_cache_misses = stats_after_first['cache_misses']
387
+
388
+ # Second call - should use cache
389
+ result2 = self.manager.enrich_contact_full(test_data)
390
+
391
+ stats_after_second = self.manager.get_enrichment_stats()
392
+
393
+ logger.info(f"βœ“ First call stats: {stats_after_first}")
394
+ logger.info(f"βœ“ Second call stats: {stats_after_second}")
395
+ logger.info(f" Cache hit improvement: {stats_after_second['cache_hits'] > stats_after_first['cache_hits']}")
396
+
397
+ # Verify caching worked
398
+ self.assertGreater(
399
+ stats_after_second['cache_hits'],
400
+ stats_after_first['cache_hits'],
401
+ "Second call should increase cache hits"
402
+ )
403
+
404
+ def test_14_cache_invalidation(self):
405
+ """
406
+ Test cache invalidation functionality.
407
+
408
+ Validates:
409
+ - Cache can be invalidated for specific contact
410
+ - Cache can be cleared entirely
411
+ - TTL-based expiration works correctly
412
+ """
413
+ logger.info("\nTest 14: Cache Invalidation")
414
+ logger.info("-" * 50)
415
+
416
+ test_key = "test_invalidation@example.com"
417
+
418
+ # Create cached data
419
+ self.manager._cache_result("test_api", test_key, {"test": "data"})
420
+
421
+ # Verify it's cached
422
+ cached = self.manager._get_cached_result("test_api", test_key)
423
+ self.assertIsNotNone(cached, "Data should be cached")
424
+
425
+ # Invalidate cache for this key
426
+ self.manager.invalidate_cache(test_key)
427
+
428
+ # Verify it's gone
429
+ cached_after = self.manager._get_cached_result("test_api", test_key)
430
+ self.assertIsNone(cached_after, "Cache should be invalidated")
431
+
432
+ logger.info(f"βœ“ Cache invalidation successful")
433
+
434
+ def test_15_error_handling(self):
435
+ """
436
+ Test error handling and graceful degradation.
437
+
438
+ Validates:
439
+ - Invalid inputs return None
440
+ - API failures are caught and logged
441
+ - Fallback providers are tried
442
+ - Stats track failures correctly
443
+ """
444
+ logger.info("\nTest 15: Error Handling")
445
+ logger.info("-" * 50)
446
+
447
+ # Test with invalid data
448
+ invalid_url = "not_a_valid_url"
449
+ result = self.manager.enrich_linkedin_profile(invalid_url)
450
+
451
+ self.assertIsNone(result, "Invalid input should return None")
452
+
453
+ logger.info(f"βœ“ Error handling verified")
454
+ logger.info(f" Invalid input handled gracefully")
455
+
456
+ def test_16_statistics_tracking(self):
457
+ """
458
+ Test statistics tracking functionality.
459
+
460
+ Validates:
461
+ - API call stats are tracked
462
+ - Success/failure rates are calculated
463
+ - Cache hit ratios are recorded
464
+ """
465
+ logger.info("\nTest 16: Statistics Tracking")
466
+ logger.info("-" * 50)
467
+
468
+ stats = self.manager.get_enrichment_stats()
469
+
470
+ self.assertIsNotNone(stats)
471
+ self.assertIn('total_api_calls', stats)
472
+ self.assertIn('cache_hits', stats)
473
+ self.assertIn('cache_misses', stats)
474
+ self.assertIn('total_enrichments', stats)
475
+ self.assertIn('successful_enrichments', stats)
476
+ self.assertIn('failed_enrichments', stats)
477
+
478
+ # Calculate cache hit rate
479
+ total_cache_ops = stats['cache_hits'] + stats['cache_misses']
480
+ if total_cache_ops > 0:
481
+ cache_hit_rate = (stats['cache_hits'] / total_cache_ops) * 100
482
+ logger.info(f"βœ“ Cache hit rate: {cache_hit_rate:.2f}%")
483
+
484
+ # Calculate success rate
485
+ if stats['total_enrichments'] > 0:
486
+ success_rate = (stats['successful_enrichments'] / stats['total_enrichments']) * 100
487
+ logger.info(f"βœ“ Enrichment success rate: {success_rate:.2f}%")
488
+
489
+ logger.info(f"βœ“ Statistics tracking verified")
490
+ logger.info(f" Current stats: {stats}")
491
+
492
+ def test_17_data_privacy_isolation(self):
493
+ """
494
+ Test per-user data isolation.
495
+
496
+ Validates:
497
+ - Each user has separate cache directory
498
+ - User data cannot access other user data
499
+ - Cache paths include username
500
+ """
501
+ logger.info("\nTest 17: Data Privacy and Per-User Isolation")
502
+ logger.info("-" * 50)
503
+
504
+ # Create second user manager
505
+ user2_manager = DataEnrichmentManager("user2_test")
506
+
507
+ # Verify different cache directories
508
+ self.assertNotEqual(
509
+ self.manager.cache_dir,
510
+ user2_manager.cache_dir,
511
+ "Different users should have different cache directories"
512
+ )
513
+
514
+ self.assertIn(self.test_username, self.manager.cache_dir)
515
+ self.assertIn("user2_test", user2_manager.cache_dir)
516
+
517
+ logger.info(f"βœ“ Per-user isolation verified")
518
+ logger.info(f" User 1 cache: {self.manager.cache_dir}")
519
+ logger.info(f" User 2 cache: {user2_manager.cache_dir}")
520
+
521
+
522
+ if __name__ == "__main__":
523
+ # Run all tests
524
+ logger.info("\n" + "="*70)
525
+ logger.info("STARTING DATA ENRICHMENT TEST SUITE")
526
+ logger.info("="*70 + "\n")
527
+
528
+ # Create test suite
529
+ suite = unittest.TestLoader().loadTestsFromTestCase(TestDataEnrichmentManager)
530
+
531
+ # Run tests with verbose output
532
+ runner = unittest.TextTestRunner(verbosity=2)
533
+ result = runner.run(suite)
534
+
535
+ # Print summary
536
+ logger.info("\n" + "="*70)
537
+ logger.info("TEST SUMMARY")
538
+ logger.info("="*70)
539
+ logger.info(f"Tests run: {result.testsRun}")
540
+ logger.info(f"Successes: {result.testsRun - len(result.failures) - len(result.errors)}")
541
+ logger.info(f"Failures: {len(result.failures)}")
542
+ logger.info(f"Errors: {len(result.errors)}")
543
+ logger.info("="*70 + "\n")
544
+
545
+ # Exit with appropriate code
546
+ sys.exit(0 if result.wasSuccessful() else 1)