fc5: /api/migrate blueprint — multipart upload for ingest kinds + JSON for the rest + apply-without-backup guard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""FC-5: /api/migrate API tests."""
|
||||
import io
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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", data={})
|
||||
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": [],
|
||||
}
|
||||
data = {
|
||||
"export_file": (io.BytesIO(json.dumps(export).encode()), "gs-export.json"),
|
||||
"dry_run": "false",
|
||||
}
|
||||
resp = await client.post("/api/migrate/gs_ingest", files=data)
|
||||
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": [],
|
||||
}
|
||||
data = {
|
||||
"export_file": (io.BytesIO(json.dumps(export).encode()), "gs-export.json"),
|
||||
"dry_run": "false",
|
||||
}
|
||||
resp = await client.post("/api/migrate/gs_ingest", files=data)
|
||||
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": [],
|
||||
}
|
||||
data = {
|
||||
"export_file": (io.BytesIO(json.dumps(export).encode()), "gs-export.json"),
|
||||
"dry_run": "true",
|
||||
}
|
||||
resp = await client.post("/api/migrate/gs_ingest", files=data)
|
||||
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
|
||||
Reference in New Issue
Block a user