nordabiz/tests/test_social_media_audit.py
Maciej Pienczyn 9cd5066afe auto-claude: subtask-3-3 - Add test_search_google_reviews_error for API error
Added test for search_google_reviews method to handle API errors gracefully.
The test mocks GooglePlacesSearcher to simulate a RequestException during
get_place_details and verifies that the method returns None values instead
of crashing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 20:39:11 +01:00

1207 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Unit Tests for Social Media Audit Functionality
================================================
Tests for:
- WebsiteAuditor (scripts/social_media_audit.py)
- GooglePlacesSearcher (scripts/social_media_audit.py)
- BraveSearcher (scripts/social_media_audit.py)
- SocialMediaAuditor (scripts/social_media_audit.py)
Run tests:
cd tests
python -m pytest test_social_media_audit.py -v
Author: Claude Code
Date: 2026-01-08
"""
import json
import sys
import unittest
from datetime import datetime, date, timedelta
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch, PropertyMock
# Add scripts directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))
# Import modules to test
from social_media_audit import (
WebsiteAuditor,
GooglePlacesSearcher,
BraveSearcher,
SocialMediaAuditor,
HOSTING_PROVIDERS,
SOCIAL_MEDIA_PATTERNS,
SOCIAL_MEDIA_EXCLUDE,
)
# ============================================================================
# WebsiteAuditor Tests
# ============================================================================
class TestWebsiteAuditor(unittest.TestCase):
"""Tests for WebsiteAuditor class."""
def setUp(self):
"""Set up test with WebsiteAuditor instance."""
self.auditor = WebsiteAuditor()
def test_auditor_initialization(self):
"""Test auditor initializes correctly."""
self.assertIsNotNone(self.auditor.session)
self.assertIn('User-Agent', self.auditor.session.headers)
def test_audit_website_empty_url(self):
"""Test audit with empty URL."""
result = self.auditor.audit_website('')
self.assertEqual(result['url'], '')
self.assertIn('No URL provided', result['errors'])
def test_audit_website_none_url(self):
"""Test audit with None URL."""
result = self.auditor.audit_website(None)
self.assertIn('No URL provided', result['errors'])
@patch.object(WebsiteAuditor, '_check_ssl')
@patch.object(WebsiteAuditor, '_detect_hosting')
@patch('requests.Session.get')
def test_audit_website_success(self, mock_get, mock_hosting, mock_ssl):
"""Test successful website audit."""
mock_ssl.return_value = {
'ssl_valid': True,
'ssl_expiry': date(2027, 1, 1),
'ssl_issuer': "Let's Encrypt",
}
mock_hosting.return_value = {
'hosting_provider': 'OVH',
'hosting_ip': '51.38.1.1',
}
mock_response = Mock()
mock_response.status_code = 200
mock_response.url = 'https://example.com/'
mock_response.headers = {
'Server': 'nginx/1.18.0',
'Last-Modified': 'Tue, 07 Jan 2026 10:00:00 GMT',
}
mock_response.text = '''
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Test Author">
<meta name="generator" content="WordPress 6.0">
</head>
<body>
<a href="https://facebook.com/testpage">Facebook</a>
</body>
</html>
'''
mock_get.return_value = mock_response
result = self.auditor.audit_website('https://example.com')
self.assertEqual(result['http_status'], 200)
self.assertTrue(result['has_ssl'])
self.assertTrue(result['ssl_valid'])
self.assertTrue(result['has_viewport_meta'])
self.assertTrue(result['is_mobile_friendly'])
self.assertEqual(result['site_author'], 'Test Author')
self.assertEqual(result['site_generator'], 'WordPress 6.0')
@patch('requests.Session.get')
def test_audit_website_ssl_error_fallback(self, mock_get):
"""Test SSL error with HTTP fallback."""
# First call (HTTPS) raises SSL error
# Second call (HTTP) succeeds
mock_http_response = Mock()
mock_http_response.status_code = 200
mock_http_response.url = 'http://example.com/'
mock_http_response.text = '<html><head></head><body></body></html>'
mock_http_response.headers = {}
import requests
mock_get.side_effect = [
requests.exceptions.SSLError('Certificate verify failed'),
mock_http_response,
]
result = self.auditor.audit_website('https://example.com')
self.assertEqual(result['http_status'], 200)
self.assertFalse(result['has_ssl'])
self.assertFalse(result['ssl_valid'])
self.assertIn('SSL Error', result['errors'][0])
def test_audit_website_normalizes_url(self):
"""Test URL normalization adds https://."""
with patch.object(self.auditor, '_check_ssl') as mock_ssl, \
patch.object(self.auditor, '_detect_hosting') as mock_hosting, \
patch('requests.Session.get') as mock_get:
mock_ssl.return_value = {}
mock_hosting.return_value = {}
mock_response = Mock()
mock_response.status_code = 200
mock_response.url = 'https://example.com/'
mock_response.text = '<html></html>'
mock_response.headers = {}
mock_get.return_value = mock_response
result = self.auditor.audit_website('example.com')
# Verify the URL was normalized before making request
mock_get.assert_called()
class TestWebsiteAuditorSSL(unittest.TestCase):
"""Tests for SSL checking functionality."""
def setUp(self):
"""Set up test with WebsiteAuditor instance."""
self.auditor = WebsiteAuditor()
@patch('socket.create_connection')
@patch('ssl.create_default_context')
def test_check_ssl_valid_certificate(self, mock_context, mock_connection):
"""Test SSL check with valid certificate."""
mock_sock = MagicMock()
mock_connection.return_value.__enter__.return_value = mock_sock
mock_ssock = MagicMock()
mock_ssock.getpeercert.return_value = {
'notAfter': 'Jan 1 00:00:00 2027 GMT',
'issuer': ((('organizationName', "Let's Encrypt"),),),
}
mock_context.return_value.wrap_socket.return_value.__enter__.return_value = mock_ssock
result = self.auditor._check_ssl('example.com')
self.assertTrue(result['ssl_valid'])
self.assertEqual(result['ssl_expiry'], date(2027, 1, 1))
self.assertEqual(result['ssl_issuer'], "Let's Encrypt")
@patch('socket.create_connection')
def test_check_ssl_connection_error(self, mock_connection):
"""Test SSL check with connection error."""
mock_connection.side_effect = ConnectionRefusedError('Connection refused')
result = self.auditor._check_ssl('example.com')
self.assertFalse(result['ssl_valid'])
class TestWebsiteAuditorHosting(unittest.TestCase):
"""Tests for hosting detection functionality."""
def setUp(self):
"""Set up test with WebsiteAuditor instance."""
self.auditor = WebsiteAuditor()
@patch('socket.gethostbyname')
def test_detect_hosting_by_ip_prefix(self, mock_gethostbyname):
"""Test hosting detection by IP prefix."""
mock_gethostbyname.return_value = '51.38.123.45'
result = self.auditor._detect_hosting('example.com')
self.assertEqual(result['hosting_ip'], '51.38.123.45')
self.assertEqual(result['hosting_provider'], 'OVH')
@patch('socket.gethostbyname')
def test_detect_hosting_cloudflare(self, mock_gethostbyname):
"""Test Cloudflare hosting detection."""
mock_gethostbyname.return_value = '104.16.123.45'
result = self.auditor._detect_hosting('example.com')
self.assertEqual(result['hosting_provider'], 'Cloudflare')
@patch('socket.gethostbyname')
@patch('socket.gethostbyaddr')
def test_detect_hosting_by_reverse_dns(self, mock_reverse, mock_gethostbyname):
"""Test hosting detection by reverse DNS."""
mock_gethostbyname.return_value = '192.168.1.1' # Unknown IP
mock_reverse.return_value = ('server.nazwa.pl', [], [])
result = self.auditor._detect_hosting('example.com')
self.assertEqual(result['hosting_provider'], 'nazwa.pl')
class TestWebsiteAuditorHTMLParsing(unittest.TestCase):
"""Tests for HTML parsing functionality."""
def setUp(self):
"""Set up test with WebsiteAuditor instance."""
self.auditor = WebsiteAuditor()
def test_parse_html_viewport_meta(self):
"""Test viewport meta detection."""
html = '''
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body></body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertTrue(result['has_viewport_meta'])
self.assertTrue(result['is_mobile_friendly'])
def test_parse_html_no_viewport(self):
"""Test detection when viewport is missing."""
html = '<html><head></head><body></body></html>'
result = self.auditor._parse_html(html)
self.assertFalse(result['has_viewport_meta'])
self.assertFalse(result['is_mobile_friendly'])
def test_parse_html_author_meta(self):
"""Test author meta extraction."""
html = '''
<html>
<head>
<meta name="author" content="Test Company">
</head>
<body></body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertEqual(result['site_author'], 'Test Company')
def test_parse_html_generator_meta(self):
"""Test generator/CMS detection."""
html = '''
<html>
<head>
<meta name="generator" content="WordPress 6.4">
</head>
<body></body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertEqual(result['site_generator'], 'WordPress 6.4')
def test_parse_html_footer_author(self):
"""Test author extraction from footer."""
html = '''
<html>
<head></head>
<body>
<footer>
Wykonanie: Test Agency
</footer>
</body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertIsNotNone(result['site_author'])
self.assertIn('Test Agency', result['site_author'])
def test_parse_html_social_media_facebook(self):
"""Test Facebook link extraction."""
html = '''
<html>
<head></head>
<body>
<a href="https://facebook.com/testcompany">Facebook</a>
</body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertIn('facebook', result['social_media_links'])
self.assertEqual(result['social_media_links']['facebook'], 'https://facebook.com/testcompany')
def test_parse_html_social_media_instagram(self):
"""Test Instagram link extraction."""
html = '''
<html>
<head></head>
<body>
<a href="https://instagram.com/testcompany">Instagram</a>
</body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertIn('instagram', result['social_media_links'])
def test_parse_html_social_media_excludes_share_links(self):
"""Test that share/sharer links are excluded."""
html = '''
<html>
<head></head>
<body>
<a href="https://facebook.com/sharer">Share</a>
<a href="https://facebook.com/share">Share</a>
</body>
</html>
'''
result = self.auditor._parse_html(html)
# Should not extract share links
self.assertEqual(result['social_media_links'], {})
def test_parse_html_all_social_platforms(self):
"""Test extraction of all social media platforms."""
html = '''
<html>
<head></head>
<body>
<a href="https://facebook.com/testfb">FB</a>
<a href="https://instagram.com/testig">IG</a>
<a href="https://youtube.com/@testyt">YT</a>
<a href="https://linkedin.com/company/testli">LI</a>
<a href="https://tiktok.com/@testtk">TT</a>
<a href="https://twitter.com/testtw">TW</a>
</body>
</html>
'''
result = self.auditor._parse_html(html)
self.assertIn('facebook', result['social_media_links'])
self.assertIn('instagram', result['social_media_links'])
self.assertIn('youtube', result['social_media_links'])
self.assertIn('linkedin', result['social_media_links'])
self.assertIn('tiktok', result['social_media_links'])
self.assertIn('twitter', result['social_media_links'])
# ============================================================================
# GooglePlacesSearcher Tests
# ============================================================================
class TestGooglePlacesSearcher(unittest.TestCase):
"""Tests for GooglePlacesSearcher class."""
def setUp(self):
"""Set up test with GooglePlacesSearcher instance."""
self.searcher = GooglePlacesSearcher(api_key='test_api_key')
def test_searcher_initialization_with_api_key(self):
"""Test searcher initializes with provided API key."""
searcher = GooglePlacesSearcher(api_key='my_api_key')
self.assertEqual(searcher.api_key, 'my_api_key')
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'env_api_key'})
def test_searcher_initialization_from_env(self):
"""Test searcher initializes from environment variable."""
searcher = GooglePlacesSearcher()
self.assertEqual(searcher.api_key, 'env_api_key')
def test_searcher_initialization_no_api_key(self):
"""Test searcher handles missing API key."""
with patch.dict('os.environ', {}, clear=True):
searcher = GooglePlacesSearcher(api_key=None)
self.assertIsNone(searcher.api_key)
class TestGooglePlacesSearcherFindPlace(unittest.TestCase):
"""Tests for GooglePlacesSearcher.find_place method."""
def setUp(self):
"""Set up test with GooglePlacesSearcher instance."""
self.searcher = GooglePlacesSearcher(api_key='test_api_key')
def test_find_place_no_api_key(self):
"""Test find_place returns None without API key."""
searcher = GooglePlacesSearcher(api_key=None)
result = searcher.find_place('Test Company')
self.assertIsNone(result)
@patch('requests.Session.get')
def test_find_place_success(self, mock_get):
"""Test successful place search."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'status': 'OK',
'candidates': [
{
'place_id': 'ChIJN1t_tDeuEmsRUsoyG83frY4',
'name': 'Test Company',
'formatted_address': 'Wejherowo, Poland',
}
]
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher.find_place('Test Company', 'Wejherowo')
self.assertEqual(result, 'ChIJN1t_tDeuEmsRUsoyG83frY4')
mock_get.assert_called_once()
# Verify API call parameters
call_args = mock_get.call_args
params = call_args.kwargs.get('params', call_args[1].get('params', {}))
self.assertEqual(params['inputtype'], 'textquery')
self.assertIn('Test Company', params['input'])
self.assertEqual(params['language'], 'pl')
self.assertEqual(params['key'], 'test_api_key')
@patch('requests.Session.get')
def test_find_place_zero_results(self, mock_get):
"""Test find_place with no results."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'status': 'ZERO_RESULTS',
'candidates': []
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher.find_place('Nonexistent Company')
self.assertIsNone(result)
@patch('requests.Session.get')
def test_find_place_api_error(self, mock_get):
"""Test find_place handles API errors."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'status': 'REQUEST_DENIED',
'error_message': 'Invalid API key',
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher.find_place('Test Company')
self.assertIsNone(result)
@patch('requests.Session.get')
def test_find_place_timeout(self, mock_get):
"""Test find_place handles timeout."""
import requests
mock_get.side_effect = requests.exceptions.Timeout('Connection timed out')
result = self.searcher.find_place('Test Company')
self.assertIsNone(result)
@patch('requests.Session.get')
def test_find_place_request_exception(self, mock_get):
"""Test find_place handles request exceptions."""
import requests
mock_get.side_effect = requests.exceptions.RequestException('Network error')
result = self.searcher.find_place('Test Company')
self.assertIsNone(result)
class TestGooglePlacesSearcherGetDetails(unittest.TestCase):
"""Tests for GooglePlacesSearcher.get_place_details method."""
def setUp(self):
"""Set up test with GooglePlacesSearcher instance."""
self.searcher = GooglePlacesSearcher(api_key='test_api_key')
def test_get_place_details_no_api_key(self):
"""Test get_place_details returns empty dict without API key."""
searcher = GooglePlacesSearcher(api_key=None)
result = searcher.get_place_details('some_place_id')
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
def test_get_place_details_no_place_id(self):
"""Test get_place_details returns empty dict without place_id."""
result = self.searcher.get_place_details(None)
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
def test_get_place_details_empty_place_id(self):
"""Test get_place_details returns empty dict with empty place_id."""
result = self.searcher.get_place_details('')
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
@patch('requests.Session.get')
def test_get_place_details_success(self, mock_get):
"""Test successful place details retrieval."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'status': 'OK',
'result': {
'name': 'Test Company',
'rating': 4.5,
'user_ratings_total': 123,
'opening_hours': {
'weekday_text': [
'Monday: 8:00 AM 6:00 PM',
'Tuesday: 8:00 AM 6:00 PM',
],
'open_now': True,
'periods': [],
},
'business_status': 'OPERATIONAL',
'formatted_phone_number': '+48 58 123 4567',
'website': 'https://example.com',
}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher.get_place_details('test_place_id')
self.assertEqual(result['google_rating'], 4.5)
self.assertEqual(result['google_reviews_count'], 123)
self.assertEqual(result['business_status'], 'OPERATIONAL')
self.assertEqual(result['formatted_phone'], '+48 58 123 4567')
self.assertEqual(result['website'], 'https://example.com')
self.assertIsNotNone(result['opening_hours'])
self.assertTrue(result['opening_hours']['open_now'])
@patch('requests.Session.get')
def test_get_place_details_partial_data(self, mock_get):
"""Test place details with partial data."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'status': 'OK',
'result': {
'name': 'Test Company',
'rating': 3.8,
# No review count, opening hours, etc.
}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher.get_place_details('test_place_id')
self.assertEqual(result['google_rating'], 3.8)
self.assertIsNone(result['google_reviews_count'])
self.assertIsNone(result['opening_hours'])
self.assertIsNone(result['business_status'])
@patch('requests.Session.get')
def test_get_place_details_api_error(self, mock_get):
"""Test get_place_details handles API errors."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'status': 'NOT_FOUND',
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher.get_place_details('invalid_place_id')
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
@patch('requests.Session.get')
def test_get_place_details_timeout(self, mock_get):
"""Test get_place_details handles timeout."""
import requests
mock_get.side_effect = requests.exceptions.Timeout('Connection timed out')
result = self.searcher.get_place_details('test_place_id')
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
@patch('requests.Session.get')
def test_get_place_details_correct_fields_requested(self, mock_get):
"""Test that correct fields are requested from API."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'status': 'OK', 'result': {}}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
self.searcher.get_place_details('test_place_id')
# Verify API call includes required fields
call_args = mock_get.call_args
params = call_args.kwargs.get('params', call_args[1].get('params', {}))
fields = params['fields'].split(',')
self.assertIn('rating', fields)
self.assertIn('user_ratings_total', fields)
self.assertIn('opening_hours', fields)
self.assertIn('business_status', fields)
self.assertIn('formatted_phone_number', fields)
self.assertIn('website', fields)
class TestGooglePlacesSearcherIntegration(unittest.TestCase):
"""Integration tests for GooglePlacesSearcher."""
def setUp(self):
"""Set up test with GooglePlacesSearcher instance."""
self.searcher = GooglePlacesSearcher(api_key='test_api_key')
@patch('requests.Session.get')
def test_find_and_get_details_workflow(self, mock_get):
"""Test complete workflow: find place, then get details."""
# Mock find_place response
find_response = Mock()
find_response.status_code = 200
find_response.json.return_value = {
'status': 'OK',
'candidates': [
{
'place_id': 'test_place_id_123',
'name': 'PIXLAB Sp. z o.o.',
'formatted_address': 'Wejherowo, Poland',
}
]
}
find_response.raise_for_status = Mock()
# Mock get_place_details response
details_response = Mock()
details_response.status_code = 200
details_response.json.return_value = {
'status': 'OK',
'result': {
'name': 'PIXLAB Sp. z o.o.',
'rating': 4.9,
'user_ratings_total': 45,
'business_status': 'OPERATIONAL',
'opening_hours': {
'weekday_text': ['Monday: 9:00 AM 5:00 PM'],
'open_now': False,
},
}
}
details_response.raise_for_status = Mock()
mock_get.side_effect = [find_response, details_response]
# Execute workflow
place_id = self.searcher.find_place('PIXLAB', 'Wejherowo')
self.assertEqual(place_id, 'test_place_id_123')
details = self.searcher.get_place_details(place_id)
self.assertEqual(details['google_rating'], 4.9)
self.assertEqual(details['google_reviews_count'], 45)
self.assertEqual(details['business_status'], 'OPERATIONAL')
# ============================================================================
# BraveSearcher Tests
# ============================================================================
class TestBraveSearcher(unittest.TestCase):
"""Tests for BraveSearcher class."""
def setUp(self):
"""Set up test with BraveSearcher instance."""
self.searcher = BraveSearcher(api_key='test_brave_api_key')
def test_searcher_initialization_with_api_key(self):
"""Test searcher initializes with provided API key."""
searcher = BraveSearcher(api_key='my_brave_key')
self.assertEqual(searcher.api_key, 'my_brave_key')
@patch.dict('os.environ', {'BRAVE_API_KEY': 'env_brave_key'})
def test_searcher_initialization_from_env(self):
"""Test searcher initializes from environment variable."""
searcher = BraveSearcher()
self.assertEqual(searcher.api_key, 'env_brave_key')
class TestBraveSearcherGoogleReviews(unittest.TestCase):
"""Tests for BraveSearcher Google reviews functionality."""
def setUp(self):
"""Set up test with BraveSearcher instance."""
self.searcher = BraveSearcher(api_key='test_brave_api_key')
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'test_places_key'})
@patch.object(GooglePlacesSearcher, 'find_place')
@patch.object(GooglePlacesSearcher, 'get_place_details')
def test_search_google_reviews(self, mock_details, mock_find):
"""Test successful Google reviews retrieval via Places API."""
# Mock successful place lookup
mock_find.return_value = 'ChIJN1t_tDeuEmsRUsoyG83frY4'
# Mock complete place details response
mock_details.return_value = {
'google_rating': 4.7,
'google_reviews_count': 156,
'opening_hours': {
'weekday_text': [
'Monday: 8:00 AM 5:00 PM',
'Tuesday: 8:00 AM 5:00 PM',
'Wednesday: 8:00 AM 5:00 PM',
'Thursday: 8:00 AM 5:00 PM',
'Friday: 8:00 AM 4:00 PM',
'Saturday: Closed',
'Sunday: Closed',
],
'open_now': True,
'periods': [],
},
'business_status': 'OPERATIONAL',
'formatted_phone': '+48 58 123 4567',
'website': 'https://example.com',
}
result = self.searcher.search_google_reviews('Test Company Sp. z o.o.', 'Wejherowo')
# Verify all fields are correctly returned
self.assertEqual(result['google_rating'], 4.7)
self.assertEqual(result['google_reviews_count'], 156)
self.assertIsNotNone(result['opening_hours'])
self.assertTrue(result['opening_hours']['open_now'])
self.assertEqual(len(result['opening_hours']['weekday_text']), 7)
self.assertEqual(result['business_status'], 'OPERATIONAL')
# Verify correct API calls were made
mock_find.assert_called_once_with('Test Company Sp. z o.o.', 'Wejherowo')
mock_details.assert_called_once_with('ChIJN1t_tDeuEmsRUsoyG83frY4')
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'test_places_key'})
@patch.object(GooglePlacesSearcher, 'find_place')
@patch.object(GooglePlacesSearcher, 'get_place_details')
def test_search_google_reviews_uses_places_api(self, mock_details, mock_find):
"""Test that search_google_reviews uses Google Places API when available."""
mock_find.return_value = 'test_place_id'
mock_details.return_value = {
'google_rating': 4.5,
'google_reviews_count': 100,
'opening_hours': {'open_now': True},
'business_status': 'OPERATIONAL',
}
result = self.searcher.search_google_reviews('Test Company', 'Wejherowo')
self.assertEqual(result['google_rating'], 4.5)
self.assertEqual(result['google_reviews_count'], 100)
mock_find.assert_called_once_with('Test Company', 'Wejherowo')
mock_details.assert_called_once_with('test_place_id')
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'test_places_key'})
@patch.object(GooglePlacesSearcher, 'find_place')
def test_search_google_reviews_no_place_found(self, mock_find):
"""Test search_google_reviews when no place is found."""
mock_find.return_value = None
result = self.searcher.search_google_reviews('Unknown Company', 'Wejherowo')
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
@patch.dict('os.environ', {}, clear=True)
def test_search_google_reviews_no_api_keys(self):
"""Test search_google_reviews without any API keys."""
searcher = BraveSearcher(api_key=None)
result = searcher.search_google_reviews('Test Company')
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'test_places_key'})
@patch.object(GooglePlacesSearcher, 'find_place')
@patch.object(GooglePlacesSearcher, 'get_place_details')
def test_search_google_reviews_error(self, mock_details, mock_find):
"""Test search_google_reviews handles API errors gracefully."""
import requests
# Mock find_place to succeed but get_place_details to raise an error
mock_find.return_value = 'test_place_id'
mock_details.side_effect = requests.exceptions.RequestException('API connection failed')
result = self.searcher.search_google_reviews('Test Company', 'Wejherowo')
# Should handle error gracefully and return None values
self.assertIsNone(result['google_rating'])
self.assertIsNone(result['google_reviews_count'])
# Verify the API calls were made
mock_find.assert_called_once_with('Test Company', 'Wejherowo')
mock_details.assert_called_once_with('test_place_id')
class TestBraveSearcherBraveReviews(unittest.TestCase):
"""Tests for Brave Search fallback for reviews."""
def setUp(self):
"""Set up test with BraveSearcher instance."""
self.searcher = BraveSearcher(api_key='test_brave_api_key')
@patch('requests.Session.get')
def test_search_brave_for_reviews_success(self, mock_get):
"""Test successful review extraction from Brave search."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'web': {
'results': [
{
'title': 'Test Company - Google Maps',
'description': 'Ocena: 4,5 (123 opinii) - Test Company',
}
]
}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher._search_brave_for_reviews('Test Company', 'Wejherowo')
self.assertIsNotNone(result)
self.assertEqual(result['google_rating'], 4.5)
self.assertEqual(result['google_reviews_count'], 123)
@patch('requests.Session.get')
def test_search_brave_for_reviews_no_rating_found(self, mock_get):
"""Test Brave search when no rating is found."""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
'web': {
'results': [
{
'title': 'Test Company Website',
'description': 'Some description without rating',
}
]
}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = self.searcher._search_brave_for_reviews('Test Company', 'Wejherowo')
self.assertIsNone(result)
def test_search_brave_for_reviews_no_api_key(self):
"""Test Brave search returns None without API key."""
searcher = BraveSearcher(api_key=None)
result = searcher._search_brave_for_reviews('Test Company', 'Wejherowo')
self.assertIsNone(result)
@patch('requests.Session.get')
def test_search_brave_for_reviews_timeout(self, mock_get):
"""Test Brave search handles timeout."""
import requests
mock_get.side_effect = requests.exceptions.Timeout('Connection timed out')
result = self.searcher._search_brave_for_reviews('Test Company', 'Wejherowo')
self.assertIsNone(result)
# ============================================================================
# SocialMediaAuditor Tests
# ============================================================================
class TestSocialMediaAuditor(unittest.TestCase):
"""Tests for SocialMediaAuditor class."""
@patch('social_media_audit.create_engine')
@patch('social_media_audit.sessionmaker')
def setUp(self, mock_sessionmaker, mock_engine):
"""Set up test with mocked database."""
self.mock_session = MagicMock()
mock_sessionmaker.return_value = Mock(return_value=self.mock_session)
self.auditor = SocialMediaAuditor(database_url='postgresql://test:test@localhost/test')
@patch('social_media_audit.create_engine')
@patch('social_media_audit.sessionmaker')
def test_auditor_initialization(self, mock_sessionmaker, mock_engine):
"""Test auditor initializes correctly."""
auditor = SocialMediaAuditor()
self.assertIsNotNone(auditor.website_auditor)
self.assertIsNotNone(auditor.brave_searcher)
@patch('social_media_audit.create_engine')
@patch('social_media_audit.sessionmaker')
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'test_key'})
def test_auditor_initializes_google_places(self, mock_sessionmaker, mock_engine):
"""Test auditor initializes Google Places searcher when API key available."""
auditor = SocialMediaAuditor()
self.assertIsNotNone(auditor.google_places_searcher)
class TestSocialMediaAuditorGetCompanies(unittest.TestCase):
"""Tests for SocialMediaAuditor.get_companies method."""
@patch('social_media_audit.create_engine')
@patch('social_media_audit.sessionmaker')
def setUp(self, mock_sessionmaker, mock_engine):
"""Set up test with mocked database."""
self.mock_session = MagicMock()
mock_sessionmaker.return_value = Mock(return_value=MagicMock(
__enter__=Mock(return_value=self.mock_session),
__exit__=Mock(return_value=False)
))
self.auditor = SocialMediaAuditor()
def test_get_companies_by_ids(self):
"""Test fetching companies by specific IDs."""
mock_result = [
Mock(_mapping={'id': 1, 'name': 'Company A', 'slug': 'company-a', 'website': 'https://a.com', 'address_city': 'Wejherowo'}),
Mock(_mapping={'id': 2, 'name': 'Company B', 'slug': 'company-b', 'website': 'https://b.com', 'address_city': 'Wejherowo'}),
]
self.mock_session.execute.return_value = mock_result
companies = self.auditor.get_companies(company_ids=[1, 2])
# Verify query was called
self.mock_session.execute.assert_called_once()
class TestSocialMediaAuditorAuditCompany(unittest.TestCase):
"""Tests for SocialMediaAuditor.audit_company method."""
@patch('social_media_audit.create_engine')
@patch('social_media_audit.sessionmaker')
def setUp(self, mock_sessionmaker, mock_engine):
"""Set up test with mocked database."""
mock_session = MagicMock()
mock_sessionmaker.return_value = Mock(return_value=mock_session)
self.auditor = SocialMediaAuditor()
self.auditor.website_auditor = Mock()
self.auditor.brave_searcher = Mock()
self.auditor.google_places_searcher = None
def test_audit_company_with_website(self):
"""Test auditing company with website."""
company = {
'id': 1,
'name': 'Test Company',
'slug': 'test-company',
'website': 'https://example.com',
'address_city': 'Wejherowo',
}
self.auditor.website_auditor.audit_website.return_value = {
'http_status': 200,
'has_ssl': True,
'social_media_links': {'facebook': 'https://facebook.com/test'},
}
self.auditor.brave_searcher.search_social_media.return_value = {}
self.auditor.brave_searcher.search_google_reviews.return_value = {
'google_rating': 4.5,
'google_reviews_count': 50,
}
result = self.auditor.audit_company(company)
self.assertEqual(result['company_id'], 1)
self.assertEqual(result['company_name'], 'Test Company')
self.assertIn('facebook', result['social_media'])
self.auditor.website_auditor.audit_website.assert_called_once_with('https://example.com')
def test_audit_company_without_website(self):
"""Test auditing company without website."""
company = {
'id': 2,
'name': 'No Website Company',
'slug': 'no-website',
'website': None,
'address_city': 'Wejherowo',
}
self.auditor.brave_searcher.search_social_media.return_value = {
'facebook': 'https://facebook.com/found',
}
self.auditor.brave_searcher.search_google_reviews.return_value = {}
result = self.auditor.audit_company(company)
self.assertEqual(result['company_id'], 2)
self.assertIn('No website URL', result['website'].get('errors', []))
self.assertIn('facebook', result['social_media'])
@patch.dict('os.environ', {'GOOGLE_PLACES_API_KEY': 'test_key'})
def test_audit_company_uses_google_places(self):
"""Test auditing uses Google Places API when available."""
company = {
'id': 3,
'name': 'Google Test',
'slug': 'google-test',
'website': 'https://example.com',
'address_city': 'Wejherowo',
}
mock_places_searcher = Mock()
mock_places_searcher.find_place.return_value = 'test_place_id'
mock_places_searcher.get_place_details.return_value = {
'google_rating': 4.8,
'google_reviews_count': 200,
'business_status': 'OPERATIONAL',
'opening_hours': {'open_now': True},
}
self.auditor.google_places_searcher = mock_places_searcher
self.auditor.website_auditor.audit_website.return_value = {'social_media_links': {}}
self.auditor.brave_searcher.search_social_media.return_value = {}
result = self.auditor.audit_company(company)
self.assertEqual(result['google_reviews']['google_rating'], 4.8)
self.assertEqual(result['google_reviews']['google_reviews_count'], 200)
mock_places_searcher.find_place.assert_called_once_with('Google Test', 'Wejherowo')
class TestSocialMediaAuditorSaveResult(unittest.TestCase):
"""Tests for SocialMediaAuditor.save_audit_result method."""
@patch('social_media_audit.create_engine')
@patch('social_media_audit.sessionmaker')
def setUp(self, mock_sessionmaker, mock_engine):
"""Set up test with mocked database."""
self.mock_session = MagicMock()
mock_sessionmaker.return_value = Mock(return_value=MagicMock(
__enter__=Mock(return_value=self.mock_session),
__exit__=Mock(return_value=False)
))
self.auditor = SocialMediaAuditor()
def test_save_audit_result_success(self):
"""Test saving audit result successfully."""
result = {
'company_id': 1,
'company_name': 'Test Company',
'audit_date': datetime.now(),
'website': {
'url': 'https://example.com',
'http_status': 200,
'has_ssl': True,
},
'social_media': {
'facebook': 'https://facebook.com/test',
},
'google_reviews': {
'google_rating': 4.5,
'google_reviews_count': 50,
},
}
success = self.auditor.save_audit_result(result)
self.assertTrue(success)
# Verify execute was called for website and social media
self.assertTrue(self.mock_session.execute.called)
self.mock_session.commit.assert_called_once()
# ============================================================================
# Constants and Patterns Tests
# ============================================================================
class TestHostingProviders(unittest.TestCase):
"""Tests for hosting provider patterns."""
def test_hosting_providers_structure(self):
"""Test that HOSTING_PROVIDERS has correct structure."""
self.assertIsInstance(HOSTING_PROVIDERS, dict)
for provider, patterns in HOSTING_PROVIDERS.items():
self.assertIsInstance(provider, str)
self.assertIsInstance(patterns, list)
for pattern in patterns:
self.assertIsInstance(pattern, str)
def test_major_providers_included(self):
"""Test that major hosting providers are included."""
expected_providers = ['OVH', 'Cloudflare', 'Google Cloud', 'AWS', 'nazwa.pl', 'home.pl']
for provider in expected_providers:
self.assertIn(provider, HOSTING_PROVIDERS)
class TestSocialMediaPatterns(unittest.TestCase):
"""Tests for social media URL patterns."""
def test_social_media_patterns_structure(self):
"""Test that SOCIAL_MEDIA_PATTERNS has correct structure."""
self.assertIsInstance(SOCIAL_MEDIA_PATTERNS, dict)
expected_platforms = ['facebook', 'instagram', 'youtube', 'linkedin', 'tiktok', 'twitter']
for platform in expected_platforms:
self.assertIn(platform, SOCIAL_MEDIA_PATTERNS)
def test_facebook_pattern_matches(self):
"""Test Facebook URL pattern matching."""
import re
patterns = SOCIAL_MEDIA_PATTERNS['facebook']
test_urls = [
('https://facebook.com/testpage', 'testpage'),
('https://www.facebook.com/testpage', 'testpage'),
('https://fb.com/testpage', 'testpage'),
]
for url, expected_match in test_urls:
matched = False
for pattern in patterns:
match = re.search(pattern, url, re.IGNORECASE)
if match:
self.assertEqual(match.group(1), expected_match)
matched = True
break
self.assertTrue(matched, f"Pattern should match {url}")
def test_instagram_pattern_matches(self):
"""Test Instagram URL pattern matching."""
import re
patterns = SOCIAL_MEDIA_PATTERNS['instagram']
test_urls = [
('https://instagram.com/testaccount', 'testaccount'),
('https://www.instagram.com/testaccount', 'testaccount'),
]
for url, expected_match in test_urls:
matched = False
for pattern in patterns:
match = re.search(pattern, url, re.IGNORECASE)
if match:
self.assertEqual(match.group(1), expected_match)
matched = True
break
self.assertTrue(matched, f"Pattern should match {url}")
class TestSocialMediaExcludes(unittest.TestCase):
"""Tests for social media exclusion patterns."""
def test_excludes_structure(self):
"""Test that SOCIAL_MEDIA_EXCLUDE has correct structure."""
self.assertIsInstance(SOCIAL_MEDIA_EXCLUDE, dict)
def test_facebook_excludes_share_links(self):
"""Test that Facebook share links are excluded."""
excludes = SOCIAL_MEDIA_EXCLUDE['facebook']
self.assertIn('sharer', excludes)
self.assertIn('share', excludes)
def test_twitter_excludes_intent(self):
"""Test that Twitter intent links are excluded."""
excludes = SOCIAL_MEDIA_EXCLUDE['twitter']
self.assertIn('intent', excludes)
self.assertIn('share', excludes)
# ============================================================================
# Run Tests
# ============================================================================
if __name__ == '__main__':
# Run with verbose output
unittest.main(verbosity=2)