"""Unit tests for the NexusCRM (Salesforce-compatible) driver. respx-mocked. Covers metadata, OAuth token + test_connection, list_full_members system-account classification, and revoke_seat: deactivate happy path, already-absent, already-inactive, the read-only (INSUFFICIENT_SCOPE) gate, and lookup transport-error safety. """ from __future__ import annotations import httpx import pytest import respx from app.core.pagination import PaginationGuardError from app.integrations.driver_base import ProvisioningContext from app.integrations.drivers.nexuscrm_software import ( NexusCrmSoftwareDriver, _is_system_account, ) # Real, public-resolving host so the SSRF DNS guard passes; respx # intercepts all HTTP (same approach as test_github_driver). _B = "https://nexuscrm-new-prod.pingcap.com" _CONFIG = { "instance_url": _B, "consumer_key": "ck_fake", "consumer_secret": "cs_fake", "api_version": "v63.0", } _TOKEN_URL = f"{_B}/services/oauth2/token" _QUERY_URL = f"{_B}/services/data/v63.0/query" def _driver() -> NexusCrmSoftwareDriver: return NexusCrmSoftwareDriver(integration_id="nx-test", config=_CONFIG) def _mock_token() -> None: respx.mock.post(_TOKEN_URL).mock( return_value=httpx.Response( 200, json={"access_token": "tok123", "instance_url": _B}, ), ) @pytest.mark.asyncio @respx.mock async def test_query_all_preserves_order_across_pages() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock(return_value=httpx.Response(200, json={ "done": False, "records": [{"Id": "1"}], "nextRecordsUrl": "/services/data/v63.0/query/A", })) respx.mock.get(f"{_B}/services/data/v63.0/query/A").mock( return_value=httpx.Response(200, json={ "done": True, "records": [{"Id": "2"}], }), ) async with httpx.AsyncClient() as client: rows = await _driver()._query_all(client, "SELECT Id FROM User") assert [row["Id"] for row in rows] == ["1", "2"] @pytest.mark.asyncio @respx.mock async def test_query_all_rejects_non_adjacent_next_url_cycle() -> None: _mock_token() url_a = "/services/data/v63.0/query/secret-A" url_b = "/services/data/v63.0/query/secret-B" respx.mock.get(_QUERY_URL).mock(return_value=httpx.Response(200, json={ "done": False, "records": [{"Id": "1"}], "nextRecordsUrl": url_a, })) route_a = respx.mock.get(f"{_B}{url_a}").mock( return_value=httpx.Response(200, json={ "done": False, "records": [{"Id": "2"}], "nextRecordsUrl": url_b, }), ) respx.mock.get(f"{_B}{url_b}").mock(return_value=httpx.Response(200, json={ "done": False, "records": [{"Id": "3"}], "nextRecordsUrl": url_a, })) async with httpx.AsyncClient() as client: with pytest.raises(PaginationGuardError) as exc_info: await _driver()._query_all(client, "SELECT Id FROM User") assert route_a.call_count == 1 assert "secret-A" not in str(exc_info.value) @pytest.mark.asyncio @respx.mock async def test_query_all_rejects_done_false_without_next_url() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock(return_value=httpx.Response(200, json={ "done": False, "records": [{"Id": "partial-only"}], })) async with httpx.AsyncClient() as client: with pytest.raises( PaginationGuardError, match="missing_next_cursor", ): await _driver()._query_all(client, "SELECT Id FROM User") @pytest.mark.asyncio @respx.mock async def test_query_all_rejects_oversized_page(monkeypatch) -> None: _mock_token() monkeypatch.setattr( "app.integrations.drivers.nexuscrm_software._MAX_PAGINATION_ITEMS", 1, ) respx.mock.get(_QUERY_URL).mock(return_value=httpx.Response(200, json={ "done": True, "records": [{"Id": "1"}, {"Id": "2"}], })) async with httpx.AsyncClient() as client: with pytest.raises(PaginationGuardError, match="max_items_exceeded"): await _driver()._query_all(client, "SELECT Id FROM User") @pytest.mark.asyncio @respx.mock @pytest.mark.parametrize("payload", [ {"records": []}, {"done": "false", "records": []}, {"done": True, "records": {"not": "a-list"}}, {"done": False, "records": [], "nextRecordsUrl": {"not": "opaque"}}, ]) async def test_query_all_rejects_malformed_page(payload) -> None: _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response(200, json=payload), ) async with httpx.AsyncClient() as client: with pytest.raises(PaginationGuardError, match="malformed_response"): await _driver()._query_all(client, "SELECT Id FROM User") # ── metadata + system-account classifier ───────────────────────── def test_metadata() -> None: meta = NexusCrmSoftwareDriver.metadata() assert meta.kind == "software_provisioning" assert meta.provider == "nexuscrm" assert any(f.key == "consumer_secret" and f.is_secret for f in meta.fields) assert any(f.key == "consumer_key" and not f.is_secret for f in meta.fields) def test_is_system_account() -> None: # Org-id domain, salesforce.com, .invalid placeholder, integration profile. assert _is_system_account("cloud@00d5g000002g54ueac", None) assert _is_system_account("noreply@salesforce.com", "Security User") assert _is_system_account("sf-user+x@example.invalid", None) assert _is_system_account("svc@corp.com", "Analytics Cloud Integration User") # Real employee — not a system account. assert not _is_system_account("yaodongsen@pingcap.com", "System Administrator") # ── test_connection ────────────────────────────────────────────── @pytest.mark.asyncio @respx.mock async def test_connection_success() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response( 200, json={"done": True, "records": [{"total": 146}]}, ), ) result = await _driver().test_connection() assert result.ok is True assert "146 active seat" in result.message @pytest.mark.asyncio @respx.mock async def test_connection_bad_token() -> None: respx.mock.post(_TOKEN_URL).mock( return_value=httpx.Response(401, json={"error": "invalid_client"}), ) result = await _driver().test_connection() assert result.ok is False # ── list_full_members ──────────────────────────────────────────── @pytest.mark.asyncio @respx.mock async def test_list_full_members_classifies_system() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response(200, json={ "done": True, "records": [ {"Id": "1", "Name": "Vince Yao", "Email": "yaodongsen@pingcap.com", "IsActive": True, "ProfileId": "p1", "Profile": {"Name": "System Administrator (SF imported)"}}, {"Id": "2", "Name": "Platform Integration User", "Email": "cloud@00d5g000002g54ueac", "IsActive": True, "ProfileId": "p2", "Profile": None}, {"Id": "3", "Name": "Watchdog", "Email": "wd@example.invalid", "IsActive": True}, ], }), ) members = await _driver().list_full_members() assert len(members) == 3 by_email = {m["email"]: m for m in members} assert by_email["yaodongsen@pingcap.com"]["is_system"] is False assert by_email["cloud@00d5g000002g54ueac"]["is_system"] is True assert by_email["wd@example.invalid"]["is_system"] is True # ── revoke_seat ────────────────────────────────────────────────── @pytest.mark.asyncio @respx.mock async def test_revoke_seat_deactivates() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response(200, json={ "done": True, "records": [{"Id": "0055abc", "IsActive": True}], }), ) patch_route = respx.mock.patch( f"{_B}/services/data/v63.0/sobjects/User/0055abc", ).mock(return_value=httpx.Response(204)) result = await _driver().revoke_seat(ProvisioningContext(user_email="bye@pingcap.com")) assert result.ok is True assert result.raw_response == {"deactivated": True} assert patch_route.called @pytest.mark.asyncio @respx.mock async def test_revoke_seat_already_absent() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response(200, json={"done": True, "records": []}), ) result = await _driver().revoke_seat(ProvisioningContext(user_email="gone@pingcap.com")) assert result.ok is True assert result.raw_response == {"already_absent": True} @pytest.mark.asyncio @respx.mock async def test_revoke_seat_already_inactive() -> None: _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response(200, json={ "done": True, "records": [{"Id": "0055zzz", "IsActive": False}], }), ) result = await _driver().revoke_seat(ProvisioningContext(user_email="x@pingcap.com")) assert result.ok is True assert result.raw_response == {"already_inactive": True} @pytest.mark.asyncio @respx.mock async def test_revoke_seat_insufficient_scope_is_actionable() -> None: """Read-only Connected App (api.read): PATCH → INSUFFICIENT_SCOPE 403. Must surface an actionable, non-transient message, not a silent success.""" _mock_token() respx.mock.get(_QUERY_URL).mock( return_value=httpx.Response(200, json={ "done": True, "records": [{"Id": "0055abc", "IsActive": True}], }), ) respx.mock.patch( f"{_B}/services/data/v63.0/sobjects/User/0055abc", ).mock(return_value=httpx.Response(403, json={ "errorCode": "INSUFFICIENT_SCOPE", "message": "This session is not valid for use with the requested API", })) result = await _driver().revoke_seat(ProvisioningContext(user_email="bye@pingcap.com")) assert result.ok is False assert "read-only" in (result.error_message or "") assert "api" in (result.error_message or "") @pytest.mark.asyncio @respx.mock async def test_revoke_seat_lookup_transport_error_not_absent() -> None: """A transport failure on the lookup MUST NOT be confirmed as already-absent (the github_software.py lesson).""" _mock_token() respx.mock.get(_QUERY_URL).mock( side_effect=httpx.ConnectError("name resolution failed"), ) result = await _driver().revoke_seat(ProvisioningContext(user_email="x@pingcap.com")) assert result.ok is False assert "transport" in (result.error_message or "").lower() @pytest.mark.asyncio async def test_find_active_user_prefers_active_row(monkeypatch) -> None: """Salesforce User.Email is not unique. When an email maps to both a stale inactive dupe AND a live account, _find_active_user must return the ACTIVE row — otherwise _deactivate reports already-inactive success and closes the offboarding blocker while the live billable seat stays active. """ drv = _driver() async def _fake_query_all(_client, _soql): # Inactive dupe deliberately first (no ORDER BY in the SOQL). return [ {"Id": "005INACTIVE", "IsActive": False}, {"Id": "005ACTIVE", "IsActive": True}, ] monkeypatch.setattr(drv, "_query_all", _fake_query_all) uid, is_active, err = await drv._find_active_user(None, "dup@pingcap.com") assert err is None assert uid == "005ACTIVE" assert is_active is True @pytest.mark.asyncio async def test_find_active_user_falls_back_to_first_when_none_active(monkeypatch) -> None: drv = _driver() async def _fake_query_all(_client, _soql): return [{"Id": "005A", "IsActive": False}, {"Id": "005B", "IsActive": False}] monkeypatch.setattr(drv, "_query_all", _fake_query_all) uid, is_active, err = await drv._find_active_user(None, "x@pingcap.com") assert err is None assert uid == "005A" assert is_active is False