mirror of
https://gitlab.com/itsulu-odoo/itsulu-blog-publisher.git
synced 2026-05-30 23:41:23 +00:00
Created comprehensive E2E test suite for ITSulu Blog Publisher using Playwright and Runboat. Includes: PHASE3_ROADMAP.md: - Goals for E2E coverage (10-30 scenarios) - Performance benchmark targets (latency, tokens, queries, throughput) - Implementation plan with layer-by-layer breakdown - Success criteria and SLO targets - Runboat integration details for CI/CD e2e/ directory structure: - conftest.py: Runboat polling, auth fixtures, page fixture - requirements.txt: pytest, playwright, requests - test_generation.py: On-demand generation workflows (5 tests) - test_scheduling.py: Schedule slot configuration and execution (6 tests) - test_error_recovery.py: Error handling and email notifications (8 tests) Total: 19 E2E test scenarios covering: - On-demand post generation with auto-publish - Scheduled generation with topic queue - Error recovery and retry mechanism - Email notifications with correct content - Social media copy generation - Concurrent post generation - Progress feedback during API calls Tests use: - Playwright sync API with 30s timeout (Odoo JS rendering) - Runboat polling with 180s timeout (instance cold-start) - Session-scoped auth to avoid repeated 30s logins - Data-test-id selectors where available, fallback to get_by_* - Proper wait_for_load_state() for async operations Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
297 lines
11 KiB
Python
297 lines
11 KiB
Python
"""
|
|
E2E tests for error handling and recovery workflows.
|
|
Tests that system gracefully handles LLM API failures and provides retry capability.
|
|
"""
|
|
from playwright.sync_api import expect
|
|
|
|
|
|
class TestErrorRecovery:
|
|
"""Tests for error handling and recovery mechanisms."""
|
|
|
|
def test_user_retries_failed_generation_after_fixing_api_key(self, page):
|
|
"""
|
|
User attempts generation with invalid API key.
|
|
System logs error. User fixes API key in Settings.
|
|
User clicks Retry on the failed generation log.
|
|
Generation succeeds with the corrected API key.
|
|
|
|
Workflow:
|
|
1. Set invalid API key in Settings
|
|
2. Attempt generation (fails)
|
|
3. Verify: error log created with error message
|
|
4. Verify: Retry button visible on log
|
|
5. Fix API key in Settings
|
|
6. Click Retry on log
|
|
7. Verify: post created successfully
|
|
"""
|
|
# Navigate to Settings
|
|
page.goto('/web/settings')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Set invalid API key (e.g., "INVALID")
|
|
page.get_by_label('Anthropic API Key').fill('INVALID')
|
|
page.get_by_role('button', name='Save').click()
|
|
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Attempt generation
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Test Topic for Error')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Should fail and show error
|
|
page.wait_for_url('**/blog/log/**', timeout=60_000)
|
|
|
|
# Verify error message
|
|
expect(page.locator('body')).to_contain_text('error|unauthorized|api key', use_regex=True)
|
|
|
|
# Verify Retry button is visible
|
|
expect(page.get_by_role('button', name='Retry')).to_be_visible()
|
|
|
|
# Fix API key in Settings
|
|
page.goto('/web/settings')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Anthropic API Key').fill('sk-ant-valid-key-here')
|
|
page.get_by_role('button', name='Save').click()
|
|
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Return to log and click Retry
|
|
page.go_back()
|
|
page.get_by_role('button', name='Retry').click()
|
|
|
|
# Should succeed this time
|
|
page.wait_for_url('**/blog/**', timeout=60_000)
|
|
|
|
# Verify post created
|
|
expect(page.locator('body')).to_contain_text('Test Topic')
|
|
|
|
def test_generation_log_shows_human_readable_error_message(self, page):
|
|
"""
|
|
When generation fails, the error log displays a human-readable message
|
|
explaining what went wrong (not a stack trace).
|
|
|
|
Workflow:
|
|
1. Trigger generation with bad API key
|
|
2. Navigate to generation logs
|
|
3. Verify: error message is clear and actionable
|
|
"""
|
|
# Navigate to Settings and set invalid key
|
|
page.goto('/web/settings')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Anthropic API Key').fill('INVALID_KEY')
|
|
page.get_by_role('button', name='Save').click()
|
|
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Attempt generation
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Error Test')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Should fail
|
|
page.wait_for_url('**/blog/log/**', timeout=60_000)
|
|
|
|
# Check error message is displayed and readable
|
|
error_msg = page.get_by_label('Error Message')
|
|
expect(error_msg).to_be_visible()
|
|
expect(error_msg).not_to_contain_text('Traceback') # No stack trace
|
|
expect(error_msg).not_to_contain_text('File "') # No file paths
|
|
|
|
# Should contain actionable advice
|
|
error_text = error_msg.text_content()
|
|
expect(error_text).to_match_regex(r'(API|key|invalid|unauthorized)', flags='i')
|
|
|
|
def test_error_log_does_not_link_incomplete_blog_post(self, page):
|
|
"""
|
|
When generation fails before blog.post is created, the error log has no
|
|
blog_post_id. Only successful logs link to the created post.
|
|
|
|
Workflow:
|
|
1. Trigger generation with bad API key (fails before post creation)
|
|
2. Verify: error log has no blog_post_id
|
|
3. Verify: "View Post" link is not available on error log
|
|
"""
|
|
# Set invalid key
|
|
page.goto('/web/settings')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Anthropic API Key').fill('BAD_KEY')
|
|
page.get_by_role('button', name='Save').click()
|
|
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Attempt generation
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Another Error')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Fails
|
|
page.wait_for_url('**/blog/log/**', timeout=60_000)
|
|
|
|
# Verify: "View Post" link should NOT exist (or be disabled)
|
|
view_post_link = page.get_by_role('link', name='View Post')
|
|
|
|
# Either link doesn't exist or it's disabled
|
|
if view_post_link.count() == 0:
|
|
# Good: link doesn't exist
|
|
pass
|
|
else:
|
|
# Link exists, check it's disabled
|
|
expect(view_post_link).to_have_attribute('disabled', 'true')
|
|
|
|
|
|
class TestNotificationEmails:
|
|
"""Tests for email notifications after generation."""
|
|
|
|
def test_notification_email_sent_for_published_posts(self, page, notification_email):
|
|
"""
|
|
When a blog post is published (auto_publish=True), a notification email
|
|
is sent to the configured recipient(s).
|
|
|
|
Workflow:
|
|
1. Configure notification email in Settings
|
|
2. Generate and auto-publish post
|
|
3. Verify: email appears in outbox with correct recipient
|
|
4. Verify: email contains post title, blog name, and social copy
|
|
"""
|
|
# Set notification email
|
|
page.goto('/web/settings')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Notification Emails').fill(notification_email)
|
|
page.get_by_role('button', name='Save').click()
|
|
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Generate and publish
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Email Notification Test')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('Auto-publish').check()
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Wait for post to be created
|
|
page.wait_for_url('**/blog/**', timeout=60_000)
|
|
|
|
# Verify post title appears
|
|
expect(page.locator('h1')).to_contain_text('Email Notification')
|
|
|
|
# Check email was sent (navigate to email outbox)
|
|
page.goto('/web#model=mail.mail&view_type=list')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Filter for recent emails to this recipient
|
|
page.get_by_role('button', name='Filters').click()
|
|
page.get_by_role('option', name='Email To').click()
|
|
page.get_by_label('Email To').fill(notification_email)
|
|
|
|
# Should find email
|
|
expect(page.locator('body')).to_contain_text(notification_email)
|
|
|
|
# Click email to verify content
|
|
page.get_by_role('link', name='Email').first.click()
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Verify subject contains blog name and post title
|
|
expect(page.locator('[data-field="subject"]')).to_contain_text('Email Notification')
|
|
|
|
# Verify body contains social copy sections (X, BlueSky, Mastodon, LinkedIn)
|
|
body = page.locator('[data-field="body_html"]')
|
|
expect(body).to_contain_text('Twitter|X', use_regex=True)
|
|
|
|
def test_no_email_sent_for_draft_posts(self, page, notification_email):
|
|
"""
|
|
When a post is saved as draft (auto_publish=False), NO email is sent.
|
|
Only published posts trigger notifications.
|
|
|
|
Workflow:
|
|
1. Configure notification email
|
|
2. Generate as draft (auto_publish=False)
|
|
3. Verify: no email sent (email count doesn't increase)
|
|
"""
|
|
# Get initial email count
|
|
page.goto('/web#model=mail.mail&view_type=list')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Count emails before
|
|
emails_before = len(page.locator('tbody tr').all())
|
|
|
|
# Generate as draft
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Draft Post - No Email')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('Auto-publish').uncheck() # Draft
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
# Wait for completion
|
|
page.wait_for_url('**/blog/log/**', timeout=60_000)
|
|
|
|
# Check email count again
|
|
page.goto('/web#model=mail.mail&view_type=list')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
emails_after = len(page.locator('tbody tr').all())
|
|
|
|
# Should be the same (no new email)
|
|
assert emails_after == emails_before, \
|
|
f"Email count increased from {emails_before} to {emails_after} for draft post"
|
|
|
|
def test_email_contains_correct_blog_name_in_subject(self, page, blog_name, notification_email):
|
|
"""
|
|
Email subject line includes the blog name in brackets,
|
|
e.g., "[ITSulu Insights] Blog Post Published: <post_title>"
|
|
|
|
Workflow:
|
|
1. Generate and publish post
|
|
2. Check email subject line
|
|
3. Verify format: [<blog_name>] Blog Post Published: <post_title>
|
|
"""
|
|
# Configure and generate
|
|
page.goto('/web/settings')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Notification Emails').fill(notification_email)
|
|
page.get_by_role('button', name='Save').click()
|
|
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.goto('/odoo/blog/generate-now')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
page.get_by_label('Topic').fill('Subject Line Test')
|
|
page.get_by_label('LLM Provider').select_option('anthropic')
|
|
page.get_by_label('Auto-publish').check()
|
|
page.get_by_role('button', name='Generate').click()
|
|
|
|
page.wait_for_url('**/blog/**', timeout=60_000)
|
|
|
|
# Check email subject
|
|
page.goto('/web#model=mail.mail&view_type=list')
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
# Find latest email
|
|
page.get_by_role('link', name='Email').first.click()
|
|
page.wait_for_load_state('networkidle')
|
|
|
|
subject = page.locator('[data-field="subject"]').input_value()
|
|
|
|
# Verify format: [ITSulu Insights] Blog Post Published: ...
|
|
assert f'[{blog_name}]' in subject, f"Subject missing blog name: {subject}"
|
|
assert 'Published' in subject, f"Subject missing 'Published': {subject}"
|
|
assert 'Subject Line' in subject, f"Subject missing post title: {subject}"
|