Include full contents of all nested repositories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""Nextcloud browser automation scenarios."""
|
||||
|
||||
from app.playwright_scenarios.nextcloud.initial_setup import NextcloudInitialSetup
|
||||
|
||||
__all__ = ["NextcloudInitialSetup"]
|
||||
@@ -0,0 +1,231 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user