import pytest from sqlalchemy import select from app.core.secret_codec import decrypt_secret_config from app.models.entities import Backend @pytest.mark.anyio async def test_list_backends_returns_default_local_backend(api_client) -> None: response = await api_client.get("/api/backends") assert response.status_code == 200 payload = response.json() assert len(payload["items"]) >= 1 assert payload["items"][0]["name"] == "local-default" assert payload["items"][0]["type"] == "local_directory" @pytest.mark.anyio async def test_create_local_backend(api_client, runtime_paths) -> None: response = await api_client.post( "/api/backends", json={ "name": "archive-local", "type": "local_directory", "stability_class": "stable", "read_priority": 50, "write_priority": 50, "config": { "base_path": runtime_paths["local_storage_dir"] + "/archive", }, }, ) assert response.status_code == 200 payload = response.json()["backend"] assert payload["name"] == "archive-local" assert payload["type"] == "local_directory" @pytest.mark.anyio async def test_create_s3_backend_metadata(api_client, session_factory) -> None: response = await api_client.post( "/api/backends", json={ "name": "s3-main", "type": "s3", "stability_class": "stable", "read_priority": 80, "write_priority": 80, "config": { "bucket": "iron-test", "region": "us-east-1", "access_key_id": "access-key", "secret_access_key": "secret-key", }, }, ) assert response.status_code == 200 payload = response.json()["backend"] assert payload["name"] == "s3-main" assert payload["type"] == "s3" assert payload["config"]["bucket"] == "iron-test" assert "access_key_id" not in payload["config"] assert "secret_access_key" not in payload["config"] assert payload["secret_fields"] == ["access_key_id", "secret_access_key"] async with session_factory() as session: backend = ( await session.execute(select(Backend).where(Backend.name == "s3-main")) ).scalar_one() assert backend.secret_config_json.startswith("fernet:v1:") assert "access-key" not in backend.secret_config_json assert "secret-key" not in backend.secret_config_json @pytest.mark.anyio async def test_create_webdav_backend_separates_password(api_client) -> None: response = await api_client.post( "/api/backends", json={ "name": "openlist-main", "type": "webdav", "stability_class": "stable", "read_priority": 60, "write_priority": 40, "config": { "endpoint_url": "http://127.0.0.1:5244/dav", "username": "iron", "password": "secret-password", "root_path": "apps/iron", }, }, ) assert response.status_code == 200 payload = response.json()["backend"] assert payload["type"] == "webdav" assert payload["config"]["endpoint_url"] == "http://127.0.0.1:5244/dav" assert payload["config"]["username"] == "iron" assert payload["config"]["root_path"] == "apps/iron" assert "password" not in payload["config"] assert payload["secret_fields"] == ["password"] @pytest.mark.anyio async def test_check_local_backend(api_client) -> None: list_response = await api_client.get("/api/backends") backend_id = list_response.json()["items"][0]["id"] response = await api_client.post(f"/api/backends/{backend_id}/check") assert response.status_code == 200 assert response.json()["status"] == "healthy" @pytest.mark.anyio async def test_check_s3_backend_returns_healthy(api_client, fake_s3) -> None: create_response = await api_client.post( "/api/backends", json={ "name": "s3-main", "type": "s3", "stability_class": "stable", "read_priority": 80, "write_priority": 80, "config": { "bucket": "iron-test", }, }, ) backend_id = create_response.json()["backend"]["id"] response = await api_client.post(f"/api/backends/{backend_id}/check") assert response.status_code == 200 assert response.json()["status"] == "healthy" @pytest.mark.anyio async def test_disable_backend(api_client) -> None: create_response = await api_client.post( "/api/backends", json={ "name": "archive-local", "type": "local_directory", "stability_class": "stable", "read_priority": 50, "write_priority": 50, "config": { "base_path": "/tmp/archive-local", }, }, ) backend_id = create_response.json()["backend"]["id"] response = await api_client.post(f"/api/backends/{backend_id}/disable") assert response.status_code == 200 assert response.json()["backend"]["is_enabled"] is False @pytest.mark.anyio async def test_enable_backend(api_client) -> None: create_response = await api_client.post( "/api/backends", json={ "name": "archive-local", "type": "local_directory", "stability_class": "stable", "read_priority": 50, "write_priority": 50, "config": { "base_path": "/tmp/archive-local", }, }, ) backend_id = create_response.json()["backend"]["id"] await api_client.post(f"/api/backends/{backend_id}/disable") response = await api_client.post(f"/api/backends/{backend_id}/enable") assert response.status_code == 200 assert response.json()["backend"]["is_enabled"] is True @pytest.mark.anyio async def test_update_s3_backend_preserves_and_replaces_secrets(api_client, session_factory) -> None: create_response = await api_client.post( "/api/backends", json={ "name": "s3-main", "type": "s3", "stability_class": "stable", "read_priority": 80, "write_priority": 80, "config": { "bucket": "iron-test", "region": "us-east-1", "access_key_id": "access-key", "secret_access_key": "secret-key", }, }, ) backend_id = create_response.json()["backend"]["id"] update_response = await api_client.patch( f"/api/backends/{backend_id}", json={ "name": "s3-archive", "read_priority": 40, "write_priority": 45, "config": { "bucket": "iron-archive", "prefix": "cold", }, }, ) assert update_response.status_code == 200 payload = update_response.json()["backend"] assert payload["name"] == "s3-archive" assert payload["read_priority"] == 40 assert payload["write_priority"] == 45 assert payload["config"]["bucket"] == "iron-archive" assert payload["config"]["prefix"] == "cold" assert payload["secret_fields"] == ["access_key_id", "secret_access_key"] async with session_factory() as session: backend = await session.get(Backend, backend_id) assert decrypt_secret_config(backend.secret_config_json)["secret_access_key"] == "secret-key" replace_response = await api_client.patch( f"/api/backends/{backend_id}", json={ "config": { "secret_access_key": "next-secret-key", }, }, ) assert replace_response.status_code == 200 async with session_factory() as session: backend = await session.get(Backend, backend_id) assert decrypt_secret_config(backend.secret_config_json)["secret_access_key"] == "next-secret-key" @pytest.mark.anyio async def test_delete_backend_requires_disabled_and_unreferenced(api_client) -> None: create_response = await api_client.post( "/api/backends", json={ "name": "archive-local", "type": "local_directory", "stability_class": "stable", "read_priority": 50, "write_priority": 50, "config": { "base_path": "/tmp/archive-local", }, }, ) backend_id = create_response.json()["backend"]["id"] enabled_delete = await api_client.delete(f"/api/backends/{backend_id}") assert enabled_delete.status_code == 409 assert enabled_delete.json()["error"]["code"] == "backend_delete_conflict" disable_response = await api_client.post(f"/api/backends/{backend_id}/disable") assert disable_response.status_code == 200 delete_response = await api_client.delete(f"/api/backends/{backend_id}") assert delete_response.status_code == 204 @pytest.mark.anyio async def test_delete_backend_rejects_policy_reference(api_client) -> None: create_response = await api_client.post( "/api/backends", json={ "name": "s3-main", "type": "s3", "stability_class": "stable", "read_priority": 80, "write_priority": 80, "config": {"bucket": "iron-test"}, }, ) backend_id = create_response.json()["backend"]["id"] await api_client.put( "/api/policies/placement/document", json={ "require_local": True, "stable_replica_count": 1, "opportunistic_replica_count": 0, "preferred_backend_ids": [backend_id], "excluded_backend_ids": [], "max_non_local_size_bytes": None, }, ) await api_client.post(f"/api/backends/{backend_id}/disable") delete_response = await api_client.delete(f"/api/backends/{backend_id}") assert delete_response.status_code == 409 assert delete_response.json()["error"]["code"] == "backend_delete_conflict" @pytest.mark.anyio async def test_create_backend_rejects_invalid_config(api_client) -> None: response = await api_client.post( "/api/backends", json={ "name": "bad-s3", "type": "s3", "stability_class": "stable", "read_priority": 10, "write_priority": 10, "config": {}, }, ) assert response.status_code == 400 assert response.json()["error"]["code"] == "backend_config_invalid"