"""FC-5: /api/migrate API tests.""" import io import json import pytest from werkzeug.datastructures import FileStorage import backend.app.tasks.migration # noqa: F401 — register celery task from backend.app import create_app from backend.app.celery_app import celery pytestmark = pytest.mark.integration def _file_storage(payload: dict, filename: str = "export.json") -> FileStorage: return FileStorage( stream=io.BytesIO(json.dumps(payload).encode("utf-8")), filename=filename, content_type="application/json", ) @pytest.fixture async def app(): return create_app() @pytest.fixture async def client(app): async with app.test_client() as c: yield c @pytest.mark.asyncio async def test_post_backup_returns_202_and_id(client, monkeypatch): monkeypatch.setattr( "backend.app.api.migrate.run_migration", type("F", (), {"delay": lambda self, *a, **k: None})(), ) resp = await client.post("/api/migrate/backup", json={}) assert resp.status_code == 202 body = await resp.get_json() assert "run_id" in body assert body["status"] == "pending" @pytest.mark.asyncio async def test_post_rejects_unknown_kind(client): resp = await client.post("/api/migrate/destroy", json={}) assert resp.status_code == 400 body = await resp.get_json() assert body["error"] == "unknown_kind" @pytest.mark.asyncio async def test_post_ingest_rejects_missing_file(client): resp = await client.post("/api/migrate/gs_ingest", form={}) assert resp.status_code == 400 body = await resp.get_json() assert body["error"] == "missing_export_file" @pytest.mark.asyncio async def test_post_ingest_accepts_multipart_file(client, monkeypatch): monkeypatch.setattr( "backend.app.api.migrate._has_recent_pre_migration_backup", lambda: True, # pretend backup exists ) monkeypatch.setattr( "backend.app.api.migrate.run_migration", type("F", (), {"delay": lambda self, *a, **k: None})(), ) export = { "source_app": "gallerysubscriber", "schema_version": 1, "subscriptions": [], "credentials": [], } resp = await client.post( "/api/migrate/gs_ingest", form={"dry_run": "false"}, files={"export_file": _file_storage(export, "gs-export.json")}, ) assert resp.status_code == 202 @pytest.mark.asyncio async def test_post_apply_without_backup_rejected(client, monkeypatch): monkeypatch.setattr( "backend.app.api.migrate._has_recent_pre_migration_backup", lambda: False, ) export = { "source_app": "gallerysubscriber", "schema_version": 1, "subscriptions": [], "credentials": [], } resp = await client.post( "/api/migrate/gs_ingest", form={"dry_run": "false"}, files={"export_file": _file_storage(export, "gs-export.json")}, ) assert resp.status_code == 400 body = await resp.get_json() assert body["error"] == "no_backup" @pytest.mark.asyncio async def test_post_dry_run_allowed_without_backup(client, monkeypatch): monkeypatch.setattr( "backend.app.api.migrate._has_recent_pre_migration_backup", lambda: False, ) monkeypatch.setattr( "backend.app.api.migrate.run_migration", type("F", (), {"delay": lambda self, *a, **k: None})(), ) export = { "source_app": "gallerysubscriber", "schema_version": 1, "subscriptions": [], "credentials": [], } resp = await client.post( "/api/migrate/gs_ingest", form={"dry_run": "true"}, files={"export_file": _file_storage(export, "gs-export.json")}, ) assert resp.status_code == 202 @pytest.mark.asyncio async def test_get_run_404_unknown(client): resp = await client.get("/api/migrate/runs/9999999") assert resp.status_code == 404 def test_run_migration_task_registered(): assert "backend.app.tasks.migration.run_migration" in celery.tasks