""" tests/test_api_verify.py ------------------------ Tests for /api/verify/* endpoints (send test envelope, status, void). All DocuSign API calls are mocked with respx. """ import pytest import respx import httpx from fastapi.testclient import TestClient from web.app import app from web.session import _serializer, _COOKIE_NAME client = TestClient(app, raise_server_exceptions=True) DS_BASE = "https://demo.docusign.net/restapi" DS_ACCOUNT = "verify-account-id" TEMPLATE_ID = "tpl-verify-001" ENVELOPE_ID = "env-abc-123" @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) def _full_session(): return _serializer.dumps({ "adobe_access_token": "adobe-tok", "docusign_access_token": "ds-tok", }) def _ds_session(): return _serializer.dumps({"docusign_access_token": "ds-tok"}) class TestVerifySend: def test_send_requires_auth(self): """No session → 401.""" resp = client.post( "/api/verify/send", json={"template_id": TEMPLATE_ID, "recipient_name": "Alice", "recipient_email": "alice@example.com"}, cookies={}, ) assert resp.status_code == 401 @respx.mock def test_send_returns_envelope_id(self): """Authenticated + valid template → role names fetched, envelope_id returned.""" respx.get( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/{TEMPLATE_ID}" ).mock(return_value=httpx.Response(200, json={ "recipients": { "signers": [{"roleName": "Customer", "recipientId": "1"}], } })) respx.post( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes" ).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID})) resp = client.post( "/api/verify/send", json={ "template_id": TEMPLATE_ID, "recipient_name": "Alice Test", "recipient_email": "alice@example.com", }, cookies={_COOKIE_NAME: _ds_session()}, ) assert resp.status_code == 200 assert resp.json()["envelope_id"] == ENVELOPE_ID assert resp.json()["roles"] == ["Customer"] @respx.mock def test_send_falls_back_to_signer_role_on_template_error(self): """Template fetch failure → falls back to 'Signer' role name.""" respx.get( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/bad-id" ).mock(return_value=httpx.Response(404, json={"message": "Not found"})) respx.post( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes" ).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID})) resp = client.post( "/api/verify/send", json={"template_id": "bad-id", "recipient_name": "X", "recipient_email": "x@x.com"}, cookies={_COOKIE_NAME: _ds_session()}, ) assert resp.status_code == 200 assert resp.json()["roles"] == ["Signer"] @respx.mock def test_send_propagates_docusign_error(self): """DocuSign 400 on envelope create → 502 with error detail.""" respx.get( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/bad-id" ).mock(return_value=httpx.Response(200, json={"recipients": {}})) respx.post( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes" ).mock(return_value=httpx.Response(400, json={"message": "Invalid templateId"})) resp = client.post( "/api/verify/send", json={"template_id": "bad-id", "recipient_name": "X", "recipient_email": "x@x.com"}, cookies={_COOKIE_NAME: _ds_session()}, ) assert resp.status_code == 502 class TestVerifyStatus: def test_status_requires_auth(self): resp = client.get(f"/api/verify/status/{ENVELOPE_ID}", cookies={}) assert resp.status_code == 401 @respx.mock def test_status_returns_envelope_state(self): """Authenticated → status and sent_at returned.""" respx.get( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes/{ENVELOPE_ID}" ).mock(return_value=httpx.Response(200, json={ "envelopeId": ENVELOPE_ID, "status": "sent", "sentDateTime": "2026-04-21T12:00:00Z", "completedDateTime": None, })) resp = client.get( f"/api/verify/status/{ENVELOPE_ID}", cookies={_COOKIE_NAME: _ds_session()}, ) assert resp.status_code == 200 data = resp.json() assert data["status"] == "sent" assert data["envelope_id"] == ENVELOPE_ID assert data["sent_at"] == "2026-04-21T12:00:00Z" class TestVerifyVoid: def test_void_requires_auth(self): resp = client.post(f"/api/verify/void/{ENVELOPE_ID}", json={"reason": "test"}, cookies={}) assert resp.status_code == 401 @respx.mock def test_void_calls_docusign(self): """Authenticated → PUT envelope status to voided → voided: true.""" respx.put( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes/{ENVELOPE_ID}" ).mock(return_value=httpx.Response(200, json={})) resp = client.post( f"/api/verify/void/{ENVELOPE_ID}", json={"reason": "Verification complete"}, cookies={_COOKIE_NAME: _ds_session()}, ) assert resp.status_code == 200 assert resp.json()["voided"] is True assert resp.json()["envelope_id"] == ENVELOPE_ID