"""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