Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- **vdsm test infrastructure fixes** (#21) — fixes conftest `instance_id` validation (dots → hyphens), adds admin credentials from golden image metadata, rewrites `setup_dsm.py` with Playwright-based user creation (ExtJS-compatible `type()` input, DOM-based popup removal, wizard step navigation), adds `container_id` property, switches to stronger test password for DSM password policy. Podman KVM passthrough works; 21/47 vdsm tests pass on bare DSM without storage volume. Storage volume automation is a follow-up.
- **GitHub Sponsors funding configuration** (#20) — adds `.github/FUNDING.yml` to enable the Sponsor button on the repository
- **Test coverage Phase 3 + Phase 4 of #14** (#19) — closes #14. Total coverage 93% → 96%, with Phase 4's `--cov-fail-under=95` guardrail enforced in `pyproject.toml` so future regressions fail CI. Three more files at 100% (`server.py` 57% → 99% — one defensive `if self._client is None` branch unreachable; `core/auth.py` 90% → 100%; `modules/__init__.py` 96% → 100%; `core/formatting.py` 97% → 99%). Test count 457 → 487 (+30 cases). New `TestSharedClientManagerLifecycle` (15 cases) directly tests the lazy `get_client` init, `with_update_notice` clearing logic, signal handler installation including SIGTERM closure invocation, `_cleanup_session` with both running-loop and no-loop paths, and `_bg_update_check` with newer-version, no-update, and error-swallowing scenarios. New `TestPlatformLabel`, `TestCreateServerInstructionPaths` cover the `_platform_label` Darwin/Linux/Windows branches and the `instructions_file` / `custom_instructions` template paths. New `TestDbusSocketMissing`, `TestLoginErrorPaths`, `TestLogout` close the remaining gaps in `core/auth.py` (D-Bus socket-not-found branch, non-2FA SynologyError propagation, "no sid" AuthenticationError, and the three logout paths). No production code touched.
- **Test coverage Phase 2 of #14** (#17) — total coverage 85% → 93%. `cli/version.py` 27% → 100% and `cli/setup.py` 63% → 100%, the two largest gaps remaining after Phase 1. Test count 392 → 457 (+65 cases) across two new test files: `tests/core/test_cli_version.py` (40 cases covering `_get_current_version`/`_get_latest_pypi_version`/`_version_tuple`/`_detect_installer`/`_load_global_state`/`_save_global_state`/`_check_for_update`/`_do_auto_upgrade`/`_do_revert`, with `urlopen` and `subprocess.run` mocked at the boundary), and `tests/core/test_cli_setup.py` (25 cases covering the async helpers `_attempt_login`/`_connect_and_login`/`_setup_login` including the 2FA bootstrap path with device-token storage, plus `_setup_credential_flow` error paths, the `setup` command's discovered-config valid-and-invalid branches, the `_setup_interactive` validation-failure exit, and the `_emit_claude_desktop_snippet` Linux DBUS fallback). No production code touched.
Expand Down
69 changes: 55 additions & 14 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,72 @@ Virtual-DSM tests run the same integration test suite against a Docker container
running full Synology DSM via QEMU/KVM. This enables testing across multiple DSM
versions without a physical NAS.

### Current Status

**21 of 47 vdsm tests pass** on a bare DSM 7.2.2 instance (wizard completed, admin + test user created). The remaining 26 tests require shared folders on a properly configured storage volume. Virtual-dsm does not auto-create a storage volume during initial setup — this requires either manual Storage Manager configuration or additional automation (tracked as a follow-up).

Tests that pass without a volume: connection (3), system info (1), resource usage (2), error handling (3), empty search cases (2), and several listing/metadata/transfer tests that validate error responses.

### Requirements

- Linux host with KVM support (`/dev/kvm` must exist)
- Docker installed and running
- **Linux host with KVM** — `/dev/kvm` must exist. macOS is not supported (KVM is Linux-only); use the real NAS integration tests on macOS.
- **Podman (recommended)** or native Docker Engine — Docker Desktop runs containers inside a VM that lacks `/dev/kvm` passthrough. Podman runs natively on the host with direct KVM access.
- ~2 GB disk per DSM version (golden images)

macOS is not currently supported — KVM is Linux-only. Use the real NAS integration
tests on macOS.
#### Podman Setup

```bash
# Enable the Podman API socket (one-time)
systemctl --user enable --now podman.socket
```

The test infrastructure auto-detects the Podman socket at `/run/user/$UID/podman/podman.sock` and prefers it over Docker Desktop. No `DOCKER_HOST` configuration needed.

### First-Time Setup

Each DSM version needs a one-time golden image creation (~15 min):
Each DSM version needs a one-time golden image creation (~5 min):

```bash
uv sync --extra dev --extra vdsm
python scripts/vdsm_setup.py --version 7.2.2
uv run playwright install chromium
echo y | uv run python scripts/vdsm_setup.py --version 7.2.2 \
--admin-user mcpadmin --admin-password 'McpTest123!'
```

The script boots a fresh virtual-dsm container. Complete the DSM setup
wizard in your browser (set admin password, basic storage). The script then
automatically creates test users, shared folders, and seed data via the DSM API.
The setup script:
1. Boots a fresh virtual-dsm container via Podman/Docker
2. Automates the DSM first-boot wizard via headless Playwright (account, updates, analytics)
3. Dismisses post-login popups (2FA, MFA promotions) by force-removing them from the DOM
4. Creates the `mcptest` test user via the Control Panel User Creation Wizard (Playwright)
5. Creates test directories and seed data via `docker exec`
6. Saves a compressed golden image to `.vdsm/golden/`

Golden images are stored in `.vdsm/golden/` (~2 GB each, gitignored).
Golden images are stored in `.vdsm/golden/` (~865 MB each, gitignored).

#### Manual Storage Volume Setup (for full 47-test suite)

After the automated setup, the golden image has no DSM storage volume. To enable shared folder tests:

1. Boot the golden image: run `vdsm_setup.py` but stop it before the "Stopping container" step (or boot manually)
2. Open the DSM web UI at the container's URL
3. Open **Storage Manager** → create a **Storage Pool** (Basic/SHR) on the virtual disk → create a **Volume**
4. Open **Control Panel** → **Shared Folder** → create `testshare` and `writable`
5. Set read/write permissions for `mcptest` on both shares
6. Upload test files to `/testshare/Documents/` (report.txt, search_target.txt) and `/testshare/Media/` (sample.mkv)
7. Stop the container and re-save the golden image

This manual step is a one-time investment per DSM version. Automating Storage Manager via Playwright is a planned follow-up.

### Running

```bash
uv run pytest -m vdsm -v --log-cli-level=INFO # Default (7.2.2)
uv run pytest -m vdsm -v --dsm-version 7.1 # Specific version
uv run pytest -m vdsm -v -k "TestSearch" # Single test class
uv run pytest -m vdsm -v --log-cli-level=INFO --no-cov # Default (7.2.2)
uv run pytest -m vdsm -v --dsm-version 7.1 --no-cov # Specific version
uv run pytest -m vdsm -v -k "TestConnection" --no-cov # Single test class
```

Note: `--no-cov` is recommended for vdsm runs since the coverage floor (95%) applies globally and vdsm tests alone won't meet it.

### Supported DSM Versions

| Version | Build | PAT |
Expand All @@ -113,10 +147,17 @@ uv run pytest -m vdsm -v -k "TestSearch" # Single test class
### How It Works

1. Restores a golden image (pre-configured DSM) to a temp storage directory
2. Boots the virtual-dsm Docker container (~2 min)
2. Boots the virtual-dsm container via Podman/Docker (~30s from golden image)
3. Runs the same test functions as the real NAS integration tests
4. Container is stopped and cleaned up automatically

### Known Limitations

- **No auto-volume** — virtual-dsm's QEMU disk is visible to DSM but requires manual Storage Manager configuration to create a storage pool and volume. The `DISK_SIZE` env var controls disk size but does not trigger DSM-level volume creation.
- **Undocumented APIs fail** — `SYNO.Core.User`, `SYNO.Core.Share`, and `SYNO.Core.Share.Permission` return error 105/403 on virtual-dsm even with valid admin sessions. User creation is done via Playwright web UI automation instead.
- **DSM password policy** — DSM 7.2.2 requires "Strong" passwords (blocks "Moderate"). The test user password is `Mcp#Test9!xK27zQ`.
- **Boot time** — first boot from PAT download takes ~2 min; subsequent boots from golden image take ~30s.

## Design Docs

Detailed specs live in `docs/specs/`. These were the original design documents — the code is authoritative where they diverge (e.g., DSM API version pinning, GET-only requests, and search behavior were discovered during live testing and are documented in `CLAUDE.md`).
Expand Down
24 changes: 20 additions & 4 deletions scripts/vdsm_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,15 @@ def setup(dsm_version: str, admin_user: str, admin_password: str) -> None:
click.echo("Aborted.")
sys.exit(0)

# 3. Create storage directory
# 3. Create clean storage directory (must be empty for fresh DSM wizard)
import shutil as _shutil

store = storage_path(dsm_version)
if store.exists():
_shutil.rmtree(store)
click.echo(f"\nCleared existing storage: {store}")
store.mkdir(parents=True, exist_ok=True)
click.echo(f"\nStorage directory: {store}")
click.echo(f"Storage directory: {store}")

# 4. Start virtual-dsm container
click.echo(f"\nStarting virtual-dsm container (DSM {dsm_version})...")
Expand All @@ -115,9 +120,20 @@ def setup(dsm_version: str, admin_user: str, admin_password: str) -> None:
click.echo("\nRunning DSM setup wizard (automated)...")
complete_wizard(base_url, admin_user, admin_password)

# 7. Run post-wizard API configuration
# 7. Wait for DSM services to initialize after wizard
click.echo("\nWaiting 30s for DSM services to initialize after wizard...")
import time

time.sleep(30)

# 8. Run post-wizard API configuration
click.echo("\nConfiguring DSM for integration testing...")
metadata = setup_dsm_for_testing(base_url, admin_password, admin_user=admin_user)
metadata = setup_dsm_for_testing(
base_url,
admin_password,
admin_user=admin_user,
container_id=container.container_id,
)

# 8. Stop container gracefully
click.echo("\nStopping container...")
Expand Down
2 changes: 1 addition & 1 deletion tests/vdsm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def storage_path(version: str) -> Path:
# Default credentials for test DSM instances
DEFAULT_ADMIN_USER: str = "mcpadmin"
DEFAULT_TEST_USER: str = "mcptest"
DEFAULT_TEST_PASSWORD: str = "McpTest123!"
DEFAULT_TEST_PASSWORD: str = "Mcp#Test9!xK27zQ"

# Container resource limits
CONTAINER_DISK_SIZE: str = "16G"
Expand Down
9 changes: 7 additions & 2 deletions tests/vdsm/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,17 @@ def vdsm_config(

config = AppConfig(
schema_version=1,
instance_id=f"vdsm-{dsm_version}",
instance_id=f"vdsm-{dsm_version.replace('.', '-')}",
connection={
"host": host,
"port": port,
"https": False,
"verify_ssl": False,
},
auth={
"username": meta.get("admin_user", "admin"),
"password": meta.get("admin_password", ""),
},
modules={
"filestation": {
"enabled": True,
Expand Down Expand Up @@ -170,7 +174,7 @@ async def admin_client(
# Build admin config
admin_config = AppConfig(
schema_version=1,
instance_id=f"vdsm-admin-{dsm_version}",
instance_id=f"vdsm-admin-{dsm_version.replace('.', '-')}",
connection={
"host": conn.host,
"port": conn.port,
Expand All @@ -179,6 +183,7 @@ async def admin_client(
},
auth={
"username": meta.get("admin_user", "admin"),
"password": meta.get("admin_password", ""),
},
modules={
"filestation": {"enabled": True, "permission": "write"},
Expand Down
9 changes: 9 additions & 0 deletions tests/vdsm/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,15 @@ def _wait_for_dsm(self) -> None:
msg = f"DSM {self.version} did not become ready within {elapsed}s"
raise TimeoutError(msg)

@property
def container_id(self) -> str:
"""Docker container ID (short hash)."""
if self._container is None:
msg = "Container is not started"
raise RuntimeError(msg)
cid: str = self._container._container.id # type: ignore[union-attr]
return cid[:12]

@property
def base_url(self) -> str:
"""HTTP base URL for the running DSM instance."""
Expand Down
Loading
Loading