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

232 lines
8.4 KiB
Python

"""Nextcloud initial setup scenario.
Automates the first-time setup wizard for a fresh Nextcloud installation.
This scenario:
1. Navigates to the Nextcloud instance
2. Creates the admin account
3. Optionally skips recommended apps installation
4. Verifies successful login to the dashboard
"""
from typing import Any
from playwright.async_api import Page, expect
from app.playwright_scenarios import register_scenario
from app.playwright_scenarios.base import BaseScenario, ScenarioOptions, ScenarioResult
@register_scenario
class NextcloudInitialSetup(BaseScenario):
"""Automate Nextcloud first-time setup wizard.
This scenario handles the initial admin account creation when
Nextcloud is freshly installed. It's idempotent - if setup is
already complete, it will detect this and succeed.
Required inputs:
base_url: The Nextcloud instance URL (e.g., https://cloud.example.com)
admin_username: Username for the admin account
admin_password: Password for the admin account
Optional inputs:
skip_recommended_apps: Skip the recommended apps step (default: True)
Result data:
admin_created: Whether a new admin was created (False if already setup)
login_successful: Whether login to dashboard succeeded
setup_skipped: True if Nextcloud was already configured
"""
@property
def name(self) -> str:
return "nextcloud_initial_setup"
@property
def required_inputs(self) -> list[str]:
return ["base_url", "admin_username", "admin_password"]
@property
def optional_inputs(self) -> list[str]:
return ["skip_recommended_apps"]
@property
def description(self) -> str:
return "Automate Nextcloud first-time admin setup wizard"
async def execute(
self,
page: Page,
inputs: dict[str, Any],
options: ScenarioOptions,
) -> ScenarioResult:
"""Execute the Nextcloud initial setup.
Args:
page: Playwright Page object
inputs: Scenario inputs (base_url, admin_username, admin_password)
options: Scenario options
Returns:
ScenarioResult with setup status
"""
base_url = inputs["base_url"].rstrip("/")
admin_username = inputs["admin_username"]
admin_password = inputs["admin_password"]
skip_recommended_apps = inputs.get("skip_recommended_apps", True)
screenshots = []
result_data = {
"admin_created": False,
"login_successful": False,
"setup_skipped": False,
}
try:
# Navigate to Nextcloud
await page.goto(base_url, wait_until="networkidle")
# Check if we're on the setup page or login page
current_url = page.url
# Detect if setup is already complete (redirects to login)
if "/login" in current_url or await page.locator('input[name="user"]').count() > 0:
# Already configured, try to login
result_data["setup_skipped"] = True
login_success = await self._try_login(
page, admin_username, admin_password
)
result_data["login_successful"] = login_success
return ScenarioResult(
success=login_success,
data=result_data,
screenshots=screenshots,
error=None if login_success else "Login failed - check credentials",
)
# We're on the setup page - create admin account
# Wait for the setup form to be visible
admin_user_input = page.locator('input[id="adminlogin"], input[name="adminlogin"]')
await admin_user_input.wait_for(state="visible", timeout=10000)
# Fill in admin credentials
await admin_user_input.fill(admin_username)
admin_pass_input = page.locator('input[id="adminpass"], input[name="adminpass"]')
await admin_pass_input.fill(admin_password)
# Check for data directory input (may or may not be present)
data_dir_input = page.locator('input[id="directory"]')
if await data_dir_input.count() > 0 and await data_dir_input.is_visible():
# Keep default data directory
pass
# Click install/finish setup button
# Nextcloud uses various button texts depending on version
install_button = page.locator(
'input[type="submit"][value*="Install"], '
'input[type="submit"][value*="Finish"], '
'button:has-text("Install"), '
'button:has-text("Finish setup")'
)
await install_button.click()
# Wait for installation to complete (this can take a while)
# Look for either dashboard or recommended apps screen
try:
await page.wait_for_url(
lambda url: "/apps" in url or "/index.php" in url or "dashboard" in url.lower(),
timeout=120000, # 2 minutes for installation
)
except Exception:
# May be on recommended apps screen
pass
result_data["admin_created"] = True
# Handle recommended apps screen if present
if skip_recommended_apps:
skip_button = page.locator(
'button:has-text("Skip"), '
'a:has-text("Skip"), '
'.skip-button'
)
if await skip_button.count() > 0:
await skip_button.first.click()
await page.wait_for_load_state("networkidle")
# Verify we're logged in by checking for user menu or dashboard elements
dashboard_indicators = page.locator(
'#user-menu, '
'.user-menu, '
'[data-id="dashboard"], '
'#nextcloud, '
'.app-dashboard'
)
try:
await dashboard_indicators.first.wait_for(state="visible", timeout=30000)
result_data["login_successful"] = True
except Exception:
# Try one more check - look for any indication we're logged in
if await page.locator('.header-menu').count() > 0:
result_data["login_successful"] = True
# Take a screenshot of the final state if requested
if options.screenshot_on_success and options.artifacts_dir:
screenshot_path = options.artifacts_dir / "setup_complete.png"
await page.screenshot(path=str(screenshot_path))
screenshots.append(str(screenshot_path))
success = result_data["admin_created"] and result_data["login_successful"]
return ScenarioResult(
success=success,
data=result_data,
screenshots=screenshots,
error=None if success else "Setup completed but verification failed",
)
except Exception as e:
return ScenarioResult(
success=False,
data=result_data,
screenshots=screenshots,
error=f"Nextcloud setup failed: {str(e)}",
)
async def _try_login(self, page: Page, username: str, password: str) -> bool:
"""Attempt to login to an already-configured Nextcloud.
Args:
page: Playwright Page object (should be on login page)
username: Username to login with
password: Password to login with
Returns:
True if login succeeded, False otherwise
"""
try:
# Fill login form
await page.locator('input[name="user"]').fill(username)
await page.locator('input[name="password"]').fill(password)
# Submit login
await page.locator('input[type="submit"], button[type="submit"]').click()
# Wait for redirect to dashboard
await page.wait_for_url(
lambda url: "/login" not in url,
timeout=30000,
)
# Check for login error message
error_msg = page.locator('.warning, .error, [class*="error"]')
if await error_msg.count() > 0 and await error_msg.first.is_visible():
return False
return True
except Exception:
return False