""" Tests for Phase 13: batch migration API. """ import asyncio import json import os from unittest.mock import patch import pytest import respx import httpx from fastapi.testclient import TestClient from web.app import app from web.session import _serializer, _COOKIE_NAME import web.routers.migrate as migrate_module client = TestClient(app, raise_server_exceptions=True) ADOBE_BASE = "https://api.eu2.adobesign.com/api/rest/v6" DS_BASE = "https://demo.docusign.net/restapi" DS_ACCOUNT = "test-account-id" TEMPLATE_NAME = "Batch Test Template" DS_NEW_ID = "ds-batch-new-001" def _full_session(): return _serializer.dumps({ "adobe_access_token": "adobe-tok", "docusign_access_token": "ds-tok", }) @pytest.fixture(autouse=True) def patch_settings(monkeypatch): import web.config as cfg monkeypatch.setattr(cfg.settings, "docusign_account_id", DS_ACCOUNT) monkeypatch.setattr(cfg.settings, "docusign_base_url", DS_BASE) monkeypatch.setattr(cfg.settings, "adobe_sign_base_url", ADOBE_BASE) @pytest.fixture(autouse=True) def temp_history(tmp_path, monkeypatch): history_path = str(tmp_path / ".history.json") monkeypatch.setattr(migrate_module, "_HISTORY_FILE", history_path) return history_path @pytest.fixture(autouse=True) def clear_batch_jobs(): """Clear in-memory batch jobs between tests.""" migrate_module._batch_jobs.clear() yield migrate_module._batch_jobs.clear() def _async_wrap(sync_fn): async def wrapper(*args, **kwargs): return sync_fn(*args, **kwargs) return wrapper def _mock_download(template_id, access_token, output_dir): os.makedirs(output_dir, exist_ok=True) with open(os.path.join(output_dir, "metadata.json"), "w") as f: json.dump({"name": f"Template {template_id}", "id": template_id}, f) with open(os.path.join(output_dir, "form_fields.json"), "w") as f: json.dump({"fields": []}, f) with open(os.path.join(output_dir, "documents.json"), "w") as f: json.dump({"documents": []}, f) return True def _mock_compose(template_dir, output_path): with open(output_path, "w") as f: json.dump({"name": TEMPLATE_NAME}, f) def _mock_validation_ok(download_dir): return {"blockers": [], "warnings": [], "has_blockers": False} class TestBatchMigrationPost: def test_batch_requires_auth(self): resp = client.post("/api/migrate/batch", json={"source_template_ids": ["id1"]}, cookies={}) assert resp.status_code == 401 def test_batch_no_ids_returns_400(self): resp = client.post( "/api/migrate/batch", json={}, cookies={_COOKIE_NAME: _full_session()}, ) assert resp.status_code == 400 @respx.mock def test_batch_returns_job_id(self): """POST /api/migrate/batch returns a job_id immediately.""" with ( patch.object(migrate_module, "_download_adobe_template", new=_async_wrap(_mock_download)), patch.object(migrate_module, "_load_compose", return_value=_mock_compose), patch.object(migrate_module, "_run_validation", side_effect=_mock_validation_ok), ): resp = client.post( "/api/migrate/batch", json={"source_template_ids": ["id1", "id2"]}, cookies={_COOKIE_NAME: _full_session()}, ) assert resp.status_code == 200 body = resp.json() assert "job_id" in body assert body["total"] == 2 assert body["status"] == "queued" @respx.mock def test_batch_job_status_endpoint(self): """GET /api/migrate/batch/{id} returns job state.""" with ( patch.object(migrate_module, "_download_adobe_template", new=_async_wrap(_mock_download)), patch.object(migrate_module, "_load_compose", return_value=_mock_compose), patch.object(migrate_module, "_run_validation", side_effect=_mock_validation_ok), ): post_resp = client.post( "/api/migrate/batch", json={"source_template_ids": ["id1"]}, cookies={_COOKIE_NAME: _full_session()}, ) job_id = post_resp.json()["job_id"] get_resp = client.get(f"/api/migrate/batch/{job_id}") assert get_resp.status_code == 200 assert get_resp.json()["job_id"] == job_id def test_batch_unknown_job_returns_404(self): resp = client.get("/api/migrate/batch/nonexistent-job-id") assert resp.status_code == 404 @respx.mock def test_batch_dry_run_option(self): """Dry run in batch: no uploads, all results are dry_run.""" with ( patch.object(migrate_module, "_download_adobe_template", new=_async_wrap(_mock_download)), patch.object(migrate_module, "_load_compose", return_value=_mock_compose), patch.object(migrate_module, "_run_validation", side_effect=_mock_validation_ok), ): resp = client.post( "/api/migrate/batch", json={"source_template_ids": ["id1"], "options": {"dry_run": True}}, cookies={_COOKIE_NAME: _full_session()}, ) assert resp.status_code == 200 assert resp.json()["status"] == "queued"