This commit is contained in:
2025-11-11 14:09:21 +01:00
parent bcd0a10576
commit 1075a91eac
57 changed files with 5598 additions and 1366 deletions

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python
"""
Integration test for newsletter with tracking.
Tests the full flow of generating a newsletter with tracking enabled.
"""
import sys
from pathlib import Path
from datetime import datetime
# Add backend directory to path
backend_dir = Path(__file__).parent.parent / 'backend'
sys.path.insert(0, str(backend_dir))
# Mock the tracking service to avoid database dependency
class MockTrackingService:
"""Mock tracking service for testing"""
@staticmethod
def create_newsletter_tracking(newsletter_id, subscriber_email, article_links=None):
"""Mock create_newsletter_tracking function"""
link_tracking_map = {}
if article_links:
for i, article in enumerate(article_links):
link_tracking_map[article['url']] = f"mock-link-{i}"
return {
'pixel_tracking_id': 'mock-pixel-123',
'link_tracking_map': link_tracking_map,
'newsletter_id': newsletter_id,
'subscriber_email': subscriber_email
}
# Import after setting up path
from tracking_integration import inject_tracking_pixel, replace_article_links, generate_tracking_urls
from jinja2 import Template
def test_newsletter_with_tracking():
"""Test generating a newsletter with tracking enabled"""
print("\n" + "="*70)
print("NEWSLETTER TRACKING INTEGRATION TEST")
print("="*70)
# Mock article data
articles = [
{
'title': 'Munich Tech Summit Announces 2025 Dates',
'author': 'Tech Reporter',
'link': 'https://example.com/tech-summit',
'summary': 'The annual Munich Tech Summit will return in 2025 with exciting new features.',
'source': 'Munich Tech News',
'published_at': datetime.now()
},
{
'title': 'New Public Transport Routes Launched',
'author': 'Transport Desk',
'link': 'https://example.com/transport-routes',
'summary': 'MVG announces three new bus routes connecting suburban areas.',
'source': 'Munich Transport',
'published_at': datetime.now()
}
]
# Configuration
newsletter_id = 'test-newsletter-2025-11-11'
subscriber_email = 'test@example.com'
api_url = 'http://localhost:5001'
print(f"\nNewsletter ID: {newsletter_id}")
print(f"Subscriber: {subscriber_email}")
print(f"Articles: {len(articles)}")
print(f"API URL: {api_url}")
# Step 1: Generate tracking URLs
print("\n" + "-"*70)
print("Step 1: Generate tracking data")
print("-"*70)
tracking_data = generate_tracking_urls(
articles=articles,
newsletter_id=newsletter_id,
subscriber_email=subscriber_email,
tracking_service=MockTrackingService
)
print(f"✓ Pixel tracking ID: {tracking_data['pixel_tracking_id']}")
print(f"✓ Link tracking map: {len(tracking_data['link_tracking_map'])} links")
for url, tracking_id in tracking_data['link_tracking_map'].items():
print(f" - {url}{tracking_id}")
# Step 2: Load and render template
print("\n" + "-"*70)
print("Step 2: Render newsletter template")
print("-"*70)
template_path = Path(__file__).parent / 'newsletter_template.html'
with open(template_path, 'r', encoding='utf-8') as f:
template_content = f.read()
template = Template(template_content)
now = datetime.now()
template_data = {
'date': now.strftime('%A, %B %d, %Y'),
'year': now.year,
'article_count': len(articles),
'articles': articles,
'unsubscribe_link': 'http://localhost:3000/unsubscribe',
'website_link': 'http://localhost:3000',
'tracking_enabled': True
}
html = template.render(**template_data)
print("✓ Template rendered")
# Step 3: Inject tracking pixel
print("\n" + "-"*70)
print("Step 3: Inject tracking pixel")
print("-"*70)
html = inject_tracking_pixel(
html,
tracking_data['pixel_tracking_id'],
api_url
)
pixel_url = f"{api_url}/api/track/pixel/{tracking_data['pixel_tracking_id']}"
if pixel_url in html:
print(f"✓ Tracking pixel injected: {pixel_url}")
else:
print(f"✗ Tracking pixel NOT found")
return False
# Step 4: Replace article links
print("\n" + "-"*70)
print("Step 4: Replace article links with tracking URLs")
print("-"*70)
html = replace_article_links(
html,
tracking_data['link_tracking_map'],
api_url
)
# Verify all article links were replaced
success = True
for article in articles:
original_url = article['link']
tracking_id = tracking_data['link_tracking_map'].get(original_url)
if tracking_id:
tracking_url = f"{api_url}/api/track/click/{tracking_id}"
if tracking_url in html:
print(f"✓ Link replaced: {original_url}")
print(f"{tracking_url}")
else:
print(f"✗ Link NOT replaced: {original_url}")
success = False
# Verify original URL is NOT in the HTML (should be replaced)
if f'href="{original_url}"' in html:
print(f"✗ Original URL still present: {original_url}")
success = False
# Step 5: Verify privacy notice
print("\n" + "-"*70)
print("Step 5: Verify privacy notice")
print("-"*70)
if "This email contains tracking to measure engagement" in html:
print("✓ Privacy notice present in footer")
else:
print("✗ Privacy notice NOT found")
success = False
# Step 6: Save output for inspection
print("\n" + "-"*70)
print("Step 6: Save test output")
print("-"*70)
output_file = 'test_newsletter_with_tracking.html'
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html)
print(f"✓ Test newsletter saved to: {output_file}")
print(f" Open it in your browser to inspect the tracking integration")
return success
if __name__ == '__main__':
print("\n" + "="*70)
print("TESTING NEWSLETTER WITH TRACKING")
print("="*70)
success = test_newsletter_with_tracking()
print("\n" + "="*70)
if success:
print("✓ ALL TESTS PASSED")
print("="*70 + "\n")
sys.exit(0)
else:
print("✗ SOME TESTS FAILED")
print("="*70 + "\n")
sys.exit(1)

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<title>Munich News Daily</title>
<!--[if mso]>
<style type="text/css">
body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
</style>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<!-- Wrapper Table -->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #f4f4f4;" width="100%">
<tr>
<td align="center" style="padding: 20px 0;">
<!-- Main Container -->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #ffffff; max-width: 600px;" width="600">
<!-- Header -->
<tr>
<td style="background-color: #1a1a1a; padding: 30px 40px; text-align: center;">
<h1 style="margin: 0 0 8px 0; font-size: 28px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">
Munich News Daily
</h1>
<p style="margin: 0; font-size: 14px; color: #999999; letter-spacing: 0.5px;">
Tuesday, November 11, 2025
</p>
</td>
</tr>
<!-- Greeting -->
<tr>
<td style="padding: 30px 40px 20px 40px;">
<p style="margin: 0; font-size: 16px; line-height: 1.5; color: #333333;">
Good morning ☀️
</p>
<p style="margin: 15px 0 0 0; font-size: 15px; line-height: 1.6; color: #666666;">
Here's what's happening in Munich today. We've summarized 2 stories using AI so you can stay informed in under 5 minutes.
</p>
</td>
</tr>
<!-- Divider -->
<tr>
<td style="padding: 0 40px;">
<div style="height: 1px; background-color: #e0e0e0;"></div>
</td>
</tr>
<!-- Articles -->
<tr>
<td style="padding: 25px 40px;">
<!-- Article Number Badge -->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tr>
<td>
<span style="display: inline-block; background-color: #000000; color: #ffffff; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;">
1
</span>
</td>
</tr>
</table>
<!-- Article Title -->
<h2 style="margin: 12px 0 8px 0; font-size: 19px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
Munich Tech Summit Announces 2025 Dates
</h2>
<!-- Article Meta -->
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
<span style="color: #000000; font-weight: 600;">Munich Tech News</span>
<span> • Tech Reporter</span>
</p>
<!-- Article Summary -->
<p style="margin: 0 0 15px 0; font-size: 15px; line-height: 1.6; color: #333333;">
The annual Munich Tech Summit will return in 2025 with exciting new features.
</p>
<!-- Read More Link -->
<a href="http://localhost:5001/api/track/click/mock-link-0" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
Read more →
</a>
</td>
</tr>
<!-- Article Divider -->
<tr>
<td style="padding: 0 40px;">
<div style="height: 1px; background-color: #f0f0f0;"></div>
</td>
</tr>
<tr>
<td style="padding: 25px 40px;">
<!-- Article Number Badge -->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tr>
<td>
<span style="display: inline-block; background-color: #000000; color: #ffffff; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;">
2
</span>
</td>
</tr>
</table>
<!-- Article Title -->
<h2 style="margin: 12px 0 8px 0; font-size: 19px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
New Public Transport Routes Launched
</h2>
<!-- Article Meta -->
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
<span style="color: #000000; font-weight: 600;">Munich Transport</span>
<span> • Transport Desk</span>
</p>
<!-- Article Summary -->
<p style="margin: 0 0 15px 0; font-size: 15px; line-height: 1.6; color: #333333;">
MVG announces three new bus routes connecting suburban areas.
</p>
<!-- Read More Link -->
<a href="http://localhost:5001/api/track/click/mock-link-1" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
Read more →
</a>
</td>
</tr>
<!-- Article Divider -->
<!-- Bottom Divider -->
<tr>
<td style="padding: 25px 40px 0 40px;">
<div style="height: 1px; background-color: #e0e0e0;"></div>
</td>
</tr>
<!-- Summary Box -->
<tr>
<td style="padding: 30px 40px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #f8f8f8; border-radius: 8px;" width="100%">
<tr>
<td style="padding: 25px; text-align: center;">
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;">
Today's Digest
</p>
<p style="margin: 0; font-size: 36px; font-weight: 700; color: #000000;">
2
</p>
<p style="margin: 8px 0 0 0; font-size: 14px; color: #666666;">
stories • AI-summarized • 5 min read
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #1a1a1a; padding: 30px 40px; text-align: center;">
<p style="margin: 0 0 15px 0; font-size: 14px; color: #ffffff; font-weight: 600;">
Munich News Daily
</p>
<p style="margin: 0 0 20px 0; font-size: 13px; color: #999999; line-height: 1.5;">
AI-powered news summaries for busy people.<br/>
Delivered daily to your inbox.
</p>
<!-- Footer Links -->
<p style="margin: 0; font-size: 12px; color: #666666;">
<a href="http://localhost:3000" style="color: #999999; text-decoration: none;">Visit Website</a>
<span style="color: #444444;"></span>
<a href="http://localhost:3000/unsubscribe" style="color: #999999; text-decoration: none;">Unsubscribe</a>
</p>
<!-- Privacy Notice -->
<p style="margin: 20px 0 0 0; font-size: 11px; color: #666666; line-height: 1.4;">
This email contains tracking to measure engagement and improve our content.<br/>
We respect your privacy and anonymize data after 90 days.
</p>
<p style="margin: 20px 0 0 0; font-size: 11px; color: #666666;">
© 2025 Munich News Daily. All rights reserved.
</p>
</td>
</tr>
</table>
<!-- End Main Container -->
</td>
</tr>
</table>
<!-- End Wrapper Table -->
<img alt="" height="1" src="http://localhost:5001/api/track/pixel/mock-pixel-123" style="display:block;" width="1"/></body>
</html>

View File

@@ -0,0 +1,187 @@
#!/usr/bin/env python
"""
Test script for tracking integration in newsletter sender.
Tests tracking pixel injection and link replacement.
"""
import sys
from pathlib import Path
# Add backend directory to path
backend_dir = Path(__file__).parent.parent / 'backend'
sys.path.insert(0, str(backend_dir))
from tracking_integration import inject_tracking_pixel, replace_article_links
def test_inject_tracking_pixel():
"""Test that tracking pixel is correctly injected into HTML"""
print("\n" + "="*70)
print("TEST 1: Inject Tracking Pixel")
print("="*70)
# Test HTML
html = """<html>
<body>
<p>Newsletter content</p>
</body>
</html>"""
tracking_id = "test-tracking-123"
api_url = "http://localhost:5001"
# Inject pixel
result = inject_tracking_pixel(html, tracking_id, api_url)
# Verify pixel is present
expected_pixel = f'<img src="{api_url}/api/track/pixel/{tracking_id}" width="1" height="1" alt="" style="display:block;" />'
if expected_pixel in result:
print("✓ Tracking pixel correctly injected")
print(f" Pixel URL: {api_url}/api/track/pixel/{tracking_id}")
return True
else:
print("✗ Tracking pixel NOT found in HTML")
print(f" Expected: {expected_pixel}")
print(f" Result: {result}")
return False
def test_replace_article_links():
"""Test that article links are correctly replaced with tracking URLs"""
print("\n" + "="*70)
print("TEST 2: Replace Article Links")
print("="*70)
# Test HTML with article links
html = """<html>
<body>
<a href="https://example.com/article1">Article 1</a>
<a href="https://example.com/article2">Article 2</a>
<a href="https://example.com/untracked">Untracked Link</a>
</body>
</html>"""
# Tracking map
link_tracking_map = {
"https://example.com/article1": "track-id-1",
"https://example.com/article2": "track-id-2"
}
api_url = "http://localhost:5001"
# Replace links
result = replace_article_links(html, link_tracking_map, api_url)
# Verify replacements
success = True
# Check article 1 link
expected_url_1 = f"{api_url}/api/track/click/track-id-1"
if expected_url_1 in result:
print(f"✓ Article 1 link replaced: {expected_url_1}")
else:
print(f"✗ Article 1 link NOT replaced")
success = False
# Check article 2 link
expected_url_2 = f"{api_url}/api/track/click/track-id-2"
if expected_url_2 in result:
print(f"✓ Article 2 link replaced: {expected_url_2}")
else:
print(f"✗ Article 2 link NOT replaced")
success = False
# Check untracked link remains unchanged
if "https://example.com/untracked" in result:
print(f"✓ Untracked link preserved: https://example.com/untracked")
else:
print(f"✗ Untracked link was modified (should remain unchanged)")
success = False
return success
def test_full_integration():
"""Test full integration: pixel + link replacement"""
print("\n" + "="*70)
print("TEST 3: Full Integration (Pixel + Links)")
print("="*70)
# Test HTML
html = """<html>
<body>
<h1>Newsletter</h1>
<a href="https://example.com/article">Read Article</a>
</body>
</html>"""
api_url = "http://localhost:5001"
pixel_tracking_id = "pixel-123"
link_tracking_map = {
"https://example.com/article": "link-456"
}
# First inject pixel
html = inject_tracking_pixel(html, pixel_tracking_id, api_url)
# Then replace links
html = replace_article_links(html, link_tracking_map, api_url)
# Verify both are present
success = True
pixel_url = f"{api_url}/api/track/pixel/{pixel_tracking_id}"
if pixel_url in html:
print(f"✓ Tracking pixel present: {pixel_url}")
else:
print(f"✗ Tracking pixel NOT found")
success = False
link_url = f"{api_url}/api/track/click/link-456"
if link_url in html:
print(f"✓ Tracking link present: {link_url}")
else:
print(f"✗ Tracking link NOT found")
success = False
if success:
print("\n✓ Full integration successful!")
print("\nFinal HTML:")
print("-" * 70)
print(html)
print("-" * 70)
return success
if __name__ == '__main__':
print("\n" + "="*70)
print("TRACKING INTEGRATION TEST SUITE")
print("="*70)
results = []
# Run tests
results.append(("Inject Tracking Pixel", test_inject_tracking_pixel()))
results.append(("Replace Article Links", test_replace_article_links()))
results.append(("Full Integration", test_full_integration()))
# Summary
print("\n" + "="*70)
print("TEST SUMMARY")
print("="*70)
passed = sum(1 for _, result in results if result)
total = len(results)
for test_name, result in results:
status = "✓ PASS" if result else "✗ FAIL"
print(f"{status}: {test_name}")
print("-" * 70)
print(f"Results: {passed}/{total} tests passed")
print("="*70 + "\n")
# Exit with appropriate code
sys.exit(0 if passed == total else 1)