adobe-to-docusign-migrator/tests/test_batch_migration.py

156 lines
5.2 KiB
Python

"""
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"