letsbe-sysadmin/app/playwright_scenarios/poste/initial_setup.py

234 lines
8.7 KiB
Python

"""Poste.io initial setup scenario.
Automates the first-time setup for a fresh Poste.io mail server installation.
This scenario:
1. Navigates to the Poste.io admin setup page
2. Configures the mailserver hostname
3. Creates the admin email account with a generated password
4. Returns the generated credentials for secure storage
"""
import secrets
import string
from typing import Any
from playwright.async_api import Page
from app.playwright_scenarios import register_scenario
from app.playwright_scenarios.base import BaseScenario, ScenarioOptions, ScenarioResult
def generate_secure_password(length: int = 24) -> str:
"""Generate a cryptographically secure password.
Args:
length: Password length (default: 24)
Returns:
A secure random password with mixed characters
"""
# Use a mix of letters, digits, and safe special characters
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
# Ensure at least one of each type
password = [
secrets.choice(string.ascii_lowercase),
secrets.choice(string.ascii_uppercase),
secrets.choice(string.digits),
secrets.choice("!@#$%^&*"),
]
# Fill the rest randomly
password.extend(secrets.choice(alphabet) for _ in range(length - 4))
# Shuffle to avoid predictable positions
password_list = list(password)
secrets.SystemRandom().shuffle(password_list)
return "".join(password_list)
@register_scenario
class PosteInitialSetup(BaseScenario):
"""Automate Poste.io first-time setup wizard.
This scenario handles the initial server configuration when
Poste.io is freshly installed. It configures the mailserver
hostname and creates the administrator email account.
Required inputs:
base_url: The Poste.io instance URL (e.g., https://mail.example.com)
admin_email: Admin email address (e.g., admin@example.com)
Optional inputs:
admin_password: Password for admin account (auto-generated if not provided)
mailserver_hostname: Override mailserver hostname (defaults to URL hostname)
Result data:
setup_completed: Whether initial setup was completed
admin_email: The configured admin email address
admin_password: The password (generated or provided) - STORE SECURELY
mailserver_hostname: The configured hostname
already_configured: True if Poste was already set up
"""
@property
def name(self) -> str:
return "poste_initial_setup"
@property
def required_inputs(self) -> list[str]:
return ["base_url", "admin_email"]
@property
def optional_inputs(self) -> list[str]:
return ["admin_password", "mailserver_hostname"]
@property
def description(self) -> str:
return "Automate Poste.io first-time mail server setup"
async def execute(
self,
page: Page,
inputs: dict[str, Any],
options: ScenarioOptions,
) -> ScenarioResult:
"""Execute the Poste.io initial setup.
Args:
page: Playwright Page object
inputs: Scenario inputs (base_url, admin_email, optional password)
options: Scenario options
Returns:
ScenarioResult with setup status and credentials
"""
base_url = inputs["base_url"].rstrip("/")
admin_email = inputs["admin_email"]
# Generate password if not provided
admin_password = inputs.get("admin_password") or generate_secure_password()
# Extract hostname from URL if not provided
from urllib.parse import urlparse
parsed_url = urlparse(base_url)
mailserver_hostname = inputs.get("mailserver_hostname") or parsed_url.netloc
screenshots = []
result_data = {
"setup_completed": False,
"admin_email": admin_email,
"admin_password": admin_password, # Return for secure storage
"mailserver_hostname": mailserver_hostname,
"already_configured": False,
}
try:
# Navigate to Poste.io
await page.goto(base_url, wait_until="networkidle")
current_url = page.url
# Check if we're on the setup page
if "/admin/install/server" not in current_url:
# Check if redirected to login (already configured)
if "/admin/login" in current_url or "/webmail" in current_url:
result_data["already_configured"] = True
result_data["setup_completed"] = True
return ScenarioResult(
success=True,
data=result_data,
screenshots=screenshots,
error=None,
)
# Try navigating directly to setup page
await page.goto(f"{base_url}/admin/install/server", wait_until="networkidle")
# If still not on setup, it's already configured
if "/admin/install/server" not in page.url:
result_data["already_configured"] = True
result_data["setup_completed"] = True
return ScenarioResult(
success=True,
data=result_data,
screenshots=screenshots,
error=None,
)
# We're on the setup page - configure the mail server
# Wait for the hostname input to be visible
hostname_input = page.locator('input[placeholder*="mail.example.com"]')
await hostname_input.wait_for(state="visible", timeout=10000)
# Clear and fill hostname (may be pre-filled)
await hostname_input.clear()
await hostname_input.fill(mailserver_hostname)
# Fill admin email
admin_email_input = page.locator('input[placeholder*="admin@example.com"]')
await admin_email_input.wait_for(state="visible", timeout=5000)
await admin_email_input.fill(admin_email)
# Fill password
password_input = page.locator('input[type="password"], input[placeholder*="Password"]').last
await password_input.wait_for(state="visible", timeout=5000)
await password_input.fill(admin_password)
# Take screenshot before submitting if requested
if options.screenshot_on_success and options.artifacts_dir:
pre_submit_path = options.artifacts_dir / "poste_pre_submit.png"
await page.screenshot(path=str(pre_submit_path))
screenshots.append(str(pre_submit_path))
# Click Submit button
submit_button = page.locator('button:has-text("Submit")')
await submit_button.click()
# Wait for setup to complete - should redirect away from install page
try:
await page.wait_for_url(
lambda url: "/admin/install" not in url,
timeout=60000, # 60 seconds for setup
)
result_data["setup_completed"] = True
except Exception:
# Check if there's an error message
error_el = page.locator('.error, .alert-danger, [class*="error"]')
if await error_el.count() > 0:
error_text = await error_el.first.text_content()
return ScenarioResult(
success=False,
data=result_data,
screenshots=screenshots,
error=f"Setup failed: {error_text}",
)
# Still on page but no error - might have succeeded
result_data["setup_completed"] = True
# Take final screenshot
if options.screenshot_on_success and options.artifacts_dir:
final_path = options.artifacts_dir / "poste_setup_complete.png"
await page.screenshot(path=str(final_path))
screenshots.append(str(final_path))
return ScenarioResult(
success=result_data["setup_completed"],
data=result_data,
screenshots=screenshots,
error=None,
)
except Exception as e:
# Take error screenshot
if options.screenshot_on_failure and options.artifacts_dir:
error_path = options.artifacts_dir / "poste_setup_error.png"
await page.screenshot(path=str(error_path))
screenshots.append(str(error_path))
return ScenarioResult(
success=False,
data=result_data,
screenshots=screenshots,
error=f"Poste.io setup failed: {str(e)}",
)