from __future__ import annotations import argparse import os import socket from pathlib import Path import httpx from govoplan_files.backend.storage.connector_browse import browse_connector_profile from govoplan_files.backend.storage.connector_imports import read_connector_file from govoplan_files.backend.storage.connector_profiles import connector_profiles_from_payload ROOT = Path(__file__).resolve().parent def main() -> int: parser = argparse.ArgumentParser(description="Smoke-test GovOPlaN connector helpers against the local dev compose stack.") parser.add_argument("--require-smb", action="store_true", help="Fail if the SMB connector cannot browse/import.") parser.add_argument("--debug-smb", action="store_true", help="Print direct smbclient probes before the connector smoke check.") args = parser.parse_args() _load_dotenv() _default_env() preflight_failures = _preflight_services(require_smb=args.require_smb) if preflight_failures: for failure in preflight_failures: print(f"FAIL {failure}") print("Start or recreate the connector stack from this directory with: docker compose up -d nextcloud nextcloud-db webdav smb") return 1 _seed_webdav_fixture() _seed_smb_fixture() try: _seed_nextcloud_fixture() except httpx.HTTPError as exc: print(f"FAIL dev-nextcloud seed failed: {exc}") return 1 profiles = {profile.id: profile for profile in connector_profiles_from_payload({"profiles": _profile_payloads()})} if args.debug_smb: _debug_smb(profiles["dev-smb"]) failures: list[str] = [] for profile_id, folder, file_path, expected in ( ("dev-webdav", "GovOPlaN", "GovOPlaN/webdav-live.txt", "webdav live fixture"), ("dev-nextcloud", "GovOPlaN", "GovOPlaN/nextcloud-live.txt", "nextcloud live fixture"), ): try: _exercise_profile(profiles[profile_id], folder=folder, file_path=file_path, expected=expected) except Exception as exc: failures.append(f"{profile_id}: {exc}") try: _exercise_profile(profiles["dev-smb"], folder="GovOPlaN", file_path="GovOPlaN/smb-live.txt", expected="smb live fixture") except Exception as exc: message = f"dev-smb: {exc}" if args.require_smb: failures.append(message) else: print(f"SKIP {message}") if failures: for failure in failures: print(f"FAIL {failure}") return 1 print("OK connector dev stack smoke checks passed") return 0 def _default_env() -> None: defaults = { "NEXTCLOUD_ADMIN_USER": "admin", "NEXTCLOUD_ADMIN_PASSWORD": "govoplan-nextcloud-admin", "WEBDAV_USER": "govoplan", "WEBDAV_PASSWORD": "govoplan-webdav", "SMB_SHARE_NAME": "files", "SMB_USER": "govoplan", "SMB_PASSWORD": "govoplan-smb", } for key, value in defaults.items(): os.environ.setdefault(key, value) def _load_dotenv() -> None: path = ROOT / ".env" if not path.exists(): return for line in path.read_text(encoding="utf-8").splitlines(): text = line.strip() if not text or text.startswith("#") or "=" not in text: continue key, value = text.split("=", 1) key = key.strip() if not key or key in os.environ: continue os.environ[key] = value.strip().strip("\"'") def _preflight_services(*, require_smb: bool) -> list[str]: failures: list[str] = [] nextcloud_url = f"http://127.0.0.1:{os.getenv('NEXTCLOUD_HOST_PORT', '9081')}/status.php" try: response = httpx.get(nextcloud_url, timeout=3.0) if response.status_code != 200: failures.append(f"dev-nextcloud expected HTTP 200 at {nextcloud_url}, got HTTP {response.status_code}") except httpx.HTTPError as exc: failures.append(f"dev-nextcloud is not reachable at {nextcloud_url}: {exc}") webdav_url = f"http://127.0.0.1:{os.getenv('WEBDAV_HOST_PORT', '9083')}/" try: response = httpx.get(webdav_url, timeout=3.0) if response.status_code not in {200, 401}: failures.append(f"dev-webdav expected HTTP 200/401 at {webdav_url}, got HTTP {response.status_code}") except httpx.HTTPError as exc: failures.append(f"dev-webdav is not reachable at {webdav_url}: {exc}") if require_smb: smb_port = int(os.getenv("SMB_HOST_PORT", "1445")) try: with socket.create_connection(("127.0.0.1", smb_port), timeout=3.0): pass except OSError as exc: failures.append(f"dev-smb is not reachable at 127.0.0.1:{smb_port}: {exc}") return failures def _profile_payloads() -> list[dict[str, object]]: return [ { "id": "dev-webdav", "provider": "webdav", "endpoint_url": f"http://127.0.0.1:{os.getenv('WEBDAV_HOST_PORT', '9083')}/", "credential_mode": "basic", "username": os.getenv("WEBDAV_USER", "govoplan"), "password_env": "WEBDAV_PASSWORD", }, { "id": "dev-nextcloud", "provider": "nextcloud", "endpoint_url": f"http://127.0.0.1:{os.getenv('NEXTCLOUD_HOST_PORT', '9081')}/remote.php/dav/files/{os.getenv('NEXTCLOUD_ADMIN_USER', 'admin')}/", "credential_mode": "basic", "username": os.getenv("NEXTCLOUD_ADMIN_USER", "admin"), "password_env": "NEXTCLOUD_ADMIN_PASSWORD", }, { "id": "dev-smb", "provider": "smb", "endpoint_url": f"smb://127.0.0.1:{os.getenv('SMB_HOST_PORT', '1445')}/{os.getenv('SMB_SHARE_NAME', 'files')}", "credential_mode": "basic", "username": os.getenv("SMB_USER", "govoplan"), "password_env": "SMB_PASSWORD", }, ] def _exercise_profile(profile, *, folder: str, file_path: str, expected: str) -> None: items = browse_connector_profile(profile, path=folder) paths = {item.path for item in items} if file_path not in paths: raise RuntimeError(f"{file_path!r} not found; saw {sorted(paths)!r}") downloaded = read_connector_file(profile, library_id="", path=file_path, max_bytes=1024 * 1024) text = downloaded.data.decode("utf-8").strip() if text != expected: raise RuntimeError(f"{file_path!r} content mismatch: {text!r}") print(f"OK {profile.id} browse/import {file_path}") def _debug_smb(profile) -> None: try: import smbclient except ImportError as exc: print(f"DEBUG smbclient import failed: {exc}") return from govoplan_files.backend.storage.connector_browse import _smb_client_kwargs, _smb_location, _smb_unc_path location = _smb_location(profile) kwargs = _smb_client_kwargs(profile, location) print(f"DEBUG SMB endpoint=//{location.server}:{location.port}/{location.share} root_path={location.root_path!r}") print(f"DEBUG SMB user={kwargs.get('username')!r} require_signing={kwargs.get('require_signing')!r} auth_protocol={kwargs.get('auth_protocol')!r}") try: smbclient.reset_connection_cache() except Exception as exc: print(f"DEBUG SMB reset cache failed: {exc}") for path in (_smb_unc_path(location, ""), _smb_unc_path(location, "GovOPlaN")): try: print(f"DEBUG SMB list {path}: {smbclient.listdir(path, **kwargs)}") except Exception as exc: print(f"DEBUG SMB list {path} failed: {type(exc).__name__}: {exc}") def _seed_webdav_fixture() -> None: path = ROOT / "data" / "webdav" / "GovOPlaN" path.mkdir(parents=True, exist_ok=True) (path / "webdav-live.txt").write_text("webdav live fixture\n", encoding="utf-8") def _seed_smb_fixture() -> None: path = ROOT / "data" / "smb" / "GovOPlaN" path.mkdir(parents=True, exist_ok=True) (path / "smb-live.txt").write_text("smb live fixture\n", encoding="utf-8") def _seed_nextcloud_fixture() -> None: base_url = f"http://127.0.0.1:{os.getenv('NEXTCLOUD_HOST_PORT', '9081')}/remote.php/dav/files/{os.getenv('NEXTCLOUD_ADMIN_USER', 'admin')}/GovOPlaN" auth = (os.getenv("NEXTCLOUD_ADMIN_USER", "admin"), os.getenv("NEXTCLOUD_ADMIN_PASSWORD", "govoplan-nextcloud-admin")) response = httpx.request("MKCOL", base_url, auth=auth, timeout=15.0) if response.status_code not in {201, 405}: raise RuntimeError(f"Nextcloud MKCOL failed with HTTP {response.status_code}: {response.text[:200]}") response = httpx.put(f"{base_url}/nextcloud-live.txt", content=b"nextcloud live fixture\n", auth=auth, timeout=15.0) if response.status_code not in {200, 201, 204}: raise RuntimeError(f"Nextcloud PUT failed with HTTP {response.status_code}: {response.text[:200]}") if __name__ == "__main__": raise SystemExit(main())