from __future__ import annotations import pytest PASSWORD = "correct horse battery staple" HOST_KEY = "ssh-ed25519 AQID" async def _setup_headers(client) -> dict[str, str]: response = await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD}) assert response.status_code == 201 return {"X-CSRF-Token": client.cookies["backup_tool_csrf"]} async def _create_secret(client, headers: dict[str, str], purpose: str) -> str: response = await client.post( "/api/v2/admin/secrets", json={"purpose": purpose, "value": "PRIVATE-KEY-CANARY"}, headers=headers, ) assert response.status_code == 201 assert "PRIVATE-KEY-CANARY" not in response.text return str(response.json()["id"]) def _source(secret_id: str) -> dict[str, object]: return { "name": "remote", "kind": "ssh", "public_config": { "hostname": "backup.example.test", "port": 22, "username": "backup", "host_key": HOST_KEY, "root": "/", }, "private_key_secret_id": secret_id, } @pytest.mark.asyncio async def test_ssh_source_persists_only_safe_public_config_and_one_key_reference( app_client, ) -> None: client, _ = app_client headers = await _setup_headers(client) secret_id = await _create_secret(client, headers, "ssh_private_key") created = await client.post("/api/v2/sources", json=_source(secret_id), headers=headers) assert created.status_code == 201 body = created.json() assert body["kind"] == "ssh" assert body["public_config"] == _source(secret_id)["public_config"] assert "secret" not in body assert "PRIVATE-KEY-CANARY" not in created.text listed = await client.get("/api/v2/sources", headers=headers) assert listed.status_code == 200 assert listed.json()["items"] == [body] assert "PRIVATE-KEY-CANARY" not in listed.text probe = await client.post(f"/api/v2/sources/{body['id']}/probe", headers=headers) assert probe.status_code == 409 assert probe.json()["code"] == "source_probe_failed" @pytest.mark.asyncio async def test_ssh_source_requires_exactly_one_existing_private_key_secret( app_client, ) -> None: client, _ = app_client headers = await _setup_headers(client) wrong_purpose = await _create_secret(client, headers, "ssh") request = _source(wrong_purpose) rejected_purpose = await client.post("/api/v2/sources", json=request, headers=headers) assert rejected_purpose.status_code == 422 assert rejected_purpose.json()["code"] == "validation_failed" assert "PRIVATE-KEY-CANARY" not in rejected_purpose.text missing_secret = await client.post( "/api/v2/sources", json=_source("00000000-0000-0000-0000-000000000000"), headers=headers, ) assert missing_secret.status_code == 422 assert missing_secret.json()["code"] == "validation_failed" valid_secret = await _create_secret(client, headers, "ssh_private_key") extra_secret_reference = { **_source(valid_secret), "secret_refs": [valid_secret, wrong_purpose], } rejected_extra = await client.post( "/api/v2/sources", json=extra_secret_reference, headers=headers ) assert rejected_extra.status_code == 422 @pytest.mark.asyncio @pytest.mark.parametrize( "payload", [ {"name": "unsupported", "kind": "sftp", "public_config": {}}, {"name": "unsupported", "kind": "postgresql", "public_config": {}}, {"name": "unsupported", "kind": "mysql", "public_config": {}}, {"name": "unsupported", "kind": "shell", "public_config": {}}, ], ) async def test_source_api_rejects_all_non_local_ssh_kinds( app_client, payload: dict[str, object] ) -> None: client, _ = app_client headers = await _setup_headers(client) response = await client.post("/api/v2/sources", json=payload, headers=headers) assert response.status_code == 422 @pytest.mark.asyncio @pytest.mark.parametrize( "public_config", [ { "hostname": "backup.example.test", "port": 22, "username": "backup", "root": "/", }, { "hostname": "backup.example.test", "port": 0, "username": "backup", "host_key": HOST_KEY, "root": "/", }, { "hostname": "backup.example.test", "port": 22, "username": "backup", "host_key": HOST_KEY, "root": "/not-the-chroot", }, { "hostname": "backup.example.test", "port": 22, "username": "backup", "host_key": HOST_KEY, "root": "/", "password": "not-supported", }, { "hostname": "backup.example.test", "port": 22, "username": "backup", "host_key": HOST_KEY, "root": "/", "remote_command": "not-supported", }, ], ) async def test_ssh_source_rejects_noncanonical_or_unsupported_config( app_client, public_config: dict[str, object] ) -> None: client, _ = app_client headers = await _setup_headers(client) secret_id = await _create_secret(client, headers, "ssh_private_key") payload = _source(secret_id) payload["public_config"] = public_config response = await client.post("/api/v2/sources", json=payload, headers=headers) assert response.status_code == 422