163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
"""
|
|
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
|