Files
2026-07-14 13:22:11 +02:00

283 lines
12 KiB
Python

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("--require-s3", action="store_true", help="Fail if the S3 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, require_s3=args.require_s3)
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 minio")
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
try:
_seed_s3_fixture()
except Exception as exc:
if args.require_s3:
print(f"FAIL dev-s3 seed failed: {exc}")
return 1
print(f"SKIP dev-s3 seed failed: {exc}")
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}")
try:
_exercise_profile(profiles["dev-s3"], folder="GovOPlaN", file_path="GovOPlaN/s3-live.txt", expected="s3 live fixture")
except Exception as exc:
message = f"dev-s3: {exc}"
if args.require_s3:
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",
"MINIO_ROOT_USER": "govoplan",
"MINIO_ROOT_PASSWORD": "govoplan-minio",
"MINIO_BUCKET": "govoplan",
}
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, require_s3: 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}")
if require_s3:
minio_url = f"http://127.0.0.1:{os.getenv('MINIO_API_HOST_PORT', '9000')}/minio/health/live"
try:
response = httpx.get(minio_url, timeout=3.0)
if response.status_code != 200:
failures.append(f"dev-s3 expected HTTP 200 at {minio_url}, got HTTP {response.status_code}")
except httpx.HTTPError as exc:
failures.append(f"dev-s3 is not reachable at {minio_url}: {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",
},
{
"id": "dev-s3",
"provider": "s3",
"endpoint_url": f"http://127.0.0.1:{os.getenv('MINIO_API_HOST_PORT', '9000')}",
"credential_mode": "basic",
"username": os.getenv("MINIO_ROOT_USER", "govoplan"),
"password_env": "MINIO_ROOT_PASSWORD",
"metadata": {
"bucket": os.getenv("MINIO_BUCKET", "govoplan"),
"region": "us-east-1",
"path_style": True,
"verify_tls": False,
},
},
]
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]}")
def _seed_s3_fixture() -> None:
try:
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
except ImportError as exc:
raise RuntimeError("boto3 is not installed; install govoplan-files[s3]") from exc
bucket = os.getenv("MINIO_BUCKET", "govoplan")
client = boto3.client(
"s3",
endpoint_url=f"http://127.0.0.1:{os.getenv('MINIO_API_HOST_PORT', '9000')}",
aws_access_key_id=os.getenv("MINIO_ROOT_USER", "govoplan"),
aws_secret_access_key=os.getenv("MINIO_ROOT_PASSWORD", "govoplan-minio"),
region_name="us-east-1",
config=Config(s3={"addressing_style": "path"}),
)
try:
client.create_bucket(Bucket=bucket)
except ClientError as exc:
code = exc.response.get("Error", {}).get("Code")
if code not in {"BucketAlreadyOwnedByYou", "BucketAlreadyExists"}:
raise
client.put_object(Bucket=bucket, Key="GovOPlaN/s3-live.txt", Body=b"s3 live fixture\n", ContentType="text/plain")
if __name__ == "__main__":
raise SystemExit(main())