Docusign
Docusign is a leading provider of electronic signature technology, allowing individuals and organizations to sign, send, and manage documents digitally.
Analytics & SIEM · Docusign
Details
| ID | Docusign |
|---|---|
| Provider | Docusign |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/auth-utils:1.0.0.10133006 |
| Supported Modules | XSIAM |
README
Docusign is a leading provider of electronic signature technology, allowing individuals and organizations to sign, send, and manage documents digitally.
Customer Events API
The Docusign Monitor API provides access to customer events, which are events that occur in your Docusign account.
User Data API
The Docusign Admin API provides access to user data, which is information about users in your Docusign account.
Configure DocuSign new application
Follow the steps below to create and configure a DocuSign application for use with this integration:
1. Access DocuSign Developer Portal
- Open the DocuSign web UI and log in.
- Navigate to Account.
- From the left sidebar, click Apps and Keys.
2. Create a New Application
- Click Add App.
- Provide a name for your application.
- Copy the Integration Key.
3. Setting Integration Type
- Select the App integration type required for your workflow.

4. Configure Application Settings
- Under User Application, select Yes.
- Under Authentication Method for your App, leave the default option, Authorization Code Grant.
5. Generate a Secret Key
- Click Add Secret Key and generate a new secret key.
6. Generate RSA Key Pair
- Under Service Integration Click Generate RSA.
- Copy the Private Key. (The public key is not used by the integration)

7. Set Redirect URI
- Navigate to Additional Settings.
- Set the Redirect URI to
https://localhost.
8. Save the Application
- Click Save to finalize your application configuration.

9. Retrieve Organization ID
- Navigate to the Organization tab from the left sidebar.
- Copy the Organization ID from the URL.
10. Configure and Test
- Configure and save the instance.
- To use the Docusign integration and allow access to Docusign events, an administrator has to approve our app using an admin consent flow by running the !docusign-generate-consent-url command.
- Run the command !docusign-auth-test to test the full authentication flow and API connectivity.
Go-Live - Customer events data type
When you are ready to launch your app in production, you will need to promote your application’s integration key from your developer account to a production Docusign account by passing a Go-Live review, similar to the Go-Live process for the eSignature REST API.
Before you can begin the Go-Live process, you must have:
- A paid production Docusign account with a plan that includes Docusign Monitor
- Completed at least 20 consecutive successful test eSignature API requests in the developer environment
The 20 successful requests must be eSignature REST API requests, not Monitor API requests.
To start the Go-Live review for your application, follow the steps described on the Go-Live overview page for the Docusign eSignature REST API.
Note: If your application fails the Go-Live review, you may be required to bring it into compliance with the Rules and resource limits before proceeding.
Once the form is processed (which can take up to three business days), your integration key will be copied into production, enabling your app to call the production API endpoints.
API Endpoints
The developer and production endpoints for most Docusign APIs use slightly different paths.
The table below shows the base endpoint paths for each DocuSign environment, helping you update your code when migrating from the developer environment to production.
| Environment | API base URI | Web Site Login URL |
|---|---|---|
| Developer | https://lens-d.docusign.net/api/v2.0/datasets/monitor/.. |
https://account-d.docusign.com |
| Production | https://lens.docusign.net/api/v2.0/datasets/monitor/... |
https://{server}.docusign.net/ |
Note: To access production API endpoints, you will need to enable your integration key in the production environment. See Go-Live for more information.
Go-Live - Audit Users data type
Before you can begin the Go-Live process for an app that uses the Admin API, you must have:
- Admin API access enabled for your account
- Completed at least 20 consecutive successful test eSignature API requests in the developer environment
The 20 successful requests must be API Reference requests, not Admin API requests.
When you are ready to start the Go-Live review for your application, follow the steps described on the Go-Live overview page for Docusign eSignature.
Note: If your application fails the Go-Live review, you may need to bring it into compliance with the API rules and resource limits before proceeding.
After the form is processed, your integration key is copied to your production account, enabling your app to call production Admin API endpoints.
Note that while the key is copied, you must configure all required values separately in the production environment; configuration settings are not copied automatically.
API Endpoints
The developer and production endpoints for the Admin API use slightly different paths.
The examples in the how-to section use the developer paths; the table below shows the production version of the base path.
| Environment | Admin API base URI | eSignature API base URI |
|---|---|---|
| Developer | https://api-d.docusign.net/management/ |
https://demo.docusign.net/ |
| Production | https://api.docusign.net/management/ |
https://{server}.docusign.net/ |
Configure Docusign in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | True | |
| Integration Key | True | |
| User ID | True | |
| Redirect URL | True | |
| Private Key | True | |
| Account ID | For fetching user data only | False |
| Organization ID | For fetching user data only | False |
| Fetch events | False | |
| Event types to fetch | False | |
| Maximum number of customer events per fetch | Due to API limitations, the maximum is 2000. | False |
| Maximum number of user data events per fetch | Due to API limitations, the maximum is 1250. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room showing the command details.
docusign-generate-consent-url
Generates the Docusign admin consent URL based on configured parameters and environment.
Base Command
docusign-generate-consent-url
Input
There is no input for this command.
Context Output
There is no context output for this command.
Command Example
!docusign-generate-login-url
Human Readable Output
Docusign Consent URL
[Click here to authorize]
docusign-auth-test
Tests the full authentication flow and API connectivity by validating configuration, exchanging a JWT for an access token, and calling the /oauth/userinfo endpoint.
Base Command
docusign-auth-test
Input
There is no input for this command.
Context Output
There is no context output for this command.
Command Example
!docusign-auth-test
Human Readable Output
ok
docusign-get-events
Fetches events from Docusign. Used for debugging purposes on failures.
Base Command
docusign-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| event_type | The type of events to fetch. Possible values are: Customer events, Audit Users. | True |
| limit | Maximum number of events to fetch, default is 10. | False |
Context Output
There is no context output for this command.
Command Example
!docusign-get-events event_type="Customer events" limit=10
Human Readable Output
Docusign Customer events (fetched 10)
eventId timestamp accountId 11111111-1111-1111-1111-111111111111 2024-06-30T07:08:06Z 11111111-1111-1111-1111-111111111111
docusign-reset-access-token
Resets the access token stored in the integration context.
Base Command
docusign-reset-access-token
Input
There is no input for this command.
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (required)integration_key— Integration Key (required)user_id— User ID (required)redirect_url— Redirect URL (required)credentials— (required)account_id— Account IDorganization_id— Organization IDisFetch— Fetch eventsevent_types— Event types to fetchmax_user_events_per_fetch— Maximum number of audit user events to fetch per requestinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (4)
-
docusign-auth-testTests the full authentication flow and API connectivity by validating configuration, exchanging a JWT for an access token, and calling the /oauth/userinfo endpoint.
-
docusign-generate-consent-urlGenerates the Docusign Admin consent URL based on the configured parameters and environment.
-
docusign-get-eventsFetches events from Docusign. Used for debugging purposes on failures.
-
docusign-reset-access-tokenResets the access token stored in the integration context.
from unittest.mock import MagicMock import pytest import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401,F403 from Docusign import ( get_customer_events, CustomerEventsClient, DEFAULT_SERVER_DEV_URL, add_fields_to_customer_events, get_user_data, fetch_audit_user_data, UserDataClient, AuthClient, get_events_command, fetch_events, get_scopes_per_type, get_env_from_server_url, is_access_token_expired, is_required_scopes_set, remove_duplicate_users, generate_consent_url, validate_configuration_params, test_module, reset_access_token, initiate_auth_client, CUSTOMER_EVENTS_SCOPE, USER_DATA_SCOPE, CUSTOMER_EVENTS_TYPE, USER_DATA_TYPE, ) class TestGetCustomerEvents: def test_get_customer_events_basic_success(self, mocker): """ Given: - last_run contains a previous cursor - API returns endCursor and two events When: - get_customer_events is called Then: - It should return the events and update last_run["cursor"] to endCursor """ # Note: each item contains partial data; these are not the full event records (additional fields exist) example_response = { "endCursor": "aa_638553500560000000_638553500560000000_0", "data": [ { "timestamp": "2024-06-30T07:08:06.3038365Z", "eventId": "11111111-1111-1111-1111-111111111111", "accountId": "11111111-1111-1111-1111-111111111111", "userId": "11111111-1111-1111-1111-111111111111", }, { "timestamp": "2024-06-30T06:44:26.8948106Z", "eventId": "22222222-2222-2222-2222-222222222222", "accountId": "22222222-2222-2222-2222-222222222222", "userId": "22222222-2222-2222-2222-222222222222", }, ], } mock_http = mocker.patch("Docusign.BaseClient._http_request", return_value=example_response) mocker.patch.object(demisto, "debug") client = CustomerEventsClient(server_url=DEFAULT_SERVER_DEV_URL, proxy=False, verify=True) last_run = {"cursor": "prev_cursor"} token = "test_token" limit = 200 # Act events, updated_last_run = get_customer_events(last_run, limit, client, token) # Assert assert len(events) == 2 assert updated_last_run["cursor"] == example_response["endCursor"] # Validate HTTP call parameters assert mock_http.call_count == 1 _, kwargs = mock_http.call_args assert kwargs["method"] == "GET" assert kwargs["headers"]["Authorization"] == f"Bearer {token}" assert kwargs["params"]["cursor"] == "prev_cursor" assert kwargs["params"]["limit"] == limit def test_get_customer_events_returns_empty_list(self, mocker): """ Given: - last_run has a cursor When: - API returns empty data but a new endCursor Then: - It should return an empty list and update last_run["cursor"] """ response = {"endCursor": "c2", "data": []} mocker.patch("Docusign.BaseClient._http_request", return_value=response) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") client = CustomerEventsClient(server_url=DEFAULT_SERVER_DEV_URL, proxy=False, verify=True) last_run = {"cursor": "c1"} events, updated_last_run = get_customer_events(last_run, limit=100, client=client, access_token="tok") assert events == [] assert updated_last_run["cursor"] == "c2" def test_get_customer_events_first_fetch_cursor_empty_in_last_run(self, mocker): """ Given: - last_run is empty (no cursor) When: - get_customer_events is called Then: - It should use timestamp_to_datestring(time.time() - 60) as the cursor (which is it one_minute_ago variable) """ # Force the default cursor string mocker.patch("Docusign.timestamp_to_datestring", return_value="one_minute_ago") # Return any valid response example_response = {"endCursor": "next_cursor", "data": []} mock_http = mocker.patch("Docusign.BaseClient._http_request", return_value=example_response) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") client = CustomerEventsClient(server_url=DEFAULT_SERVER_DEV_URL, proxy=False, verify=True) events, updated_last_run = get_customer_events({}, limit=50, client=client, access_token="tok") assert events == [] assert updated_last_run["cursor"] == "next_cursor" # Verify the cursor used in the request params _, kwargs = mock_http.call_args assert kwargs["params"]["cursor"] == "one_minute_ago" def test_add_fields_to_customer_events(self, mocker): """ Given: - A list of customer events When: - add_fields_to_customer_events is called Then: - It should add source_log_type and _time fields to each event """ events = [{"timestamp": "2024-06-30T07:08:06.3038365Z"}, {"timestamp": "2024-06-30T06:44:26.8948106Z"}] add_fields_to_customer_events(events) assert events[0]["source_log_type"] == "customerevent" assert events[0]["_time"] == "2024-06-30T07:08:06Z" assert events[1]["source_log_type"] == "customerevent" assert events[1]["_time"] == "2024-06-30T06:44:26Z" class TestGetUserData: """ - limit ≤ per_page -> per_page = limit -> fetch full page, remaining logs mechanism is not used. - limit > per_page -> per_page = max_users_per_page -> fetch partial page, remaining logs mechanism is used. CRITICAL: per_page is ALWAYS constant during all the fetch user data runs. """ def test_get_user_data_with_continuing_fetch_when_limit_less_than_max_logs_per_page(self, mocker): """ Given: - assume maximum audit users per fetch = 10 - limit = 8 - next_page is not None (next_page=2) When: get_user_data is called with per_page=limit=8 Then: It should fetch exactly 8 users in one request and set continuing_fetch_info for remaining logs. full page fetched because the limit is less than the max users per page-> no remaining logs from fetched partial page. """ limit = 8 mock_params = { "url": "url", } mocker.patch.object(demisto, "params", return_value=mock_params) mocker.patch.object(demisto, "debug") # --- FIRST FETCH: no continuing fetch mechanism --- # Mock 8 users from first page (per_page=8, but more users available) users_1 = [ # note: "id" key is the only relevant key, this is a partial response (each user includes more fields) {"id": "11111111-1111-1111-1111-111111111111", "user_name": "user1"}, {"id": "22222222-2222-2222-2222-222222222222", "user_name": "user2"}, {"id": "33333333-3333-3333-3333-333333333333", "user_name": "user3"}, {"id": "44444444-4444-4444-4444-444444444444", "user_name": "user4"}, {"id": "55555555-5555-5555-5555-555555555555", "user_name": "user5"}, {"id": "66666666-6666-6666-6666-666666666666", "user_name": "user6"}, {"id": "77777777-7777-7777-7777-777777777777", "user_name": "user7"}, {"id": "88888888-8888-8888-8888-888888888888", "user_name": "user8"}, ] next_url = "next_url_1" paging = { "result_set_size": 8, "result_set_start_position": 0, "result_set_end_position": 7, "total_set_size": 1000, "next": next_url, } mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"users": users_1, "paging": paging} # Create a mock UserClient mock_client = mocker.MagicMock() mock_client.get_users_first_request.return_value = (mock_response.json(), "request_url") # Initial state - first fetch initial_last_run = {} result_logs, result_last_run = get_user_data( initial_last_run, limit=limit, users_per_page=limit, client=mock_client, access_token="tok" ) assert len(result_logs) == limit assert result_last_run["continuing_fetch_info"] is not None # More pages available assert result_last_run["continuing_fetch_info"]["url"] == next_url # Verify only 1 request was made (limit reached after one request) assert mock_client.get_users_first_request.call_count == 1 # Verify request used per_page = limit assert mock_client.get_users_first_request.call_args[0][1]["take"] == limit # --- SECOND FETCH: Continuing fetch mechanism, page=2--- # Mock page 2 users - 8 more users users_2 = [ {"id": "99999999-9999-9999-9999-999999999999", "user_name": "user9"}, {"id": "10101010-1010-1010-1010-101010101010", "user_name": "user10"}, {"id": "11111111-1111-1111-1111-111111111111", "user_name": "user11"}, {"id": "12121212-1212-1212-1212-121212121212", "user_name": "user12"}, {"id": "13131313-1313-1313-1313-131313131313", "user_name": "user13"}, {"id": "14141414-1414-1414-1414-141414141414", "user_name": "user14"}, {"id": "15151515-1515-1515-1515-151515151515", "user_name": "user15"}, {"id": "16161616-1616-1616-1616-161616161616", "user_name": "user16"}, ] next_url = "next_url_2" paging = { "result_set_size": 8, "result_set_start_position": 8, "result_set_end_position": 15, "total_set_size": 1000, "next": next_url, } mock_response_page2 = MagicMock() mock_response_page2.status_code = 200 mock_response_page2.json.return_value = {"users": users_2, "paging": paging} # Reset mock to return page 2 response mock_client.get_users_request.return_value = mock_response_page2.json() mock_client.reset_mock() continuing_last_run = result_last_run result_logs_2, result_last_run_2 = get_user_data( continuing_last_run, limit, limit, client=mock_client, access_token="tok" ) assert len(result_logs_2) == limit assert result_last_run_2["continuing_fetch_info"] is not None # More pages available assert result_last_run_2["continuing_fetch_info"]["url"] == next_url assert mock_client.get_users_request.call_count == 1 def test_fetch_audit_user_data_with_remaining_logs_mechanism(self, mocker): """ Test the complete remaining logs mechanism using fetch_audit_logs function. Given: - max_user_events_per_fetch = 8 (limit) - MAX_USER_DATA_PER_PAGE = 5 (per_page) - Available logs: 12 total (more than limit) When: fetch_audit_logs is called with limit > per_page Then: It should: 1. First call: fetch 5 logs (per_page=5), remaining=3 2. Second call: fetch 5 logs (per_page=5), but only use 3 to reach limit=8 3. Set excess_logs_info for the 2 unused logs from second call 4. Return exactly 8 logs total """ limit = 8 per_page = 5 mocker.patch("Docusign.MAX_USER_DATA_PER_PAGE", per_page) mocker.patch("Docusign.MAX_USER_DATA_PER_FETCH", limit) mock_params = { "url": "server_url", "max_user_events_per_fetch": limit, } mocker.patch.object(demisto, "params", return_value=mock_params) mocker.patch.object(demisto, "debug") # --- FIRST FETCH - FIRST API CALL: Page 1, fetch 5 users --- users_1 = [ {"id": "1", "user_name": "user1"}, {"id": "2", "user_name": "user2"}, {"id": "3", "user_name": "user3"}, {"id": "4", "user_name": "user4"}, {"id": "5", "user_name": "user5"}, ] # --- FIRST FETCH - SECOND API CALL: Page 2, fetch 5 users (but only need 3) --- users_2 = [ {"id": "6", "user_name": "user6"}, {"id": "7", "user_name": "user7"}, {"id": "8", "user_name": "user8"}, {"id": "9", "user_name": "user9"}, # Excess log 1 {"id": "10", "user_name": "user10"}, # Excess log 2 ] # --- SECOND FETCH - THIRD API CALL: Page 3, fetch 5 users users_3 = [ {"id": "11", "user_name": "user11"}, {"id": "12", "user_name": "user12"}, {"id": "13", "user_name": "user13"}, {"id": "14", "user_name": "user14"}, {"id": "15", "user_name": "user15"}, ] # --- SECOND FETCH - FOURTH API CALL: Page 4, fetch 5 users (but only need 1)--- users_4 = [ {"id": "16", "user_name": "user16"}, {"id": "17", "user_name": "user17"}, # Excess log 1 {"id": "18", "user_name": "user18"}, # Excess log 2 {"id": "19", "user_name": "user19"}, # Excess log 3 {"id": "20", "user_name": "user20"}, # Excess log 4 ] users_5 = [ {"id": "21", "user_name": "user21"}, {"id": "22", "user_name": "user22"}, ] # Mock responses for both API calls mock_response_page1 = MagicMock() mock_response_page1.status_code = 200 mock_response_page1.json.return_value = {"users": users_1, "paging": {"next": "next_url_1"}} mock_response_page2 = MagicMock() mock_response_page2.status_code = 200 mock_response_page2.json.return_value = {"users": users_2, "paging": {"next": "next_url_2"}} mock_response_page3 = MagicMock() mock_response_page3.status_code = 200 mock_response_page3.json.return_value = {"users": users_3, "paging": {"next": "next_url_3"}} mock_response_page4 = MagicMock() mock_response_page4.status_code = 200 mock_response_page4.json.return_value = {"users": users_4, "paging": {"next": "next_url_4"}} mock_response_page1_1 = MagicMock() mock_response_page1_1.status_code = 200 mock_response_page1_1.json.return_value = {"users": users_5, "paging": {"next": None}} mock_first_request = mocker.patch( "Docusign.UserDataClient.get_users_first_request", side_effect=[(mock_response_page1.json(), "next_url_1")], ) # Configure mock to return different responses for each call mock_requests_get = mocker.patch( "Docusign.UserDataClient.get_users_request", side_effect=[ mock_response_page2.json(), mock_response_page2.json(), mock_response_page3.json(), mock_response_page4.json(), mock_response_page4.json(), mock_response_page1_1.json(), ], ) mock_timestamp_to_datestring = mocker.patch("Docusign.timestamp_to_datestring") mock_timestamp_to_datestring.return_value = "2024-06-03T15:00:00.000Z" user_data_client = UserDataClient( account_id="account_id", organization_id="organization_id", env="dev", proxy=False, verify=True ) mocker.patch("Docusign.initiate_user_data_client", return_value=user_data_client) # create a mock for auth client mock_auth_client = mocker.MagicMock() mock_auth_client.access_token = "access_token" # Initial state - first fetch initial_last_run = {} result_last_run, users = fetch_audit_user_data(initial_last_run, mock_auth_client, limit=limit, test_mode=True) assert len(users) == limit assert result_last_run["continuing_fetch_info"].get("url") == "next_url_2" # Should contain logs from both pages (first 5 + first 3 from second page) expected_users_ids = ["1", "2", "3", "4", "5", "6", "7", "8"] actual_users_ids = [user["id"] for user in users] assert actual_users_ids == expected_users_ids # Should make exactly 2 API calls (5 logs from page 1 + 3 logs from page 2) assert mock_requests_get.call_count + mock_first_request.call_count == 2 # Should set excess_users_info for unused users from page 2 assert result_last_run["excess_users_info"].get("offset") == 3 assert result_last_run["excess_users_info"].get("url") == "next_url_1" # --- SECOND CALL TO fetch_audit_user_data: Process excess users from previous fetch before fetching new page --- # Second fetch using last_run from first fetch result_last_run_2, users_2 = fetch_audit_user_data(result_last_run, mock_auth_client, limit=limit, test_mode=True) assert len(users_2) == limit # Should contain excess users first (9, 10), then new users (11-16) expected_users_ids_2 = ["9", "10", "11", "12", "13", "14", "15", "16"] # 2 excess + 6 new actual_users_ids_2 = [user["id"] for user in users_2] assert actual_users_ids_2 == expected_users_ids_2 # Second fetch should make exactly 3 API calls(2 users from page 2 + 5 users from page 3 + 1 user from page 4) # (First fetch made 2 API calls) assert mock_requests_get.call_count + mock_first_request.call_count == 5 # Should set excess_users_info for unused users from page 4 assert result_last_run_2["excess_users_info"] is not None excess_info = result_last_run_2["excess_users_info"] assert excess_info["offset"] == 1 # Only first 1 log from page 4 were used assert excess_info["url"] == "next_url_3" # Third fetch using last_run from second fetch result_last_run_3, users_3 = fetch_audit_user_data(result_last_run_2, mock_auth_client, limit=limit, test_mode=True) # Should return exactly 6 users assert len(users_3) == 6 expected_users_ids_3 = ["17", "18", "19", "20", "21", "22"] actual_users_ids_3 = [user["id"] for user in users_3] assert actual_users_ids_3 == expected_users_ids_3 # Third fetch should make exactly 1 API call (4 users remaining from page 5) # (First fetch made 2 API calls, second fetch made 3 API calls) assert mock_requests_get.call_count + mock_first_request.call_count == 7 # Should remove excess_users_info from last run (already fetched remaining users) assert result_last_run_3["excess_users_info"] is None # Should remove continuing_fetch_info from last run (next page is none, next fetch will start a new fetch window) assert result_last_run_3["continuing_fetch_info"] is None class TestGetEventsCommand: def test_get_events_command_customer_events(self, mocker): """ Given: - event_type is 'Customer events' and limit is 2 When: - get_events_command is called Then: - It should call fetch_customer_events and return the events as CommandResults """ mock_events = [ { "timestamp": "2024-06-30T07:08:06.3038365Z", "eventId": "event-1", "source_log_type": "customerevent", "_time": "2024-06-30T07:08:06Z", }, { "timestamp": "2024-06-30T06:44:26.8948106Z", "eventId": "event-2", "source_log_type": "customerevent", "_time": "2024-06-30T06:44:26Z", }, ] mocker.patch.object(demisto, "args", return_value={"event_type": CUSTOMER_EVENTS_TYPE, "limit": "2"}) mocker.patch.object(demisto, "params", return_value={"url": DEFAULT_SERVER_DEV_URL}) mocker.patch.object(demisto, "debug") mock_fetch = mocker.patch("Docusign.fetch_customer_events", return_value=({}, mock_events)) mock_auth_client = mocker.MagicMock() mock_auth_client.access_token = "test_token" result = get_events_command(mock_auth_client) assert "fetched 2" in result.readable_output assert result.raw_response == mock_events mock_fetch.assert_called_once_with(last_run={}, access_token="test_token", limit=2) def test_get_events_command_audit_users(self, mocker): """ Given: - event_type is 'Audit Users' and limit is 3 When: - get_events_command is called Then: - It should call fetch_audit_user_data and return the events as CommandResults """ mock_users = [ {"id": "user-1", "user_name": "alice"}, {"id": "user-2", "user_name": "bob"}, {"id": "user-3", "user_name": "charlie"}, ] mocker.patch.object(demisto, "args", return_value={"event_type": USER_DATA_TYPE, "limit": "3"}) mocker.patch.object(demisto, "params", return_value={"url": DEFAULT_SERVER_DEV_URL}) mocker.patch.object(demisto, "debug") mock_fetch = mocker.patch("Docusign.fetch_audit_user_data", return_value=({}, mock_users)) mock_auth_client = mocker.MagicMock() mock_auth_client.access_token = "test_token" result = get_events_command(mock_auth_client) assert "fetched 3" in result.readable_output assert result.raw_response == mock_users mock_fetch.assert_called_once_with(last_run={}, auth_client=mock_auth_client, limit=3, test_mode=True) def test_get_events_command_unknown_event_type(self, mocker): """ Given: - event_type is an unknown value When: - get_events_command is called Then: - It should raise a DemistoException """ mocker.patch.object(demisto, "args", return_value={"event_type": "InvalidType", "limit": "10"}) mocker.patch.object(demisto, "params", return_value={}) mocker.patch.object(demisto, "debug") mock_auth_client = mocker.MagicMock() import pytest with pytest.raises(DemistoException, match="Unknown event type"): get_events_command(mock_auth_client) class TestScopes: """Regression tests for XSUP-70500: the JWT Grant requires the 'impersonation' scope for every fetch type, otherwise DocuSign returns 400 {"error": "consent_required"}. """ def test_baseline_jwt_scopes_in_all_scope_sets(self): """ Given: - The scope lists used to build the consent URL and the JWT. When: - Inspecting each fetch type's scopes. Then: - The DocuSign JWT Grant baseline scopes 'signature' and 'impersonation' must be present in both, since all flows use the JWT Bearer grant. """ for scope_set in (CUSTOMER_EVENTS_SCOPE, USER_DATA_SCOPE): assert "signature" in scope_set assert "impersonation" in scope_set def test_get_scopes_per_type_audit_users_includes_impersonation(self): """ Given: - Only the 'Audit Users' fetch type is selected. When: - get_scopes_per_type resolves the required scopes. Then: - The resolved scopes include 'impersonation' (the XSUP-70500 regression). """ scopes = get_scopes_per_type(USER_DATA_TYPE) assert "impersonation" in scopes assert "organization_read" in scopes assert "user_read" in scopes assert "signature" in scopes def test_get_scopes_per_type_customer_events(self): scopes = get_scopes_per_type(CUSTOMER_EVENTS_TYPE) assert "impersonation" in scopes assert "signature" in scopes def test_get_scopes_per_type_both_types(self): scopes = get_scopes_per_type(f"{CUSTOMER_EVENTS_TYPE},{USER_DATA_TYPE}") for expected in ("signature", "impersonation", "organization_read", "user_read"): assert expected in scopes def test_get_scopes_per_type_none_selected(self): assert get_scopes_per_type("") == [] class TestEnvFromServerUrl: def test_dev_host(self): assert get_env_from_server_url("https://account-d.docusign.com") == "dev" def test_prod_host(self): assert get_env_from_server_url("https://account.docusign.com") == "prod" def test_unknown_host_defaults_to_prod(self): assert get_env_from_server_url("https://example.com") == "prod" class TestTokenScopeHelpers: def test_is_access_token_expired_true(self, mocker): mocker.patch.object(demisto, "debug") assert is_access_token_expired("2000-01-01T00:00:00Z") is True def test_is_access_token_expired_false(self, mocker): mocker.patch.object(demisto, "debug") assert is_access_token_expired("2999-01-01T00:00:00Z") is False def test_is_required_scopes_set_true(self): assert is_required_scopes_set(["a", "b"], ["a", "b", "c"]) is True def test_is_required_scopes_set_false(self): assert is_required_scopes_set(["a", "z"], ["a", "b", "c"]) is False class TestRemoveDuplicateUsers: def test_removes_matching_id_and_time(self): users = [ {"id": "1", "_time": "t1"}, {"id": "2", "_time": "t1"}, {"id": "3", "_time": "t2"}, ] result = remove_duplicate_users(users, ids_to_remove=["1"], time_to_remove="t1") assert {u["id"] for u in result} == {"2", "3"} def test_keeps_when_time_differs(self): users = [{"id": "1", "_time": "t2"}] result = remove_duplicate_users(users, ids_to_remove=["1"], time_to_remove="t1") assert result == users def test_empty_inputs_returns_original(self): users = [{"id": "1", "_time": "t1"}] assert remove_duplicate_users(users, ids_to_remove=[], time_to_remove="t1") == users assert remove_duplicate_users([], ids_to_remove=["1"], time_to_remove="t1") == [] class TestAuthClientValidatePrivateKey: def _client(self, mocker): mocker.patch.object(AuthClient, "get_access_token", return_value="tok") mocker.patch.object(demisto, "debug") return AuthClient( server_url="https://account-d.docusign.com", integration_key="key", user_id="user", private_key_pem="-----BEGIN RSA PRIVATE KEY-----\nAAAA BBBB CCCC\n-----END RSA PRIVATE KEY-----", ) def test_validate_private_key_normalizes(self, mocker): client = self._client(mocker) assert client.private_key_pem.startswith("-----BEGIN RSA PRIVATE KEY-----\n") assert client.private_key_pem.endswith("\n-----END RSA PRIVATE KEY-----") assert "AAAABBBBCCCC" in client.private_key_pem.replace("\n", "") def test_validate_private_key_bad_prefix(self, mocker): mocker.patch.object(AuthClient, "get_access_token", return_value="tok") mocker.patch.object(demisto, "debug") with pytest.raises(DemistoException, match="must start with"): AuthClient( server_url="https://account-d.docusign.com", integration_key="key", user_id="user", private_key_pem="not a key", ) class TestAuthClientToken: def _make_client(self, mocker): mocker.patch.object(AuthClient, "get_access_token", return_value="tok") mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") return AuthClient( server_url="https://account-d.docusign.com", integration_key="key", user_id="user", private_key_pem="-----BEGIN RSA PRIVATE KEY-----\nAAAA\n-----END RSA PRIVATE KEY-----", ) def test_exchange_jwt_success(self, mocker): client = self._make_client(mocker) mocker.patch.object( client, "_http_request", return_value={"access_token": "abc", "expires_in": 3600, "scope": "signature impersonation"}, ) access_token, expired_at, scope = client.exchange_jwt_to_access_token("jwt") assert access_token == "abc" assert scope == "signature impersonation" assert expired_at def test_exchange_jwt_missing_token_raises(self, mocker): client = self._make_client(mocker) mocker.patch.object(client, "_http_request", return_value={"scope": "signature"}) with pytest.raises(DemistoException, match="Token exchange failed"): client.exchange_jwt_to_access_token("jwt") def test_exchange_jwt_consent_required_propagates(self, mocker): """consent_required from DocuSign must propagate (XSUP-70500 symptom).""" client = self._make_client(mocker) mocker.patch.object( client, "_http_request", side_effect=DemistoException('Error in API call [400] - Bad Request {"error": "consent_required"}'), ) with pytest.raises(DemistoException, match="consent_required"): client.exchange_jwt_to_access_token("jwt") def test_get_user_info(self, mocker): client = self._make_client(mocker) mocker.patch.object(client, "_http_request", return_value={"accounts": [{"account_id": "a1"}]}) assert client.get_user_info("tok")["accounts"][0]["account_id"] == "a1" def test_get_base_uri_from_context(self, mocker): client = self._make_client(mocker) mocker.patch("Docusign.get_integration_context", return_value={"base_uri": "https://cached"}) assert client.get_base_uri("tok", "a1") == "https://cached" def test_get_base_uri_fetches_and_caches(self, mocker): client = self._make_client(mocker) mocker.patch("Docusign.get_integration_context", return_value={}) set_ctx = mocker.patch("Docusign.set_integration_context") mocker.patch.object( client, "get_user_info", return_value={"accounts": [{"account_id": "a1", "base_uri": "https://fetched"}]}, ) assert client.get_base_uri("tok", "a1") == "https://fetched" set_ctx.assert_called_once() def test_get_base_uri_account_not_found(self, mocker): client = self._make_client(mocker) mocker.patch("Docusign.get_integration_context", return_value={}) mocker.patch.object(client, "get_user_info", return_value={"accounts": [{"account_id": "other"}]}) with pytest.raises(DemistoException, match="missing configuration account id"): client.get_base_uri("tok", "a1") class TestGenerateConsentUrl: def test_includes_impersonation_for_audit_users(self, mocker): """XSUP-70500: the consent URL for Audit Users must request the 'impersonation' scope.""" mocker.patch.object( demisto, "params", return_value={ "event_types": USER_DATA_TYPE, "url": "https://account-d.docusign.com", "integration_key": "key", "redirect_url": "https://localhost", }, ) mocker.patch.object(demisto, "debug") mocker.patch("Docusign.get_integration_context", return_value={}) mocker.patch("Docusign.set_integration_context") result = generate_consent_url() assert "impersonation" in result.readable_output assert "signature" in result.readable_output def test_all_scopes_already_consented(self, mocker): mocker.patch.object(demisto, "params", return_value={"event_types": USER_DATA_TYPE}) mocker.patch.object(demisto, "debug") mocker.patch("Docusign.get_integration_context", return_value={"consent_scopes": list(USER_DATA_SCOPE)}) result = generate_consent_url() assert "already set" in result.readable_output def test_no_event_types_selected_raises(self, mocker): mocker.patch.object(demisto, "params", return_value={"event_types": ""}) mocker.patch.object(demisto, "debug") with pytest.raises(DemistoException, match="select Event Types"): generate_consent_url() def test_missing_auth_params_raises(self, mocker): mocker.patch.object( demisto, "params", return_value={"event_types": USER_DATA_TYPE, "url": "", "integration_key": "", "redirect_url": ""}, ) mocker.patch.object(demisto, "debug") mocker.patch("Docusign.get_integration_context", return_value={}) with pytest.raises(DemistoException, match="Server URL, Integration Key and Redirect URL"): generate_consent_url() class TestValidateConfigurationParams: def test_ok(self, mocker): mocker.patch.object( demisto, "params", return_value={ "url": "u", "redirect_url": "r", "integration_key": "k", "user_id": "uid", "credentials": {"password": "pem"}, "event_types": CUSTOMER_EVENTS_TYPE, }, ) assert validate_configuration_params() == "ok" def test_missing_auth(self, mocker): mocker.patch.object(demisto, "params", return_value={"url": "", "event_types": ""}) assert "authentication flow" in validate_configuration_params() def test_missing_audit_user_params(self, mocker): mocker.patch.object( demisto, "params", return_value={ "url": "u", "redirect_url": "r", "integration_key": "k", "user_id": "uid", "credentials": {"password": "pem"}, "event_types": USER_DATA_TYPE, }, ) assert "Account ID and Organization ID" in validate_configuration_params() class TestModuleAndReset: def test_test_module_validation_fails(self, mocker): mocker.patch("Docusign.validate_configuration_params", return_value="some error") assert test_module() == "some error" def test_test_module_success(self, mocker): mocker.patch("Docusign.validate_configuration_params", return_value="ok") mock_client = mocker.MagicMock() mock_client.access_token = "tok" mocker.patch("Docusign.initiate_auth_client", return_value=mock_client) mocker.patch.object(demisto, "debug") assert test_module() == "Test completed successfully." mock_client.get_user_info.assert_called_once_with("tok") def test_reset_access_token(self, mocker): mocker.patch( "Docusign.get_integration_context", return_value={"access_token": "x", "expired_at": "y", "access_token_scopes": ["s"], "base_uri": "keep"}, ) set_ctx = mocker.patch("Docusign.set_integration_context") result = reset_access_token() assert "deleted successfully" in result.readable_output saved = set_ctx.call_args[0][0] assert "access_token" not in saved assert saved.get("base_uri") == "keep" class TestInitiateAuthClient: def test_missing_required_params_raises(self, mocker): mocker.patch.object(demisto, "params", return_value={"url": "u"}) mocker.patch.object(demisto, "debug") with pytest.raises(DemistoException, match="Integration Key, User ID"): initiate_auth_client() class TestFetchEvents: """Covers the fetch-events command entry point (fetch_events) across all branches.""" def _auth_client(self, mocker): client = mocker.MagicMock() client.access_token = "tok" return client def test_only_customer_events(self, mocker): """ Given: - event_types includes only Customer events. When: - fetch_events runs. Then: - Only fetch_customer_events is called; audit users is NOT called; events returned. """ mocker.patch.object(demisto, "params", return_value={"event_types": CUSTOMER_EVENTS_TYPE}) mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") customer_events = [{"eventId": "c1"}] mock_customer = mocker.patch("Docusign.fetch_customer_events", return_value=({"cursor": "next"}, customer_events)) mock_audit = mocker.patch("Docusign.fetch_audit_user_data") last_run, events = fetch_events(self._auth_client(mocker)) assert events == customer_events mock_customer.assert_called_once() mock_audit.assert_not_called() assert last_run[CUSTOMER_EVENTS_TYPE] == {"cursor": "next"} def test_only_audit_users(self, mocker): """ Given: - event_types includes only Audit Users. When: - fetch_events runs. Then: - Only fetch_audit_user_data is called; customer events is NOT called. """ mocker.patch.object(demisto, "params", return_value={"event_types": USER_DATA_TYPE}) mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") user_events = [{"id": "u1"}] mock_customer = mocker.patch("Docusign.fetch_customer_events") mock_audit = mocker.patch("Docusign.fetch_audit_user_data", return_value=({"latest_modifiedDate": "t"}, user_events)) last_run, events = fetch_events(self._auth_client(mocker)) assert events == user_events mock_audit.assert_called_once() mock_customer.assert_not_called() assert last_run[USER_DATA_TYPE] == {"latest_modifiedDate": "t"} def test_both_event_types(self, mocker): """ Given: - event_types includes both Customer events and Audit Users. When: - fetch_events runs. Then: - Both fetchers are called and their events are merged. """ mocker.patch.object(demisto, "params", return_value={"event_types": f"{CUSTOMER_EVENTS_TYPE},{USER_DATA_TYPE}"}) mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") mock_customer = mocker.patch("Docusign.fetch_customer_events", return_value=({}, [{"eventId": "c1"}])) mock_audit = mocker.patch("Docusign.fetch_audit_user_data", return_value=({}, [{"id": "u1"}])) last_run, events = fetch_events(self._auth_client(mocker)) assert {"eventId": "c1"} in events assert {"id": "u1"} in events assert len(events) == 2 mock_customer.assert_called_once() mock_audit.assert_called_once() assert CUSTOMER_EVENTS_TYPE in last_run assert USER_DATA_TYPE in last_run def test_empty_last_run_initialization(self, mocker): """ Given: - demisto.getLastRun returns an empty object and both event types are selected. When: - fetch_events runs. Then: - It initializes a fresh last_run with both type keys and passes empty per-type state. """ mocker.patch.object(demisto, "params", return_value={"event_types": f"{CUSTOMER_EVENTS_TYPE},{USER_DATA_TYPE}"}) mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "info") mock_customer = mocker.patch("Docusign.fetch_customer_events", return_value=({}, [])) mock_audit = mocker.patch("Docusign.fetch_audit_user_data", return_value=({}, [])) last_run, events = fetch_events(self._auth_client(mocker)) # Per-type state passed to each fetcher should start empty. assert mock_customer.call_args[0][0] == {} assert mock_audit.call_args[0][0] == {} assert events == [] assert set(last_run.keys()) == {CUSTOMER_EVENTS_TYPE, USER_DATA_TYPE}