update
This commit is contained in:
451
tests/backend/test_analytics.py
Normal file
451
tests/backend/test_analytics.py
Normal file
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test analytics functionality for email tracking
|
||||
Run from backend directory with venv activated:
|
||||
cd backend
|
||||
source venv/bin/activate # or venv\Scripts\activate on Windows
|
||||
python test_analytics.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Add backend directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from services.analytics_service import (
|
||||
get_open_rate,
|
||||
get_click_rate,
|
||||
get_newsletter_metrics,
|
||||
get_article_performance,
|
||||
get_subscriber_activity_status,
|
||||
update_subscriber_activity_statuses
|
||||
)
|
||||
from database import (
|
||||
newsletter_sends_collection,
|
||||
link_clicks_collection,
|
||||
subscriber_activity_collection
|
||||
)
|
||||
from app import app
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("Analytics Service Tests")
|
||||
print("="*80)
|
||||
|
||||
# Test counters
|
||||
tests_passed = 0
|
||||
tests_failed = 0
|
||||
|
||||
def test_result(test_name, passed, message=""):
|
||||
"""Print test result"""
|
||||
global tests_passed, tests_failed
|
||||
if passed:
|
||||
tests_passed += 1
|
||||
print(f"✓ {test_name}")
|
||||
if message:
|
||||
print(f" {message}")
|
||||
else:
|
||||
tests_failed += 1
|
||||
print(f"❌ {test_name}")
|
||||
if message:
|
||||
print(f" {message}")
|
||||
|
||||
|
||||
# Setup test data
|
||||
print("\n" + "-"*80)
|
||||
print("Setting up test data...")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
# Clean up existing test data
|
||||
newsletter_sends_collection.delete_many({'newsletter_id': {'$regex': '^test-analytics-'}})
|
||||
link_clicks_collection.delete_many({'newsletter_id': {'$regex': '^test-analytics-'}})
|
||||
subscriber_activity_collection.delete_many({'email': {'$regex': '^test-analytics-'}})
|
||||
|
||||
# Create test newsletter sends
|
||||
test_newsletter_id = 'test-analytics-newsletter-001'
|
||||
|
||||
# Create 10 newsletter sends: 7 opened, 3 not opened
|
||||
for i in range(10):
|
||||
opened = i < 7 # First 7 are opened
|
||||
doc = {
|
||||
'newsletter_id': test_newsletter_id,
|
||||
'subscriber_email': f'test-analytics-user{i}@example.com',
|
||||
'tracking_id': f'test-pixel-{i}',
|
||||
'sent_at': datetime.utcnow(),
|
||||
'opened': opened,
|
||||
'first_opened_at': datetime.utcnow() if opened else None,
|
||||
'last_opened_at': datetime.utcnow() if opened else None,
|
||||
'open_count': 1 if opened else 0,
|
||||
'created_at': datetime.utcnow()
|
||||
}
|
||||
newsletter_sends_collection.insert_one(doc)
|
||||
|
||||
# Create test link clicks for an article
|
||||
test_article_url = 'https://example.com/test-analytics-article'
|
||||
|
||||
# Create 10 link tracking records: 4 clicked, 6 not clicked
|
||||
for i in range(10):
|
||||
clicked = i < 4 # First 4 are clicked
|
||||
doc = {
|
||||
'tracking_id': f'test-link-{i}',
|
||||
'newsletter_id': test_newsletter_id,
|
||||
'subscriber_email': f'test-analytics-user{i}@example.com',
|
||||
'article_url': test_article_url,
|
||||
'article_title': 'Test Analytics Article',
|
||||
'clicked': clicked,
|
||||
'clicked_at': datetime.utcnow() if clicked else None,
|
||||
'user_agent': 'Test Agent' if clicked else None,
|
||||
'created_at': datetime.utcnow()
|
||||
}
|
||||
link_clicks_collection.insert_one(doc)
|
||||
|
||||
print("✓ Test data created")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error setting up test data: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Test 1: Open Rate Calculation
|
||||
print("\n" + "-"*80)
|
||||
print("Test 1: Open Rate Calculation")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
open_rate = get_open_rate(test_newsletter_id)
|
||||
|
||||
# Expected: 7 out of 10 = 70%
|
||||
is_correct = open_rate == 70.0
|
||||
test_result("Calculate open rate", is_correct, f"Open rate: {open_rate}% (expected 70%)")
|
||||
|
||||
# Test with non-existent newsletter
|
||||
open_rate_empty = get_open_rate('non-existent-newsletter')
|
||||
handles_empty = open_rate_empty == 0.0
|
||||
test_result("Handle non-existent newsletter", handles_empty,
|
||||
f"Open rate: {open_rate_empty}% (expected 0%)")
|
||||
|
||||
except Exception as e:
|
||||
test_result("Open rate calculation", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Test 2: Click Rate Calculation
|
||||
print("\n" + "-"*80)
|
||||
print("Test 2: Click Rate Calculation")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
click_rate = get_click_rate(test_article_url)
|
||||
|
||||
# Expected: 4 out of 10 = 40%
|
||||
is_correct = click_rate == 40.0
|
||||
test_result("Calculate click rate", is_correct, f"Click rate: {click_rate}% (expected 40%)")
|
||||
|
||||
# Test with non-existent article
|
||||
click_rate_empty = get_click_rate('https://example.com/non-existent')
|
||||
handles_empty = click_rate_empty == 0.0
|
||||
test_result("Handle non-existent article", handles_empty,
|
||||
f"Click rate: {click_rate_empty}% (expected 0%)")
|
||||
|
||||
except Exception as e:
|
||||
test_result("Click rate calculation", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Test 3: Newsletter Metrics
|
||||
print("\n" + "-"*80)
|
||||
print("Test 3: Newsletter Metrics")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
metrics = get_newsletter_metrics(test_newsletter_id)
|
||||
|
||||
# Verify all expected fields
|
||||
has_all_fields = all(key in metrics for key in [
|
||||
'newsletter_id', 'total_sent', 'total_opened', 'open_rate',
|
||||
'total_clicks', 'unique_clickers', 'click_through_rate'
|
||||
])
|
||||
test_result("Returns all required fields", has_all_fields)
|
||||
|
||||
# Verify values
|
||||
correct_sent = metrics['total_sent'] == 10
|
||||
test_result("Correct total_sent", correct_sent, f"Total sent: {metrics['total_sent']}")
|
||||
|
||||
correct_opened = metrics['total_opened'] == 7
|
||||
test_result("Correct total_opened", correct_opened, f"Total opened: {metrics['total_opened']}")
|
||||
|
||||
correct_open_rate = metrics['open_rate'] == 70.0
|
||||
test_result("Correct open_rate", correct_open_rate, f"Open rate: {metrics['open_rate']}%")
|
||||
|
||||
correct_clicks = metrics['total_clicks'] == 4
|
||||
test_result("Correct total_clicks", correct_clicks, f"Total clicks: {metrics['total_clicks']}")
|
||||
|
||||
correct_unique_clickers = metrics['unique_clickers'] == 4
|
||||
test_result("Correct unique_clickers", correct_unique_clickers,
|
||||
f"Unique clickers: {metrics['unique_clickers']}")
|
||||
|
||||
correct_ctr = metrics['click_through_rate'] == 40.0
|
||||
test_result("Correct click_through_rate", correct_ctr,
|
||||
f"CTR: {metrics['click_through_rate']}%")
|
||||
|
||||
except Exception as e:
|
||||
test_result("Newsletter metrics", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Test 4: Article Performance
|
||||
print("\n" + "-"*80)
|
||||
print("Test 4: Article Performance")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
performance = get_article_performance(test_article_url)
|
||||
|
||||
# Verify all expected fields
|
||||
has_all_fields = all(key in performance for key in [
|
||||
'article_url', 'total_sent', 'total_clicks', 'click_rate',
|
||||
'unique_clickers', 'newsletters'
|
||||
])
|
||||
test_result("Returns all required fields", has_all_fields)
|
||||
|
||||
# Verify values
|
||||
correct_sent = performance['total_sent'] == 10
|
||||
test_result("Correct total_sent", correct_sent, f"Total sent: {performance['total_sent']}")
|
||||
|
||||
correct_clicks = performance['total_clicks'] == 4
|
||||
test_result("Correct total_clicks", correct_clicks, f"Total clicks: {performance['total_clicks']}")
|
||||
|
||||
correct_click_rate = performance['click_rate'] == 40.0
|
||||
test_result("Correct click_rate", correct_click_rate, f"Click rate: {performance['click_rate']}%")
|
||||
|
||||
correct_unique = performance['unique_clickers'] == 4
|
||||
test_result("Correct unique_clickers", correct_unique,
|
||||
f"Unique clickers: {performance['unique_clickers']}")
|
||||
|
||||
has_newsletters = len(performance['newsletters']) > 0
|
||||
test_result("Returns newsletter list", has_newsletters,
|
||||
f"Newsletters: {performance['newsletters']}")
|
||||
|
||||
except Exception as e:
|
||||
test_result("Article performance", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Test 5: Activity Status Classification
|
||||
print("\n" + "-"*80)
|
||||
print("Test 5: Activity Status Classification")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
# Create test data for activity classification
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Active user (opened 10 days ago)
|
||||
newsletter_sends_collection.insert_one({
|
||||
'newsletter_id': 'test-analytics-activity',
|
||||
'subscriber_email': 'test-analytics-active@example.com',
|
||||
'tracking_id': 'test-active-pixel',
|
||||
'sent_at': now - timedelta(days=10),
|
||||
'opened': True,
|
||||
'first_opened_at': now - timedelta(days=10),
|
||||
'last_opened_at': now - timedelta(days=10),
|
||||
'open_count': 1,
|
||||
'created_at': now - timedelta(days=10)
|
||||
})
|
||||
|
||||
# Inactive user (opened 45 days ago)
|
||||
newsletter_sends_collection.insert_one({
|
||||
'newsletter_id': 'test-analytics-activity',
|
||||
'subscriber_email': 'test-analytics-inactive@example.com',
|
||||
'tracking_id': 'test-inactive-pixel',
|
||||
'sent_at': now - timedelta(days=45),
|
||||
'opened': True,
|
||||
'first_opened_at': now - timedelta(days=45),
|
||||
'last_opened_at': now - timedelta(days=45),
|
||||
'open_count': 1,
|
||||
'created_at': now - timedelta(days=45)
|
||||
})
|
||||
|
||||
# Dormant user (opened 90 days ago)
|
||||
newsletter_sends_collection.insert_one({
|
||||
'newsletter_id': 'test-analytics-activity',
|
||||
'subscriber_email': 'test-analytics-dormant@example.com',
|
||||
'tracking_id': 'test-dormant-pixel',
|
||||
'sent_at': now - timedelta(days=90),
|
||||
'opened': True,
|
||||
'first_opened_at': now - timedelta(days=90),
|
||||
'last_opened_at': now - timedelta(days=90),
|
||||
'open_count': 1,
|
||||
'created_at': now - timedelta(days=90)
|
||||
})
|
||||
|
||||
# New user (never opened)
|
||||
newsletter_sends_collection.insert_one({
|
||||
'newsletter_id': 'test-analytics-activity',
|
||||
'subscriber_email': 'test-analytics-new@example.com',
|
||||
'tracking_id': 'test-new-pixel',
|
||||
'sent_at': now - timedelta(days=5),
|
||||
'opened': False,
|
||||
'first_opened_at': None,
|
||||
'last_opened_at': None,
|
||||
'open_count': 0,
|
||||
'created_at': now - timedelta(days=5)
|
||||
})
|
||||
|
||||
# Test classifications
|
||||
active_status = get_subscriber_activity_status('test-analytics-active@example.com')
|
||||
is_active = active_status == 'active'
|
||||
test_result("Classify active user", is_active, f"Status: {active_status}")
|
||||
|
||||
inactive_status = get_subscriber_activity_status('test-analytics-inactive@example.com')
|
||||
is_inactive = inactive_status == 'inactive'
|
||||
test_result("Classify inactive user", is_inactive, f"Status: {inactive_status}")
|
||||
|
||||
dormant_status = get_subscriber_activity_status('test-analytics-dormant@example.com')
|
||||
is_dormant = dormant_status == 'dormant'
|
||||
test_result("Classify dormant user", is_dormant, f"Status: {dormant_status}")
|
||||
|
||||
new_status = get_subscriber_activity_status('test-analytics-new@example.com')
|
||||
is_new = new_status == 'new'
|
||||
test_result("Classify new user", is_new, f"Status: {new_status}")
|
||||
|
||||
except Exception as e:
|
||||
test_result("Activity status classification", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Test 6: Batch Update Activity Statuses
|
||||
print("\n" + "-"*80)
|
||||
print("Test 6: Batch Update Activity Statuses")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
updated_count = update_subscriber_activity_statuses()
|
||||
|
||||
# Should update all test subscribers
|
||||
has_updates = updated_count > 0
|
||||
test_result("Updates subscriber records", has_updates,
|
||||
f"Updated {updated_count} subscribers")
|
||||
|
||||
# Verify a record was created
|
||||
activity_record = subscriber_activity_collection.find_one({
|
||||
'email': 'test-analytics-active@example.com'
|
||||
})
|
||||
|
||||
record_exists = activity_record is not None
|
||||
test_result("Creates activity record", record_exists)
|
||||
|
||||
if activity_record:
|
||||
has_required_fields = all(key in activity_record for key in [
|
||||
'email', 'status', 'total_opens', 'total_clicks',
|
||||
'newsletters_received', 'newsletters_opened', 'updated_at'
|
||||
])
|
||||
test_result("Activity record has required fields", has_required_fields)
|
||||
|
||||
correct_status = activity_record['status'] == 'active'
|
||||
test_result("Activity record has correct status", correct_status,
|
||||
f"Status: {activity_record['status']}")
|
||||
|
||||
except Exception as e:
|
||||
test_result("Batch update activity statuses", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Test 7: Analytics API Endpoints
|
||||
print("\n" + "-"*80)
|
||||
print("Test 7: Analytics API Endpoints")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
with app.test_client() as client:
|
||||
# Test newsletter analytics endpoint
|
||||
response = client.get(f'/api/analytics/newsletter/{test_newsletter_id}')
|
||||
|
||||
is_200 = response.status_code == 200
|
||||
test_result("Newsletter endpoint returns 200", is_200, f"Status: {response.status_code}")
|
||||
|
||||
if is_200:
|
||||
data = response.get_json()
|
||||
has_data = data is not None and 'open_rate' in data
|
||||
test_result("Newsletter endpoint returns data", has_data)
|
||||
|
||||
# Test article analytics endpoint
|
||||
response = client.get(f'/api/analytics/article/{test_article_url}')
|
||||
|
||||
is_200 = response.status_code == 200
|
||||
test_result("Article endpoint returns 200", is_200, f"Status: {response.status_code}")
|
||||
|
||||
if is_200:
|
||||
data = response.get_json()
|
||||
has_data = data is not None and 'click_rate' in data
|
||||
test_result("Article endpoint returns data", has_data)
|
||||
|
||||
# Test subscriber analytics endpoint
|
||||
response = client.get('/api/analytics/subscriber/test-analytics-active@example.com')
|
||||
|
||||
is_200 = response.status_code == 200
|
||||
test_result("Subscriber endpoint returns 200", is_200, f"Status: {response.status_code}")
|
||||
|
||||
if is_200:
|
||||
data = response.get_json()
|
||||
has_data = data is not None and 'status' in data
|
||||
test_result("Subscriber endpoint returns data", has_data)
|
||||
|
||||
# Test update activity endpoint
|
||||
response = client.post('/api/analytics/update-activity')
|
||||
|
||||
is_200 = response.status_code == 200
|
||||
test_result("Update activity endpoint returns 200", is_200, f"Status: {response.status_code}")
|
||||
|
||||
if is_200:
|
||||
data = response.get_json()
|
||||
has_count = data is not None and 'updated_count' in data
|
||||
test_result("Update activity endpoint returns count", has_count)
|
||||
|
||||
except Exception as e:
|
||||
test_result("Analytics API endpoints", False, f"Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Clean up test data
|
||||
print("\n" + "-"*80)
|
||||
print("Cleaning up test data...")
|
||||
print("-"*80)
|
||||
|
||||
try:
|
||||
newsletter_sends_collection.delete_many({'newsletter_id': {'$regex': '^test-analytics-'}})
|
||||
link_clicks_collection.delete_many({'newsletter_id': {'$regex': '^test-analytics-'}})
|
||||
subscriber_activity_collection.delete_many({'email': {'$regex': '^test-analytics-'}})
|
||||
print("✓ Test data cleaned up")
|
||||
except Exception as e:
|
||||
print(f"⚠ Error cleaning up: {str(e)}")
|
||||
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*80)
|
||||
print("TEST SUMMARY")
|
||||
print("="*80)
|
||||
print(f"Total tests: {tests_passed + tests_failed}")
|
||||
print(f"✓ Passed: {tests_passed}")
|
||||
print(f"❌ Failed: {tests_failed}")
|
||||
|
||||
if tests_failed == 0:
|
||||
print("\n🎉 All tests passed!")
|
||||
else:
|
||||
print(f"\n⚠ {tests_failed} test(s) failed")
|
||||
|
||||
print("="*80 + "\n")
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if tests_failed == 0 else 1)
|
||||
Reference in New Issue
Block a user