diff --git a/backend/app/services/migrators/gs_ingest.py b/backend/app/services/migrators/gs_ingest.py index b7a3d88..18b832a 100644 --- a/backend/app/services/migrators/gs_ingest.py +++ b/backend/app/services/migrators/gs_ingest.py @@ -115,7 +115,7 @@ async def migrate_async( encrypted = fc_crypto.encrypt(cred["plaintext"]) db.add(Credential( platform=cred["platform"], - kind=cred.get("credential_type") or "cookies", + credential_type=cred.get("credential_type") or "cookies", encrypted_blob=encrypted, expires_at=cred.get("expires_at"), )) diff --git a/backend/app/services/migrators/verify.py b/backend/app/services/migrators/verify.py index df8c951..52bf37c 100644 --- a/backend/app/services/migrators/verify.py +++ b/backend/app/services/migrators/verify.py @@ -18,7 +18,7 @@ async def verify_async(db: AsyncSession, *, expected: dict | None = None) -> dic checks = { "artist_subscriptions": ( - select(func.count(Artist.id)).where(Artist.is_subscription == True) + select(func.count(Artist.id)).where(Artist.is_subscription.is_(True)) ), "source_count": select(func.count(Source.id)), "credential_count": select(func.count(Credential.id)), @@ -51,12 +51,15 @@ async def verify_sha256_sample( samples: list[dict] = [] for img_id, path, expected_sha in rows: p = Path(path) - if not p.exists(): + # Sync stdlib filesystem ops are intentional: this verify pass runs + # inside a Celery task under asyncio.run; no other awaitables compete + # for the loop. Same pattern as download_service.py. + if not p.exists(): # noqa: ASYNC240 missing += 1 samples.append({"id": img_id, "path": path, "result": "missing"}) continue h = hashlib.sha256() - with p.open("rb") as f: + with p.open("rb") as f: # noqa: ASYNC230 for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) if h.hexdigest() == expected_sha: diff --git a/backend/app/tasks/migration.py b/backend/app/tasks/migration.py index dd200bd..bdedb93 100644 --- a/backend/app/tasks/migration.py +++ b/backend/app/tasks/migration.py @@ -20,15 +20,10 @@ from ..celery_app import celery from ..config import get_config from ..models import MigrationRun from ..services.credential_crypto import CredentialCrypto -from ..services.migrators import ( - backup as backup_mod, - gs_ingest, - ir_ingest, - ml_queue, - rollback as rollback_mod, - tag_apply, - verify, -) +from ..services.migrators import backup as backup_mod +from ..services.migrators import gs_ingest, ir_ingest, ml_queue +from ..services.migrators import rollback as rollback_mod +from ..services.migrators import tag_apply, verify log = logging.getLogger(__name__) diff --git a/tests/test_api_migrate.py b/tests/test_api_migrate.py index 0c7bbf2..dffb529 100644 --- a/tests/test_api_migrate.py +++ b/tests/test_api_migrate.py @@ -3,15 +3,23 @@ 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() @@ -46,7 +54,7 @@ async def test_post_rejects_unknown_kind(client): @pytest.mark.asyncio async def test_post_ingest_rejects_missing_file(client): - resp = await client.post("/api/migrate/gs_ingest", data={}) + 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" @@ -67,11 +75,11 @@ async def test_post_ingest_accepts_multipart_file(client, monkeypatch): "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) + 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 @@ -85,11 +93,11 @@ async def test_post_apply_without_backup_rejected(client, monkeypatch): "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) + 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" @@ -109,11 +117,11 @@ async def test_post_dry_run_allowed_without_backup(client, monkeypatch): "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) + 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