ServiceNow v2

Use The ServiceNow IT Service Management (ITSM) solution to modernize the way you manage and deliver services to your users.

Case Management · ServiceNow

Details

IDServiceNow v2
ProviderServiceNow
CategoryCase Management
From Version5.0.0
Docker Imagedemisto/auth-utils:1.0.0.10608895
Supported ModulesAttack Surface Management Exposure Management Agentix Cortex Cloud Cloud Runtime Security Cloud Posture Security XSIAM EDR

README

IT service management. Cortex interfaces with ServiceNow to help streamline security-related service management and IT operations. For example, you can use the ServiceNow integration in order to:

  • View, create, update or delete a ServiceNow ticket directly from the Cortex CLI, and enrich it with Cortex data.
  • View, create, update and delete records from any ServiceNow table.
  • Query ServiceNow data with the ServiceNow query syntax.
  • Manage Security Incident Response (SIR) tickets with Cortex, update tickets and enrich them with data.

Please refer to ServiceNow documentation for additional information. We especially recommend the Operators available for filters and queries page.

This integration was integrated and tested with the Xanadu version of ServiceNow.

This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Use cases

  1. Get, update, create, and delete ServiceNow tickets, as well as add links and comments, or upload files to the tickets.
  2. Fetch newly created incidents.
  3. Get, update, create, delete records from any ServiceNow table.

Required Permissions

To use ServiceNow on Cortex, ensure your user account has the snc_platform_rest_api_access role.
This role is required to make API calls.
Also add to your user account the specific tables that you want to have access to.
However, these permissions may not suffice for managing records in some tables. Make sure you have the correct role so you have permissions to work with the relevant table.

Wrapper Scripts

There are 3 scripts that serve as examples for wrapping the following generic commands:
servicenow-query-table - ServiceNowQueryIncident
servicenow-create-record - ServiceNowCreateIncident
servicenow-update-record - ServiceNowUpdateIncident

You can use these scripts if you want to wrap these commands around a ServiceNow table of your choice.
These scripts are wrapped around the incident table, so to wrap them around another table simply copy the scripts and edit the code, arguments and outputs accordingly.

Configure ServiceNow v2 on Cortex

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for ServiceNow v2.
  3. Click Add instance to create and configure a new integration instance.
  4. To ensure that mirroring works:
    1. Select the Fetches incidents radio button.
    2. Under Classifier, select ServiceNow Classifier.
    3. Under Mapper (incoming), select ServiceNow - Incoming Mapper.
    4. Under Mapper (outgoing), select ServiceNow - Outgoing Mapper.
    5. To enable mirroring to close a ticket in Cortex XSOAR, under the Mirrored XSOAR Ticket closure method dropdown, select the ticket closing method,
      or set the Mirrored XSOAR Ticket custom close resolution code or Mirrored XSOAR Ticket custom close state code parameter, in order to override the default closure method with a custom close code or custom state.
      In order to use Mirrored XSOAR Ticket custom close resolution code or Mirrored XSOAR Ticket custom close state code parameter, it must follow this format: “custom_state_code1=custom_label1,custom_state_code2=custom_label2,…”,
      for example: “10=Design,11=Development,12=Testing”.
      Also, a matching user-defined list of customized incident close reasons must be configured as a “Server configuration” in Cortex XSOAR. (Meaning each Service Now custom state label will have a matching Cortex XSOAR custom close reason with the same name). Not following this format will result in a server error!
      For more information about Customize Incident Close Reasons, see Cortex XSOAR 6.13 or Cortex XSOAR 8 Cloud or Cortex XSOAR 8.7 On-prem.
      image
    6. To enable mirroring to close an incident in ServiceNow, under the Mirrored ServiceNow Ticket closure method dropdown, select the ticket closing method,
      or set the Mirrored ServiceNow Ticket custom close state code parameter, in order to override the default closure method with a custom state.

      image

Instance Creation Flow

This integration supports three types of authorization:

  1. Basic authorization using username and password.
  2. OAuth 2.0 authorization
  3. JWT authentication.

OAuth 2.0 Authorization

To use OAuth 2.0 authorization follow the next steps:

  1. Login to your ServiceNow instance and create an endpoint for XSOAR to access your instance (please see Snow OAuth for more information).
  2. Copy the Client Id and Client Secret (click the lock next to the client secret to reveal it) that were automatically generated when creating the endpoint into the Client ID and Client Secret fields of the integration instance configuration.
  3. (Recommended) Enter the ServiceNow account Username and Password in the integration instance configuration. When provided, the integration automatically performs the initial OAuth login on first use. It also automatically renews the refresh token when it expires.
  4. Select the Use OAuth Login checkbox and click Done.
  5. If you did not provide the Username and Password in step 3, run the command !servicenow-oauth-login from the Cortex XSOAR CLI and fill in the ServiceNow user’s User ID (admin or a dedicated integration username) and corresponding password of the ServiceNow integration instance. This step generates an access token to the ServiceNow integration instance and is required only the first time after configuring a new integration instance in Cortex XSOAR. If you provided Username and Password, this step is handled automatically.
  6. (Optional) Test the created integration instance by running the !servicenow-oauth-test command.

Notes:

  1. When running the !servicenow-oauth-login command, a refresh token is generated and will be used to produce new access tokens after the current access token has expired.
  2. If the Username and Password fields are configured, the integration will automatically perform the initial login and renew the refresh token when it expires — no manual commands are needed. Otherwise, you will have to run the servicenow-oauth-login command for the initial login and again each time the refresh token expires.
  3. The grant type used to get an access token is Resource owner password credentials. See the Snow documentation for more information.

JWT Authentication

Prerequisites in order to support JWT

  1. Create a Java Key Store and upload it to the instance by accessing from the upper menu: All > System Definition > Certificates. The private key will be used as an integration parameter.
  2. Configure a JWT signing key by accessing: All→System OAuth→JWT Keys using the keystore from above and keep the Key ID as it will be used as kid integration parameter.
  3. Create a JWT provider with a JWT signing key by accessing: All→System OAuth→JWT providers. Claim Name sub in Standard Claims has to be existing non-admin servicenow user with all necessary roles.
  4. Connect to an OAuth provider and create an OAuth application registry by accessing All→System OAuth→Application Registry:
    1. aud in JWT provider has to be equal to Client ID from OAuth JWT application - update JWT provider If necessary.
    2. The value of kid in JWT Verifier Maps has to be the same as Key Id in JWT signing key.
      The value can be updated if necessary.
  5. Create API Access Policy or add Authentication profile to existing Policy by accessing: All→System Web Services→API Access Policies→Rest API Access Policies

IMPORTANT:

  1. The Standard Authentication Profile of type Oauth should be already present in ServiceNow and has to be added to the Policy.
    API Access Policy should be configured as global in order to cover all available resources and not just now/table
  2. Granting JWT to admin is not allowed.
    You should have a non-admin user with all necessary roles (only non-admin roles) in addition to the existing role snc_platform_rest_api_access that is required to make API calls.

Using Multi-Factor Authentication (MFA)

MFA can be used both when using basic authorization and when using OAuth 2.0 authorization, however we strongly recommend using OAuth 2.0 when using MFA.
If MFA is enabled for your user, follow the next steps:

  1. Open the Google Authenticator application on your mobile device and make note of the number. The number refreshes every 30 seconds.
  2. Enter your username and password, and append the One Time Password (OTP) that you currently see on your mobile device to your password without any extra spaces. For example, if your password is 12345 and the current OTP code is 424 058, enter 12345424058.

Notes:

  1. When using basic authorization, you will have to update your password with the current OTP every time the current code expires (30 seconds), hence we recommend using OAuth 2.0 authorization.
  2. For using OAuth 2.0 see the above instructions. The OTP code should be appended to the password parameter in the !servicenow-oauth-login command.
Parameter Description Required
ServiceNow URL, in the format https://company.service-now.com/   True
Use OAuth Login Select this checkbox if to use OAuth 2.0 authentication. See (?) for more information. False
Use JWT Authentication Select this checkbox to use JWT authentication. See (?) for more information. False
Client ID/JWT Audience (aud)   False
Client Secret   False
Username   False
Password   False
Private key   False
kid (Key Id)   False
sub   False
iss (Issuer) If not set, Client ID will be used. False
Default ticket type for running ticket commands and fetching incidents The ticket type can be: incident, problem, change_request, sc_request, sc_task or sc_req_item. False
ServiceNow API Version (e.g. ‘v1’)   False
Fetch incidents   False
The query to use when fetching incidents   False
How many incidents to fetch each time   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)   False
Timestamp field to filter by (e.g., opened_at) This is how the filter is applied to the query: “ORDERBYopened_at^opened_at>[Last Run]”.
To prevent duplicate incidents, this field is mandatory for fetching incidents.
  False
ServiceNow ticket column to be set as the incident name. Default is the incident number   False
Incident type   False
Get incident attachments   False
Incident Mirroring Direction Choose the direction to mirror the incident: Incoming (from ServiceNow to Cortex XSOAR), Outgoing (from Cortex XSOAR to ServiceNow), or Incoming and Outgoing (from/to Cortex XSOAR and ServiceNow). False
Use Display Value Select this checkbox to retrieve comments and work notes without accessing the `sys_field_journal` table. False
Instance Date Format Select the date format of your ServiceNow instance. Mandatory when using the `Use Display Value` option. More details under the troubleshooting section in the documentation of the integration. The integration supports the ServiceNow default time format (full form) `HH:mm:ss` with support to `a` notation for AM/PM. False
Comment Entry Tag To ServiceNow Choose the tag to add to an entry to mirror it as a comment in ServiceNow. False
Comment Entry Tag From ServiceNow Choose the tag to add to an entry to mirror it as a comment from ServiceNow. False
Work Note Entry Tag To ServiceNow Choose the tag to add to an entry to mirror it as a work note in ServiceNow. False
Work Note Entry Tag From ServiceNow Choose the tag to add to an entry to mirror it as a work note from ServiceNow. False
File Entry Tag To ServiceNow Choose the tag to add to an entry to mirror it as a file in ServiceNow. False
File Entry Tag From ServiceNow Choose the tag to add to an entry to mirror it as a file from ServiceNow. False
Timestamp field to query for updates as part of the mirroring flow According to the timestamp in this field, records will be queried to check for updates. False
How many incidents to mirror incoming each time If a greater number of incidents than the limit were modified, then they won’t be mirrored in. False
Custom Fields to Mirror Custom (user defined) fields in the format: u_fieldname1,u_fieldname2 custom fields start with a ‘u_’. These fields will be included in the mirroring capabilities, if added here. False
Mirrored XSOAR Ticket closure method When selected, closing the ServiceNow ticket is mirrored in Cortex XSOAR. False
Mirrored XSOAR Ticket custom close state code Define how to close the mirrored tickets in Cortex XSOAR with a custom state. Enter here a comma-separated list of custom closure state codes and their labels (acceptable format example: “10=Design,11=Development,12=Testing”) to override the default closure method. Note that a matching user-defined list of custom close reasons must be configured as a “Server configuration” in Cortex XSOAR. Not following this format will result in closing the incident with a default close reason. False
Mirrored XSOAR Ticket custom close resolution code (overwrites the custom close state) Define how to close the mirrored tickets in Cortex XSOAR with a custom resolution code. Enter a comma-separated list of custom resolution codes and their labels (acceptable format example: “10=Design,11=Development,12=Testing”) to override the default closure method. Note that a matching user-defined list of custom close reasons must be configured as a “Server configuration” in Cortex XSOAR. Not following this format will result in closing the incident with a default close reason. False
Mirrored ServiceNow Ticket closure method Define how to close the mirrored tickets in ServiceNow. Choose ‘resolved’ to enable reopening from the UI. Otherwise, choose ‘closed’. False
Mirrored ServiceNow Ticket custom close state code Define how to close the mirrored tickets in ServiceNow with a custom state. Enter here the custom closure state code (should be an integer) to override the default closure method. If the closure code does not exist, the default code will be used instead. False
Mirror Existing Notes For New Fetched Incidents When enabled, comments and work notes are mirrored as note entries for each newly fetched incident. Note: This setting triggers an API call for each incident during the first mirroring, potentially causing overload if numerous incidents are present. False
Use system proxy settings   False
Trust any certificate (not secure)   False
Incidents Fetch Interval   False
Advanced: Minutes to look back when fetching Use this parameter to determine how long backward to look in the search for incidents that were created before the last run time and did not match the query when they were created. False
Interpret comments in the specific format The format of the ServiceNow comments. Default is Source, the format supplied by ServiceNow during the fetch as part of the comments data. This can be changed to a different format for all comments. Possible values are Source (default), Text, and HTML. Use HTML if a html editor is used within ServiceNow to add comments. False
Mark attachment as notes Mark the file attachment as notes. This ensure that note and comments can be found withing the warroom as Notes. Defaults to “false”. False
  1. Click Test to validate the URLs, token, and connection.
  2. Click Done.

Fetch Incidents

The integration fetches newly created tickets according to the following parameters,
which you define in the instance configuration: ticket_type, query, and limit.
For the first fetch, the integration will fetch incidents that were created 10 minutes earlier.
After that, the integration will fetch incidents created after the timestamp of the last fetch.

look-back parameter note:
In case the look-back parameter is initialized with a certain value and during a time that incidents were fetched, if changing
the look back to a number that is greater than the previous value, then in the initial incident fetching there will be incidents duplications.
If the integration was already set with look back > 0, and the look-back is not being increased at any point of time, then those incident duplications would not occur.
Note that using a look-back value that is very large (more than an hour) can lead to an increase in the memory usage of the system in some cases, and it is highly unrecommended.
If there is a need to fetch incidents that require a long look-back to get fetched (for tracking issues, for example), use the mirroring feature and filter the results using the relevant tags instead of using the look-back feature. You can create a custom mapper and track the relevant fields.

Configure Incident Mirroring

This feature is compliant with XSOAR version 6.0 and above.
When mirroring incidents, you can make changes in ServiceNow that will be reflected in Cortex XSOAR, or vice versa.
You can also attach files from either of the systems which will then be available in the other system.

The following instructions include steps for configuring the integration and incoming and outgoing mappers. However, they do not cover every option available in the integration nor classification and mapping features.
For information about classification and mapping see Classification and Mapping (Cortex XSOAR 6.13) or Classification and Mapping (Cortex XSOAR 8 Cloud) or Classification and Mapping (Cortex XSOAR 8.7 On-prem).

Note:

  • For Cortex XSOAR version 6.1 only, the final source of truth for an incident are the values in Cortex XSOAR. For example, if you change the severity in Cortex XSOAR and then change it back in ServiceNow, the final value that will be presented is the one in Cortex XSOAR. For versions 6.2 and later, if mirroring is in both directions then the latest update is the source of truth.
  • The mirroring settings apply only for incidents that are fetched after applying the settings. Pre-existing comments or work notes are not fetched/mirrored at the time of incident creation.
  • To use a custom mapper, you must first duplicate the mapper and edit the field in the copy of the mapper. If you detach the out of the box mapper and make changes to it, the pack does not automatically get updates.

To set up incident mirroring you need to:

  1. Configure the ServiceNow Service Account roles.
  2. Configure mirroring for ServiceNow trigger incidents or configure mirroring for other trigger incidents.

Configure the ServiceNow Service Account Roles

To use ServiceNow on Cortex XSOAR, ensure your service account has the following roles required to make API calls:

  • Rest_api_explorer
  • Snc_platform_rest_api_access
  • itil (optional)

Note:
If you choose to give permissions only for specific tables, you then need to add to your user account the specific tables you want to have access to. Make sure you have the correct role so you have permissions to work with the relevant table. Keep in mind that these permissions may not suffice for managing records in some tables.

  • Read access to sys_journal_field (this is an elevated privilege) for accessing comments and work notes and for incoming mirroring. This is not required if you have Use Display Value enabled with Instance Date Format defined.

Note:
See this ServiceNow community link for giving elevated read access and potential risks.

Configure Incident Mirroring When the Trigger Incident is ServiceNow

When the trigger incident is ServiceNow, you use the ServiceNow Classifier and leave the Incident type as N/A, with either the default incoming and outgoing mappers or optional custom mappers.

STEP 1 - Configure the ServiceNow v2 Integration Instance for Mirroring

  1. Navigate to Integrations and search for ServiceNow v2.
  2. Click Add instance.
  3. Select Fetches incidents.
  4. Under Classifier, select ServiceNow Classifier.
    Note:
    You define either the Classifier or the Incident type (not both). It is recommended to define the Classifier and leave Incident type N/A to enable labeling custom incident types under the ServiceNow Classifier.

  5. Under Mapper (incoming), for default mapping select ServiceNow - Incoming Mapper. For custom mapping, follow the instructions in STEP 2 and then select the custom mapper name.
  6. Under Mapper (outgoing), for default mapping select ServiceNow - Outgoing Mapper. For
    custom mapping, follow the instructions in STEP 3 and then select the custom mapper name.

image

  1. Enter the connection parameters.
    • Confirm whether your organization uses basic authorization or OAuth authorization (most use basic) and enter the relevant authorization details.
    • Leave ServiceNow API Version empty since ServiceNow typically automatically provides the appropriate version.
  2. Under The query to use when fetching incidents, the default query is to filter for new incidents: stateNOT IN6,7, where 6= resolved incidents and 7= closed incidents. For a different query, ask your ServiceNow representative to provide the exact states and their numbers.
  3. Select Get incident attachments to retrieve attachments from ServiceNow incident creation in Cortex XSOAR.
  4. Select the Incident Mirroring Direction:
    • In - Mirrors changes on the ServiceNow ticket in to the Cortex XSOAR ticket.
    • Out - Mirrors changes on the Cortex XSOAR ticket to the ServiceNow ticket.
    • Both - Mirrors changes both in and out on both tickets.
  5. Enable the checkbox for Use Display Value if you want to fetch comments and work notes without using sys_journal_field table which required an elevated read only permission.
  6. If Use Display Value is enabled, Instance Date Format needs to be set to the date format that matches the date format used in ServiceNow by the user account used to configure the instance.

image

  1. Set the Timestamp field to query as part of the mirroring flow. This defines the ticket_last_update - the epoch timestamp when the ServiceNow incident was last updated. The default is sys_updated_on.
  2. Enter the relevant Comment Entry Tag, Work Note Entry Tag, File Entry Tag To ServiceNow and File Entry Tag From ServiceNow values.
    These values are mapped to the dbotMirrorTags incident field in Cortex XSOAR, which defines how Cortex XSOAR handles comments when you tag them in the War Room.
    Note:
    These tags work only for mirroring comments, work notes, and files from Cortex XSOAR to ServiceNow.

image

  1. Configure any Custom Fields to Mirror. These must start with “u_”. This is available for ServiceNow v2 version 2.2.10 and later.
    Note:
    To enable mirroring custom fields, make a copy of the incoming and outgoing mappers and add the custom fields to the copies (see STEP 2 and STEP 3). Select these copies in the integration instance Mapper (incoming) and Mapper (outgoing) settings.
  2. To enable mirroring when closing an incident or ticket in Cortex XSOAR and ServiceNow, select the closed option from the Mirrored XSOAR Ticket closure method dropdown and Mirrored ServiceNow Ticket closure method respectively.
  3. Click Done.

STEP 2 (Optional) Configure the Incoming Mapper by Incident Type for Custom Fields

Note:
Any modifications require that the mappers be cloned before any changes can be applied.

  1. Navigate to Classification and Mapping and for Incidents search for the ServiceNow - Incoming Mapper.
  2. Select it and click Duplicate.
  3. Under the Incident Type dropdown, select ServiceNow Create Ticket and Mirror.
  4. Verify the mapper has these fields mapped. They will pull the values configured on the integration instance settings at the time of ingestion.

    image

    • dbotMirrorId - dbotMirrorId - the field used by the third-party integration to identify the ticket. This should be the sys_id of the ServiceNow ticket. The value is mapped to incident.servicenowticketid.
    • dbotMirrorDirection - determines whether mirroring is incoming, outgoing, or both. Default is Both. This should match the instance configuration.
    • dbotMirrorInstance - determines the ServiceNow instance with which to mirror. This should match the instance configuration.
    • dbotMirrorLastSync - determines the field by which to indicate the last time that the systems synchronized.
    • dbotMirrorTags - determines the tags that you need to add in Cortex XSOAR for entries to be pushed to ServiceNow. They should be copied from the tags in the instance configuration. These are also the tags that must be put on the War Room record in order for it to sync.
      • To mirror files, use the ForServiceNow tag.
      • To mirror general notes, use the comments tag.
      • To mirror private notes that can be read only by users with the necessary permissions, use the work_notes tag.
    • Configure any custom fields you want mapped to Cortex XSOAR. Custom fields start with “u_” and are available for ServiceNow v2 version 2.2.10 and later. These must be added to the integration instance Custom Fields to Mirror setting.

STEP 3 - Modify the Outgoing Mapper

Note:
Any modifications require that the mappers be cloned before any changes can be applied.

  1. Navigate to Classification and Mapping, and for Incidents search for the ServiceNow - Outgoing Mapper.
  2. Select it and click Duplicate.
    The left side of the screen shows the ServiceNow fields to which to map and the right side of the
    screen shows the Cortex XSOAR fields by which you are mapping.
  3. Under the Incident Type dropdown, select the relevant incident type (for example ServiceNow Ticket).
  4. Under Schema Type, select incident. The Schema Type represents the ServiceNow entity that
    you are mapping to. In our example it is an incident, but it can also be any other kind of ticket that
    ServiceNow supports.
  5. On the right side of the screen, under Incident, select the incident based on which you want to
    match.
  6. Change the mapping according to your needs, including any fields you want mapped outward to ServiceNow and any custom fields. Make sure the custom fields you want mirrored are added to the integration instance settings.
  7. Save your changes.

image

STEP 4 - Create an Incident in ServiceNow

For purposes of this use case, it can be a simple incident. The new ticket will be ingested in Cortex XSOAR in approximately one minute.

STEP 5 - Add a Note to the Incident in Cortex XSOAR

In the example below, we have written A comment from Cortex XSOAR to ServiceNow.

  1. Click Actions > Tags and add the comments tag.
  2. Add a file to the incident and mark it with the ForServiceNow tag.

image

  1. Navigate back to the incident in ServiceNow and within approximately one minute, the changes will be reflected there, too.
    You can make additional changes like closing the incident or changing severity and those will be reflected in both systems.

image

Configure Incident Mirroring When the Trigger Incident is Not ServiceNow

You can set up any source integration to create a ServiceNow ticket based on a fetched incident and mirror the ticket in Cortex XSOAR. To do this you need to:

  • Configure the ServiceNow v2 integration to map the appropriate fields from the ServiceNow Create Ticket and Mirror incident type to the relevant trigger incident type (for example, Phishing Custom).
  • Set up the source integration to create a ServiceNow ticket and start mirroring.

STEP 1 - Configure the ServiceNow v2 Integration Instance for Mirroring

  1. Navigate to Classification and Mapping. For Incidents, search for ServiceNow - Incoming Mapper and ServiceNow - Outgoing Mapper.
  2. For each mapper, click Duplicate.
    Your copied mappers will be called ServiceNow - Incoming Mapper_copy and ServiceNow - Outgoing Mapper_copy, you can rename them. The copied mappers appear in the drop down for the Mapper (incoming) and Mapper (outgoing) integration instance settings fields.
  3. Navigate to Integrations and search for ServiceNow v2.
  4. Click Add instance.
  5. Select Do not Fetch.
  6. Under Classifier, select ServiceNow Classifier.
    Note:
    You define either the Classifier or the Incident type (not both). It is recommended to define the Classifier and leave Incident type N/A to enable labeling custom incident types under the ServiceNow Classifier.

  7. Under Mapper (incoming), select ServiceNow - Incoming Mapper_copy (or whatever you renamed it).
  8. Under Mapper (outgoing), select ServiceNow - Outgoing Mapper_copy (or whatever you renamed it).
  9. Enter the connection parameters.
    • Confirm whether your organization uses basic authorization or OAuth authorization (most use basic) and enter the relevant authorization details.
    • Leave ServiceNow API Version empty since ServiceNow typically automatically provides the appropriate version.
  10. Under The query to use when fetching incidents, the default query is to filter for new incidents: stateNOT IN6,7, where 6= resolved incidents and 7= closed incidents. For a different query, ask your ServiceNow representative to provide the exact states and their numbers.
  11. Select Get incident attachments to retrieve attachments from ServiceNow incident creation in Cortex XSOAR.
  12. Select the Incident Mirroring Direction:
    • In - Mirrors changes on the ServiceNow ticket in to the Cortex XSOAR ticket.
    • Out - Mirrors changes on the Cortex XSOAR ticket to the ServiceNow ticket.
    • Both - Mirrors changes both in and out on both tickets.
  13. Enable the checkbox for Use Display Value if you want to fetch comments and work notes without using sys_journal_field table which required an elevated read only permission.
  14. If Use Display Value is enabled, Instance Date Format needs to be set to the date format that matches the date format used in ServiceNow by the user account used to configure the instance.

image

  1. Set the Timestamp field to query as part of the mirroring flow. This defines the ticket_last_update - the epoch timestamp when the ServiceNow incident was last updated. The default is sys_updated_on.
  2. Enter the relevant Comment Entry Tag, Work Note Entry Tag, File Entry Tag To ServiceNow and File Entry Tag From ServiceNow values.
    These values are mapped to the dbotMirrorTags incident field in Cortex XSOAR, which defines how Cortex XSOAR handles comments when you tag them in the War Room.
    Note:
    These tags work only for mirroring comments from Cortex XSOAR to ServiceNow.

image

  1. Configure any Custom Fields to Mirror. These must start with “u_”. This is available for ServiceNow v2 version 2.2.10 and later.
    Note:
    To enable mirroring custom fields, make a copy of the incoming and outgoing mappers and add the custom fields to the copies (see STEP 2 and STEP 3). Select these copies in the integration instance Mapper (incoming) and Mapper (outgoing) settings.
  2. To enable mirroring when closing an incident or ticket in Cortex XSOAR and ServiceNow, select the closed option from the Mirrored XSOAR Ticket closure method dropdown and Mirrored ServiceNow Ticket closure method respectively.
  3. Click Done.

STEP 2 (Optional) Configure the Incoming Mapper by Incident Type for Custom Fields

Note:
Any modifications require that the mappers be cloned before any changes can be applied.

  1. Navigate to Classification and Mapping and for Incidents search for the ServiceNow - Incoming Mapper_copy (or whatever you renamed it).
  2. Under the Incident Type dropdown, select the relevant triggering incident type, for example Phishing.
  3. Verify the mapper has these fields mapped. They will pull the values configured on the integration instance settings at the time of ingestion.

    image

    • dbotMirrorId - dbotMirrorId - the field used by the third-party integration to identify the ticket. This should be the sys_id of the ServiceNow ticket. The value is mapped to incident.servicenowticketid.
    • dbotMirrorDirection - determines whether mirroring is incoming, outgoing, or both. Default is Both. This should match the instance configuration.
    • dbotMirrorInstance - determines the ServiceNow instance with which to mirror. This should match the instance configuration.
    • dbotMirrorLastSync - determines the field by which to indicate the last time that the systems synchronized.
    • dbotMirrorTags - determines the tags that you need to add in Cortex XSOAR for entries to be pushed to ServiceNow. They should be copied from the tags in the instance configuration. These are also the tags that must be put on the War Room record in order for it to sync.
      • To mirror files from XSOAR to ServiceNow, use the ForServiceNow tag.
      • Mirrored files from ServiceNow to XSOAR will be tagged by default with the FromServiceNow tag.
      • To mirror general notes, use the comments tag.
      • To mirror private notes that can be read only by users with the necessary permissions, use the work_notes tag.
    • Configure any custom fields you want mapped to Cortex XSOAR. Custom fields start with “u_” and are available for ServiceNow v2 version 2.2.10 and later. These must be added to the integration instance Custom Fields to Mirror setting.
  4. Save your changes.

STEP 3 - Modify the Outgoing Mapper for Custom Fields

Note:
Any modifications require that the mappers be cloned before any changes can be applied.

  1. Navigate to Classification and Mapping, and for Incidents search for the ServiceNow - Outgoing Mapper_copy (or whatever you renamed it).
  2. Under the Incident Type dropdown, select the relevant incident type (for example ServiceNow Ticket).
  3. Under Schema Type, select incident. The Schema Type represents the ServiceNow entity that
    you are mapping to. In our example it is an incident, but it can also be any other kind of ticket that
    ServiceNow supports.
  4. On the right side of the screen, under Incident, select the incident based on which you want to
    match.
    The left side of the screen shows the ServiceNow fields to which to map and the right side of the screen shows the Cortex XSOAR fields by which you are mapping.
  5. Change the mapping according to your needs, including any fields you want mapped outward to ServiceNow and any custom fields. Make sure the custom fields you want mirrored are added to the integration instance settings.
  6. Save your changes.

image

STEP 4 - Set up Your Source Integration

Set up your source integration so that after fetching a trigger incident a ServiceNow ticket is created and mirroring starts.

  1. Fetch an incident with your chosen integration. For example, for Phishing using any email integration (Gmail, MSGraph, O365).
  2. Classify and map the incident fields.
  3. Create a task in the playbook that creates a ServiceNow ticket followed by a set incident task that starts the mirroring capability.

Example:
The following shows the Create New Record playbook task, which creates a ServiceNow ticket.

image

The Create New Record task is followed by the Set Mirroring Fields task, which starts the mirroring capability.

image

The new ServiceNow ticket will be ingested in Cortex XSOAR in approximately one minute.

STEP 5 - Add a Note to the Incident in Cortex XSOAR

In the example below, we have written A comment from Cortex XSOAR to ServiceNow.

  1. Click Actions > Tags and add the comments tag.
  2. Add a file to the incident and mark it with the ForServiceNow tag.

image

  1. Navigate back to the incident in ServiceNow and within approximately one minute, the changes will be reflected there, too.
    You can make additional changes like closing the incident or changing severity and those will be reflected in both systems.

image

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

All commands implement a retry mechanism for 401 (Unauthorized) error codes due to potential ServiceNow authorization issues. This mechanism attempts the request multiple times. If the issue persists, it likely indicates an authorization configuration problem.

servicenow-login


This function should be used once before running any command when using OAuth authentication.

Base Command

servicenow-login

Input

Argument Name Description Required
username The username that should be used for login. Required
password The password that should be used for login. Required

Context Output

There is no context output for this command.

Command Example

!servicenow-login username=username password=password

Context Example

{}

Human Readable Output

Logged in successfully

servicenow-test


Test the instance configuration when using OAuth authorization.

Base Command

servicenow-test

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Context Example

```json
{}

Human Readable Output

Instance Configured Successfully

servicenow-get-ticket


Retrieves ticket information by ticket ID.

Base Command

servicenow-get-ticket

Input

Argument Name Description Required
id Ticket system ID for which to retrieve information. Optional
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional
number Ticket number to retrieve. Optional
get_attachments If “true” will retrieve ticket attachments.Note this option will always use the v1 API version, as it is not supported in v2. Default is “false”. Optional
custom_fields Custom fields on which to query. For example: state_code=AR,time_zone=PST. Optional
additional_fields Additional fields to display in the War Room entry and incident context. Optional

Context Output

Path Type Description
ServiceNow.Ticket.ID string ServiceNow ticket ID.
ServiceNow.Ticket.OpenedBy string ServiceNow ticket opener ID.
ServiceNow.Ticket.CreatedOn date ServiceNow ticket creation date.
ServiceNow.Ticket.Assignee string ServiceNow ticket assignee ID.
ServiceNow.Ticket.State string ServiceNow ticket state.
ServiceNow.Ticket.Summary string ServiceNow ticket short summary.
ServiceNow.Ticket.Number string ServiceNow ticket number.
ServiceNow.Ticket.Active boolean ServiceNow ticket active.
ServiceNow.Ticket.AdditionalComments string ServiceNow ticket comments.
ServiceNow.Ticket.Priority string ServiceNow ticket priority.
ServiceNow.Ticket.OpenedAt date ServiceNow ticket opening time.
ServiceNow.Ticket.ResolvedBy string ServiceNow ticket resolver ID.
ServiceNow.Ticket.CloseCode string ServiceNow ticket close code.
File.Info string Attachment file info.
File.Name string Attachment file name.
File.Size number Attachment file size.
File.SHA1 string Attachment file SHA1 hash.
File.SHA256 string Attachment file SHA256 hash.
File.EntryID string Attachment file entry ID.
File.Type string Attachment file type.
File.MD5 string Attachment file MD5 hash.

Command Example

!servicenow-get-ticket number=INC0000040

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "Active": "true",
            "Assignee": "admin",
            "CreatedOn": "2020-01-26 00:43:54",
            "Creator": "admin",
            "ID": "id",
            "Number": "INC0000040",
            "OpenedAt": "2020-01-26 00:42:45",
            "OpenedBy": "admin",
            "Priority": "3 - Moderate",
            "State": "3",
            "Summary": "JavaScript error on hiring page of corporate website"
        }
    },
    "Ticket": {
        "Active": "true",
        "Assignee": "admin",
        "CreatedOn": "2020-01-26 00:43:54",
        "Creator": "admin",
        "ID": "id",
        "Number": "INC0000040",
        "OpenedAt": "2020-01-26 00:42:45",
        "OpenedBy": "admin",
        "Priority": "3 - Moderate",
        "State": "3",
        "Summary": "JavaScript error on hiring page of corporate website"
    }
}

Human Readable Output

ServiceNow ticket

System ID Number Impact Urgency Severity Priority State Created On Created By Active Description Opened At Short Description
id INC0000040 2 - Medium 2 - Medium 3 - Low 3 - Moderate 3 - On Hold 2020-01-26 00:43:54 admin true Seeing JavaScript error message on hiring page on Explorer and Firefox. 2020-01-26 00:42:45 JavaScript error on hiring page of corporate website

servicenow-create-ticket


Creates new ServiceNow ticket.

Base Command

servicenow-create-ticket

Input

Argument Name Description Required
short_description Short description of the ticket. Optional
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, “sn_si_incident” or “std_change_template”. Default is “incident”. Optional
urgency Ticket urgency. You can either select from the predefined options or enter another value, for example: “Urgent” or “5”. Optional
severity Ticket severity. You can either select from the predefined options or enter another value, for example: “Urgent” or “5”. Optional
impact Ticket impact. Optional
active Whether to set the ticket as Active. Can be “true” or “false”. Optional
activity_due The ticket activity due date, in the format “2016-07-02 21:51:11”. Optional
additional_assignee_list List of users assigned to the ticket. Optional
approval_history Ticket history approval. Optional
approval_set The ticket approval set date, in the format “2016-07-02 21:51:11”. Optional
assigned_to User assigned to the ticket. Optional
business_duration Business duration, in the format: YYYY-MM-DD HH:MM:SS. Optional
business_service Business service. Optional
business_stc Business source. Optional
business_criticality Business criticality of the ticket. Optional
calendar_duration Calendar duration, in the format: YYYY-MM-DD HH:MM:SS. Optional
caller_id Caller ID (UID format). Optional
category Category of the ticket. Optional
caused_by UID Format Optional
close_code Ticket’s close code. Can be “Solved (Work Around)”, “Solved (Permanently)”, “Solved Remotely (Work Around)”, “Solved Remotely (Permanently)”, “Not Solved (Not Reproducible)”, “Not Solved (Too Costly)”, or “Closed/Resolved by Caller”. Optional
close_notes Close notes of the ticket. Optional
closed_at When the ticket was closed, in the format: YYYY-MM-DD HH:MM:SS. Optional
closed_by User who closed the ticket. Optional
cmdb_ci UID Format. Optional
comments Format type journal input. Optional
comments_and_work_notes Format type journal input. Optional
company Company (UID format). Optional
contact_type Contact type. Optional
correlation_display Correlation display. Optional
correlation_id Correlation ID. Optional
delivery_plan Delivery plan (UID format). Optional
display Whether to display comments, work notes, and so on. Can be “true” or “false”. Optional
description Ticket description. Optional
due_date Ticket due date, in the format: YYYY-MM-DD HH:MM:SS. Optional
escalation Escalation Optional
expected_start Expected start date/time, in the format: YYYY-MM-DD HH:MM:SS. Optional
follow_up Follow up date/time, in the format: YYYY-MM-DD HH:MM:SS. Optional
group_list UID format list (group). Optional
knowledge Whether the ticket is solved in the knowledge base. Can be “true” or “false”. Optional
location Location of the ticket. Optional
made_sla SLA of the ticket. Optional
notify Whether to be notified about this ticket. Can be “1” or “0”. Optional
order Order number. Optional
parent UID Format Optional
parent_incident UID Format Optional
problem_id UID Format Optional
reassignment_count The number of users included in this ticket. Optional
reopen_count How many times the ticket has been reopened. Optional
resolved_at The date/time that the ticket was resolved, in the format: YYYY-MM-DD HH:MM:SS. Optional
resolved_by ID of the user that resolved the ticket. Optional
risk_score Incident risk score. Optional
rfc UID Optional
sla_due SLA due date/time, in the format: YYYY-MM-DD HH:MM:SS. Optional
subcategory Ticket subcategory. Optional
sys_updated_by Last updated by. Optional
sys_updated_on Last date/time that the system was updated, in the format: YYYY-MM-DD HH:MM:SS. Optional
user_input Input from the end user. Optional
watch_list A list of watched tickets. Optional
work_end Format: YYYY-MM-DD HH:MM:SS Optional
work_notes Format journal list Optional
work_notes_list List work notes UIDs. Optional
work_start Date/time when work started on the ticket. Optional
assignment_group The sys_id of the group to assign. Optional
incident_state The number that represents the incident state. Optional
number Ticket number. Optional
priority Priority of the ticket. Optional
template Template name to use as a base to create new tickets. Optional
custom_fields Custom (user defined) fields in the format: fieldname1=value;fieldname2=value; custom fields start with a “u_”. Optional
change_type Type of Change Request ticket. Can be “normal”, “standard”, or “emergency”. Default is “normal”. Optional
state State of the ticket, for example: “Closed” or “7” or “7 - Closed”. Optional
opened_at Date/time the ticket was opened, in the format: YYYY-MM-DD HH:MM:SS. Optional
caller Caller system ID. Optional
approval Ticket approval. Optional
additional_fields Additional fields in the format: fieldname1=value;fieldname2=value; Optional
input_display_value Flag that indicates whether to set field values using the display value or the actual value. True will treat the input value as the display value. False treats the input values as actual values. The default setting is false. Optional

For more information regarding the input_display_value argument, see: https://docs.servicenow.com/bundle/xanadu-platform-administration/page/administer/exporting-data/concept/query-parameters-display-value.html

Context Output

Path Type Description
ServiceNow.Ticket.ID string ServiceNow ticket ID.
ServiceNow.Ticket.OpenedBy string ServiceNow ticket opener ID.
ServiceNow.Ticket.CreatedOn date ServiceNow ticket creation date.
ServiceNow.Ticket.Assignee string ServiceNow ticket assignee ID.
ServiceNow.Ticket.State string ServiceNow ticket state.
ServiceNow.Ticket.Summary string ServiceNow ticket short summary.
ServiceNow.Ticket.Number string ServiceNow ticket number.
ServiceNow.Ticket.Active boolean ServiceNow ticket active.
ServiceNow.Ticket.AdditionalComments string ServiceNow ticket comments.
ServiceNow.Ticket.Priority string ServiceNow ticket priority.
ServiceNow.Ticket.OpenedAt date ServiceNow ticket opening time.
ServiceNow.Ticket.ResolvedBy string ServiceNow ticket resolver ID.
ServiceNow.Ticket.CloseCode string ServiceNow ticket close code.

Command Example

!servicenow-create-ticket active=true severity="2 - Medium" short_description="Ticket example"

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "Active": "true",
            "CreatedOn": "2020-05-10 09:04:06",
            "Creator": "admin",
            "ID": "id",
            "Number": "INC0010002",
            "OpenedAt": "2020-05-10 09:04:06",
            "OpenedBy": "admin",
            "Priority": "5 - Planning",
            "State": "1",
            "Summary": "Ticket exmaple"
        }
    },
    "Ticket": {
        "Active": "true",
        "CreatedOn": "2020-05-10 09:04:06",
        "Creator": "admin",
        "ID": "id",
        "Number": "INC0010002",
        "OpenedAt": "2020-05-10 09:04:06",
        "OpenedBy": "admin",
        "Priority": "5 - Planning",
        "State": "1",
        "Summary": "Ticket example"
    }
}

Human Readable Output

ServiceNow ticket was created successfully

System ID Number Impact Urgency Severity Priority State Created On Created By Active Opened At Short Description
id INC0010002 3 - Low 3 - Low 2 - Medium 5 - Planning 1 - New 2020-05-10 09:04:06 admin true 2020-05-10 09:04:06 Ticket example

servicenow-update-ticket


Updates the specified ticket.

Base Command

servicenow-update-ticket

Input

Argument Name Description Required
short_description Short description of the ticket. Optional
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional
urgency Ticket urgency. You can either select from the predefined options or enter another value, for example: “Urgent” or “5”. Optional
severity Ticket severity. You can either select from the predefined options or enter another value, for example: “Urgent” or “5”. Optional
impact Ticket impact. Optional
active Whether the ticket is Active. Can be “true” or “false”. Optional
activity_due The ticket activity due date, in the format: “2016-07-02 21:51:11”. Optional
additional_assignee_list List of users assigned to the ticket. Optional
approval_history Ticket history approval. Optional
approval_set The ticket approval set date/time, in the format: “2016-07-02 21:51:11”. Optional
assigned_to User assigned to the ticket. Optional
business_duration Business duration, in the format: YYYY-MM-DD HH:MM:SS. Optional
business_service Business service. Optional
business_stc Business source. Optional
business_criticality Business criticality of the ticket. Optional
calendar_duration Calendar duration, in the format: YYYY-MM-DD HH:MM:SS. Optional
caller_id Caller ID (UID format). Optional
category Category name. Optional
caused_by UID format. Optional
close_code Ticket’s close code. Ticket’s close code. Can be “Solved (Work Around)”, “Solved (Permanently)”, “Solved Remotely (Work Around)”, “Solved Remotely (Permanently)”, “Not Solved (Not Reproducible)”, “Not Solved (Too Costly)”, or “Closed/Resolved by Caller”. Optional
close_notes Close notes of the ticket. Optional
closed_at Date/time the ticket was closed, in the format: YYYY-MM-DD HH:MM:SS. Optional
closed_by User who closed the ticket. Optional
cmdb_ci UID Format. Optional
comments Format type journal input. Optional
comments_and_work_notes Format type journal input. Optional
company UID Format. Optional
contact_type Contact type. Optional
correlation_display Correlation display. Optional
correlation_id Correlation ID. Optional
delivery_plan UID Format. Optional
display Whether to display comments, work notes, and so on. Can be “true” or “false”. Optional
description Ticket description. Optional
due_date Ticket due date, in the format: YYYY-MM-DD HH:MM:SS. Optional
escalation Escalation. Optional
expected_start Expected start date/time, in the format: YYYY-MM-DD HH:MM:SS. Optional
follow_up Follow up date/time, in the format: YYYY-MM-DD HH:MM:SS. Optional
group_list UID format list. Optional
knowledge Whether the ticket is solved in the knowledge base. Can be “true” or “false”. Optional
location Location of the ticket. Optional
made_sla SLA of the ticket. Optional
notify Whether to be notified about this ticket. Can be “1” or “0”. Optional
order Order number. Optional
parent Parent (UID format). Optional
parent_incident Parent incident (UID format). Optional
problem_id Problem ID (UID format). Optional
reassignment_count The number of users included in this ticket. Optional
reopen_count The number of times the ticket has been reopened. Optional
resolved_at Date/time the ticket was resolved, in the format: YYYY-MM-DD HH:MM:SS. Optional
resolved_by Resolved by (UID format). Optional
risk_score Incident risk score. Optional
rfc UID Optional
sla_due SLA due date/time, in the format: YYYY-MM-DD HH:MM:SS. Optional
subcategory Ticket subcategory. Optional
sys_updated_by Last updated by Optional
sys_updated_on Date/time the system was last updated. Optional
user_input Input from the end user. Optional
watch_list A list of watched tickets. Optional
work_end Format: YYYY-MM-DD HH:MM:SS Optional
work_notes Format journal list. Optional
work_notes_list Comma-separated list of work notes UIDs. Optional
work_start Date/time when work started on the ticket. Optional
assignment_group Assignment group UID. Optional
incident_state Number representing the incident state. Optional
number Ticket number. Optional
priority Priority of the ticket. Optional
id System ID of the ticket to update. Required
custom_fields Custom (user defined) fields in the format: fieldname1=value;fieldname2=value; custom fields start with a “u_”. Optional
change_type Type of Change Request ticket. Can be “normal”, “standard”, or “emergency”. Default is “normal”. Optional
state State of the ticket, for example: “Closed” or “7” or “7 - Closed”. Optional
caller Caller system ID. Optional
approval Ticket approval. Optional
additional_fields Additional fields in the format: fieldname1=value;fieldname2=value; Optional
input_display_value Flag that indicates whether to set field values using the display value or the actual value. True will treat the input value as the display value. False treats the input values as actual values. The default setting is false. Optional
clear_fields A comma-separated list of fields to clear. Optional

For more information regarding the input_display_value argument, see: https://docs.servicenow.com/bundle/xanadu-platform-administration/page/administer/exporting-data/concept/query-parameters-display-value.html

Context Output

There is no context output for this command.

Command Example

!servicenow-update-ticket id=id severity="2 - Medium"

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "Active": "true",
            "Assignee": "admin",
            "CreatedOn": "2020-01-26 00:43:54",
            "Creator": "admin",
            "ID": "id",
            "Number": "INC0000040",
            "OpenedAt": "2020-01-26 00:42:45",
            "OpenedBy": "admin",
            "Priority": "3 - Moderate",
            "State": "3",
            "Summary": "JavaScript error on hiring page of corporate website"
        }
    }
}

Human Readable Output

ServiceNow ticket updated successfully

Ticket type: incident

Active Created By Created On Description Impact Number Opened At Priority Severity Short Description State System ID Urgency
true admin 2020-01-26 00:43:54 Seeing JavaScript error message on hiring page on Explorer and Firefox. 2 - Medium INC0000040 2020-01-26 00:42:45 3 - Moderate 2 - Medium JavaScript error on hiring page of corporate website 3 - On Hold 471d4732a9fe198100affbf655e59172 2 - Medium

servicenow-delete-ticket


Deletes a ticket from ServiceNow.

Base Command

servicenow-delete-ticket

Input

Argument Name Description Required
id Ticket System ID. Required
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Optional

Context Output

Path Type Description
ServiceNow.Ticket.ID string Ticket ID.
ServiceNow.Ticket.DeleteMessage string Message indicating the result of the ticket deletion operation.

Command Example

!servicenow-delete-ticket id=existingId"

Human Readable Output

Ticket with ID id was successfully deleted from incident table.

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "ID": "id",
            "DeleteMessage": "Ticket with ID id was successfully deleted from incident table.",
        }
    }
}

servicenow-query-tickets


Retrieves ticket information according to the supplied query.

Base Command

servicenow-query-tickets

Input

Argument Name Description Required
limit The maximum number of tickets to retrieve. Optional
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional
query The query to run. To learn about querying in ServiceNow, see https://docs.servicenow.com/bundle/istanbul-servicenow-platform/page/use/common-ui-elements/reference/r_OpAvailableFiltersQueries.html Optional
offset Starting record index to begin retrieving records from. Optional
additional_fields Additional fields to present in the War Room entry and incident context. Optional
system_params System parameters in the format: fieldname1=value;fieldname2=value. For example: “sysparm_display_value=al;&sysparm_exclude_reference_link=True” Optional
system_params System parameters in the format: fieldname1=value;fieldname2=value. For example: “sysparm_display_value=true;sysparm_exclude_reference_link=True” Optional

Context Output

Path Type Description
Ticket.ID string The unique ticket identifier.
Ticket.Creator string A string field that indicates the user who created the ticket.
Ticket.CreatedOn date The date/time when the ticket was created.
Ticket.Assignee string Specifies the user assigned to complete the ticket. By default, this field uses a reference qualifier to only display users with the itil role.
Ticket.State string Status of the ticket.
Ticket.Summary string A human-readable title for the record.
Ticket.Number string The display value of the ticket.
Ticket.Active boolean Specifies whether work is still being done on a task or whether the work for the task is complete.
Ticket.AdditionalComments Unknown Comments about the task record.
Ticket.Priority string Specifies the ticket priority for the assignee.
Ticket.OpenedAt date The date/time when the ticket was first opened.
Ticket.Escalation string Indicates how long the ticket has been open.

Command Example

!servicenow-query-tickets limit="3" query="impact<2^short_descriptionISNOTEMPTY" ticket_type="incident"

Context Example

{
    "ServiceNow": {
        "Ticket": [
            {
                "Active": "false",
                "Assignee": "admin",
                "CloseCode": "Closed/Resolved by Caller",
                "CreatedOn": "2018-08-24 18:24:13",
                "Creator": "admin",
                "ID": "id",
                "Number": "INC0000001",
                "OpenedAt": "2020-01-23 23:09:51",
                "OpenedBy": "admin",
                "Priority": "1 - Critical",
                "ResolvedBy": "admin",
                "State": "7",
                "Summary": "Can't read email"
            },
            {
                "Active": "true",
                "Assignee": "admin",
                "CreatedOn": "2018-08-13 22:30:06",
                "Creator": "admin",
                "ID": "id",
                "Number": "INC0000002",
                "OpenedAt": "2020-01-17 23:07:12",
                "OpenedBy": "admin",
                "Priority": "1 - Critical",
                "State": "3",
                "Summary": "Network file shares access issue"
            },
            {
                "Active": "true",
                "Assignee": "admin",
                "CreatedOn": "2018-08-28 14:41:46",
                "Creator": "admin",
                "ID": "id",
                "Number": "INC0000003",
                "OpenedAt": "2020-01-24 23:07:30",
                "OpenedBy": "admin",
                "Priority": "1 - Critical",
                "State": "2",
                "Summary": "Wireless access is down in my area"
            }
        ]
    },
    "Ticket": [
        {
            "Active": "false",
            "Assignee": "admin",
            "CloseCode": "Closed/Resolved by Caller",
            "CreatedOn": "2018-08-24 18:24:13",
            "Creator": "admin",
            "ID": "id",
            "Number": "INC0000001",
            "OpenedAt": "2020-01-23 23:09:51",
            "OpenedBy": "admin",
            "Priority": "1 - Critical",
            "ResolvedBy": "admin",
            "State": "7",
            "Summary": "Can't read email"
        },
        {
            "Active": "true",
            "Assignee": "admin",
            "CreatedOn": "2018-08-13 22:30:06",
            "Creator": "admin",
            "ID": "id",
            "Number": "INC0000002",
            "OpenedAt": "2020-01-17 23:07:12",
            "OpenedBy": "admin",
            "Priority": "1 - Critical",
            "State": "3",
            "Summary": "Network file shares access issue"
        },
        {
            "Active": "true",
            "Assignee": "admin",
            "CreatedOn": "2018-08-28 14:41:46",
            "Creator": "admin",
            "ID": "id",
            "Number": "INC0000003",
            "OpenedAt": "2020-01-24 23:07:30",
            "OpenedBy": "admin",
            "Priority": "1 - Critical",
            "State": "2",
            "Summary": "Wireless access is down in my area"
        }
    ]
}

Human Readable Output

ServiceNow tickets

System ID Number Impact Urgency Severity Priority State Created On Created By Active Close Notes Close Code Description Opened At Resolved By Resolved At Short Description
id INC0000001 1 - High 1 - High 1 - High 1 - Critical 7 - Closed 2018-08-24 18:24:13 pat false Closed before close notes were made mandatory
Closed/Resolved by Caller User can’t access email on mail.company.com.
2020-01-23 23:09:51 admin 2020-04-24 19:56:12 Can’t read email
id INC0000002 1 - High 1 - High 1 - High 1 - Critical 3 - On Hold 2018-08-13 22:30:06 pat true     User can’t get to any of his files on the file server. 2020-01-17 23:07:12     Network file shares access issue
id INC0000003 1 - High 1 - High 1 - High 1 - Critical 2 - In Progress 2018-08-28 14:41:46 admin true     I just moved from floor 2 to floor 3 and my laptop cannot connect to any wireless network. 2020-01-24 23:07:30     Wireless access is down in my area

servicenow-add-link


Adds a link to the specified ticket.

Base Command

servicenow-add-link

Input

Argument Name Description Required
id Ticket System ID. Required
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional
link The actual link to publish in ServiceNow ticket, in a valid URL format, for example, http://www.demisto.com. Required
post-as-comment Whether to publish the link as comment on the ticket. Can be “true” or “false”. If false will publish the link as WorkNote. Optional
text The text to represent the link. Optional

Context Output

There is no context output for this command.

Command Example

!servicenow-add-link id=id link="http://www.demisto.com" text=demsito_link

Context Example

{}

Human Readable Output

Link successfully added to ServiceNow ticket

System ID Number Impact Urgency Severity Priority State Created On Created By Active Description Opened At Short Description
id INC0000040 2 - Medium 2 - Medium 2 - Medium 3 - Moderate 3 - On Hold 2020-01-26 00:43:54 admin true Seeing JavaScript error message on hiring page on Explorer and Firefox. 2020-01-26 00:42:45 JavaScript error on hiring page of corporate website

servicenow-add-comment


Adds a comment to the specified ticket, by ticket ID.

Base Command

servicenow-add-comment

Input

Argument Name Description Required
id Ticket System ID. Required
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional
comment Comment to add. Required
post-as-comment Whether to publish the note as comment on the ticket. Can be “true” or “false”. Default is “false”. Optional

Context Output

There is no context output for this command.

Command Example

!servicenow-add-comment id=id comment="Nice work!"

Context Example

{}

Human Readable Output

Comment successfully added to ServiceNow ticket

System ID Number Impact Urgency Severity Priority State Created On Created By Active Description Opened At Short Description
id INC0000040 2 - Medium 2 - Medium 2 - Medium 3 - Moderate 3 - On Hold 2020-01-26 00:43:54 admin true Seeing JavaScript error message on hiring page on Explorer and Firefox. 2020-01-26 00:42:45 JavaScript error on hiring page of corporate website

servicenow-upload-file


Uploads a file to the specified ticket.

Base Command

servicenow-upload-file

Input

Argument Name Description Required
id Ticket System ID. Required
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional
file_id War Room entry ID that includes the file. Required
file_name Filename of the uploaded file to override the existing file name in the entry. Optional

Context Output

Path Type Description
ServiceNow.Ticket.File.Filename string Name of the file.
ServiceNow.Ticket.File.Link string Download link for the file.
ServiceNow.Ticket.File.SystemID string System ID of the file.

Command Example

!servicenow-upload-file id="{TicketID}" ticket_type="incident" file_id="123@456"

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "File": {
                "Filename": "test.txt",
                "Link": "https://{ServerURL}/api/now/attachment/{SystemID}/file",
                "SystemID": "{SystemID}"
            },
            "ID": "{TicketID}"
        }
    },
    "Ticket": {
        "File": {
            "Filename": "test.txt",
            "Link": "https://{TicketID}/api/now/attachment/{SystemID}/file",
            "SystemID": "{SystemID}"
        },
        "ID": "{TicketID}"
    }
}

Human Readable Output

File uploaded successfully to ticket {TICKET ID}

Download link Filename System ID
https://{ServerURL}/api/now/attachment/{SystemID}/file test.txt {SystemID}

servicenow-delete-file


Delete an attachment from a ticket.

Base Command

servicenow-delete-file

Input

Argument Name Description Required
file_sys_id Attachment File ID. Required

Context Output

There is no context output for this command.

Command example

!servicenow-delete-file file_sys_id=1234

Human Readable Output

Attachment with Sys ID 1234 was successfully deleted.

servicenow-get-record


Retrieves record information, by record ID.

Base Command

servicenow-get-record

Input

Argument Name Description Required
id Record System ID. Required
fields Comma-separated list of table fields to display and output to the context, for example: name,tag,company. ID field is added by default. Optional
table_name The name of the table from which to get the record. Required

Context Output

Path Type Description
ServiceNow.Record.ID string The unique record identifier for the record.
ServiceNow.Record.UpdatedBy string A string field that indicates the user who most recently updated the record.
ServiceNow.Record.UpdatedAt date A time-stamp field that indicates the date and time of the most recent update.
ServiceNow.Record.CreatedBy string A string field that indicates the user who created the record.
ServiceNow.Record.CreatedOn date A time-stamp field that indicates when a record was created.

Command Example

!servicenow-get-record table_name=alm_asset id=id fields=asset_tag,sys_updated_by,display_name

Context Example

{
    "ServiceNow": {
        "Record": {
            "ID": "id",
            "asset_tag": "P1000807",
            "display_name": "P1000807 - Apple MacBook Pro 17\"",
            "sys_updated_by": "system"
        }
    }
}

Human Readable Output

ServiceNow record

ID asset_tag display_name sys_updated_by
id P1000807 P1000807 - Apple MacBook Pro 17” system

servicenow-query-table


Queries the specified table in ServiceNow.

Base Command

servicenow-query-table

Input

Argument Name Description Required
table_name The name of the table to query Required
limit The maximum number of tickets to retrieve. Optional
query The query to run. For more information about querying in ServiceNow, see https://docs.servicenow.com/bundle/istanbul-servicenow-platform/page/use/common-ui-elements/reference/r_OpAvailableFiltersQueries.html Optional
fields Comma-separated list of table fields to display and output to the context, for example: name,tag,company. ID field is added by default. Optional
offset Starting record index to begin retrieving records from. Optional
system_params System parameters in the format: fieldname1=value;fieldname2=value. For example: “sysparm_display_value=true;sysparm_exclude_reference_link=True” Optional

Context Output

Path Type Description
ServiceNow.Record.ID string The unique record identifier for the record.
ServiceNow.Record.UpdatedBy string A string field that indicates the user who most recently updated the record.
ServiceNow.Record.UpdatedAt date A time-stamp field that indicates the date and time of the most recent update.
ServiceNow.Record.CreatedBy string A string field that indicates the user who created the record.
ServiceNow.Record.CreatedOn date A time-stamp field that indicates when a record was created.

Command Example

!servicenow-query-table table_name=alm_asset fields=asset_tag,sys_updated_by,display_name query=display_nameCONTAINSMacBook limit=4
!servicenow-query-table table_name=sys_journal_field query=element_id=<SYS_ID>^ORDERBYsys_created_on limit=10 fields=value,name,element,sys_created_by,sys_created_on

Context Example

{
    "ServiceNow": {
        "Record": [
            {
                "ID": "id",
                "asset_tag": "P1000637",
                "display_name": "P1000637 - Apple MacBook Air 13\"",
                "sys_updated_by": "system"
            },
            {
                "ID": "id",
                "asset_tag": "P1000412",
                "display_name": "P1000412 - Apple MacBook Pro 17\"",
                "sys_updated_by": "system"
            },
            {
                "ID": "id",
                "asset_tag": "P1000563",
                "display_name": "P1000563 - Apple MacBook Pro 15\"",
                "sys_updated_by": "system"
            },
            {
                "ID": "id",
                "asset_tag": "P1000626",
                "display_name": "P1000626 - Apple MacBook Air 13\"",
                "sys_updated_by": "system"
            }
        ]
    }
}

Human Readable Output

ServiceNow records

ID asset_tag display_name sys_updated_by
id P1000637 P1000637 - Apple MacBook Air 13” system
id P1000412 P1000412 - Apple MacBook Pro 17” system
id P1000563 P1000563 - Apple MacBook Pro 15” system
id P1000626 P1000626 - Apple MacBook Air 13” system

servicenow-create-record


Creates a new record in the specified ServiceNow table.

Base Command

servicenow-create-record

Input

Argument Name Description Required
table_name The name of the table in which to create a record. Required
fields Fields and their values to create the record with, in the format: fieldname1=value;fieldname2=value;… Optional
custom_fields Custom (user defined) fields in the format: fieldname1=value;fieldname2=value;… Optional
input_display_value Flag that indicates whether to set field values using the display value or the actual value. True will treat the input value as the display value. False treats the input values as actual values. The default setting is false. Optional

For more information regarding the input_display_value argument, see: https://docs.servicenow.com/bundle/xanadu-platform-administration/page/administer/exporting-data/concept/query-parameters-display-value.html

Context Output

Path Type Description
ServiceNow.Record.ID string The unique record identifier for the record.
ServiceNow.Record.UpdatedBy string A string field that indicates the user who most recently updated the record.
ServiceNow.Record.UpdatedAt date A time-stamp field that indicates the date and time of the most recent update.
ServiceNow.Record.CreatedBy string A string field that indicates the user who created the record.
ServiceNow.Record.CreatedOn date A time-stamp field that indicates when a record was created.

Command Example

!servicenow-create-record table_name=alm_asset fields="asset_tag=P1000807"

Context Example

{
    "ServiceNow": {
        "Record": {
            "CreatedAt": "2020-05-10 09:04:27",
            "CreatedBy": "admin",
            "ID": "id",
            "UpdatedAt": "2020-05-10 09:04:27",
            "UpdatedBy": "admin"
        }
    }
}

Human Readable Output

ServiceNow record created successfully

CreatedAt CreatedBy ID UpdatedAt UpdatedBy
2020-05-10 09:04:27 admin id 2020-05-10 09:04:27 admin

servicenow-update-record


Updates a record in the specified ServiceNow table.

Base Command

servicenow-update-record

Input

Argument Name Description Required
table_name The name of the table to update the record in. Required
id The system ID of the ticket to update. Required
fields Fields and their values to update in the record, in the format: fieldname1=value;fieldname2=value;… Optional
custom_fields Custom (user defined) fields and their values to update in the record, in the format: fieldname1=value;fieldname2=value;… Optional
input_display_value Flag that indicates whether to set field values using the display value or the actual value. True will treat the input value as the display value. False treats the input values as actual values. The default setting is false. Optional
clear_fields A comma-separated list of fields to clear. Optional

For more information regarding the input_display_value argument, see: https://docs.servicenow.com/bundle/xanadu-platform-administration/page/administer/exporting-data/concept/query-parameters-display-value.html

Context Output

Path Type Description
ServiceNow.Record.ID string The unique record identifier for the record.
ServiceNow.Record.UpdatedBy string A string field that indicates the user who most recently updated the record.
ServiceNow.Record.UpdatedAt date A time-stamp field that indicates the date and time of the most recent update.
ServiceNow.Record.CreatedBy string A string field that indicates the user who created the record.
ServiceNow.Record.CreatedOn date A time-stamp field that indicates when a record was created.

Command Example

!servicenow-update-record table_name=alm_asset id=id custom_fields="display_name=test4"

Context Example

{
    "ServiceNow": {
        "Record": {
            "CreatedAt": "2019-07-16 08:14:09",
            "CreatedBy": "admin",
            "ID": "id",
            "UpdatedAt": "2020-05-09 19:08:42",
            "UpdatedBy": "system"
        }
    }
}

Human Readable Output

ServiceNow record with ID 01a92c0d3790200044e0bfc8bcbe5d36 updated successfully

CreatedAt CreatedBy ID UpdatedAt UpdatedBy
2019-07-16 08:14:09 admin id 2020-05-09 19:08:42 system

servicenow-delete-record


Deletes a record in the specified ServiceNow table.

Base Command

servicenow-delete-record

Input

Argument Name Description Required
table_name The table name. Required
id The system ID of the ticket to delete. Required

Context Output

There is no context output for this command.

Command Example

!servicenow-delete-record table_name=alm_asset id=id

Context Example

{}

Human Readable Output

ServiceNow record with ID id was successfully deleted.

servicenow-list-table-fields


Lists API fields for the specified ServiceNow table.

Base Command

servicenow-list-table-fields

Input

Argument Name Description Required
table_name Table name Required

Context Output

Path Type Description
ServiceNow.Field string Table API field name.

Command Example

!servicenow-list-table-fields table_name=alm_asset

Context Example

{
    "ServiceNow": {
        "Field": [
            {
                "Name": "parent"
            },
            {
                "Name": "skip_sync"
            },
            {
                "Name": "residual_date"
            },
            {
                "Name": "residual"
            },
            {
                "Name": "sys_updated_on"
            },
            {
                "Name": "request_line"
            },
            {
                "Name": "sys_updated_by"
            },
            {
                "Name": "due_in"
            },
            {
                "Name": "model_category"
            },
            {
                "Name": "sys_created_on"
            },
            {
                "Name": "sys_domain"
            },
            {
                "Name": "disposal_reason"
            },
            {
                "Name": "model"
            },
            {
                "Name": "install_date"
            },
            {
                "Name": "gl_account"
            },
            {
                "Name": "invoice_number"
            },
            {
                "Name": "sys_created_by"
            },
            {
                "Name": "warranty_expiration"
            },
            {
                "Name": "depreciated_amount"
            },
            {
                "Name": "substatus"
            },
            {
                "Name": "pre_allocated"
            },
            {
                "Name": "owned_by"
            },
            {
                "Name": "checked_out"
            },
            {
                "Name": "display_name"
            },
            {
                "Name": "sys_domain_path"
            },
            {
                "Name": "delivery_date"
            },
            {
                "Name": "retirement_date"
            },
            {
                "Name": "beneficiary"
            },
            {
                "Name": "install_status"
            },
            {
                "Name": "cost_center"
            },
            {
                "Name": "supported_by"
            },
            {
                "Name": "assigned"
            },
            {
                "Name": "purchase_date"
            },
            {
                "Name": "work_notes"
            },
            {
                "Name": "managed_by"
            },
            {
                "Name": "sys_class_name"
            },
            {
                "Name": "sys_id"
            },
            {
                "Name": "po_number"
            },
            {
                "Name": "stockroom"
            },
            {
                "Name": "checked_in"
            },
            {
                "Name": "resale_price"
            },
            {
                "Name": "vendor"
            },
            {
                "Name": "company"
            },
            {
                "Name": "retired"
            },
            {
                "Name": "justification"
            },
            {
                "Name": "department"
            },
            {
                "Name": "expenditure_type"
            },
            {
                "Name": "depreciation"
            },
            {
                "Name": "assigned_to"
            },
            {
                "Name": "depreciation_date"
            },
            {
                "Name": "old_status"
            },
            {
                "Name": "comments"
            },
            {
                "Name": "cost"
            },
            {
                "Name": "quantity"
            },
            {
                "Name": "acquisition_method"
            },
            {
                "Name": "ci"
            },
            {
                "Name": "sys_mod_count"
            },
            {
                "Name": "old_substatus"
            },
            {
                "Name": "sys_tags"
            },
            {
                "Name": "order_date"
            },
            {
                "Name": "support_group"
            },
            {
                "Name": "reserved_for"
            },
            {
                "Name": "due"
            },
            {
                "Name": "location"
            },
            {
                "Name": "lease_id"
            },
            {
                "Name": "salvage_value"
            }
        ]
    }
}

Human Readable Output

ServiceNow Table fields - alm_asset

Name
parent
skip_sync
residual_date
residual
sys_updated_on
request_line
sys_updated_by
due_in
model_category
sys_created_on
sys_domain
disposal_reason
model
install_date
gl_account
invoice_number
sys_created_by
warranty_expiration
depreciated_amount
substatus
pre_allocated
owned_by
checked_out
display_name
sys_domain_path
delivery_date
retirement_date
beneficiary
install_status
cost_center
supported_by
assigned
purchase_date
work_notes
managed_by
sys_class_name
sys_id
po_number
stockroom
checked_in
resale_price
vendor
company
retired
justification
department
expenditure_type
depreciation
assigned_to
depreciation_date
old_status
comments
cost
quantity
acquisition_method
ci
sys_mod_count
old_substatus
sys_tags
order_date
support_group
reserved_for
due
location
lease_id
salvage_value

servicenow-query-computers


Queries the cmdb_ci_computer table in ServiceNow.

Base Command

servicenow-query-computers

Input

Argument Name Description Required
computer_id Query by computer sys_id. Optional
computer_name Query by computer name. Optional
query Query by specified query, for more information about querying in ServiceNow, see https://docs.servicenow.com/bundle/istanbul-servicenow-platform/page/use/common-ui-elements/reference/r_OpAvailableFiltersQueries.html Optional
asset_tag Query by asset tag. Optional
limit Maximum number of query results. Default is 10. Optional
offset Starting record index to begin retrieving records from. Optional

Context Output

Path Type Description
ServiceNow.Computer.ID string Computer system ID.
ServiceNow.Computer.AssetTag string Computer Asset tag.
ServiceNow.Computer.Name string Computer name.
ServiceNow.Computer.DisplayName string Computer display name.
ServiceNow.Computer.SupportGroup string Computer support group.
ServiceNow.Computer.OperatingSystem string Computer operating system.
ServiceNow.Computer.Company string Computer company system ID.
ServiceNow.Computer.AssignedTo string Computer assigned to user system ID.
ServiceNow.Computer.State string Computer state.
ServiceNow.Computer.Cost string Computer cost.
ServiceNow.Computer.Comments string Computer comments.

Command Example

!servicenow-query-computers asset_tag=P1000412

Context Example

{
    "ServiceNow": {
        "Computer": {
            "AssetTag": "P1000412",
            "AssignedTo": "admin",
            "Company": "admin",
            "Cost": "2499.99 USD",
            "DisplayName": "P1000412 - MacBook Pro 17\"",
            "ID": "id",
            "Name": "MacBook Pro 17\"",
            "OperatingSystem": "Mac OS 10 (OS/X)",
            "State": "In use"
        }
    }
}

Human Readable Output

ServiceNow Computers

ID Asset Tag Name Display Name Operating System Company Assigned To State Cost
id P1000412 MacBook Pro 17” P1000412 - MacBook Pro 17” Mac OS 10 (OS/X) admin admin In use 2499.99 USD

servicenow-query-groups


Queries the sys_user_group table in ServiceNow.

Base Command

servicenow-query-groups

Input

Argument Name Description Required
group_id Query by group system ID. Optional
group_name Query by group name. Optional
query Query by specified query, for more information about querying in ServiceNow, see https://docs.servicenow.com/bundle/istanbul-servicenow-platform/page/use/common-ui-elements/reference/r_OpAvailableFiltersQueries.html Optional
limit Maximum number of query results. Default is 10. Optional
offset Starting record index to begin retrieving records from. Optional

Context Output

Path Type Description
ServiceNow.Group.ID string Group system ID.
ServiceNow.Group.Description string Group description.
ServiceNow.Group.Name string Group name.
ServiceNow.Group.Manager string Group manager system ID.
ServiceNow.Group.Updated date Date/time the group was last updated.

Command Example

!servicenow-query-groups group_name=test1

Context Example

{}

Human Readable Output

No groups found.

servicenow-query-users


Queries the sys_user table in ServiceNow.

Base Command

servicenow-query-users

Input

Argument Name Description Required
user_id Query by user system ID. Optional
user_name Query by username. Optional
query Query by specified query, for more information about querying in ServiceNow, see https://docs.servicenow.com/bundle/istanbul-servicenow-platform/page/use/common-ui-elements/reference/r_OpAvailableFiltersQueries.html Optional
limit Maximum number of query results. Default is 10. Optional
offset Starting record index to begin retrieving records from. Optional

Context Output

Path Type Description
ServiceNow.User.ID string User system ID.
ServiceNow.User.Name string User name (first and last).
ServiceNow.User.UserName string User username.
ServiceNow.User.Email string User email address.
ServiceNow.User.Created date Date/time the user was created.
ServiceNow.User.Updated date Date/time the user was last updated.

Command Example

!servicenow-query-users user_name=sean.bonnet

Context Example

{
    "ServiceNow": {
        "User": {
            "Created": "2012-02-18 03:04:50",
            "Email": "sean.bonnet@example.com",
            "ID": "id",
            "Name": "Sean Testman",
            "Updated": "2020-04-25 19:01:46",
            "UserName": "sean.bonnet"
        }
    }
}

Human Readable Output

ServiceNow Users

ID Name User Name Email Created Updated
id Sean Testman sean.bonnet sean.bonnet@example.com 2012-02-18 03:04:50 2020-04-25 19:01:46

servicenow-get-table-name


Gets table names by a label to use in commands.

Base Command

servicenow-get-table-name

Input

Argument Name Description Required
label The table label, for example: Asset, Incident, IP address, and so on. Required
limit Maximum number of query results. Default is 10. Optional
offset Starting record index to begin retrieving records from. Optional

Context Output

Path Type Description
ServiceNow.Table.ID string Table system ID.
ServiceNow.Table.Name string Table name to use in commands, for example: alm_asset.
ServiceNow.Table.SystemName string Table system name, for example: Asset.

Command Example

!servicenow-get-table-name label=ACE

Context Example

{
    "ServiceNow": {
        "Table": {
            "ID": "id",
            "Name": "cmdb_ci_lb_ace",
            "SystemName": "CMDB CI Lb Ace"
        }
    }
}

Human Readable Output

ServiceNow Tables for label - ACE

ID Name System Name
id cmdb_ci_lb_ace CMDB CI Lb Ace

servicenow-get-ticket-notes


Gets notes from the specified ServiceNow ticket. Notes can be retrieved either by granting Read permissions for the sys_journal_field table, or by setting the use_display_value parameter to true.

Base Command

servicenow-get-ticket-notes

Input

Argument Name Description Required
id Ticket System ID. Required
limit Maximum number of ticket notes. Default is 10. Optional
offset Offset of the ticket notes. Default is 0. Optional
use_display_value Whether to use sysparm_display_value to retrieve comments and work notes. Overrides the value set in the instance configuration. Possible values are: true, false. Optional
ticket_type The ticket type that notes should be retrieved for when using the use_display_value option. Possible values are: incident, problem, change_request, sc_request, sc_task, sc_req_item, sn_si_incident. Default is incident. Optional
add_as_entry Whether to add ticket notes and work notes as notes in the War Room. Possible values are true, false. Optional

Context Output

Path Type Description
ServiceNow.Ticket.ID string Ticket ID.
ServiceNow.Ticket.Note.Value unknown Ticket note value.
ServiceNow.Ticket.Note.CreatedOn date Date/time the ticket note was created.
ServiceNow.Ticket.Note.CreatedBy string User who created the ticket note.
ServiceNow.Ticket.Note.Type string Ticket note type.

Command Example

!servicenow-get-ticket-notes id=id

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "ID": "id",
            "Note": [
                {
                    "CreatedBy": "admin",
                    "CreatedOn": "2020-01-26 00:43:54",
                    "Type": "Comment",
                    "Value": "JavaScript error (line 202) on the home page. Not sure what is\n\t\t\tgoing on, does not happen on my Windows machine!\n\t\t"
                },
                {
                    "CreatedBy": "admin",
                    "CreatedOn": "2020-04-17 23:12:43",
                    "Type": "Comment",
                    "Value": "Added an attachment"
                },
                {
                    "CreatedBy": "admin",
                    "CreatedOn": "2020-05-10 09:04:15",
                    "Type": "Work Note",
                    "Value": "[code]<a class=\"web\" target=\"_blank\" href=\"http://www.demisto.com\" >demsito_link</a>[/code]"
                },
                {
                    "CreatedBy": "admin",
                    "CreatedOn": "2020-05-10 09:04:18",
                    "Type": "Work Note",
                    "Value": "Nice work!"
                }
            ]
        }
    }
}

Human Readable Output

ServiceNow notes for ticket 471d4732a9fe198100affbf655e59172

Value Created On Created By Type
JavaScript error (line 202) on the home page. Not sure what is
going on, does not happen on my Windows machine!
2020-01-26 00:43:54 admin Comment
Added an attachment 2020-04-17 23:12:43 admin Comment
[code]demsito_link[/code] 2020-05-10 09:04:15 admin Work Note
Nice work! 2020-05-10 09:04:18 admin Work Note

servicenow-add-tag


Adds a tag to a ticket. The tag will be visible in the label_entry table and can be retrieved using the “!servicenow-query-table table_name=label_entry fields=title,table,sys_id,id_display,id_type” command.

Base Command

servicenow-add-tag

Input

Argument Name Description Required
id Ticket System ID. Required
tag_id Tag system ID. Can be retrieved using the “!servicenow-query-table table_name=label fields=name,active,sys_id” command. Required
title Tag title. For example: “Incident - INC000001”. Required
ticket_type Ticket type. Can be “incident”, “problem”, “change_request”, “sc_request”, “sc_task”, “sc_req_item”, or “sn_si_incident”. Default is “incident”. Optional

Context Output

Path Type Description
ServiceNow.Ticket.ID String The unique ticket identifier.
ServiceNow.Ticket.TagTitle String Ticket tag title.
ServiceNow.Ticket.TagID String Ticket tag ID.

servicenow-query-items


Queries the sc_cat_item table in ServiceNow.

Base Command

servicenow-query-items

Input

Argument Name Description Required
name Query by name. Does not require an exact match. Optional
offset Starting record index to begin retrieving records from. Optional
limit Maximum number of query results. Default is 10. Optional

Context Output

Path Type Description
ServiceNow.CatalogItem.ID String Catalog item system ID.
ServiceNow.CatalogItem.Name String Catalog item name.
ServiceNow.CatalogItem.Description String Catalog item description.
ServiceNow.CatalogItem.Price Number Catalog item price.

Command Example

!servicenow-query-items name=laptop limit=2

Context Example

{
    "ServiceNow": {
        "CatalogItem": [
            {
                "Description": "Lenovo - Carbon x1",
                "ID": "id",
                "Name": "Standard Laptop",
                "Price": "1100"
            },
            {
                "Description": "Dell XPS 13",
                "ID": "id",
                "Name": "Development Laptop (PC)",
                "Price": "1100"
            }
        ]
    }
}

Human Readable Output

ServiceNow Catalog Items

ID Name Price Description
id Standard Laptop 1100 Lenovo - Carbon x1
id Development Laptop (PC) 1100 Dell XPS 13

servicenow-get-item-details


Retrieves item details by system ID.

Base Command

servicenow-get-item-details

Input

Argument Name Description Required
id Catalog item system ID. Required

Context Output

Path Type Description
ServiceNow.CatalogItem.ID String Catalog item system ID.
ServiceNow.CatalogItem.Name String Catalog item name.
ServiceNow.CatalogItem.Description String Catalog item description.
ServiceNow.CatalogItem.Price Number Catalog item price.
ServiceNow.CatalogItem.Variables.Mandatory Boolean Is the variable mandatory as part of the ordering process.
ServiceNow.CatalogItem.Variables.Name String A name to identify the question.
ServiceNow.CatalogItem.Variables.Question String Question to ask users ordering the catalog item.
ServiceNow.CatalogItem.Variables.Type String The variable type.

Command Example

!servicenow-get-item-details id=id

Context Example

{
    "ServiceNow": {
        "CatalogItem": {
            "Description": "Dell XPS 13",
            "ID": "id",
            "Name": "Development Laptop (PC)",
            "Price": "$1,000.00",
            "Variables": [
                {
                    "Mandatory": false,
                    "Name": "hard_drive",
                    "Question": "What size solid state drive do you want?",
                    "Type": "Multiple Choice"
                },
                {
                    "Mandatory": false,
                    "Name": "requested_os",
                    "Question": "Please specify an operating system",
                    "Type": "Multiple Choice"
                }
            ]
        }
    }
}

Human Readable Output

ServiceNow Catalog Item

ID Name Description
id Development Laptop (PC) Dell XPS 13

Item Variables

Question Type Name Mandatory
What size solid state drive do you want? Multiple Choice hard_drive false
Please specify an operating system Multiple Choice requested_os false

servicenow-create-item-order


Orders the specified catalog item.

Base Command

servicenow-create-item-order

Input

Argument Name Description Required
id Catalog item system ID. Required
quantity Quantity of the item to order. Required
variables If there are mandatory variables defined for the item, they must be passed to the endpoint. Can be retrieved using the servicenow-get-item-details command. For example, var1=value1;var2=value2. Optional

Context Output

Path Type Description
ServiceNow.OrderRequest.ID String Generated request system ID.
ServiceNow.OrderRequest.RequestNumber String Number of the generated request.

Command Example

!servicenow-create-item-order id=id quantity=1 variables="hard_drive=16GB;requested_os=linux"

Context Example

{
    "ServiceNow": {
        "OrderRequest": {
            "ID": "id",
            "RequestNumber": "REQ0010004"
        }
    }
}

Human Readable Output

ServiceNow Order Request

ID Request Number
id REQ0010004

servicenow-document-route-to-queue


Documents a route to a queue. Requires an installation of the Advanced Work Assignments plugin. An active queue and service channel to the designated table.

Base Command

servicenow-document-route-to-queue

Input

Argument Name Description Required
queue_id Queue ID. Can be retrieved using the “!servicenow-query-table table_name=awa_queue fields=name,number,order” command. Required
document_table Document table. Optional
document_id Document ID. Required

Context Output

Path Type Description
ServiceNow.WorkItem.WorkItemID String Unique ID of the work item assigned to the queue.
ServiceNow.WorkItem.DocumentTable String Name of the table associated with the document
ServiceNow.WorkItem.DocumentID String Unique ID of the document to be routed to the queue.
ServiceNow.WorkItem.QueueID String Unique ID of the queue on which to route a document.
ServiceNow.WorkItem.DisplayName String Name of the document to be routed by this work item, for example: case record.

get-mapping-fields


Returns the list of fields for an incident type. This command is for debugging purposes.

Base Command

get-mapping-fields

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

get-remote-data


Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.

Base Command

get-remote-data

Input

Argument Name Description Required
id The ticket ID. Required
lastUpdate Retrieve entries that were created after lastUpdate. Required

Context Output

There is no context output for this command.

servicenow-get-tasks-for-co


gets the tasks associated to change request

Base Command

servicenow-get-tasks-for-co

Input

Argument Name Description Required
id ID of the change request. Required

Context Output

Path Type Description
ServiceNow.Tasks.ID String Task ID
ServiceNow.Tasks.Description Unknown Description of task
ServiceNow.Tasks.Name Unknown Name of task
ServiceNow.Tasks.State Unknown state of task

Command example

!servicenow-get-tasks-for-co id="1234"

Context Example

{
    "ServiceNow": {
        "Tasks": {
            "ServiceNow.Tasks(val.ID===obj.ID)": [
                {
                    "Description": "test",
                    "ID": "1234",
                    "Name": "CTASK0010007",
                    "State": "1 - New"
                }
            ]
        }
    }
}

Human Readable Output

ServiceNow Catalog Items

ID Name State Description
1234 CTASK0010007 1 - New test

servicenow-create-co-from-template


Create a change request from a template.

Base Command

servicenow-create-co-from-template

Input

Argument Name Description Required
template Template for creating a standard change request. Required
force_default_url Whether to force-use the default api version (not versioned), ignoring the API version parameter. Default is false Optional

Context Output

Path Type Description
ServiceNow.Ticket.ID Unknown ServiceNow ticket ID.
ServiceNow.Ticket.OpenedBy Unknown ServiceNow ticket opener ID.
ServiceNow.Ticket.CreatedOn Unknown ServiceNow ticket creation date.
ServiceNow.Ticket.Assignee Unknown ServiceNow ticket assignee ID.
ServiceNow.Ticket.State Unknown ServiceNow ticket state.
ServiceNow.Ticket.Summary Unknown ServiceNow ticket short summary.
ServiceNow.Ticket.Number Unknown ServiceNow ticket number.
ServiceNow.Ticket.Active Unknown ServiceNow ticket active.
ServiceNow.Ticket.AdditionalComments Unknown ServiceNow ticket comments.
ServiceNow.Ticket.Priority Unknown ServiceNow ticket priority.
ServiceNow.Ticket.OpenedAt Unknown ServiceNow ticket opening time.
ServiceNow.Ticket.ResolvedBy Unknown ServiceNow ticket resolver ID.
ServiceNow.Ticket.CloseCode Unknown ServiceNow ticket close code.

Command example

!servicenow-create-co-from-template template=1234

Context Example

{
    "ServiceNow": {
        "Ticket": {
            "ServiceNow.Ticket(val.ID===obj.ID)": {
                "Active": {
                    "display_value": "true",
                    "value": true
                },
                "AdditionalComments": {
                    "display_value": "",
                    "value": ""
                },
                "CloseCode": {
                    "display_value": "",
                    "value": ""
                },
                "CreatedOn": {
                    "display_value": "03/05/2022 08:17:00",
                    "display_value_internal": "2022-05-03 08:17:00",
                    "value": "2022-05-03 15:17:00"
                },
                "Creator": "1234",
                "ID": {
                    "display_value": "1234",
                    "value": "1234"
                },
                "Number": {
                    "display_value": "CHG001234",
                    "value": "CHG001234"
                },
                "OpenedAt": {
                    "display_value": "03/05/2022 08:17:00",
                    "display_value_internal": "2022-05-03 08:17:00",
                    "value": "2022-05-03 15:17:00"
                },
                "OpenedBy": "1234",
                "Priority": [
                    "4 - Low"
                ],
                "State": {
                    "display_value": "New",
                    "value": -5
                },
                "Summary": {
                    "display_value": "Add network switch to cabinet",
                    "value": "Add network switch to cabinet"
                }
            },
            "Ticket(val.ID===obj.ID)": {
                "Active": {
                    "display_value": "true",
                    "value": true
                },
                "AdditionalComments": {
                    "display_value": "",
                    "value": ""
                },
                "CloseCode": {
                    "display_value": "",
                    "value": ""
                },
                "CreatedOn": {
                    "display_value": "03/05/2022 08:17:00",
                    "display_value_internal": "2022-05-03 08:17:00",
                    "value": "2022-05-03 15:17:00"
                },
                "Creator": "1234",
                "ID": {
                    "display_value": "1234",
                    "value": "1234"
                },
                "Number": {
                    "display_value": "CHG001234",
                    "value": "CHG001234"
                },
                "OpenedAt": {
                    "display_value": "03/05/2022 08:17:00",
                    "display_value_internal": "2022-05-03 08:17:00",
                    "value": "2022-05-03 15:17:00"
                },
                "OpenedBy": "6b0e49021b158150042611b4bd4bcb5e",
                "Priority": [
                    "4 - Low"
                ],
                "State": {
                    "display_value": "New",
                    "value": -5
                },
                "Summary": {
                    "display_value": "Add network switch to cabinet",
                    "value": "Add network switch to cabinet"
                }
            }
        }
    }
}

Human Readable Output

ServiceNow ticket was created successfully

System ID Number Impact Urgency Priority State Created On Created By Active Description Opened At Short Description
1234 CHG001234 3 - Ministry 3 - Low 4 - Low -5 - New 2022-05-03 15:17:00 admin_cnt_test_Jan2022-01 true This standard change template describes adding a new network switch to a datacenter cabinet 2022-05-03 15:17:00 Add network switch to cabinet

servicenow-generic-api-call


Generic call to ServiceNow api

Base Command

servicenow-generic-api-call

Input

Argument Name Description Required
method action to be performed on path. Possible values are: GET, POST, PATCH, DELETE. Default is 0. Required
path the API path starting with forward slash (/). Required
json_body whether or not the request body is json. Possible values are: true, false. Default is false. Optional
body json to send in body. Optional
headers json of headers to add. Optional
sc_api Service Catalog Call. Possible values are: true, false. Default is false. Optional
cr_api Change Request Call. Possible values are: true, false. Default is false. Optional
custom_api Define a custom API root (like /api/custom/tasks). Optional

Context Output

Path Type Description
ServiceNow.Generic.Response string Generic response to servicenow api

Command example

!servicenow-generic-api-call method=GET path="/table/sn_cmdb_workspace_cmdb_ci_demo"

Context Example

{
    "ServiceNow": {
        "Generic": {
            "Response": {
                "result": [
                    {
                        "sys_created_by": "admin_test_Nov2020-01",
                        "sys_created_on": "2021-02-13 13:36:55",
                        "sys_id": "1234",
                        "sys_mod_count": "0",
                        "sys_tags": "",
                        "sys_updated_by": "admin_test_Nov2020-01",
                        "sys_updated_on": "2021-02-13 13:36:55"
                    }
                ]
            }
        }
    }
}

Human Readable Output

Request for GET method is successful

servicenow-get-ticket-attachments


Retrieve file attachments from tickets.

Base Command

servicenow-get-ticket-attachments

Input

Argument Name Description Required
sys_id sys_id of the ticket containing the file. Required

Context Output

There is no context output for this command.

Human Readable Output

Successfully retrieved attachments for ticket with sys id 111

Troubleshooting

The following are tips for handling issues with mirroring incidents between ServiceNow and Cortex XSOAR.

Issue Recommendation
Unauthorized connection error. Check the credentials.
</br>Make sure the oauth ID and secret are created using the account configured in Cortex XSOAR. See the ServiceNow article.
</br>Check the roles to ensure the following roles are granted - itil (For ServiceNow v2), cmdb_inst_admin (For ServiceNow CMDB), rest_api_explorer, snc_platform_rest_api_access, web_service_admin.
</br>Enter the client ID and secret in the dedicated Client ID and Client Secret fields, and the account username and password in the Username and Password`fields of the integration instance configuration.
</br>
Mirroring is not working. Open Context Data and search for dbot. Confirm the dbot fields are configured correctly either through the mapper for that specific incident type or using setIncident. Specifically, make sure the integration instance is configured correctly for the mirroring direction (ingoing, outgoing, both) - dbotMirrorId, dbotMirrorDirection, dbotMirrorInstance, dbotMirrorTags
Comments and work notes from before incident creation are not fetched/mirrored. Mirroring settings apply only for incidents that are fetched after applying the settings.
</br> For example, if a user creates a ServiceNow incident with added comments/work notes and then defines a ServiceNow integration instance with mirroring in Cortex XSOAR, those comments/work notes are not fetched with the incident in Cortex XSOAR.
</br> To fetch pre-existing comments/work notes, you can use the servicenow-get-ticket-notes command.
</br> To retrieve comments and work notes using this command follow one of these steps:
</br> - Grant elevated privileges to the sys_journal_field. This is done by assigning the Itil role (see this ServiceNow community link on giving elevated read access and potential risks).
</br> - Select the Use Display Value checkbox in the instance configuration. This will retrieve comments and work notes using their display value from ServiceNow. When choosing this option, you must select the date format of your instance from the Instance Date Format dropdown menu (see the ServiceNow documentation for an explanation how to find the date format of your instance). Note that in this case, the values that will appear under the sys_created_by field of the retrieved notes are the display values from ServiceNow and not the system values, e.g., “System Administrator” instead of “admin”.
Ticket is not closing. Verify the integration instance is configured correctly for the Mirrored XSOAR Ticket closure method or Mirrored ServiceNow Ticket closure method fields.
Required fields are not getting sent. This may be a mapping issue, specifically if close reason and close notes are required.
Auto-Extract extracts sys_id as a file. This is by design. To resolve this:
</br> - Turn off Auto-Extract for all fields and only used on specific fields.
</br> - Turn off Auto-Extract at the incident level and use the task level for extraction.
Date displayed in the Cortex XSOAR layout is not aligned with ServiceNow. 1. Navigate to the incoming-mapper you are using.
</br> 2. For every field that uses the DateStringToISOFormat script, change the dayfirst argument to true.

Configuration parameters

  • url — ServiceNow URL, in the format https://company.service-now.com/ (required)
  • use_oauth — Use OAuth Login
  • use_jwt — Use JWT Authentication
  • credentials — Client ID/JWT Audience (aud)
  • basic_credentials — Username
  • private_key
  • kid — kid (Key Id)
  • sub — sub
  • iss — iss (Issuer)
  • ticket_type — Default ticket type for running ticket commands and fetching incidents
  • api_version — ServiceNow API Version (e.g. 'v1')
  • isFetch — Fetch incidents
  • sysparm_query — The query to use when fetching incidents
  • fetch_limit — How many incidents to fetch each time
  • fetch_time — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)
  • timestamp_field — Timestamp field to filter by (e.g., `opened_at`) This is how the filter is applied to the query: "ORDERBYopened_at^opened_at>[Last Run]". To prevent duplicate incidents, this field is mandatory for fetching incidents.
  • incident_name — ServiceNow ticket column to be set as the incident name. Default is the incident number
  • incidentType — Incident type
  • get_attachments — Get incident attachments
  • mirror_direction — Incident Mirroring Direction
  • use_display_value — Use Display Value
  • display_date_format — Instance Date Format
  • comment_tag — Comment Entry Tag To ServiceNow
  • comment_tag_from_servicenow — Comment Entry Tag From ServiceNow
  • work_notes_tag — Work Note Entry Tag To ServiceNow
  • work_notes_tag_from_servicenow — Work Note Entry Tag From ServiceNow
  • file_tag — File Entry Tag To ServiceNow
  • file_tag_from_service_now — File Entry Tag From ServiceNow
  • update_timestamp_field — Timestamp field to query for updates as part of the mirroring flow
  • mirror_limit — How many incidents to mirror incoming each time
  • custom_fields — Custom Fields to Mirror
  • close_incident — Mirrored XSOAR Ticket closure method
  • server_close_custom_state — Mirrored XSOAR Ticket custom close state code
  • server_custom_close_code — Mirrored XSOAR Ticket custom close resolution code (overwrites the custom close state)
  • close_ticket_multiple_options — Mirrored ServiceNow Ticket closure method
  • close_custom_state — Mirrored ServiceNow Ticket custom close state code
  • close_ticket — Close Mirrored ServiceNow Ticket
  • mirror_notes_for_new_incidents — Mirror Existing Notes For New Fetched Incidents
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)
  • incidentFetchInterval — Incidents Fetch Interval
  • look_back — Advanced: Minutes to look back when fetching
  • comment_format — Interpret comments in a specific format
  • mark_attachments_as_note — Mark attachment as note

Commands (36)

  • get-mapping-fields

    Returns the list of fields for an incident type.

  • get-modified-remote-data

    Gets the list of incidents that were modified since the last update time. Note that this method is here for debugging purposes. The get-modified-remote-data command is used as part of a Mirroring feature, which is available from version 6.1.

  • get-remote-data

    Get remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.

  • get-remote-data-preview

    Get remote data preview from a remote incident. This method does not update the current incident, and is used to preview the ticket which would be updated.

  • servicenow-add-comment

    Adds a comment to the specified ticket, by ticket ID.

  • servicenow-add-link

    Adds a link to the specified ticket.

  • servicenow-add-tag

    Adds a tag to a ticket. The added tag entry will be visible in the label_entry table and can be retrieved using the "!servicenow-query-table table_name=label_entry fields=title,table,sys_id,id_display,id_type" command.

  • servicenow-create-co-from-template

    Create a change request from a template.

  • servicenow-create-item-order

    Orders the specified catalog item.

  • servicenow-create-record

    Creates a new record in the specified ServiceNow table.

  • servicenow-create-ticket

    Creates new ServiceNow ticket.

  • servicenow-create-ticket-quick-action

    Creates new ServiceNow ticket.

  • servicenow-delete-file

    Delete an attachment from a ticket.

  • servicenow-delete-record

    Deletes a record in the specified ServiceNow table.

  • servicenow-delete-ticket

    Deletes a ticket from ServiceNow.

  • servicenow-document-route-to-queue

    Documents a route to a queue. Requires an installation of the Advanced Work Assignments plugin. An active queue and service channel to the designated table.

  • servicenow-generic-api-call

    Generic call to ServiceNow api.

  • servicenow-get-item-details

    Retrieves item details by system ID.

  • servicenow-get-record

    Retrieves record information, by record ID.

  • servicenow-get-table-name

    Gets table names by a label to use in commands.

  • servicenow-get-tasks-for-co

    gets the tasks associated to change request.

  • servicenow-get-ticket

    Retrieves ticket information by ticket ID.

  • servicenow-get-ticket-attachments

    Retrieves attachments from a ticket.

  • servicenow-get-ticket-notes

    Gets notes from the specified ServiceNow ticket. Notes can be retrieved either by granting "Read permissions" for the sys_journal_field table, or by setting the `use_display_value` parameter to true.

  • servicenow-list-table-fields

    Lists API fields for the specified ServiceNow table.

  • servicenow-oauth-login

    This function should be used once before running any command when using OAuth2 authentication.

  • servicenow-oauth-test

    Test the instance configuration when using OAuth2 authentication.

  • servicenow-query-computers

    Queries the cmdb_ci_computer table in ServiceNow.

  • servicenow-query-groups

    Queries the sys_user_group table in ServiceNow.

  • servicenow-query-items

    Queries the sc_cat_item table in ServiceNow.

  • servicenow-query-table

    Queries the specified table in ServiceNow.

  • servicenow-query-tickets

    Retrieves ticket information according to the supplied query.

  • servicenow-query-users

    Queries the sys_user table in ServiceNow.

  • servicenow-update-record

    Updates a record in the specified ServiceNow table.

  • servicenow-update-ticket

    Updates the specified ticket.

  • servicenow-upload-file

    Uploads a file to the specified ticket.

import json
import re
from datetime import datetime, timedelta
from typing import Any
from unittest.mock import MagicMock, patch
from urllib.parse import urlencode

import demistomock as demisto
import pytest
import requests
import os

import ServiceNowv2

from CommonServerPython import CommandResults, DemistoException, EntryType, QuickActionPreview, EntryFormat
from freezegun import freeze_time
from pytest_mock import MockerFixture
from requests_mock import MockerCore
from ServiceNowv2 import (
    DATE_FORMAT,
    DATE_FORMAT_OPTIONS,
    MAX_RETRY,
    Client,
    ServiceNowClient,
    add_comment_command,
    add_link_command,
    add_tag_command,
    check_assigned_to_field,
    convert_to_notes_result,
    converts_close_code_or_state_to_close_reason,
    create_order_item_command,
    create_record_command,
    create_ticket_command,
    delete_attachment_command,
    delete_record_command,
    delete_ticket_command,
    document_route_to_table,
    fetch_incidents,
    format_incidents_response_with_display_values,
    generate_body,
    generic_api_call_command,
    get_attachment_command,
    get_closure_case,
    get_entries_for_notes,
    get_item_details_command,
    get_mapping_fields_command,
    get_modified_remote_data_command,
    get_record_command,
    get_remote_data_command,
    get_server_url,
    get_table_name_command,
    get_ticket_context,
    get_ticket_fields,
    get_ticket_human_readable,
    get_ticket_notes_command,
    get_timezone_offset,
    is_new_incident,
    is_time_field,
    list_table_fields_command,
    login_command,
    main,
    oauth_test_module,
    parse_dict_ticket_fields,
    query_computers_command,
    query_items_command,
    query_table_command,
    query_tickets_command,
    split_fields,
    split_notes,
    update_record_command,
    update_remote_system_command,
    update_remote_system_with_entries,
    update_ticket_command,
    upload_file_command,
)
from ServiceNowv2 import test_module as module
from test_data.created_ticket_context import (
    CREATED_TICKET_CONTEXT_CREATE_CO_FROM_TEMPLATE_COMMAND,
    CREATED_TICKET_CONTEXT_GET_TASKS_FOR_CO_COMMAND,
)
from test_data.response_constants import (
    MIRROR_COMMENTS_RESPONSE,
    MIRROR_COMMENTS_RESPONSE_FROM_XSOAR,
    MIRROR_ENTRIES,
    MIRROR_ENTRIES_WITH_EMPTY_USERNAME,
    OAUTH_PARAMS,
    RESPONSE_ADD_COMMENT,
    RESPONSE_ADD_LINK,
    RESPONSE_ADD_TAG,
    RESPONSE_ASSIGNMENT_GROUP,
    RESPONSE_CLOSING_TICKET_MIRROR_CLOSED,
    RESPONSE_CLOSING_TICKET_MIRROR_CUSTOM,
    RESPONSE_CLOSING_TICKET_MIRROR_RESOLVED,
    RESPONSE_COMMENTS_DISPLAY_VALUE,
    RESPONSE_COMMENTS_DISPLAY_VALUE_AFTER_FORMAT,
    RESPONSE_COMMENTS_DISPLAY_VALUE_NO_COMMENTS,
    RESPONSE_CREATE_ITEM_ORDER,
    RESPONSE_CREATE_RECORD,
    RESPONSE_CREATE_TICKET,
    RESPONSE_CREATE_TICKET_WITH_OUT_JSON,
    RESPONSE_DOCUMENT_ROUTE,
    RESPONSE_FETCH,
    RESPONSE_FETCH_ATTACHMENTS_FILE,
    RESPONSE_FETCH_ATTACHMENTS_TICKET,
    RESPONSE_FETCH_USE_DISPLAY_VALUE,
    RESPONSE_GENERIC_TICKET,
    RESPONSE_GET_RECORD,
    RESPONSE_GET_TABLE_NAME,
    RESPONSE_GET_TICKET_NOTES,
    RESPONSE_ITEM_DETAILS,
    RESPONSE_LIST_TABLE_FIELDS,
    RESPONSE_MIRROR_FILE_ENTRY,
    RESPONSE_MIRROR_FILE_ENTRY_FROM_XSOAR,
    RESPONSE_MULTIPLE_TICKET,
    RESPONSE_QUERY_COMPUTERS,
    RESPONSE_QUERY_ITEMS,
    RESPONSE_QUERY_TABLE,
    RESPONSE_QUERY_TABLE_SYS_PARAMS,
    RESPONSE_QUERY_TICKETS,
    RESPONSE_QUERY_TICKETS_EXCLUDE_REFERENCE_LINK,
    RESPONSE_TICKET,
    RESPONSE_TICKET_ASSIGNED,
    RESPONSE_TICKET_MIRROR,
    RESPONSE_UPDATE_RECORD,
    RESPONSE_UPDATE_TICKET,
    RESPONSE_UPDATE_TICKET_ADDITIONAL,
    RESPONSE_UPDATE_TICKET_SC_REQ,
    RESPONSE_UPLOAD_FILE,
    USER_RESPONSE,
)
from test_data.result_constants import (
    EXPECTED_ADD_COMMENT_HR,
    EXPECTED_ADD_LINK_HR,
    EXPECTED_ADD_TAG,
    EXPECTED_CREATE_ITEM_ORDER,
    EXPECTED_CREATE_RECORD,
    EXPECTED_CREATE_TICKET,
    EXPECTED_CREATE_TICKET_WITH_OUT_JSON,
    EXPECTED_DOCUMENT_ROUTE,
    EXPECTED_GET_RECORD,
    EXPECTED_GET_TABLE_NAME,
    EXPECTED_GET_TICKET_NOTES,
    EXPECTED_GET_TICKET_NOTES_DISPLAY_VALUE,
    EXPECTED_ITEM_DETAILS,
    EXPECTED_LIST_TABLE_FIELDS,
    EXPECTED_MAPPING,
    EXPECTED_MULTIPLE_TICKET_CONTEXT,
    EXPECTED_MULTIPLE_TICKET_HR,
    EXPECTED_QUERY_COMPUTERS,
    EXPECTED_QUERY_ITEMS,
    EXPECTED_QUERY_TABLE,
    EXPECTED_QUERY_TABLE_SYS_PARAMS,
    EXPECTED_QUERY_TICKETS,
    EXPECTED_QUERY_TICKETS_EXCLUDE_REFERENCE_LINK,
    EXPECTED_TICKET_CONTEXT,
    EXPECTED_TICKET_CONTEXT_WITH_ADDITIONAL_FIELDS,
    EXPECTED_TICKET_CONTEXT_WITH_NESTED_ADDITIONAL_FIELDS,
    EXPECTED_TICKET_HR,
    EXPECTED_UPDATE_RECORD,
    EXPECTED_UPDATE_TICKET,
    EXPECTED_UPDATE_TICKET_ADDITIONAL,
    EXPECTED_UPDATE_TICKET_SC_REQ,
    EXPECTED_UPLOAD_FILE,
)


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def test_force_default_url_arg(mocker: MockerFixture, requests_mock: MockerCore):
    """Unit test
    Given
        - The argument force_default_url=true
    When
        - Calling the command servicenow-create-co-from-template
    Then
        - Validate that the api version configured as a parameter was not used in the API request
    """
    url = "https://test.service-now.com"
    api_endpoint = "/api/sn_chg_rest/change/standard/dummy_template"
    api_version = "2"
    mocker.patch.object(
        demisto,
        "params",
        return_value={
            "isFetch": True,
            "url": url,
            "credentials": {
                "identifier": "identifier",
                "password": "password",
            },
            "api_version": api_version,  # << We test overriding this value
            "incident_name": None,
            "file_tag_from_service_now": "FromServiceNow",
            "file_tag_to_service_now": "ToServiceNow",
            "comment_tag": "comments",
            "comment_tag_from_servicenow": "CommentFromServiceNow",
            "work_notes_tag": "work_notes",
            "work_notes_tag_from_servicenow": "WorkNoteFromServiceNow",
        },
    )
    mocker.patch.object(demisto, "args", return_value={"template": "dummy_template", "force_default_url": "true"})
    mocker.patch.object(demisto, "command", return_value="servicenow-create-co-from-template")
    requests_mock.post(f"{url}{api_endpoint}", json=util_load_json("test_data/create_co_from_template_result.json"))
    main()
    assert requests_mock.request_history[0].path == api_endpoint


def test_get_server_url():
    assert get_server_url("http://www.demisto.com//") == "http://www.demisto.com/"


def test_get_ticket_context():
    assert get_ticket_context(RESPONSE_TICKET) == EXPECTED_TICKET_CONTEXT

    assert EXPECTED_MULTIPLE_TICKET_CONTEXT[0] in get_ticket_context(RESPONSE_MULTIPLE_TICKET)
    assert EXPECTED_MULTIPLE_TICKET_CONTEXT[1] in get_ticket_context(RESPONSE_MULTIPLE_TICKET)


def test_get_ticket_context_additional_fields():
    """Unit test
    Given
        - additional keys of a ticket alongside regular keys.
    When
        - getting a ticket context
    Then
        - validate that all the details of the ticket were updated, and all the updated keys are shown in
        the context with do duplicates.
    """
    assert get_ticket_context(RESPONSE_TICKET, ["Summary", "sys_created_by"]) == EXPECTED_TICKET_CONTEXT_WITH_ADDITIONAL_FIELDS


def test_get_ticket_context_nested_additional_fields():
    """Unit test
    Given
        - nested additional keys of a ticket (in the form of a.b), alongside regular keys.
    When
        - getting a ticket context
    Then
        - validate that all the details of the ticket were updated, and all the updated keys are shown in
        the context with do duplicates.
    """
    assert (
        get_ticket_context(RESPONSE_TICKET, ["Summary", "opened_by.link"])
        == EXPECTED_TICKET_CONTEXT_WITH_NESTED_ADDITIONAL_FIELDS
    )


def test_get_ticket_human_readable():
    assert get_ticket_human_readable(RESPONSE_TICKET, "incident") == EXPECTED_TICKET_HR

    assert EXPECTED_MULTIPLE_TICKET_HR[0] in get_ticket_human_readable(RESPONSE_MULTIPLE_TICKET, "incident")
    assert EXPECTED_MULTIPLE_TICKET_HR[1] in get_ticket_human_readable(RESPONSE_MULTIPLE_TICKET, "incident")


def test_generate_body():
    fields = {"a_field": "test"}
    custom_fields = {"a_custom_field": "test"}
    expected_body = {"a_field": "test", "u_a_custom_field": "test"}
    assert expected_body == generate_body(fields, custom_fields)


def test_split_fields():
    expected_dict_fields = {"a": "b", "c": "d", "e": ""}
    assert expected_dict_fields == split_fields("a=b;c=d;e=")

    expected_custom_field = {"u_customfield": "<a href='https://google.com'>Link text</a>"}
    assert expected_custom_field == split_fields("u_customfield=<a href='https://google.com'>Link text</a>")

    expected_custom_sys_params = {
        "sysparm_display_value": "all",
        "sysparm_exclude_reference_link": "True",
        "sysparm_query": "number=TASK0000001",
    }

    assert expected_custom_sys_params == split_fields(
        "sysparm_display_value=all;sysparm_exclude_reference_link=True;sysparm_query=number=TASK0000001"
    )

    with pytest.raises(Exception) as err:
        split_fields("a")
    assert "must contain a '=' to specify the keys and values" in str(err)


def test_split_fields_with_special_delimiter():
    """Unit test
    Given
    - split_fields method
    - the default delimiter is ;
    When
    - splitting values with a different delimiter - ','
    Then
    -  Validate the fields were created correctly
    """
    expected_dict_fields = {"a": "b", "c": "d"}
    assert expected_dict_fields == split_fields("a=b,c=d", ",")

    expected_custom_field = {"u_customfield": "<a href='https://google.com'>Link text<;/a>"}
    assert expected_custom_field == split_fields("u_customfield=<a href='https://google.com'>Link text<;/a>", ",")

    with pytest.raises(Exception) as e:
        split_fields("a")
    assert "must contain a '=' to specify the keys and values" in str(e)


def test_convert_to_notes_result():
    """
    Given:
        - The full response for a ticket from SNOW.
    When:
        - Converting the comments and work notes to the format used in the integration.
    Then:
        - Verify that the expected notes are returned in the correct format.
    """
    # Note: the 'display_value' time is the local time of the SNOW instance, and the 'value' is in UTC.
    # The results returned for notes are expected to be in UTC time.

    expected_result = {
        "result": [
            {
                "sys_created_on": "2022-11-21 21:50:34",
                "value": "Second comment\n\n Mirrored from Cortex XSOAR",
                "sys_created_by": "System Administrator",
                "element": "comments",
            },
            {
                "sys_created_on": "2022-11-21 20:45:37",
                "value": "First comment",
                "sys_created_by": "Test User",
                "element": "comments",
            },
        ]
    }
    assert (
        convert_to_notes_result(
            RESPONSE_COMMENTS_DISPLAY_VALUE_AFTER_FORMAT,
            time_info={"display_date_format": DATE_FORMAT, "timezone_offset": timedelta(minutes=-60)},
        )
        == expected_result
    )

    # Filter comments by creation time (filter is given in UTC):
    expected_result = {
        "result": [
            {
                "sys_created_on": "2022-11-21 21:50:34",
                "value": "Second comment\n\n Mirrored from Cortex XSOAR",
                "sys_created_by": "System Administrator",
                "element": "comments",
            }
        ]
    }
    assert (
        convert_to_notes_result(
            RESPONSE_COMMENTS_DISPLAY_VALUE_AFTER_FORMAT,
            time_info={
                "display_date_format": DATE_FORMAT,
                "filter": datetime.strptime("2022-11-21 21:44:37", DATE_FORMAT),
                "timezone_offset": timedelta(minutes=-60),
            },
        )
        == expected_result
    )

    ticket_response = {}
    assert convert_to_notes_result(ticket_response, time_info={"display_date_format": DATE_FORMAT}) == {}

    assert convert_to_notes_result(
        RESPONSE_COMMENTS_DISPLAY_VALUE_NO_COMMENTS, time_info={"display_date_format": DATE_FORMAT}
    ) == {"result": []}


def test_split_notes():
    """
    Given:
        - Notes response from SNOW.
        - The type of the note (comment or work_note).
        - The UTC timezone offset and (optionally) a time filter.
    When:
        - Converting the given notes to the note format used in the integration with different time filters.
    Then:
        - Verify that the expected notes are returned in the correct format.
    """
    # Note: the timezone in the raw_notes should mimic the local time of the SNOW instance,
    # the time in the filter is in UTC (to mimic the behaviour of fetching).
    # timezone_offset is the difference between UTC and local time, e.g. offset = -60, means that local time is UTC+1.
    # The 'sys_created_on' time, returned by the command is normalized to UTC timezone.

    raw_notes = (
        "2022-11-21 22:50:34 - System Administrator (Additional comments)\nSecond comment\n\n Mirrored from "
        "Cortex XSOAR\n\n2022-11-21 21:45:37 - Test User (Additional comments)\nFirst comment\n\n"
    )

    time_info = {
        "timezone_offset": timedelta(minutes=0),
        "filter": datetime.strptime("2022-11-21 21:44:37", DATE_FORMAT),
        "display_date_format": DATE_FORMAT,
    }
    notes = split_notes(raw_notes, "comments", time_info)
    expected_notes = [
        {
            "sys_created_on": "2022-11-21 22:50:34",
            "value": "Second comment\n\n Mirrored from Cortex XSOAR",
            "sys_created_by": "System Administrator",
            "element": "comments",
        },
        {"sys_created_on": "2022-11-21 21:45:37", "value": "First comment", "sys_created_by": "Test User", "element": "comments"},
    ]
    assert notes == expected_notes

    raw_notes = (
        "21/11/2022 22:50:34 - System Administrator (Additional comments)\nSecond comment\n\n Mirrored from "
        "Cortex XSOAR\n\n21/11/2022 21:45:37 - Test User (Additional comments)\nFirst comment\n\n"
    )
    time_info = {
        "timezone_offset": timedelta(minutes=-60),
        "filter": datetime.strptime("2022-11-21 21:44:37", DATE_FORMAT),
        "display_date_format": DATE_FORMAT_OPTIONS.get("dd/MM/yyyy"),
    }
    notes = split_notes(raw_notes, "comments", time_info)
    expected_notes = [
        {
            "sys_created_on": "2022-11-21 21:50:34",
            "value": "Second comment\n\n Mirrored from Cortex XSOAR",
            "sys_created_by": "System Administrator",
            "element": "comments",
        }
    ]
    assert notes == expected_notes

    raw_notes = (
        "21.11.2022 22:50:34 - System Administrator (Additional comments)\nSecond comment\n\n Mirrored from "
        "Cortex XSOAR\n\n21.11.2022 21:45:37 - Test User (Additional comments)\nFirst comment\n\n"
    )
    time_info = {
        "timezone_offset": timedelta(minutes=-60),
        "filter": datetime.strptime("2022-11-21 21:44:37", DATE_FORMAT),
        "display_date_format": DATE_FORMAT_OPTIONS.get("dd.MM.yyyy"),
    }
    notes = split_notes(raw_notes, "comments", time_info)
    expected_notes = [
        {
            "sys_created_on": "2022-11-21 21:50:34",
            "value": "Second comment\n\n Mirrored from Cortex XSOAR",
            "sys_created_by": "System Administrator",
            "element": "comments",
        }
    ]
    assert notes == expected_notes

    raw_notes = (
        "11-21-2022 22:50:34 - System Administrator (Additional comments)\nSecond comment\n\n Mirrored from "
        "Cortex XSOAR\n\n11-21-2022 21:45:37 - Test User (Additional comments)\nFirst comment\n\n"
    )
    time_info = {
        "timezone_offset": timedelta(minutes=-120),
        "filter": datetime.strptime("2022-11-21 21:44:37", DATE_FORMAT),
        "display_date_format": DATE_FORMAT_OPTIONS.get("MM-dd-yyyy"),
    }
    notes = split_notes(raw_notes, "comments", time_info)
    assert len(notes) == 0


def test_get_timezone_offset():
    """
    Given:
        - A response from a SNOW ticket created with 'sysparm_display_value=all'.
    When:
        - Testing different instance and UTC times.
    Then:
        - Assert the offset between the UTC and the instance times are correct.
    """
    full_response = {"sys_created_on": {"display_value": "2022-12-07 05:38:52", "value": "2022-12-07 13:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT)
    assert offset == timedelta(minutes=480)

    full_response = {"sys_created_on": {"display_value": "12-07-2022 15:47:34", "value": "2022-12-07 13:47:34"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("MM-dd-yyyy"))
    assert offset == timedelta(minutes=-120)

    full_response = {"sys_created_on": {"display_value": "06/12/2022 23:38:52", "value": "2022-12-07 09:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd/MM/yyyy"))
    assert offset == timedelta(minutes=600)

    full_response = {"sys_created_on": {"display_value": "06/12/2022 23:38:52 PM", "value": "2022-12-07 09:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd/MM/yyyy"))
    assert offset == timedelta(minutes=600)

    full_response = {"sys_created_on": {"display_value": "07.12.2022 0:38:52", "value": "2022-12-06 19:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd.MM.yyyy"))
    assert offset == timedelta(minutes=-300)

    full_response = {"sys_created_on": {"display_value": "Dec-07-2022 00:38:52", "value": "2022-12-06 19:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("mmm-dd-yyyy"))
    assert offset == timedelta(minutes=-300)

    full_response = {"sys_created_on": {"display_value": "Dec-07-2022 00:38:52 AM", "value": "2022-12-06 19:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("mmm-dd-yyyy"))
    assert offset == timedelta(minutes=-300)

    full_response = {"sys_created_on": {"display_value": "Dec-07-2022 00:38:52 AM    ", "value": "2022-12-06 19:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("mmm-dd-yyyy"))
    assert offset == timedelta(minutes=-300)

    full_response = {"sys_created_on": {"display_value": "07-Dec-2022 00:38:52", "value": "2022-12-06 19:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd-MMM-yyyy"))
    assert offset == timedelta(minutes=-300)

    full_response = {"sys_created_on": {"display_value": "07-Dec-2022 00:38:52 AM", "value": "2022-12-06 19:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd-MMM-yyyy"))
    assert offset == timedelta(minutes=-300)

    full_response = {"sys_created_on": {"display_value": "12-Mar-2026 11:40:52", "value": "2026-03-12 06:10:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd-MMM-yyyy"))
    assert offset == timedelta(minutes=-330)

    full_response = {"sys_created_on": {"display_value": "08/19/26 15:38:52", "value": "2026-08-19 13:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("MM/dd/yy"))
    assert offset == timedelta(minutes=-120)

    full_response = {"sys_created_on": {"display_value": "19/08/26 15:38:52", "value": "2026-08-19 13:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("dd/MM/yy"))
    assert offset == timedelta(minutes=-120)

    full_response = {"sys_created_on": {"display_value": "2022/12/07 05:38:52", "value": "2022-12-07 13:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("yyyy/MM/dd"))
    assert offset == timedelta(minutes=480)

    full_response = {"sys_created_on": {"display_value": "2022.12.07 05:38:52", "value": "2022-12-07 13:38:52"}}
    offset = get_timezone_offset(full_response, display_date_format=DATE_FORMAT_OPTIONS.get("yyyy.MM.dd"))
    assert offset == timedelta(minutes=480)


def test_get_ticket_notes_command_success(mocker):
    """
    Given
    - A mock client and args input to the get_ticket_notes_command function
    - A mock successful API response

    When
    - The get_ticket_notes_command function is called

    Then
    - Ensure the expected API call is made
    - Validate the expected CommandResults are returned
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    args = {"id": "sys_id"}

    mock_send_request = mocker.patch.object(Client, "send_request")
    mock_send_request.return_value = RESPONSE_GET_TICKET_NOTES
    result = get_ticket_notes_command(client, args, {})

    assert isinstance(result[0], CommandResults)
    assert mock_send_request.called
    assert len(result[0].raw_response.get("result")) == 5
    assert result[0].outputs_prefix == "ServiceNow.Ticket"
    assert result[0].outputs == EXPECTED_GET_TICKET_NOTES


def test_get_ticket_notes_command_use_display_value(mocker):
    """
    Given
    - A mock client and args input to the get_ticket_notes_command function
    - A mock successful API response

    When
    - The get_ticket_notes_command function is called with use_display_value

    Then
    - Ensure the expected API call is made
    - Validate the expected CommandResults are returned
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
        use_display_value=True,
        display_date_format="yyyy-MM-dd",
    )
    args = {"id": "sys_id"}

    mock_send_request = mocker.patch.object(Client, "send_request")
    mock_send_request.return_value = RESPONSE_COMMENTS_DISPLAY_VALUE
    result = get_ticket_notes_command(client, args, {})

    assert isinstance(result[0], CommandResults)
    assert mock_send_request.called
    assert len(result[0].raw_response.get("result")) == 2
    assert result[0].outputs_prefix == "ServiceNow.Ticket"
    assert result[0].outputs == EXPECTED_GET_TICKET_NOTES_DISPLAY_VALUE


def test_get_ticket_notes_command_use_display_value_no_comments(mocker):
    """
    Given
    - A mock client and args input to the get_ticket_notes_command function
    - A mock successful API response

    When
    - The get_ticket_notes_command function is called with use_display_value but no comments

    Then
    - Ensure the expected API call is made
    - Validate the expected CommandResults are returned
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
        use_display_value=True,
        display_date_format="yyyy-MM-dd",
    )
    args = {"id": "sys_id"}

    mock_send_request = mocker.patch.object(Client, "send_request")
    mock_send_request.return_value = RESPONSE_COMMENTS_DISPLAY_VALUE_NO_COMMENTS
    result = get_ticket_notes_command(client, args, {})

    assert isinstance(result[0], CommandResults)
    assert mock_send_request.called
    assert result[0].raw_response == "No comment found on ticket sys_id."


@pytest.mark.parametrize(
    "notes, params, expected",
    [
        (
            [
                {
                    "value": "First comment",
                    "sys_created_by": "Test User",
                    "sys_created_on": "2022-11-21 20:45:37",
                    "element": "comments",
                }
            ],
            {"comment_tag_from_servicenow": "CommentFromServiceNow"},
            [
                {
                    "Type": 1,
                    "Category": None,
                    "HumanReadable": "Type: comments\nCreated By: Test User\nCreated On: 2022-11-21 20:45:37\nFirst comment",
                    "Contents": "Type: comments\nCreated By: Test User\nCreated On: 2022-11-21 20:45:37\nFirst comment",
                    "created": "2022-11-21 20:45:37",
                    "ContentsFormat": None,
                    "Tags": ["CommentFromServiceNow"],
                    "Note": True,
                    "EntryContext": {"comments_and_work_notes": "First comment"},
                }
            ],
        )
    ],
)
def test_get_entries_for_notes_with_comment(notes, params, expected):
    """
    Given
        - A list of notes
        - Params containing comment tag
    When
        - Calling get_entries_for_notes
    Then
        - Should return a list of entry contexts
    """
    assert get_entries_for_notes(notes, params) == expected


@pytest.mark.parametrize(
    "notes, params, expected",
    [
        (
            [
                {
                    "value": "[code]<p>test</p>[/code]",
                    "sys_created_by": "Test User",
                    "sys_created_on": "2022-11-21 20:45:37",
                    "element": "comments",
                }
            ],
            {"comment_tag_from_servicenow": "CommentFromServiceNow", "comment_format": "html"},
            [
                {
                    "Type": 1,
                    "Category": None,
                    "Contents": "Type: comments<br>Created By: Test User<br>Created On: 2022-11-21 20:45:37<br><p>test</p>",
                    "created": "2022-11-21 20:45:37",
                    "ContentsFormat": "html",
                    "Tags": ["CommentFromServiceNow"],
                    "Note": True,
                    "EntryContext": {"comments_and_work_notes": "[code]<p>test</p>[/code]"},
                }
            ],
        )
    ],
)
def test_get_entries_for_notes_with_comment_and_format(notes, params, expected):
    """
    Given
        - A list of notes
        - Params containing comment tag
    When
        - Calling get_entries_for_notes
    Then
        - Should return a list of entry contexts
    """
    assert get_entries_for_notes(notes, params) == expected


@pytest.mark.parametrize(
    "notes, params, expected_format, expected_value",
    [
        # comment_format="source" (explicit) -> use the note's own format, value unchanged.
        (
            [
                {
                    "value": "[code]<p>test</p>[/code]",
                    "sys_created_by": "Test User",
                    "sys_created_on": "2022-11-21 20:45:37",
                    "element": "comments",
                    "format": "html",
                }
            ],
            {"comment_tag_from_servicenow": "CommentFromServiceNow", "comment_format": "source"},
            "html",
            "[code]<p>test</p>[/code]",
        ),
        # comment_format="text" -> force the format, no [code] stripping.
        (
            [
                {
                    "value": "[code]<p>test</p>[/code]",
                    "sys_created_by": "Test User",
                    "sys_created_on": "2022-11-21 20:45:37",
                    "element": "comments",
                }
            ],
            {"comment_tag_from_servicenow": "CommentFromServiceNow", "comment_format": "text"},
            "text",
            "[code]<p>test</p>[/code]",
        ),
        # comment_format="html" but no [code] wrapper -> value left intact.
        (
            [
                {
                    "value": "<p>plain</p>",
                    "sys_created_by": "Test User",
                    "sys_created_on": "2022-11-21 20:45:37",
                    "element": "comments",
                }
            ],
            {"comment_tag_from_servicenow": "CommentFromServiceNow", "comment_format": "html"},
            "html",
            "<p>plain</p>",
        ),
        # No comment_format -> defaults to "source", uses the note's own format.
        (
            [
                {
                    "value": "Plain comment",
                    "sys_created_by": "Test User",
                    "sys_created_on": "2022-11-21 20:45:37",
                    "element": "comments",
                    "format": "text",
                }
            ],
            {"comment_tag_from_servicenow": "CommentFromServiceNow"},
            "text",
            "Plain comment",
        ),
    ],
)
def test_get_entries_for_notes_comment_format_variations(notes, params, expected_format, expected_value):
    """
    Given
        - A list of notes and a comment_format param (source / text / html-without-code / absent)
    When
        - Calling get_entries_for_notes
    Then
        - The entry ContentsFormat and value reflect the comment_format handling:
          'source' (or absent) uses the note's own format, non-html formats are passed through without
          stripping, and 'html' only strips a [code]...[/code] wrapper when present.
    """
    result = get_entries_for_notes(notes, params)
    assert len(result) == 1
    assert result[0]["ContentsFormat"] == expected_format
    assert result[0]["EntryContext"]["comments_and_work_notes"] == notes[0]["value"]
    # "html" entries only carry "Contents" (no "HumanReadable"); other formats carry both.
    assert result[0]["Contents"].endswith(expected_value)


@pytest.mark.parametrize(
    "command, args, response, expected_result, expected_auto_extract",
    [
        (update_ticket_command, {"id": "1234", "impact": "2"}, RESPONSE_UPDATE_TICKET, EXPECTED_UPDATE_TICKET, True),
        (
            update_ticket_command,
            {"id": "1234", "ticket_type": "sc_req_item", "approval": "requested"},
            RESPONSE_UPDATE_TICKET_SC_REQ,
            EXPECTED_UPDATE_TICKET_SC_REQ,
            True,
        ),
        (
            update_ticket_command,
            {"id": "1234", "severity": "3", "additional_fields": "approval=rejected"},
            RESPONSE_UPDATE_TICKET_ADDITIONAL,
            EXPECTED_UPDATE_TICKET_ADDITIONAL,
            True,
        ),
        (
            create_ticket_command,
            {"active": "true", "severity": "3", "description": "creating a test ticket", "sla_due": "2020-10-10 10:10:11"},
            RESPONSE_CREATE_TICKET,
            EXPECTED_CREATE_TICKET,
            True,
        ),
        (
            create_ticket_command,
            {"active": "true", "severity": "3", "description": "creating a test ticket", "sla_due": "2020-10-10 10:10:11"},
            RESPONSE_CREATE_TICKET_WITH_OUT_JSON,
            EXPECTED_CREATE_TICKET_WITH_OUT_JSON,
            True,
        ),
        (
            query_tickets_command,
            {"limit": "3", "query": "impact<2^short_descriptionISNOTEMPTY", "ticket_type": "incident"},
            RESPONSE_QUERY_TICKETS,
            EXPECTED_QUERY_TICKETS,
            True,
        ),
        (
            query_tickets_command,
            {"ticket_type": "incident", "query": "number=INC0000001", "system_params": "sysparm_exclude_reference_link=true"},
            RESPONSE_QUERY_TICKETS_EXCLUDE_REFERENCE_LINK,
            EXPECTED_QUERY_TICKETS_EXCLUDE_REFERENCE_LINK,
            True,
        ),
        (
            upload_file_command,
            {"id": "sys_id", "file_id": "entry_id", "file_name": "test_file"},
            RESPONSE_UPLOAD_FILE,
            EXPECTED_UPLOAD_FILE,
            True,
        ),
        (
            get_record_command,
            {"table_name": "alm_asset", "id": "sys_id", "fields": "asset_tag,display_name"},
            RESPONSE_GET_RECORD,
            EXPECTED_GET_RECORD,
            True,
        ),
        (
            update_record_command,
            {"name": "alm_asset", "id": "1234", "custom_fields": "display_name=test4"},
            RESPONSE_UPDATE_RECORD,
            EXPECTED_UPDATE_RECORD,
            True,
        ),
        (
            create_record_command,
            {"table_name": "alm_asset", "fields": "asset_tag=P4325434;display_name=my_test_record"},
            RESPONSE_CREATE_RECORD,
            EXPECTED_CREATE_RECORD,
            True,
        ),
        (
            query_table_command,
            {
                "table_name": "alm_asset",
                "fields": "asset_tag,sys_updated_by,display_name",
                "query": "display_nameCONTAINSMacBook",
                "limit": 3,
            },
            RESPONSE_QUERY_TABLE,
            EXPECTED_QUERY_TABLE,
            False,
        ),
        (
            query_table_command,
            {
                "table_name": "sc_task",
                "system_params": "sysparm_display_value=all;sysparm_exclude_reference_link=True;sysparm_query=number=TASK0000001",
                "fields": "approval,state,escalation,number,description",
            },
            RESPONSE_QUERY_TABLE_SYS_PARAMS,
            EXPECTED_QUERY_TABLE_SYS_PARAMS,
            False,
        ),
        (list_table_fields_command, {"table_name": "alm_asset"}, RESPONSE_LIST_TABLE_FIELDS, EXPECTED_LIST_TABLE_FIELDS, False),
        (query_computers_command, {"computer_id": "1234"}, RESPONSE_QUERY_COMPUTERS, EXPECTED_QUERY_COMPUTERS, False),
        (get_table_name_command, {"label": "ACE"}, RESPONSE_GET_TABLE_NAME, EXPECTED_GET_TABLE_NAME, False),
        (add_tag_command, {"id": "123", "tag_id": "1234", "title": "title"}, RESPONSE_ADD_TAG, EXPECTED_ADD_TAG, True),
        (query_items_command, {"name": "ipad", "limit": "2"}, RESPONSE_QUERY_ITEMS, EXPECTED_QUERY_ITEMS, True),
        (get_item_details_command, {"id": "1234"}, RESPONSE_ITEM_DETAILS, EXPECTED_ITEM_DETAILS, True),
        (
            create_order_item_command,
            {"id": "1234", "quantity": "3", "variables": "Additional_software_requirements=best_pc"},
            RESPONSE_CREATE_ITEM_ORDER,
            EXPECTED_CREATE_ITEM_ORDER,
            True,
        ),
        (
            document_route_to_table,
            {"queue_id": "queue_id", "document_id": "document_id"},
            RESPONSE_DOCUMENT_ROUTE,
            EXPECTED_DOCUMENT_ROUTE,
            True,
        ),
    ],
)  # noqa: E124
def test_commands(command, args, response, expected_result, expected_auto_extract, mocker):
    """Unit test
    Given
    - command main func
    - command args
    - command raw response
    When
    - mock the ServiceNow response
    Then
    - convert the result to human readable table
    - create the context
    validate the entry context
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
        display_date_format="yyyy-MM-dd",
    )
    mocker.patch.object(client, "send_request", return_value=response)
    if command == create_ticket_command:
        result = command(client, args, is_quick_action=True)
    else:
        result = command(client, args)
    assert expected_result == result[1]  # entry context is found in the 2nd place in the result of the command
    assert expected_auto_extract == result[3]  # ignore_auto_extract is in the 4th place in the result of the command


@pytest.mark.parametrize(
    "command, args, response, expected_hr, expected_auto_extract",
    [
        (
            add_link_command,
            {"id": "1234", "link": "http://www.demisto.com", "text": "demsito_link"},
            RESPONSE_ADD_LINK,
            EXPECTED_ADD_LINK_HR,
            True,
        ),
        (add_comment_command, {"id": "1234", "comment": "Nice work!"}, RESPONSE_ADD_COMMENT, EXPECTED_ADD_COMMENT_HR, True),
        (
            delete_record_command,
            {"table_name": "alm_asset", "id": "1234"},
            {},
            "ServiceNow record with ID 1234 was successfully deleted.",
            True,
        ),
    ],
)  # noqa: E124
def test_no_ec_commands(command, args, response, expected_hr, expected_auto_extract, mocker):
    """Unit test
    Given
    - command main func
    - command args
    - command raw response
    When
    - mock the ServiceNow response
    Then
    - convert the result to human readable table
    - create the context
    validate the human readable
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    mocker.patch.object(client, "send_request", return_value=response)
    result = command(client, args)
    assert expected_hr in result[0]  # HR is found in the 1st place in the result of the command
    assert expected_auto_extract == result[3]  # ignore_auto_extract is in the 4th place in the result of the command


def test_delete_attachment_command(mocker):
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )

    mocker.patch.object(client, "delete_attachment", return_value=None)
    result = delete_attachment_command(client=client, args={"file_sys_id": "1234"})
    assert "Attachment with Sys ID 1234 was successfully deleted." in result[0]


def test_delete_attachment_command_failed(mocker):
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )

    mocker.patch.object(client, "delete_attachment", return_value="Error")
    with pytest.raises(DemistoException) as e:
        delete_attachment_command(client=client, args={"file_sys_id": "1234"})
    assert "Error: No record found. Record doesn't exist or ACL restricts the record retrieval." in str(e)


@freeze_time("2022-05-01 12:52:29")
def test_fetch_incidents(mocker):
    """Unit test
    Given
    - fetch incidents command
    - command args
    - command raw response
    When
    - mock the parse_date_range.
    - mock the Client's send_request.
    Then
    - run the fetch incidents command using the Client.
    - Validate The length of the results.
    - Ensure the incident sys IDs are stored in integration context for the first mirroring.
    """
    RESPONSE_FETCH["result"][0]["opened_at"] = (datetime.utcnow() - timedelta(minutes=15)).strftime("%Y-%m-%d %H:%M:%S")
    RESPONSE_FETCH["result"][1]["opened_at"] = (datetime.utcnow() - timedelta(minutes=8)).strftime("%Y-%m-%d %H:%M:%S")
    mocker.patch("CommonServerPython.get_fetch_run_time_range", return_value=("2022-05-01 01:05:07", "2022-05-01 12:08:29"))
    mocker.patch("ServiceNowv2.parse_dict_ticket_fields", return_value=RESPONSE_FETCH["result"])
    mocker.patch.object(demisto, "params", return_value={"mirror_notes_for_new_incidents": True})
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "2 days",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="number",
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH)
    incidents = fetch_incidents(client)
    assert len(incidents) == 2
    assert incidents[0].get("name") == "ServiceNow Incident INC0000040"
    assert demisto.getIntegrationContext()["last_fetched_incident_ids"] == ["sys_id1", "sys_id2"]


@freeze_time("2022-05-01 12:52:29")
def test_fetch_incidents_with_changed_fetch_limit(mocker):
    """Unit test
    Given
    - fetch incidents command
    - command args
    - command raw response
    When
    - mock the parse_date_range.
    - mock the Client's send_request.
    Then
    - run the fetch incidents command using the Client
    Validate The number of fetch_limit in the last_run
    """
    RESPONSE_FETCH["result"][0]["opened_at"] = (datetime.utcnow() - timedelta(minutes=15)).strftime("%Y-%m-%d %H:%M:%S")
    RESPONSE_FETCH["result"][1]["opened_at"] = (datetime.utcnow() - timedelta(minutes=8)).strftime("%Y-%m-%d %H:%M:%S")
    mocker.patch("CommonServerPython.get_fetch_run_time_range", return_value=("2022-05-01 01:05:07", "2022-05-01 12:08:29"))
    mocker.patch("ServiceNowv2.parse_dict_ticket_fields", return_value=RESPONSE_FETCH["result"])
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "2 days",
        "sysparm_query",
        sysparm_limit=20,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="number",
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH)
    mocker.patch.object(demisto, "getLastRun", return_value={"limit": 10})
    set_last_run = mocker.patch.object(demisto, "setLastRun")
    fetch_incidents(client)

    assert set_last_run.call_args[0][0].get("limit") == 20


@freeze_time("2022-05-01 12:52:29")
def test_fetch_incidents_with_attachments(mocker):
    """Unit test
    Given
    - fetch incidents command
    - command args
    - command raw response
    When
    - mock the parse_date_range.
    - mock the Client's send_request.
    - mock the Client's get_ticket_attachment_entries.
    Then
    - run the fetch incidents command using the Client
    Validate The length of the results and the attachment content.
    """
    RESPONSE_FETCH_ATTACHMENTS_TICKET["result"][0]["opened_at"] = (datetime.utcnow() - timedelta(minutes=15)).strftime(
        "%Y-%m-%d %H:%M:%S"
    )
    mocker.patch("CommonServerPython.get_fetch_run_time_range", return_value=("2022-05-01 01:05:07", "2022-05-01 12:08:29"))
    mocker.patch("ServiceNowv2.parse_dict_ticket_fields", return_value=RESPONSE_FETCH["result"])
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "2 days",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=True,
        incident_name="number",
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH_ATTACHMENTS_TICKET)
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=RESPONSE_FETCH_ATTACHMENTS_FILE)

    incidents = fetch_incidents(client)

    assert len(incidents) == 1
    assert incidents[0].get("attachment")[0]["name"] == "wireframe"
    assert incidents[0].get("attachment")[0]["path"] == "file_id"


@freeze_time("2022-05-01 12:52:29")
def test_fetch_incidents_with_incident_name(mocker):
    """Unit test
    Given
    - fetch incidents command
    - command args
    - command raw response
    When
    - mock the parse_date_range.
    - mock the Client's send_request.
    Then
    - run the fetch incidents command using the Client
    Validate The length of the results.
    """
    RESPONSE_FETCH["result"][0]["opened_at"] = (datetime.utcnow() - timedelta(minutes=15)).strftime("%Y-%m-%d %H:%M:%S")
    RESPONSE_FETCH["result"][1]["opened_at"] = (datetime.utcnow() - timedelta(minutes=8)).strftime("%Y-%m-%d %H:%M:%S")
    mocker.patch("ServiceNowv2.parse_dict_ticket_fields", return_value=RESPONSE_FETCH["result"])
    mocker.patch("CommonServerPython.get_fetch_run_time_range", return_value=("2022-05-01 01:05:07", "2022-05-01 12:08:29"))
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "2 days",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH)
    incidents = fetch_incidents(client)
    assert incidents[0].get("name") == "ServiceNow Incident Unable to access Oregon mail server. Is it down?"


def start_freeze_time(timestamp):
    _start_freeze_time = freeze_time(timestamp)
    _start_freeze_time.start()
    return datetime.now()


class TestFetchIncidentsWithLookBack:
    LAST_RUN = {}

    API_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
    FREEZE_TIMESTAMP = "2022-05-01 12:52:29"

    def set_last_run(self, new_last_run):
        self.LAST_RUN = new_last_run

    @pytest.mark.parametrize(
        "start_incidents, phase2_incident, phase3_incident, look_back",
        [
            (
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=10)).strftime(API_TIME_FORMAT),
                            "severity": "2",
                            "number": "2",
                            "sys_id": "2",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=5)).strftime(API_TIME_FORMAT),
                            "severity": "1",
                            "number": "4",
                            "sys_id": "4",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=2)).strftime(API_TIME_FORMAT),
                            "severity": "2",
                            "number": "5",
                            "sys_id": "5",
                        },
                    ]
                },
                {
                    "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=8)).strftime(API_TIME_FORMAT),
                    "severity": "1",
                    "number": "3",
                    "sys_id": "3",
                },
                {
                    "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=11)).strftime(API_TIME_FORMAT),
                    "severity": "1",
                    "number": "1",
                    "sys_id": "1",
                },
                15,
            ),
            (
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=3, minutes=20)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "2",
                            "number": "2",
                            "sys_id": "2",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=2, minutes=26)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "1",
                            "number": "4",
                            "sys_id": "4",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=1, minutes=20)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "2",
                            "number": "5",
                            "sys_id": "5",
                        },
                    ]
                },
                {
                    "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=2, minutes=45)).strftime(API_TIME_FORMAT),
                    "severity": "1",
                    "number": "3",
                    "sys_id": "3",
                },
                {
                    "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=3, minutes=50)).strftime(API_TIME_FORMAT),
                    "severity": "1",
                    "number": "1",
                    "sys_id": "1",
                },
                1000,
            ),
        ],
    )
    def test_fetch_incidents_with_look_back_greater_than_zero(
        self, mocker, start_incidents, phase2_incident, phase3_incident, look_back
    ):
        """
        Given
        - fetch incidents parameters including look back according to their opened time.
        - first scenario - fetching with minutes when look_back=60 minutes
        - second scenario - fetching with hours when look_back=1000 minutes

        When
        - trying to fetch incidents for 3 rounds.

        Then
        - first fetch - should fetch incidents 2, 4, 5 (because only them match the query)
        - second fetch - should fetch incident 3 (because now incident 2, 4, 5, 3 matches the query too)
        - third fetch - should fetch incident 1 (because now incident 2, 4, 5, 3, 1 matches the query too)
        - fourth fetch - should fetch nothing as there are not new incidents who match the query
        - make sure that incidents who were already fetched would not be fetched again.
        """
        client = Client(
            server_url="",
            sc_server_url="",
            cr_server_url="",
            username="",
            password="",
            verify=False,
            fetch_time="6 hours",
            sysparm_query="stateNOT IN6,7^assignment_group=123",
            sysparm_limit=10,
            timestamp_field="opened_at",
            ticket_type="incident",
            get_attachments=False,
            incident_name="number",
            look_back=look_back,
        )

        # reset last run
        self.LAST_RUN = {}

        mocker.patch.object(demisto, "getLastRun", return_value=self.LAST_RUN)
        mocker.patch.object(demisto, "setLastRun", side_effect=self.set_last_run)

        mocker.patch.object(client, "send_request", return_value=start_incidents)

        # first fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 3
        for expected_incident_id, ticket in zip(["2", "4", "5"], tickets):
            assert ticket.get("name") == f"ServiceNow Incident {expected_incident_id}"

        # second fetch preparation
        start_incidents.get("result").append(phase2_incident)

        # second fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 1
        assert tickets[0].get("name") == "ServiceNow Incident 3"

        # third fetch preparation
        start_incidents.get("result").append(phase3_incident)

        # third fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 1
        assert tickets[0].get("name") == "ServiceNow Incident 1"

        # forth fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 0

    @pytest.mark.parametrize(
        "incidents, phase2_incident, phase3_incident",
        [
            (
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=10)).strftime(API_TIME_FORMAT),
                            "severity": "2",
                            "number": "1",
                            "sys_id": "1",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=8)).strftime(API_TIME_FORMAT),
                            "severity": "1",
                            "number": "2",
                            "sys_id": "2",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=7)).strftime(API_TIME_FORMAT),
                            "severity": "2",
                            "number": "3",
                            "sys_id": "3",
                        },
                    ]
                },
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=5)).strftime(API_TIME_FORMAT),
                            "severity": "1",
                            "number": "4",
                            "sys_id": "4",
                        }
                    ]
                },
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(minutes=4)).strftime(API_TIME_FORMAT),
                            "severity": "1",
                            "number": "5",
                            "sys_id": "5",
                        }
                    ]
                },
            ),
            (
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=8, minutes=51)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "2",
                            "number": "1",
                            "sys_id": "1",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=7, minutes=45)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "1",
                            "number": "2",
                            "sys_id": "2",
                        },
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=7, minutes=44)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "2",
                            "number": "3",
                            "sys_id": "3",
                        },
                    ]
                },
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=7, minutes=44)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "1",
                            "number": "4",
                            "sys_id": "4",
                        }
                    ]
                },
                {
                    "result": [
                        {
                            "opened_at": (start_freeze_time(FREEZE_TIMESTAMP) - timedelta(hours=1, minutes=34)).strftime(
                                API_TIME_FORMAT
                            ),
                            "severity": "1",
                            "number": "5",
                            "sys_id": "5",
                        }
                    ]
                },
            ),
        ],
    )
    def test_fetch_incidents_with_look_back_equals_zero(self, mocker, incidents, phase2_incident, phase3_incident):
        """
        Given
        - fetch incidents parameters with any look back according to their opened time (normal fetch incidents).
        - first scenario - fetching with minutes when look_back=0
        - second scenario - fetching with hours when look_back=0

        When
        - trying to fetch incidents for 3 rounds.

        Then
        - first fetch - should fetch incidents 1, 2, 3 (because only them match the query)
        - second fetch - should fetch incident 4
        - third fetch - should fetch incident 5
        - fourth fetch - should fetch nothing as there are not new incidents who match the query
        """
        client = Client(
            server_url="",
            sc_server_url="",
            cr_server_url="",
            username="",
            password="",
            verify=False,
            fetch_time="12 hours",
            sysparm_query="stateNOT IN6,7^assignment_group=123",
            sysparm_limit=10,
            timestamp_field="opened_at",
            ticket_type="incident",
            get_attachments=False,
            incident_name="number",
            look_back=0,
        )

        # reset last fetch and tickets
        self.LAST_RUN = {}

        mocker.patch.object(demisto, "getLastRun", return_value=self.LAST_RUN)
        mocker.patch.object(demisto, "setLastRun", side_effect=self.set_last_run)
        mocker.patch.object(client, "send_request", return_value=incidents)

        # first fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 3
        for expected_incident_id, ticket in zip(["1", "2", "3"], tickets):
            assert ticket.get("name") == f"ServiceNow Incident {expected_incident_id}"

        # second fetch preparation
        incidents = phase2_incident
        mocker.patch.object(client, "send_request", return_value=incidents)

        # second fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 1
        assert tickets[0].get("name") == "ServiceNow Incident 4"

        # third fetch preparation
        incidents = phase3_incident
        mocker.patch.object(client, "send_request", return_value=incidents)

        # third fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 1
        assert tickets[0].get("name") == "ServiceNow Incident 5"

        # forth fetch preparation
        incidents = {"result": []}
        mocker.patch.object(client, "send_request", return_value=incidents)

        # forth fetch
        tickets = fetch_incidents(client=client)
        assert len(tickets) == 0


def test_incident_name_is_initialized(mocker, requests_mock):
    """
    Given:
     - Integration instance initialized with fetch enabled and without changing incident name

    When:
     - Clicking on Test button (running test-module)

    Then:
     - Verify expected exception is raised as default incident name value is not in response
    """
    url = "https://test.service-now.com"
    mocker.patch.object(
        demisto,
        "params",
        return_value={
            "isFetch": True,
            "url": url,
            "credentials": {
                "identifier": "identifier",
                "password": "password",
            },
            "incident_name": None,
            "file_tag_from_service_now": "FromServiceNow",
            "file_tag_to_service_now": "ToServiceNow",
            "comment_tag": "comments",
            "comment_tag_from_servicenow": "CommentFromServiceNow",
            "work_notes_tag": "work_notes",
            "work_notes_tag_from_servicenow": "WorkNoteFromServiceNow",
        },
    )
    mocker.patch.object(demisto, "command", return_value="test-module")

    def return_error_mock(message, error):
        raise

    mocker.patch("ServiceNowv2.return_error", side_effect=return_error_mock)
    requests_mock.get(f"{url}/api/now/table/incident?sysparm_limit=1", json={"result": [{"opened_at": "sometime"}]})
    with pytest.raises(ValueError) as e:
        main()
    assert str(e.value) == "The field [number] does not exist in the ticket."


def test_file_tags_names_are_the_same_main_flow(mocker):
    """
    Given:
     - file tags from service now & file tag to service now that are identical

    When:
     - running main flow

    Then:
     - make sure an exception is raised
    """
    import ServiceNowv2

    mocker.patch.object(demisto, "params", return_value={"file_tag_from_service_now": "ServiceNow", "file_tag": "ServiceNow"})
    mocker.patch.object(ServiceNowv2, "get_server_url", return_value="test")
    with pytest.raises(
        Exception,
        match=re.escape("File Entry Tag To ServiceNow and File Entry Tag From ServiceNow cannot be the same name [ServiceNow]."),
    ):
        main()


def test_not_authenticated_retry_positive(requests_mock, mocker):
    """
    Given
    - ServiceNow client

    When
    - Sending HTTP request and getting 401 status code (not authenticated) twice, followed by 200 status code (success)

    Then
    - Verify debug messages
    - Ensure the send_request function runs successfully without exceptions
    """
    mocker.patch.object(demisto, "debug")
    client = Client(
        "http://server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    requests_mock.get(
        "http://server_url",
        [
            {
                "status_code": 401,
                "json": {
                    "error": {"message": "User Not Authenticated", "detail": "Required to provide Auth information"},
                    "status": "failure",
                },
            },
            {
                "status_code": 401,
                "json": {
                    "error": {"message": "User Not Authenticated", "detail": "Required to provide Auth information"},
                    "status": "failure",
                },
            },
            {"status_code": 200, "json": {}},
        ],
    )
    assert client.send_request("") == {}
    debug = demisto.debug.call_args_list

    assert debug[0][0][0] == "Sending request to ServiceNow. Method: GET, Path: "
    assert debug[1][0][0] == (
        "Constructed URL: http://server_url\nRequest headers: "
        "{'Accept': 'application/json', 'Content-Type': 'application/json'}\nRequest params: {}"
    )
    assert debug[2][0][0] == f"Request attempt 1 of {MAX_RETRY}"
    assert debug[3][0][0] == "Sending regular request"
    assert debug[4][0][0] == "Response status code: 401"
    assert debug[5][0][0] == f"Got status code 401. Retrying... (Attempt 1 of {MAX_RETRY})"
    assert debug[6][0][0] == f"Request attempt 2 of {MAX_RETRY}"
    assert debug[7][0][0] == "Sending regular request"
    assert debug[8][0][0] == "Response status code: 401"
    assert debug[9][0][0] == f"Got status code 401. Retrying... (Attempt 2 of {MAX_RETRY})"
    assert debug[10][0][0] == f"Request attempt 3 of {MAX_RETRY}"
    assert debug[11][0][0] == "Sending regular request"
    assert debug[12][0][0] == "Response status code: 200"


def test_not_authenticated_retry_negative(requests_mock, mocker: MockerFixture):
    """
    Given
    - ServiceNow client

    When
    - Sending HTTP request and getting 401 status code (not authenticated) 3 times

    Then
    - Verify debug messages
    - Ensure the send_request function fails and raises expected error message
    """
    mocker.patch.object(demisto, "debug")
    client = Client(
        "http://server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    requests_mock.get(
        "http://server_url",
        [
            {
                "status_code": 401,
                "json": {
                    "error": {"message": "User Not Authenticated", "detail": "Required to provide Auth information"},
                    "status": "failure",
                },
            },
        ]
        * MAX_RETRY,
    )
    with pytest.raises(Exception) as ex:
        client.send_request("")
    assert (
        str(ex.value) == "ServiceNow Error: User Not Authenticated, details: Required to provide Auth information "
        'Got status code 401 with url http://server_url with body b\'{"error": {"message": '
        '"User Not Authenticated", "detail": "Required to provide Auth information"}, '
        '"status": "failure"}\' with response headers {}'
    )

    debug = demisto.debug.call_args_list

    assert debug[0][0][0] == "Sending request to ServiceNow. Method: GET, Path: "
    assert debug[1][0][0] == (
        "Constructed URL: http://server_url\nRequest headers: "
        "{'Accept': 'application/json', 'Content-Type': 'application/json'}\nRequest params: {}"
    )
    assert debug[2][0][0] == f"Request attempt 1 of {MAX_RETRY}"
    assert debug[3][0][0] == "Sending regular request"
    assert debug[4][0][0] == "Response status code: 401"
    assert debug[5][0][0] == f"Got status code 401. Retrying... (Attempt 1 of {MAX_RETRY})"


def test_oauth_authentication(mocker, requests_mock):
    """
    Given:
     - Integration instance, initialized with the `Use OAuth Login` checkbox selected.

    When:
     - Clicking on running the !servicenow-oauth-test command.

    Then:
     - Verify that oauth authorization flow is used by checking that the get_access_token is called.
    """
    from unittest.mock import MagicMock

    url = "https://test.service-now.com"
    mocker.patch.object(demisto, "command", return_value="servicenow-oauth-test")
    mocker.patch.object(ServiceNowClient, "get_access_token")
    requests_mock.get(f"{url}/api/now/table/incident?sysparm_limit=1", json={"result": [{"opened_at": "sometime"}]})

    # Assert that get_access_token is called when `Use OAuth Login` checkbox is selected:
    mocker.patch.object(
        demisto,
        "params",
        return_value={
            "url": url,
            "credentials": {"identifier": "client_id", "password": "client_secret"},
            "use_oauth": True,
            "file_tag_from_service_now": "FromServiceNow",
            "file_tag": "ForServiceNow",
            "comment_tag": "comments",
            "comment_tag_from_servicenow": "CommentFromServiceNow",
            "work_notes_tag": "work_notes",
            "work_notes_tag_from_servicenow": "WorkNoteFromServiceNow",
        },
    )
    ServiceNowClient.get_access_token = MagicMock()
    main()
    assert ServiceNowClient.get_access_token.called


def test_test_module(mocker):
    """Unit test
    Given
    - test module command
    - command args
    - command raw response
    When
    (a)
        - mock the parse_date_range.
        - mock the Client's send_request.
    (b) - calling the test module when using OAuth 2.0 authorization.
    Then
    (a)
        - run the test module command using the Client
        Validate the content of the HumanReadable.
    (b)
        Validate that an error is returned, indicating that the `Test` button can't be used when using OAuth 2.0.
    """
    mocker.patch("ServiceNowv2.parse_date_range", return_value=("2019-02-23 08:14:21", "never mind"))
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH)
    result = module(client)
    assert result[0] == "ok"

    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
        oauth_params=OAUTH_PARAMS,
    )

    assert result[0] == "ok"


def test_oauth_test_module(mocker):
    """
    Given:
    - oauth_test_module command
    When:
    - (a) trying to call the command when using basic auth.
    - (b)
        - trying to call the command when using OAuth 2.0
        - mock the parse_date_range.
        - mock the Client's send_request.
    Then:
    - (a) validate that an error is returned, indicating that the function should be called when using OAuth only.
    - (b) Validate that the instance was configured successfully.
    """
    mocker.patch("ServiceNowv2.parse_date_range", return_value=("2019-02-23 08:14:21", "never mind"))
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    with pytest.raises(Exception) as e:
        oauth_test_module(client)
    assert "command should be used only when using OAuth 2.0 authorization." in str(e)

    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
        oauth_params=OAUTH_PARAMS,
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH)
    result = oauth_test_module(client)
    assert "### Instance Configured Successfully." in result[0]


def test_oauth_login_command(mocker):
    """
    Given:
    - login command
    When:
    - (a) trying to call the command when using basic auth.
    - (b)
        - trying to call the command when using OAuth 2.0.
        - mocking the login command of ServiceNowClient.
    Then:
    - (a) validate that an error is returned, indicating that the function should be called when using OAuth only.
    - (b) Validate that the login was successful.
    """
    mocker.patch("ServiceNowv2.ServiceNowClient.login")
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    with pytest.raises(Exception) as e:
        login_command(client, args={"username": "username", "password": "password"})
    assert "!servicenow-oauth-login command can be used only when using OAuth 2.0 authorization" in str(e)

    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
        oauth_params=OAUTH_PARAMS,
    )
    mocker.patch.object(client, "send_request", return_value=RESPONSE_FETCH)
    result = login_command(client, args={"username": "username", "password": "password"})
    assert "### Logged in successfully." in result[0]


def test_sysparm_input_display_value(mocker, requests_mock):
    """Unit test
    Given
    - create_record_command function
    - command args, including input_display_value
    - command raw response
    When
    - mock the requests url destination.
    Then
    - run the create command using the Client
    Validate that the sysparm_input_display_value parameter has the correct value
    """

    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )

    mocker.patch.object(
        demisto,
        "args",
        return_value={
            "input_display_value": "true",
            "table_name": "alm_asset",
            "fields": "asset_tag=P4325434;display_name=my_test_record",
        },
    )
    requests_mock.post("https://server_url.com/table/alm_asset?sysparm_input_display_value=True", json={})
    # will raise a requests_mock.exceptions.NoMockAddress if the url address will not be as given in the requests_mock
    create_record_command(client, demisto.args())
    assert requests_mock.request_history[0].method == "POST"

    mocker.patch.object(
        demisto,
        "args",
        return_value={
            "input_display_value": "false",
            "table_name": "alm_asset",
            "fields": "asset_tag=P4325434;display_name=my_test_record",
        },
    )
    requests_mock.post("https://server_url.com/table/alm_asset?sysparm_input_display_value=False", json={})
    # will raise a requests_mock.exceptions.NoMockAddress if the url address will not be as given in the requests_mock
    create_record_command(client, demisto.args())
    assert requests_mock.request_history[1].method == "POST"


def test_get_mapping_fields():
    """
    Given:
        -  ServiceNow client
        -  ServiceNow mapping fields
    When
        - running get_mapping_fields_command
    Then
        - the result fits the expected mapping.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    res = get_mapping_fields_command(client)
    assert res.extract_mapping() == EXPECTED_MAPPING


def test_get_remote_data(mocker):
    """
    Given:
        -  ServiceNow client
        -  arguments: id and LastUpdate(set to lower then the modification time).
        -  ServiceNow ticket
    When
        - running get_remote_data_command.
    Then
        - The ticket was updated with the entries.
    """

    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 0}
    params = {"file_tag_from_service_now": "FromServiceNow"}
    mocker.patch.object(client, "get", return_value=RESPONSE_TICKET_MIRROR)
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=RESPONSE_MIRROR_FILE_ENTRY)
    mocker.patch.object(client, "query", return_value=MIRROR_COMMENTS_RESPONSE)
    mocker.patch.object(client, "get", return_value=RESPONSE_ASSIGNMENT_GROUP)

    res = get_remote_data_command(client, args, params)

    assert res[1]["Tags"] == ["FromServiceNow"]
    assert res[1]["File"] == "test.txt"
    assert res[2]["Contents"] == "Type: comments\nCreated By: admin\nCreated On: 2020-08-17 06:31:49\nThis is a comment"


def test_get_remote_data_last_fetched_incidents_entries(mocker):
    """
    Given:
        -  LastUpdate argument set to higher then the modification time.
        -  Integration context containing the last fetched ids to get their entries.
    When
        - running get_remote_data_command.
    Then
        - The ticket was updated with the entries even the lastUpdate is higher than modification time.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 9999999999}
    params = {"file_tag_from_service_now": "FromServiceNow"}
    demisto.setIntegrationContext({"last_fetched_incident_ids": ["sys_id"]})
    mocker.patch.object(client, "get", side_effect=[RESPONSE_TICKET_MIRROR, RESPONSE_ASSIGNMENT_GROUP])
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=[])
    client_query_mocker = mocker.patch.object(client, "query", return_value=MIRROR_COMMENTS_RESPONSE)

    res = get_remote_data_command(client, args, params)

    assert "sys_created_on" not in client_query_mocker.call_args[0][3]
    assert res[1]["Contents"] == "Type: comments\nCreated By: admin\nCreated On: 2020-08-17 06:31:49\nThis is a comment"
    assert not demisto.getIntegrationContext()["last_fetched_incident_ids"]


def test_get_remote_data_no_last_fetched_incidents(mocker):
    """
    Given:
        -  LastUpdate argument set to higher then the modification time.
        -  Integration context does not containing the last fetched ids to get their entries.
    When
        - running get_remote_data_command.
    Then
        - The ticket is not updated with the entries.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 9999999999}
    params = {"file_tag_from_service_now": "FromServiceNow"}
    demisto.setIntegrationContext({"last_fetched_incident_ids": []})
    mocker.patch.object(demisto, "params", return_value={"isFetch": True})
    mocker.patch.object(client, "get", side_effect=[RESPONSE_TICKET_MIRROR, RESPONSE_ASSIGNMENT_GROUP])
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=[])
    client_query_mocker = mocker.patch.object(client, "query", return_value={"result": []})

    res = get_remote_data_command(client, args, params)

    assert "sys_created_on" in client_query_mocker.call_args[0][3]
    assert len(res) == 1
    assert not res[0]


def test_get_remote_data_last_fetched_incidents_use_display_value(mocker):
    """
    Given:
        -  LastUpdate argument set to higher then the modification time.
        -  Integration context containing the last fetched ids to get their entries.
        -  Using display value.
    When
        - running get_remote_data_command.
    Then
        - The ticket was updated with the entries even the lastUpdate is higher than modification time.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
        use_display_value=True,
        display_date_format="yyyy-MM-dd",
    )

    args = {"id": "sys_id", "lastUpdate": 9999999999}
    params = {"file_tag_from_service_now": "FromServiceNow"}
    demisto.setIntegrationContext({"last_fetched_incident_ids": ["sys_id"]})
    mocker.patch.object(client, "get", side_effect=[RESPONSE_QUERY_TABLE_SYS_PARAMS, RESPONSE_ASSIGNMENT_GROUP])
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=[])
    client_query_mocker = mocker.patch.object(ServiceNowv2, "convert_to_notes_result", return_value=MIRROR_COMMENTS_RESPONSE)

    res = get_remote_data_command(client, args, params)

    assert "filter" not in client_query_mocker.call_args[0][1]
    assert res[1]["Contents"] == "Type: comments\nCreated By: admin\nCreated On: 2020-08-17 06:31:49\nThis is a comment"
    assert not demisto.getIntegrationContext()["last_fetched_incident_ids"]


def test_assigned_to_field_no_user():
    """
    Given:
        -  Client class
        -  Assigned_to field for user that doesn't exist in SNOW
    When
        - run check_assigned_to_field command
    Then
        - Check that assign_to value is empty
    """

    class Client:
        def get(self, table, value, no_record_found_res):
            return {"results": {}}

    assigned_to = {"link": "https://test.service-now.com/api/now/table/sys_user/oscar@example.com", "value": "oscar@example.com"}
    res = check_assigned_to_field(Client(), assigned_to)
    assert res == ""


def test_assigned_to_field_user_exists():
    """
    Given:
        -  Client class
        -  Assigned_to field for user that does exist in SNOW
    When
        - run check_assigned_to_field command
    Then
        - Check that assign_to value is filled with the right email
    """

    class Client:
        def get(self, table, value, no_record_found_res):
            return USER_RESPONSE

    assigned_to = {"link": "https://test.service-now.com/api/now/table/sys_user/oscar@example.com", "value": "oscar@example.com"}
    res = check_assigned_to_field(Client(), assigned_to)
    assert res == "oscar@example.com"


CLOSING_RESPONSE = {"dbotIncidentClose": True, "closeNotes": "Test", "closeReason": "Resolved"}
CLOSING_RESPONSE_CUSTOM = {"dbotIncidentClose": True, "closeNotes": "Test", "closeReason": "Test"}

closed_ticket_state = (RESPONSE_CLOSING_TICKET_MIRROR_CLOSED, {"close_incident": "closed"}, "closed_at", CLOSING_RESPONSE)
resolved_ticket_state = (RESPONSE_CLOSING_TICKET_MIRROR_RESOLVED, {"close_incident": "resolved"}, "resolved_at", CLOSING_RESPONSE)
custom_ticket_state = (
    RESPONSE_CLOSING_TICKET_MIRROR_CUSTOM,
    {"close_incident": "closed", "server_close_custom_state": "9=Test"},
    "",
    CLOSING_RESPONSE_CUSTOM,
)


@pytest.mark.parametrize(
    "response_closing_ticket_mirror, parameters, time, closing_response",
    [closed_ticket_state, resolved_ticket_state, custom_ticket_state],
)
def test_get_remote_data_closing_incident(mocker, response_closing_ticket_mirror, parameters, time, closing_response):
    """
    Given:
        -  ServiceNow client
        -  arguments: id and LastUpdate(set to lower then the modification time).
        -  ServiceNow ticket in closed state
        -  close_incident parameter is set to closed
    When
        - running get_remote_data_command.
    Then
        - The closed_at field exists in the ticket data.
        - dbotIncidentClose exists.
        - Closed notes exists.
    """

    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="sc_task",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 0}
    params = parameters
    mocker.patch.object(client, "get", return_value=response_closing_ticket_mirror)
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=[])
    mocker.patch.object(client, "query", return_value=MIRROR_COMMENTS_RESPONSE)

    res = get_remote_data_command(client, args, params)
    if time:
        assert time in res[0]
    assert closing_response == res[2]["Contents"]


def test_get_remote_data_closing_incident_with_different_closing_state(mocker):
    """
    Given:
        -  ServiceNow client
        -  arguments: id and LastUpdate(set to lower then the modification time).
        -  ServiceNow ticket in closed state
        -  close_incident parameter is set to closed
        -  server_close_custom_state parameter differs from the ticket's closing state
    When
        - running get_remote_data_command.
    Then
        - Validate that the incident does not get closed
    """

    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="sc_task",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 0}
    params = {"close_incident": "closed", "server_close_custom_state": "6=Design"}
    mocker.patch.object(client, "get", return_value=RESPONSE_CLOSING_TICKET_MIRROR_CUSTOM)
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=[])
    mocker.patch.object(client, "query", return_value=MIRROR_COMMENTS_RESPONSE)
    res = get_remote_data_command(client, args, params)
    assert len(res) == 2
    # This means that the entry is of type Note, which does not indicate the closing of the incident
    assert res[1].get("Note", False) is True


def test_get_remote_data_no_attachment(mocker):
    """
    Given:
        -  ServiceNow client
        -  arguments: id and LastUpdate(set to lower then the modification time).
        -  ServiceNow ticket
    When
        - running get_remote_data_command.
    Then
        - The ticket was updated with no attachment.
    """

    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 0}
    params = {}
    mocker.patch.object(client, "get", return_value=RESPONSE_TICKET_MIRROR)
    mocker.patch.object(client, "get_ticket_attachments", return_value=[])
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=[])
    mocker.patch.object(client, "query", return_value=MIRROR_COMMENTS_RESPONSE)
    mocker.patch.object(client, "get", return_value=RESPONSE_ASSIGNMENT_GROUP)

    res = get_remote_data_command(client, args, params)
    assert res[1]["Contents"] == "Type: comments\nCreated By: admin\nCreated On: 2020-08-17 06:31:49\nThis is a comment"
    assert len(res) == 2


def test_get_remote_data_no_entries(mocker):
    """
    Given:
        -  ServiceNow client
        -  arguments: id and LastUpdate(set to lower then the modification time).
        -  ServiceNow ticket
        -  File and comment entries sent from XSOAR.
    When
        - running get_remote_data_command.
    Then
        - The checked entries was not returned.
    """

    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )

    args = {"id": "sys_id", "lastUpdate": 0}
    params = {}
    mocker.patch.object(client, "get", return_value=[RESPONSE_TICKET_MIRROR, RESPONSE_ASSIGNMENT_GROUP])
    mocker.patch.object(client, "get_ticket_attachment_entries", return_value=RESPONSE_MIRROR_FILE_ENTRY_FROM_XSOAR)
    mocker.patch.object(client, "query", return_value=MIRROR_COMMENTS_RESPONSE_FROM_XSOAR)

    res = get_remote_data_command(client, args, params)

    assert "This is a comment\n\n Mirrored from Cortex XSOAR" not in res
    assert "test_mirrored_from_xsoar.txt" not in res


def upload_file_request(*args):
    assert args[2] == "test_mirrored_from_xsoar.txt"
    return {"id": "sys_id", "file_id": "entry_id", "file_name": "test.txt"}


def add_comment_request(*args):
    assert args[3] == "(dbot): This is a comment\n\n Mirrored from Cortex XSOAR"
    return {"id": "1234", "comment": "This is a comment"}


@pytest.mark.parametrize("mirror_entries", [MIRROR_ENTRIES, MIRROR_ENTRIES_WITH_EMPTY_USERNAME])
def test_upload_entries_update_remote_system_command(mocker, mirror_entries):
    """
    Given:
        -  ServiceNow client
        -  File and comment entries sent from XSOAR.
    When
        - running update_remote_system_command.
    Then
        - The checked entries was sent as expected with suffix.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    params = {}
    args = {"remoteId": "1234", "data": {}, "entries": mirror_entries, "incidentChanged": False, "delta": {}}
    mocker.patch.object(client, "upload_file", side_effect=upload_file_request)
    mocker.patch.object(client, "add_comment", side_effect=add_comment_request)

    update_remote_system_command(client, args, params)


TICKET_FIELDS = {
    "close_notes": "This is closed",
    "closed_at": "2020-10-29T13:19:07.345995+02:00",
    "impact": "3",
    "priority": "4",
    "resolved_at": "2020-10-29T13:19:07.345995+02:00",
    "severity": "1 - Low",
    "short_description": "Post parcel",
    "sla_due": "0001-01-01T00:00:00Z",
    "urgency": "3",
    "state": "1",
    "work_start": "0001-01-01T00:00:00Z",
}


def ticket_fields(*args, **kwargs):
    state = "7" if kwargs.get("ticket_type") == "incident" else "3"
    if state == "7":
        assert args[0] == {
            "close_code": "Resolved by caller",
            "close_notes": "This is the resolution note required by ServiceNow to move " "the incident to the Resolved state.",
            "state": state,
        }
    else:
        assert args[0] == {"state": state}

    return {"state": "3"}


def update_ticket(*args):
    state = "7" if "incident" in args else "3"
    return {"state": state}


@pytest.mark.parametrize("ticket_type", ["sc_task", "sc_req_item", "incident"])
def test_update_remote_data_sc_task_sc_req_item(mocker, ticket_type):
    """
    Given:
    -  ServiceNow client
    -  ServiceNow ticket of type sc_task
    -  ServiceNow ticket of type sc_req_item
    -  ServiceNow ticket of type incident

    When
        - running update_remote_system_command.
    Then
        - The state is changed to 3 (closed) after update for sc_task and sc_req_item.
        - The state is changed to 7 (closed) after update for incident.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type=ticket_type,
        get_attachments=False,
        incident_name="description",
    )
    params = {"ticket_type": ticket_type, "close_ticket_multiple_options": "None", "close_ticket": True}
    args = {"remoteId": "1234", "data": TICKET_FIELDS, "entries": [], "incidentChanged": True, "delta": {}, "status": 2}
    mocker.patch("ServiceNowv2.get_ticket_fields", side_effect=ticket_fields)
    mocker.patch.object(client, "update", side_effect=update_ticket)
    update_remote_system_command(client, args, params)


@pytest.mark.parametrize(
    "command, args",
    [
        (query_tickets_command, {"limit": "50", "query": "assigned_to=123^active=true", "ticket_type": "sc_task"}),
        (query_table_command, {"limit": "50", "query": "assigned_to=123^active=true", "table_name": "sc_task"}),
    ],
)
def test_multiple_query_params(requests_mock, command, args):
    """
    Given:
     - Query with multiple arguments

    When:
     - Using servicenow-query-tickets command with multiple sysparm_query arguments.
     - Using servicenow-query-table command with multiple sysparm_query arguments.

    Then:
     - Verify the right request is called with '^' distinguishing different arguments.
    """
    url = "https://test.service-now.com/api/now/v2/"
    client = Client(
        url,
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    requests_mock.request(
        "GET",
        f"{url}table/sc_task?sysparm_limit=50&sysparm_offset=0&sysparm_query=assigned_to%3D123^active%3Dtrue",
        json=RESPONSE_TICKET_ASSIGNED,
    )
    human_readable, entry_context, result, bol = command(client, args)

    assert result == RESPONSE_TICKET_ASSIGNED


@pytest.mark.parametrize(
    "api_response",
    [
        ({"result": []}),
        ({"result": [{"sys_id": "sys_id1"}, {"sys_id": "sys_id2"}]}),
    ],
)
def test_get_modified_remote_data(requests_mock, mocker, api_response):
    """
    Given:
        - Case A: No updated records
        - Case B: 2 updated records

    When:
     - Running get-modified-remote-data

    Then:
        - Case A: Ensure no record IDs returned
        - Case B: Ensure the 2 records IDs returned
    """
    mocker.patch.object(demisto, "debug")
    url = "https://test.service-now.com/api/now/v2/"
    client = Client(
        url,
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    last_update = "2020-11-18T13:16:52.005381+02:00"
    params = {
        "sysparm_limit": "100",
        "sysparm_offset": "0",
        "sysparm_query": "sys_updated_on>2020-11-18 11:16:52",
        "sysparm_fields": "sys_id",
    }
    requests_mock.request("GET", f"{url}table/ticket_type?{urlencode(params)}", json=api_response)
    result = get_modified_remote_data_command(client, {"lastUpdate": last_update})

    assert sorted(result.modified_incident_ids) == sorted(
        [record.get("sys_id") for record in api_response.get("result") if "sys_id" in record]
    )


def test_get_modified_remote_data_unparseable_last_update(requests_mock, mocker):
    """
    Given:
        - lastUpdate is "0" (uninitialized dbotMirrorLastSync sent by XSOAR)

    When:
        - Running get-modified-remote-data

    Then:
        - The command does not raise an error and falls back to epoch time (1970-01-01 00:00:00)
    """
    mocker.patch.object(demisto, "debug")
    url = "https://test.service-now.com/api/now/v2/"
    client = Client(
        url,
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    params = {
        "sysparm_limit": "100",
        "sysparm_offset": "0",
        "sysparm_query": "sys_updated_on>1970-01-01 00:00:00",
        "sysparm_fields": "sys_id",
    }
    requests_mock.request("GET", f"{url}table/ticket_type?{urlencode(params)}", json={"result": []})
    result = get_modified_remote_data_command(client, {"lastUpdate": "0"})
    assert result.modified_incident_ids == []


@pytest.mark.parametrize(
    "sys_created_on, expected",
    [
        (None, "table_sys_id=id"),
        ("", "table_sys_id=id"),
        ("2020-11-18 11:16:52", "table_sys_id=id^sys_created_on>2020-11-18 11:16:52"),
    ],
)
def test_get_ticket_attachments(mocker, sys_created_on, expected):
    """
    Given:
        - Cases A+B: sys_created_on argument was not provided
        - Case C: sys_created_on argument was provided

    When:
        - Getting a ticket attachments.

    Then:
        - Case A+B: Ensure that the query parameters do not include ^sys_created_on>
        - Case C: Ensure that the query parameters include ^sys_created_on>
    """
    client = Client(
        "url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )
    mocker.patch.object(client, "send_request", return_value=[])

    client.get_ticket_attachments("id", sys_created_on)
    client.send_request.assert_called_with("attachment", "GET", params={"sysparm_query": f"{expected}"}, get_attachments=True)


@pytest.mark.parametrize(
    "args,expected_ticket_fields",
    [
        ({"clear_fields": "assigned_to,severity"}, {"assigned_to": "", "severity": ""}),
        ({"clear_fields": "assigned_to,severity", "assigned_to": "assigned@to.com"}, {"assigned_to": "", "severity": ""}),
        ({}, {}),
    ],
)
def test_clear_fields_in_get_ticket_fields(args, expected_ticket_fields):
    if "assigned_to" in args:
        with pytest.raises(DemistoException) as e:
            res = get_ticket_fields(args)
        assert (
            str(e.value)
            == "Could not set a value for the argument 'assigned_to' and add it to the clear_fields. \
                You can either set or clear the field value."
        )
    else:
        res = get_ticket_fields(args)
        assert res == expected_ticket_fields


def test_add_default_closure_fields_to_delta_sets_defaults():
    """
    Given a delta dict missing all closure fields,
    When add_default_closure_fields_to_delta is called,
    Then it sets all defaults.
    """
    from ServiceNowv2 import add_default_closure_fields_to_delta

    delta = {}
    result = add_default_closure_fields_to_delta(delta.copy())
    assert result["close_code"] == "Resolved by caller"
    assert (
        result["close_notes"] == "This is the resolution note required by ServiceNow to move the incident to the Resolved state."
    )


def test_add_default_closure_fields_to_delta_preserves_existing():
    """
    Given a delta dict with some closure fields set,
    When add_default_closure_fields_to_delta is called,
    Then it does not overwrite existing fields.
    """
    from ServiceNowv2 import add_default_closure_fields_to_delta

    delta = {"state": "6", "close_code": "Already closed"}
    result = add_default_closure_fields_to_delta(delta.copy(), close_code="Resolved", close_notes="Closed.")
    assert result["state"] == "6"  # Should not overwrite
    assert result["close_code"] == "Already closed"  # Should not overwrite
    assert result["close_notes"] == "Closed."  # Should set default if missing


def test_add_default_closure_fields_to_delta_custom_values():
    """
    Given custom close_code and close_notes,
    When add_default_closure_fields_to_delta is called,
    Then it sets the custom values if missing in delta.
    """
    from ServiceNowv2 import add_default_closure_fields_to_delta

    delta = {}
    result = add_default_closure_fields_to_delta(delta.copy(), close_code="CustomCode", close_notes="CustomNotes")
    assert result["close_code"] == "CustomCode"
    assert result["close_notes"] == "CustomNotes"


def test_add_default_closure_fields_to_delta_partial():
    """
    Given a delta dict missing some closure fields,
    When add_default_closure_fields_to_delta is called,
    Then it only sets missing fields.
    """
    from ServiceNowv2 import add_default_closure_fields_to_delta

    delta = {"close_code": "Manual"}
    result = add_default_closure_fields_to_delta(delta.copy(), close_code="CustomCode", close_notes="CustomNotes")
    assert result["close_code"] == "Manual"  # Should not overwrite
    assert result["close_notes"] == "CustomNotes"


def test_clear_fields_for_update_remote_system():
    """
    Given:
        - The fields from the parsed_args.data (from update_remote_system)
    When:
        - Run get_ticket_fields
    Then:
        - Validate that the ampty fields exists in the fields that returns.
    """
    parsed_args_data = {
        "assigned_to": "",
        "category": "Software",
        "description": "",
        "impact": "3 - Low",
        "notify": "1 - Do Not Notify",
        "priority": "5 - Planning",
        "severity": "1 - High - Low",
        "short_description": "Testing 3",
        "sla_due": "0001-01-01T02:22:42+02:20",
        "state": "2 - In Progress",
        "subcategory": "",
        "urgency": "3 - Low",
        "work_start": "0001-01-01T02:22:42+02:20",
    }

    res = get_ticket_fields(parsed_args_data)
    assert "assigned_to" in res


def test_query_table_with_fields(mocker):
    """
    Given:
        - Fields for query table

    When:
        - Run query table command

    Then:
        - Validate the fields was sent as params in the request and sys_id appear in fields
    """

    # prepare
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )

    mocker.patch.object(
        client,
        "send_request",
        return_value={
            "result": [{"sys_id": "test_id", "sys_updated_by": "test_updated_name", "opened_by.name": "test_opened_name"}]
        },
    )
    fields = "sys_updated_by,opened_by.name"
    fields_with_sys_id = f"{fields},sys_id"
    args = {"table_name": "alm_asset", "fields": fields, "query": "display_nameCONTAINSMacBook", "limit": 3}

    # run
    result = query_table_command(client, args)

    # validate
    assert client.send_request.call_args[1]["params"]["sysparm_fields"] == fields_with_sys_id
    # validate that the '.' in the key was replaced to '_'
    assert result[1]["ServiceNow.Record(val.ID===obj.ID)"][0]["opened_by_name"] == "test_opened_name"


def test_create_co_from_template_command(mocker):
    """
    Given:
        - template to create change request from it.

    When:
        - Using servicenow-create-co-from-template command.

    Then:
        - Validate the output is correct.
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )

    args = {"template": "Add network switch to datacenter cabinet"}
    mocker.patch.object(client, "send_request", return_value=util_load_json("test_data/create_co_from_template_result.json"))
    result = ServiceNowv2.create_co_from_template_command(client, args)
    assert result.outputs_prefix == "ServiceNow.Ticket"
    assert result.outputs == {
        "Ticket(val.ID===obj.ID)": CREATED_TICKET_CONTEXT_CREATE_CO_FROM_TEMPLATE_COMMAND,
        "ServiceNow.Ticket(val.ID===obj.ID)": CREATED_TICKET_CONTEXT_CREATE_CO_FROM_TEMPLATE_COMMAND,
    }
    assert result.raw_response == util_load_json("test_data/create_co_from_template_result.json")


def test_get_tasks_for_co_command(mocker):
    """
    Given:
        - id to get tasks from it.

    When:
        - Using servicenow-get-tasks-for-co command.

    Then:
        - Validate the output is correct.
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "problem",
        "get_attachments",
        "incident_name",
    )

    args = {"id": "a9e9c33dc61122760072455df62663d2"}
    mocker.patch.object(client, "send_request", return_value=util_load_json("test_data/get_tasks_for_co_command.json"))
    result = ServiceNowv2.get_tasks_for_co_command(client, args)
    assert result.outputs_prefix == "ServiceNow.Tasks"
    assert result.outputs == {"ServiceNow.Tasks(val.ID===obj.ID)": CREATED_TICKET_CONTEXT_GET_TASKS_FOR_CO_COMMAND}
    assert result.raw_response == util_load_json("test_data/get_tasks_for_co_command.json")


def test_get_ticket_attachment_entries_with_oauth_token(mocker):
    """
    The purpose of this test is to verify that it is possible to get a file attachment of a ServiceNow ticket by using
    an OAuth 2.0 client.

    Given:
        - A client with 'oauth_params' - i.e a client that is configured with an OAuth 2.0 Authorization.
        - Mock responses for 'get_ticket_attachments', 'get_access_token' and 'requests.get' functions.

    When:
        - Running the 'client.get_ticket_attachment_entries' function.

    Then:
        - Verify that the 'requests.get' function's arguments are arguments of a call with OAuth 2.0 Authorization.
    """
    # Preparations and mocking:
    client = Client(
        "url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
        oauth_params={"oauth_params": ""},
    )

    mock_res_for_get_ticket_attachments = {
        "result": [
            {
                "file_name": "attachment for test.txt",
                "download_link": "https://ven03941.service-now.com/api/now/attachment/12b7ea411b15cd10042611b4bd4/file",
            }
        ]
    }

    mock_res_for_get_access_token = "access_token"

    mocker.patch.object(client, "get_ticket_attachments", return_value=mock_res_for_get_ticket_attachments)
    mocker.patch.object(client.snow_client, "get_access_token", return_value=mock_res_for_get_access_token)
    requests_get_mocker = mocker.patch("requests.get", return_value=None)

    # Running get_ticket_attachment_entries function:
    client.get_ticket_attachment_entries(ticket_id="id")

    # Validate Results are as expected:
    assert (
        requests_get_mocker.call_args.kwargs.get("auth") is None
    ), "When An OAuth 2.0 client is configured the 'auth' argument shouldn't be passed to 'requests.get' function"
    assert (
        requests_get_mocker.call_args.kwargs.get("headers").get("Authorization") == f"Bearer {mock_res_for_get_access_token}"
    ), "When An OAuth 2.0 client is configured the 'Authorization' Header argument should be passed to 'requests.get' function"


@pytest.mark.parametrize(
    "command, args, response",
    [
        (
            generic_api_call_command,
            {
                "method": "GET",
                "path": "table/sn_si_incident?sysparam_limit=1&sysparam_query=active=true^ORDERBYDESCnumber",
                "body": {},
                "headers": {},
            },
            RESPONSE_GENERIC_TICKET,
        ),
        (
            generic_api_call_command,
            {
                "method": "GET",
                "path": "/table/sn_si_incident?sysparam_limit=1&sysparam_query=active=true^ORDERBYDESCnumber",
                "body": {},
                "headers": {},
                "custom_api": "/api/custom",
            },
            RESPONSE_GENERIC_TICKET,
        ),
    ],
)
def test_generic_api_call_command(command, args, response, mocker):
    """test case for `generic_api_call_command`"""

    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
    )

    mocker.patch.object(client, "send_request", return_value=response)
    result = command(client, args)
    assert result.outputs == response


@pytest.mark.parametrize(
    "file_type , expected", [(EntryType.FILE, True), (3, True), (EntryType.IMAGE, True), (EntryType.NOTE, False), (15, False)]
)
def test_is_entry_type_mirror_supported(file_type, expected):
    """
    Given:
        - an entry file type
    When:
        - running the update_remote_system_command checking if the entry supports mirroring
    Then:
        - return True if the file entry type supports mirroring else return False
    """
    assert ServiceNowv2.is_entry_type_mirror_supported(file_type) == expected


@pytest.mark.parametrize(
    "params, expected",
    [
        ({"close_ticket_multiple_options": "None", "close_ticket": True}, "closed"),
        ({"close_ticket_multiple_options": "None", "close_ticket": False}, None),
        ({"close_ticket_multiple_options": "resolved", "close_ticket": True}, "resolved"),
        ({"close_ticket_multiple_options": "resolved", "close_ticket": False}, "resolved"),
        ({"close_ticket_multiple_options": "closed", "close_ticket": True}, "closed"),
        ({"close_ticket_multiple_options": "closed", "close_ticket": False}, "closed"),
    ],
)
def test_get_closure_case(params, expected):
    """
    Given:
        - params dict with both old and new close_ticket integration params.
        - case 1: params dict with none configured new param and old param configured to True.
        - case 2: params dict with none configured new param and old param configured to False.
        - case 3: params dict with resolved configured new param and old param configured to True.
        - case 4: params dict with resolved configured new param and old param configured to False.
        - case 5: params dict with closed configured new param and old param configured to True.
        - case 6: params dict with closed configured new param and old param configured to False.
    When:
        - running get_closure_case method.
    Then:
        - Ensure the right closure method was returned.
        - case 1: Should return 'closed'
        - case 2: Should return None
        - case 3: Should return 'resolved'
        - case 4: Should return 'resolved'
        - case 5: Should return 'closed'
        - case 6: Should return 'closed'
    """
    assert get_closure_case(params) == expected


@pytest.mark.parametrize(
    "ticket_state, ticket_close_code, server_close_custom_state, server_close_custom_code, expected_res",
    [
        ("1", "default close code", "", "", "Other"),
        ("7", "default close code", "", "", "Resolved"),
        ("6", "default close code", "", "", "Resolved"),
        ("10", "default close code", "10=Test", "", "Test"),
        ("10", "default close code", "10=Test,11=Test2", "", "Test"),
        # If builtin state was override by custom.
        ("6", "default close code", "6=Test", "", "Test"),
        ("corrupt_state", "default close code", "", "", "Other"),
        ("corrupt_state", "default close code", "custom_state=Test", "", "Other"),
        ("6", "default close code", "custom_state=Test", "", "Resolved"),
        # custom close_code overwrites custom sate.
        ("10", "custom close code", "10=Test,11=Test2", "custom close code=Custom,90=90 Custom", "Custom"),
        ("10", "90", "10=Test,11=Test2", "80=Custom, 90=90 Custom", "90 Custom"),
    ],
)
def test_converts_close_code_or_state_to_close_reason(
    ticket_state, ticket_close_code, server_close_custom_state, server_close_custom_code, expected_res
):
    """
    Given:
        - ticket_state: The state for the closed service now ticket
        - ticket_close_code: The Service now ticket close code
        - server_close_custom_state: The custom state for the closed service now ticket
        - server_close_custom_code: The custom close code for the closed service now ticket
    When:
        - closing a ticket on service now
    Then:
        - return the matching XSOAR incident state.
    """
    assert (
        converts_close_code_or_state_to_close_reason(
            ticket_state, ticket_close_code, server_close_custom_state, server_close_custom_code
        )
        == expected_res
    )


def ticket_fields_mocker(*args, **kwargs):
    state = "88" if kwargs.get("ticket_type") == "incident" else "90"
    if state == "88":
        fields = {
            "close_code": "Resolved by caller",
            "close_notes": "This is the resolution note required by ServiceNow to move " "the incident to the Resolved state.",
            "state": state,
        }
    else:
        fields = {"state": state}
    assert fields == args[0]
    return fields


@pytest.mark.parametrize(
    "file_name , expected",
    [
        ("123.png", "image/png"),
        ("123.gif", "image/gif"),
        ("123.jpeg", "image/jpeg"),
        ("123.pdf", "application/pdf"),
        ("123", "*/*"),
    ],
)
def test_upload_file_types(file_name, expected):
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        get_attachments=False,
        incident_name="description",
        ticket_type="incident",
    )
    assert client.get_content_type(file_name) == expected


@pytest.mark.parametrize(
    "ticket_type, ticket_state, close_custom_state, result_close_state, update_call_count",
    [
        # case 1 - SIR ticket closed by custom state
        ("sn_si_incident", "16", "90", "90", 1),
        # case 2 - custom state doesn't exist, closed by default state code - '3'
        ("sn_si_incident", "16", "90", "3", 2),
        # case 3 - ticket closed by custom state
        ("incident", "1", "88", "88", 1),
        # case 4 - custom state doesn't exist, closed by default state code - '7'
        ("incident", "1", "88", "7", 2),
    ],
    ids=["case - 1", "case - 2", "case - 3", "case - 4"],
)
def test_update_remote_data_custom_state(
    mocker, ticket_type, ticket_state, close_custom_state, result_close_state, update_call_count
):
    """
    Given:
    -  ServiceNow client
    -  ServiceNow ticket of type sn_si_incident
    -  ServiceNow ticket of type incident
    -  close_custom_state exist/not exist in ServiceNow
    When
        - running update_remote_system_command.
    Then
        - The state is changed accordingly
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type=ticket_type,
        get_attachments=False,
        incident_name="description",
    )
    params = {
        "ticket_type": ticket_type,
        "close_ticket_multiple_options": "None",
        "close_ticket": True,
        "close_custom_state": close_custom_state,
    }

    TICKET_FIELDS["state"] = ticket_state
    args = {"remoteId": "1234", "data": TICKET_FIELDS, "entries": [], "incidentChanged": True, "delta": {}, "status": 2}

    def update_ticket_mocker(*args):
        # Represents only the response of the last call to client.update
        # In case the custom state doesn't exist -
        # in the first call will return the ticket's state as before (in case2 - '16', case4 - '1')
        return {
            "result": {
                "short_description": "Post parcel",
                "close_notes": "This is closed",
                "closed_at": "2020-10-29T13:19:07.345995+02:00",
                "impact": "3",
                "priority": "4",
                "resolved_at": "2020-10-29T13:19:07.345995+02:00",
                "severity": "1 - High - Low",
                "sla_due": "0001-01-01T00:00:00Z",
                "state": result_close_state,
                "urgency": "3",
                "work_start": "0001-01-01T00:00:00Z",
            }
        }

    mocker.patch("ServiceNowv2.get_ticket_fields", side_effect=ticket_fields_mocker)
    mocker_update = mocker.patch.object(client, "update", side_effect=update_ticket_mocker)
    update_remote_system_command(client, args, params)
    # assert the state argument in the last call to client.update
    assert mocker_update.call_args[0][2]["state"] == result_close_state
    assert mocker_update.call_count == update_call_count


def test_update_remote_data_upload_file_exception(mocker):
    """
    Given:
        -  ServiceNow client
        -  Two file entries to sent from XSOAR which one of them is invalid.
    When
        - running update_remote_system_command.
    Then
        - The invalid entry raised an exception and function has continued.
    """
    client = Client(
        server_url="https://server_url.com/",
        sc_server_url="sc_server_url",
        cr_server_url="cr_server_url",
        username="username",
        password="password",
        verify=False,
        fetch_time="fetch_time",
        sysparm_query="sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
    )
    params = {}
    args = {
        "remoteId": "1234",
        "data": {},
        "entries": [MIRROR_ENTRIES[0], MIRROR_ENTRIES[0]],
        "incidentChanged": True,
        "delta": {},
        "status": 2,
    }

    def upload_file_mock(*args):
        raise Exception("ERROR!!!")

    def add_comment_mock(*args):
        assert "An attempt to mirror a file from Cortex XSOAR was failed." in args[3]

    mocker.patch.object(client, "update", side_effect=update_ticket)
    mocker.patch.object(client, "upload_file", side_effect=upload_file_mock)
    mocker.patch.object(client, "add_comment", side_effect=add_comment_mock)

    demisto_mocker = mocker.patch.object(demisto, "error")
    res = update_remote_system_command(client, args, params)

    assert (
        demisto_mocker.call_args[0][0] == "An attempt to mirror a file has failed. entry_id=entry-id, "
        "file_name='test'\nERROR!!!"
    )
    assert res == "1234"


@pytest.mark.parametrize(
    "mock_json, expected_results",
    [
        ({"error": "invalid client."}, "ServiceNow Error: invalid client."),
        (
            {"error": {"message": "invalid client", "detail": "the client you have entered is invalid."}},
            "ServiceNow Error: invalid client, details: the client you have entered is invalid. "
            "Got status code 400 with url server_urltable with body  with response headers {}",
        ),
    ],
)
def test_send_request_with_str_error_response(mocker, mock_json, expected_results):
    """
    Given:
     - a client and a mock response.
     - case 1: a mock response where the error field is a string.
     - case 2: a mock response where the error field is a dict.

    When:
     - Running send_request function.

    Then:
     - Verify that the function extracted the data from the response without problems and the expected exception is raised.
     - case 1: Shouldn't attempt to extract inner fields from the error field, only present the error value.
     - case 2: Should attempt to extract inner fields from the error field, present the parsed extracted error values.
    """
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        "sysparm_limit",
        "timestamp_field",
        "ticket_type",
        "get_attachments",
        "incident_name",
        display_date_format="yyyy-MM-dd",
    )

    class MockResponse:
        def __init__(self, mock_json):
            self.text = "some text"
            self.json_data = mock_json
            self.status_code = 400
            self.content = ""
            self.headers = {}

        def json(self):
            return self.json_data

    mocker.patch.object(requests, "request", return_value=MockResponse(mock_json))
    with pytest.raises(Exception) as e:
        client.send_request(path="table")
    assert str(e.value) == expected_results


@pytest.mark.parametrize(
    "ticket, expected_ticket",
    [
        ({}, {}),
        ({"assigned_to": ""}, {"assigned_to": ""}),
        (
            {
                "assigned_to": {
                    "link": "https://test.service-now.com/api/now/table/sys_user/oscar@example.com",
                    "value": "oscar@example.com",
                }
            },
            {"assigned_to": "oscar@example.com"},
        ),
    ],
)
def test_parse_dict_ticket_fields_empty_ticket(ticket, expected_ticket):
    """
    Given:
     - a ticket
     - case 1: Ticket is completely empty (obtained from the case where last_update > ticket_last_update).
     - case 2: Ticket contains assigned_to field with an empty string as a value.
     - case 3: Ticket contains assigned_to field with a user dict as a value.

    When:
     - Running parse_dict_ticket_fields function.

    Then:
     - Verify that the ticket fields were updated correctly.
     - case 1: Shouldn't add the assigned_to field to the obtained ticket.
     - case 2: Should add assigned_to field with an empty string as a value.
     - case 3: Should add assigned_to field with the user email as a value.
    """

    class Client:
        def get(self, table, value, no_record_found_res):
            return USER_RESPONSE

    parse_dict_ticket_fields(Client(), ticket)  # type: ignore
    assert ticket == expected_ticket


def test_format_incidents_response_with_display_values_with_no_incidents():
    """
    Given:
        No incidents
    When:
        Calling format_incidents_response_with_display_values
    Then:
        Returns empty list
    """
    incidents_res = []
    result = format_incidents_response_with_display_values(incidents_res)

    assert result == []


def test_format_incidents_response_with_display_values_with_incidents():
    """
    Given:
        Incidents response containing opened_by, sys_domain, assignment_group and other fields
    When:
        Calling format_incidents_response_with_display_values
    Then:
        Returns formatted incidents with display_value
    """
    incidents_res = RESPONSE_FETCH_USE_DISPLAY_VALUE["result"]
    result = format_incidents_response_with_display_values(incidents_res)

    assert len(result) == 2
    assert result[0]["sys_updated_on"] == "2024-02-29 13:09:46"
    assert result[0]["opened_at"] == "2024-02-29 13:08:46"
    assert result[0]["opened_by"] == incidents_res[0]["opened_by"]
    assert result[0]["sys_domain"] == incidents_res[0]["sys_domain"]
    assert result[0]["assignment_group"] == incidents_res[0]["assignment_group"]

    assert result[1]["sys_updated_on"] == "2024-02-29 11:08:44"
    assert result[1]["opened_at"] == "2024-02-29 11:07:48"
    assert result[1]["opened_by"] == incidents_res[1]["opened_by"]
    assert result[1]["sys_domain"] == incidents_res[1]["sys_domain"]
    assert result[1]["assignment_group"] == ""


@pytest.mark.parametrize(
    "input_string, expected",
    [
        ("2023-02-15 10:30:45", True),
        ("invalid", False),
        ("15.02.2023 10:30:45", False),
        ("a2023-02-15 10:30:45", False),
        ("2023-02-15 10:30:45a", False),
        ("2023-02-15 10:30:45 a", False),
    ],
)
def test_is_time_field(input_string, expected):
    """
    Given:
        Input strings of varying validity
    When:
        is_time_field is called on those strings
    Then:
        It should return True if string contains valid datetime, False otherwise
    """
    assert is_time_field(input_string) is expected


def test_get_attachment_command_success():
    client = MagicMock()
    args = {"sys_id": "12345"}
    mock_attachments = [
        {"file_name": "file1.txt", "content": "file1 content"},
        {"file_name": "file2.txt", "content": "file2 content"},
    ]
    client.get_ticket_attachment_entries = MagicMock(return_value=mock_attachments)
    result = get_attachment_command(client, args)
    client.get_ticket_attachment_entries.assert_called_once_with("12345")
    assert isinstance(result, list)
    assert isinstance(result[0], CommandResults)
    assert result[0].readable_output == "Successfully retrieved attachments for ticket with sys id 12345."
    assert result[1] == mock_attachments


def test_get_attachment_command_missing_sys_id():
    client = MagicMock()
    args = {"sys_id": "12345"}
    mock_attachments = []
    client.get_ticket_attachment_entries = MagicMock(return_value=mock_attachments)
    result = get_attachment_command(client, args)
    client.get_ticket_attachment_entries.assert_called_once_with("12345")
    assert isinstance(result, CommandResults)
    assert result.readable_output == "Ticket with sys id 12345 has no attachments to retrieve."


def test_incident_id_in_last_fetched_updates_correctly(mocker):
    """
    Given:
        Ticket ID to remove
    When:
        is_new_incident is called
    Then:
        It should remove the id without modifying the existing integration context keys
    """
    mocker.patch.object(
        ServiceNowv2,
        "get_integration_context",
        return_value={"access_token": "token", "last_fetched_incident_ids": ["ABC123", "XYZ789"]},
    )
    res = mocker.patch.object(ServiceNowv2, "set_integration_context")

    # Executing the function with the incident id to be checked
    is_new_incident("XYZ789")

    # Setup verification context with wrapper to cover the whole integration context if necessary
    expected_context = {"access_token": "token", "last_fetched_incident_ids": ["ABC123"]}

    # Verifying that set_integration_context was called with the correct new context
    res.assert_called_once_with(expected_context)


def test_incident_id_not_in_last_fetched(mocker):
    """
    Given:
        Ticket ID that should not be removed
    When:
        is_new_incident is called
    Then:
        It should not modify the integration context
    """
    # Mock the get_integration_context to return some incident IDs which does not include the tested ID
    mocker.patch.object(
        ServiceNowv2,
        "get_integration_context",
        return_value={"access_token": "token", "last_fetched_incident_ids": ["ABC123", "XYZ789"]},
    )
    # Mock the set_integration_context to check it is not called
    res = mocker.patch.object(ServiceNowv2, "set_integration_context")

    # Executing the function with an incident id that is not in the context's list
    is_new_incident("DEF456")

    # Assert that set_integration_context was never called because no incident ID was removed
    res.assert_not_called()


class TestQuickActionPreview:
    """
    Unit tests for the QuickActionPreview dataclass.

    Tests:
        - Initialization with full data
        - Initialization with partial data and logging missing fields
        - Conversion of instance to context dictionary
    """

    @pytest.fixture
    def full_data(self) -> dict[str, Any]:
        """
        Given a complete dataset,
        When used to initialize QuickActionPreview,
        Then it provides all necessary fields.
        """
        return {
            "id": "123",
            "title": "Test Ticket",
            "description": "This is a test description.",
            "status": "Open",
            "assignee": "John Doe",
            "creation_date": "2024-05-14T12:00:00Z",
            "severity": "High",
        }

    @pytest.fixture
    def partial_data(self) -> dict[str, Any]:
        """
        Given a dataset with some missing fields,
        When used to initialize QuickActionPreview,
        Then it simulates a scenario with incomplete data.
        """
        return {
            "id": "456",
            "title": None,
            "description": "Another test description.",
            "status": None,
            "assignee": "Jane Doe",
            "creation_date": None,
            "severity": "Low",
        }

    def test_full_init(self, full_data: dict[str, Any]) -> None:
        """
        Given a full dataset,
        When initializing QuickActionPreview,
        Then all fields should be set correctly.
        """
        preview = QuickActionPreview(**full_data)
        assert preview.id == full_data["id"]
        assert preview.title == full_data["title"]
        assert preview.status == full_data["status"]
        assert preview.assignee == full_data["assignee"]

    def test_partial_init_logs_missing_fields(self, mocker, partial_data: dict[str, Any]) -> None:
        """
        Given a partial dataset with missing fields,
        When initializing QuickActionPreview,
        Then demisto.debug should log the missing fields.
        """
        mock_debug = mocker.patch("demistomock.debug")
        QuickActionPreview(**partial_data)
        mock_debug.assert_called_once()
        args, _ = mock_debug.call_args
        assert "title" in args[0]
        assert "status" in args[0]
        assert "creation_date" in args[0]

    def test_to_context(self, full_data: dict[str, Any]) -> None:
        """
        Given a fully initialized QuickActionPreview,
        When calling to_context,
        Then it should return the correct dictionary representation.
        """
        preview = QuickActionPreview(**full_data)
        context = preview.to_context()
        assert context == full_data


@pytest.fixture
def mock_client():
    """
    Pytest fixture to create a mocked ServiceNow Client instance.
    This provides a fresh mock for each test function.
    """
    client = MagicMock(spec=Client)
    client.ticket_type = "incident"
    client.use_display_value = False
    client.display_date_format = None
    client.sys_param_limit = 50
    client.sys_param_offset = 0
    return client


@pytest.fixture
def mock_params():
    """
    Pytest fixture for mock integration parameters.
    """
    return {
        "close_incident": "closed",
        "file_tag_from_service_now": "file_from_snow",
        "comment_tag_from_servicenow": "comment_from_snow",
        "work_notes_tag_from_servicenow": "work_note_from_snow",
        "server_close_custom_state": "",
        "server_custom_close_code": "",
    }


def test_get_remote_data_ticket_not_found(mock_client: MagicMock, mock_params) -> None:
    """
    Tests that the function returns a 'Ticket was not found' message when client.get fails.

    Args:
        mock_client: The mocked ServiceNow client.
        mock_params: The mocked integration parameters.
    """
    # Arrange
    ticket_id = "INC12345"
    last_update_ts = int((datetime.now() - timedelta(days=1)).timestamp())
    args = {"id": ticket_id, "lastUpdate": str(last_update_ts)}

    # Configure mock to return an empty result, simulating a non-existent ticket
    mock_client.get.return_value = {"result": []}

    # Act
    result = get_remote_data_command(mock_client, args, mock_params)

    # Assert
    assert result == "Ticket was not found."
    mock_client.get.assert_called_once_with(mock_client.ticket_type, ticket_id, use_display_value=False)


@patch("ServiceNowv2.is_new_incident", return_value=False)
def test_get_remote_data_no_updates(mock_is_new_incident: MagicMock, mock_client: MagicMock, mock_params) -> None:
    """
    Tests that the function returns an empty dictionary if the ticket has not been updated
    since the last fetch.

    Args:
        mock_is_new_incident: Mock of is_new_incident function.
        mock_client: The mocked ServiceNow client.
        mock_params: The mocked integration parameters.
    """
    # Arrange
    ticket_id = "INC12345"
    # Last update from XSOAR is now, ticket was updated 1 hour ago
    last_update_ts = int(datetime.now().timestamp())
    ticket_updated_on = datetime.now() - timedelta(hours=1)

    args = {"id": ticket_id, "lastUpdate": str(last_update_ts)}

    ticket_data = {
        "result": [
            {
                "sys_id": ticket_id,
                "sys_updated_on": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
            }
        ]
    }
    mock_client.get.return_value = ticket_data

    with patch("ServiceNowv2.demisto") as mock_demisto:
        # isFetch is a parameter of the integration, so we mock it here
        mock_demisto.params.return_value = {"isFetch": True}

        # Act
        result = get_remote_data_command(mock_client, args, mock_params)

        # Assert
        assert result == [{}]  # An empty dict inside a list indicates no incident update


@patch("ServiceNowv2.is_new_incident", return_value=False)
def test_get_remote_data_with_new_comments(mock_is_new_incident: MagicMock, mock_client: MagicMock, mock_params) -> None:
    """
    Tests that new comments are correctly fetched and formatted into entries when a ticket is updated.

    Args:
        mock_is_new_incident: Mock of is_new_incident function.
        mock_client: The mocked ServiceNow client.
        mock_params: The mocked integration parameters.
    """
    # Arrange
    ticket_id = "INC12345"
    last_update_ts = int((datetime.now() - timedelta(days=1)).timestamp())
    ticket_updated_on = datetime.now()

    args = {"id": ticket_id, "lastUpdate": str(last_update_ts)}

    ticket_data = {
        "result": [
            {
                "sys_id": ticket_id,
                "sys_updated_on": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
                "short_description": "Updated description",
            }
        ]
    }

    comments_data = {
        "result": [
            {
                "element": "comments",
                "sys_created_by": "abel.tuter",
                "sys_created_on": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
                "value": "This is a new comment.",
            }
        ]
    }

    mock_client.get.return_value = ticket_data
    mock_client.get_ticket_attachment_entries.return_value = []
    mock_client.query.return_value = comments_data

    with patch("ServiceNowv2.demisto") as mock_demisto:
        mock_demisto.params.return_value = {"isFetch": True}

        # Act
        result = get_remote_data_command(mock_client, args, mock_params)

        # Assert
        # Expecting a list with two items: the updated incident data and the new comment entry
        assert isinstance(result, list)
        assert len(result) == 2

        # Check incident data
        updated_incident = result[0]
        assert updated_incident["short_description"] == "Updated description"

        # Check entry data
        comment_entry = result[1]
        assert "This is a new comment." in comment_entry["Contents"]
        assert comment_entry["Note"] is True


@patch("ServiceNowv2.is_new_incident", return_value=False)
def test_get_remote_data_with_new_attachment(mock_is_new_incident: MagicMock, mock_client: MagicMock, mock_params) -> None:
    """
    Tests that new file attachments are fetched and formatted correctly.

    Args:
        mock_is_new_incident: Mock of is_new_incident function.
        mock_client: The mocked ServiceNow client.
        mock_params: The mocked integration parameters.
    """
    # Arrange
    ticket_id = "INC12345"
    last_update_ts = int((datetime.now() - timedelta(days=1)).timestamp())
    ticket_updated_on = datetime.now()

    args = {"id": ticket_id, "lastUpdate": str(last_update_ts)}

    ticket_data = {
        "result": [
            {
                "sys_id": ticket_id,
                "sys_updated_on": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
            }
        ]
    }

    attachment_entry = {"File": "evidence.txt", "FileID": "mock_file_id", "Tags": [mock_params["file_tag_from_service_now"]]}

    mock_client.get.return_value = ticket_data
    mock_client.get_ticket_attachment_entries.return_value = [attachment_entry]
    mock_client.query.return_value = {"result": []}  # No new comments

    with patch("ServiceNowv2.demisto") as mock_demisto:
        mock_demisto.params.return_value = {"isFetch": True}

        # Act
        result = get_remote_data_command(mock_client, args, mock_params)

        # Assert
        assert isinstance(result, list)
        assert len(result) == 2
        assert result[1]["File"] == "evidence.txt"
        assert result[1]["Tags"] == [mock_params["file_tag_from_service_now"]]
        # mark_attachments_as_note is not set -> the attachment entry must not be marked as a note.
        assert "Note" not in result[1]


@patch("ServiceNowv2.is_new_incident", return_value=False)
def test_get_remote_data_with_new_attachment_attachment_is_note(
    mock_is_new_incident: MagicMock, mock_client: MagicMock, mock_params
) -> None:
    """
    Tests that new file attachments are fetched and formatted correctly.

    Args:
        mock_is_new_incident: Mock of is_new_incident function.
        mock_client: The mocked ServiceNow client.
        mock_params: The mocked integration parameters.
    """
    # Arrange
    ticket_id = "INC12345"
    last_update_ts = int((datetime.now() - timedelta(days=1)).timestamp())
    ticket_updated_on = datetime.now()

    args = {"id": ticket_id, "lastUpdate": str(last_update_ts)}

    ticket_data = {
        "result": [
            {
                "sys_id": ticket_id,
                "sys_updated_on": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
            }
        ]
    }

    new_mock_params = mock_params | {"mark_attachments_as_note": True}

    attachment_entry = {"File": "evidence.txt", "FileID": "mock_file_id", "Tags": [mock_params["file_tag_from_service_now"]]}

    mock_client.get.return_value = ticket_data
    mock_client.get_ticket_attachment_entries.return_value = [attachment_entry]
    mock_client.query.return_value = {"result": []}  # No new comments

    with patch("ServiceNowv2.demisto") as mock_demisto:
        mock_demisto.params.return_value = {"isFetch": True}

        # Act
        result = get_remote_data_command(mock_client, args, new_mock_params)

        # Assert
        assert isinstance(result, list)
        assert len(result) == 2
        assert result[1]["File"] == "evidence.txt"
        assert result[1]["Note"] is True
        assert result[1]["Tags"] == [mock_params["file_tag_from_service_now"]]


@patch("ServiceNowv2.is_new_incident", return_value=False)
def test_get_remote_data_incident_closed(mock_is_new_incident: MagicMock, mock_client: MagicMock, mock_params) -> None:
    """
    Tests that a closing entry is created when a ticket is found to be closed in ServiceNow.

    Args:
        mock_is_new_incident: Mock of is_new_incident function.
        mock_client: The mocked ServiceNow client.
        mock_params: The mocked integration parameters.
    """
    # Arrange
    ticket_id = "INC12345"
    last_update_ts = int((datetime.now() - timedelta(days=1)).timestamp())
    ticket_updated_on = datetime.now()
    args = {"id": ticket_id, "lastUpdate": str(last_update_ts)}

    # state '7' corresponds to 'Closed' in the TICKET_STATES mapping
    ticket_data = {
        "result": [
            {
                "sys_id": ticket_id,
                "sys_updated_on": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
                "state": "7",
                "close_notes": "Issue resolved.",
                "closed_at": ticket_updated_on.strftime("%Y-%m-%d %H:%M:%S"),
            }
        ]
    }

    mock_client.get.return_value = ticket_data
    mock_client.get_ticket_attachment_entries.return_value = []
    mock_client.query.return_value = {"result": []}

    with patch("ServiceNowv2.demisto") as mock_demisto:
        mock_demisto.params.return_value = {"isFetch": True}

        # Act
        result = get_remote_data_command(mock_client, args, mock_params)

        # Assert
        assert isinstance(result, list)
        assert len(result) == 2  # Incident update + closing note

        # Find the closing entry
        closing_entry = None
        for entry in result:
            if (
                isinstance(entry, dict)
                and entry.get("Type") == EntryType.NOTE
                and isinstance(entry.get("Contents"), dict)
                and entry["Contents"].get("dbotIncidentClose")
            ):
                closing_entry = entry
                break

        assert closing_entry is not None
        assert closing_entry["Type"] == EntryType.NOTE
        assert closing_entry["ContentsFormat"] == EntryFormat.JSON
        assert closing_entry["Contents"]["dbotIncidentClose"] is True
        assert closing_entry["Contents"]["closeNotes"] == "Issue resolved."
        assert closing_entry["Contents"]["closeReason"] == "Resolved"


def test_get_remote_data_preview_missing_id(mock_client: MagicMock) -> None:
    """
    Tests that the function raises a ValueError when the 'id' argument is missing.

    Args:
        mock_client: The mocked ServiceNow client.
    """
    # Arrange
    args = {}  # 'id' is missing

    # Act & Assert
    with pytest.raises(ValueError, match=r"ServiceNow Ticket ID \('id'\) is required for preview."):
        ServiceNowv2.get_remote_data_preview_command(mock_client, args)


@patch("ServiceNowv2.DemistoException", DemistoException)  # Use the real exception for checking
def test_get_remote_data_preview_api_error(mock_client: MagicMock) -> None:
    """
    Tests that the function raises a DemistoException when the client API call fails.

    Args:
        mock_client: The mocked ServiceNow client.
    """
    # Arrange
    args = {"id": "INC12345"}
    mock_client.get.side_effect = Exception("API connection failed")

    # Act & Assert
    with pytest.raises(DemistoException, match="Failed to fetch ticket INC12345 from ServiceNow. Error: API connection failed"):
        ServiceNowv2.get_remote_data_preview_command(mock_client, args)


@patch("ServiceNowv2.CommandResults", CommandResults)  # Use the real class to build the object
@patch("ServiceNowv2.QuickActionPreview", QuickActionPreview)  # Use the real class to build the object
def test_get_remote_data_preview_success(mock_client: MagicMock) -> None:
    """
    Tests the successful generation of a ticket preview.

    Args:
        mock_client: The mocked ServiceNow client.
    """
    # Arrange
    ticket_id = "INC0010005"
    args = {"id": ticket_id}

    # A realistic API response with display_value sub-keys
    mock_api_response = {
        "result": {
            "number": {"display_value": ticket_id},
            "short_description": {"display_value": "Email server is down"},
            "description": {"display_value": "Users are unable to send or receive emails."},
            "state": {"display_value": "In Progress"},
            "assigned_to": {"display_value": "Beth Anglin"},
            "sys_created_on": {"display_value": "2024-01-01 10:00:00"},
            "priority": {"display_value": "1 - Critical"},
        }
    }
    mock_client.get.return_value = mock_api_response

    # Act
    result = ServiceNowv2.get_remote_data_preview_command(mock_client, args)

    # Assert
    # 1. Check that the client's get method was called correctly
    mock_client.get.assert_called_once_with(mock_client.ticket_type, ticket_id, use_display_value=True)

    # 2. Check the returned CommandResults object and its contents
    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "QuickActionPreview"
    assert result.outputs_key_field == "id"

    # 3. Check the outputs, which should be the context from QuickActionPreview
    expected_outputs = {
        "id": "INC0010005",
        "title": "Email server is down",
        "description": "Users are unable to send or receive emails.",
        "status": "In Progress",
        "assignee": "Beth Anglin",
        "creation_date": "2024-01-01 10:00:00",
        "severity": "1 - Critical",
    }
    assert result.outputs == expected_outputs

    # 4. Check the raw response
    assert result.raw_response == mock_api_response


@patch("ServiceNowv2.CommandResults", CommandResults)
@patch("ServiceNowv2.QuickActionPreview", QuickActionPreview)
def test_get_remote_data_preview_success_with_list_response(mock_client: MagicMock) -> None:
    """
    Tests successful preview generation when the API returns a list with one item.

    Args:
        mock_client: The mocked ServiceNow client.
    """
    # Arrange
    ticket_id = "INC0010006"
    args = {"id": ticket_id}

    # API response as a list containing one dictionary
    mock_api_response = {
        "result": [
            {
                "number": {"display_value": ticket_id},
                "short_description": {"display_value": "Network printer offline"},
                "state": {"display_value": "New"},
                # Other fields omitted for brevity
            }
        ]
    }
    mock_client.get.return_value = mock_api_response

    # Act
    result = ServiceNowv2.get_remote_data_preview_command(mock_client, args)

    # Assert
    assert isinstance(result, CommandResults)
    assert result.outputs["id"] == ticket_id
    assert result.outputs["title"] == "Network printer offline"
    assert result.outputs["status"] == "New"


class UpdateRemoteSystemArgs:
    def __init__(self, delta):
        self.delta = delta


# Sample delta dict to test mutation
DEFAULT_DELTA = {"key": "value"}


@pytest.mark.parametrize(
    "state,ticket_type,custom_state,should_patch",
    [
        ("7", "incident", None, True),  # Given closed state (7)
        ("6", "incident", None, True),  # Given resolved state (6)
        ("9", "incident", "9", True),  # Given custom close state (match) and type incident
        ("9", "problem", "9", False),  # Given custom state match but non-incident type
        ("5", "incident", "9", False),  # Given wrong state
        (None, "incident", None, False),  # Given missing state and no custom close state
    ],
)
@patch("ServiceNowv2.add_default_closure_fields_to_delta")
def test_set_default_fields_behavior(mock_add_defaults, state, ticket_type, custom_state, should_patch):
    """
    GIVEN: an UpdateRemoteSystemArgs object with a delta containing various 'state' values,
    AND different combinations of ticket_type and custom_state,

    WHEN: set_default_fields is called,

    THEN: it should call add_default_closure_fields_to_delta and log a debug message
         only if the state is "6", "7", or matches custom_state and ticket_type is "incident".
    """
    initial_delta = {"state": state} if state is not None else {}
    args = UpdateRemoteSystemArgs(delta=initial_delta.copy())
    modified_delta = initial_delta.copy()
    modified_delta["close_code"] = "default_code"
    modified_delta["close_notes"] = "default_notes"

    mock_add_defaults.return_value = modified_delta

    result = ServiceNowv2.set_default_fields(args, ticket_type, custom_state)

    if should_patch:
        mock_add_defaults.assert_called_once_with(initial_delta)
        assert result.delta == modified_delta
    else:
        mock_add_defaults.assert_not_called()
        assert result.delta == initial_delta


def test_delete_ticket_command_success(mock_client: MagicMock):
    """
    Tests successful ticket deletion.
    Verifies that when a ticket is successfully deleted, the function returns
    the correct success status and message.
    """

    mock_client.delete = MagicMock(return_value="")
    mock_client.get_table_name = MagicMock(return_value="incident")

    args = {"id": "12345", "ticket_type": "incident"}

    result = delete_ticket_command(mock_client, args)

    assert "Ticket with ID 12345 was successfully deleted from incident table." in result.readable_output
    assert result.outputs is not None
    assert result.outputs["ID"] == "12345"
    assert result.outputs["DeleteMessage"] == "Ticket with ID 12345 was successfully deleted from incident table."


def test_delete_ticket_command_not_found(mock_client: MagicMock):
    """
    Tests ticket deletion when record is not found.
    Verifies that when attempting to delete a non-existent ticket, the function
    returns the correct failure status and error message.
    """
    mock_client.delete = MagicMock(return_value={"result": []})
    mock_client.get_table_name.return_value = "incident"
    args = {"id": "99999", "ticket_type": "incident"}

    result = delete_ticket_command(mock_client, args)

    assert "Failed to delete ticket 99999 from incident table. Record may not exist." in result.readable_output
    assert result.outputs is not None
    assert result.outputs["ID"] == "99999"
    assert result.outputs["DeleteMessage"] == "Failed to delete ticket 99999 from incident table. Record may not exist."


def test_client_jwt_param_usage(mocker):
    """
    Given:
    - JWT params provided to the ServiceNow CMDB Client
    When:
    - Initializing the Client with jwt_params
    Then:
    - ServiceNowClient is instantiated with the same jwt_params
    - The jwt attribute is set on the inner ServiceNowClient
    """
    jwt_params = {
        "private_key": "-----BEGIN PRIVATE KEY-----test-----END PRIVATE KEY-----",
        "kid": "test_kid",
        "sub": "test_sub",
        "aud": "test_aud",
        "iss": "test_iss",
    }
    mocker.patch("ServiceNowApiModule.jwt.encode", return_value="jwt_token_stub")
    client = Client(
        "server_url",
        "sc_server_url",
        "cr_server_url",
        "username",
        "password",
        "verify",
        "fetch_time",
        "sysparm_query",
        sysparm_limit=10,
        timestamp_field="opened_at",
        ticket_type="incident",
        get_attachments=False,
        incident_name="description",
        oauth_params=OAUTH_PARAMS,
        jwt_params=jwt_params,
    )
    assert hasattr(client.snow_client, "jwt")
    assert client.snow_client.jwt == "jwt_token_stub"


class TestCredentialFlowEndToEnd:
    """End-to-end tests for the new basic_credentials / credentials flow in ServiceNowv2 main()."""

    BASE_PARAMS = {
        "url": "https://test.service-now.com",
        "insecure": False,
        "proxy": False,
        "use_oauth": False,
        "use_jwt": False,
        "incident_name": None,
        "file_tag_from_service_now": "FromServiceNow",
        "file_tag": "ForServiceNow",
        "comment_tag": "comments",
        "comment_tag_from_servicenow": "CommentFromServiceNow",
        "work_notes_tag": "work_notes",
        "work_notes_tag_from_servicenow": "WorkNoteFromServiceNow",
    }

    def test_basic_auth_with_basic_credentials(self, mocker, requests_mock):
        """
        Given:
            - basic_credentials param provides username and password.
            - OAuth is not enabled.
        When:
            - main() is called with the 'test-module' command.
        Then:
            - The request uses basic auth with the credentials from basic_credentials.
        """
        url = "https://test.service-now.com"
        params = {
            **self.BASE_PARAMS,
            "basic_credentials": {"identifier": "basic_user", "password": "basic_pass"},
            "credentials": {"identifier": "oauth_id", "password": "oauth_secret"},
        }
        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "command", return_value="test-module")
        requests_mock.get(
            f"{url}/api/now/table/incident",
            json={"result": [{"opened_at": "sometime", "number": "INC001"}]},
        )
        return_outputs_mock = mocker.patch("ServiceNowv2.return_outputs")

        main()

        # Verify basic auth was used with basic_credentials values
        assert requests_mock.called
        auth = requests_mock.request_history[0].headers.get("Authorization", "")
        # Basic auth header should be present (base64 encoded basic_user:basic_pass)
        assert "Basic" in auth
        return_outputs_mock.assert_called_once()

    def test_basic_auth_legacy_fallback(self, mocker, requests_mock):
        """
        Given:
            - basic_credentials param is empty (no username/password).
            - credentials param has identifier and password.
            - OAuth is not enabled.
        When:
            - main() is called with the 'test-module' command.
        Then:
            - The request uses basic auth with legacy fallback from credentials.
        """
        url = "https://test.service-now.com"
        params = {
            **self.BASE_PARAMS,
            "basic_credentials": {},
            "credentials": {"identifier": "legacy_user", "password": "legacy_pass"},
        }
        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "command", return_value="test-module")
        mocker.patch.object(demisto, "debug")
        requests_mock.get(
            f"{url}/api/now/table/incident",
            json={"result": [{"opened_at": "sometime", "number": "INC001"}]},
        )
        return_outputs_mock = mocker.patch("ServiceNowv2.return_outputs")

        main()

        # Verify basic auth was used with legacy fallback values
        assert requests_mock.called
        auth = requests_mock.request_history[0].headers.get("Authorization", "")
        assert "Basic" in auth
        return_outputs_mock.assert_called_once()

    def test_oauth_uses_credentials_for_client_id_secret(self, mocker, requests_mock):
        """
        Given:
            - use_oauth is True.
            - credentials provides client_id (identifier) and client_secret (password).
        When:
            - main() is called with the 'servicenow-oauth-test' command.
        Then:
            - OAuth flow is used (get_access_token is called).
        """
        url = "https://test.service-now.com"
        params = {
            **self.BASE_PARAMS,
            "use_oauth": True,
            "basic_credentials": {"identifier": "basic_user", "password": "basic_pass"},
            "credentials": {"identifier": "my_client_id", "password": "my_client_secret"},
        }
        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "command", return_value="servicenow-oauth-test")
        mocker.patch.object(ServiceNowClient, "get_access_token", return_value="mock_token")
        requests_mock.get(
            f"{url}/api/now/table/incident",
            json={"result": [{"opened_at": "sometime", "number": "INC001"}]},
        )

        main()

        # Verify OAuth was used (Bearer token in request)
        assert requests_mock.called
        auth = requests_mock.request_history[0].headers.get("Authorization", "")
        assert "Bearer" in auth

    def test_jwt_and_oauth_both_enabled_raises_error(self, mocker):
        """
        Given:
            - Both use_jwt and use_oauth are True.
        When:
            - main() is called.
        Then:
            - A ValueError is raised indicating only one auth method should be chosen.
        """
        params = {
            **self.BASE_PARAMS,
            "use_jwt": True,
            "use_oauth": True,
            "basic_credentials": {},
            "credentials": {"identifier": "id", "password": "secret"},
        }
        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "command", return_value="test-module")

        with pytest.raises(ValueError, match="authentication method"):
            main()

    def test_basic_auth_partial_credentials_triggers_fallback(self, mocker, requests_mock):
        """
        Given:
            - basic_credentials has username but no password.
            - credentials has identifier and password.
            - OAuth is not enabled.
        When:
            - main() is called with the 'test-module' command.
        Then:
            - The Client falls back to credentials for both username and password.
        """
        url = "https://test.service-now.com"
        params = {
            **self.BASE_PARAMS,
            "basic_credentials": {"identifier": "partial_user", "password": ""},
            "credentials": {"identifier": "fallback_user", "password": "fallback_pass"},
        }
        mocker.patch.object(demisto, "params", return_value=params)
        mocker.patch.object(demisto, "command", return_value="test-module")
        mocker.patch.object(demisto, "debug")
        requests_mock.get(
            f"{url}/api/now/table/incident",
            json={"result": [{"opened_at": "sometime", "number": "INC001"}]},
        )
        mocker.patch("ServiceNowv2.return_outputs")

        main()

        # Verify basic auth was used (fallback to credentials)
        assert requests_mock.called
        auth = requests_mock.request_history[0].headers.get("Authorization", "")
        assert "Basic" in auth


class TestCreateItemOrderFixes:
    """Tests for the two bugs fixed in XSUP-65101:
    1. servicecatalog order_now endpoint must use v1 even when instance is configured with v2.
    2. sysparm_no_validation support via the no_validation argument.
    """

    BASE_CLIENT_ARGS = (
        "https://test.service-now.com/api/now/v2/",  # server_url (v2 configured)
        "https://test.service-now.com/api/sn_sc/v2/",  # sc_server_url (v2 configured)
        "https://test.service-now.com/api/sn_chg_rest/v2/",  # cr_server_url
        "username",
        "password",
        False,  # verify
        "7 days",
        "",
        10,
        "opened_at",
        "incident",
        False,
        "incident",
    )

    def _make_client(self) -> Client:
        return Client(*self.BASE_CLIENT_ARGS, display_date_format="yyyy-MM-dd")

    def test_construct_url_order_now_downgrades_v2_to_v1(self):
        """
        Given
        - A Client configured with API version v2 (sc_server_url contains /v2/)
        When
        - _construct_url is called with sc_api=True and a path ending in /order_now
        Then
        - The resulting URL must use /v1/ instead of /v2/ (ServiceNow does not support v2 for order_now)
        """
        client = self._make_client()
        url = client._construct_url(
            custom_api="",
            sc_api=True,
            cr_api=False,
            path="servicecatalog/items/abc123/order_now",
            get_attachments=False,
        )
        assert "/v2/" not in url, "order_now URL must not contain /v2/"
        assert "/v1/" in url, "order_now URL must be downgraded to /v1/"

    def test_construct_url_order_now_no_downgrade_when_sc_api_false(self):
        """
        Given
        - A Client configured with API version v2
        When
        - _construct_url is called with sc_api=False and a path ending in /order_now
        Then
        - The resulting URL must still contain /v2/ (the downgrade is exclusive to sc_api calls)
        """
        client = self._make_client()
        url = client._construct_url(
            custom_api="",
            sc_api=False,
            cr_api=False,
            path="some/items/abc123/order_now",
            get_attachments=False,
        )
        assert "/v2/" in url, "Non-sc_api order_now URL must NOT be downgraded to /v1/"
        assert "/v1/" not in url, "Non-sc_api order_now URL must not contain /v1/"

    def test_create_item_order_no_validation_false_by_default(self, mocker):
        """
        Given
        - create_item_order is called without the no_validation argument
        When
        - The method builds the request body
        Then
        - sysparm_no_validation must NOT be present in the body (preserves existing behaviour)
        """
        client = self._make_client()
        mock_send = mocker.patch.object(
            client, "send_request", return_value={"result": {"sys_id": "12", "request_number": "REQ001"}}
        )

        client.create_item_order("item_id", "1", {})

        call_kwargs = mock_send.call_args
        body = call_kwargs[1].get("body") or call_kwargs[0][2]
        assert "sysparm_no_validation" not in body

    def test_create_item_order_no_validation_true_adds_flag(self, mocker):
        """
        Given
        - create_item_order is called with no_validation=True
        When
        - The method builds the request body
        Then
        - sysparm_no_validation=True must be present in the body
        """
        client = self._make_client()
        mock_send = mocker.patch.object(
            client, "send_request", return_value={"result": {"sys_id": "12", "request_number": "REQ001"}}
        )

        client.create_item_order("item_id", "1", {}, no_validation=True)

        call_kwargs = mock_send.call_args
        body = call_kwargs[1].get("body") or call_kwargs[0][2]
        assert body.get("sysparm_no_validation") == "true"


def test_upload_file_command_uses_basename(mocker):
    """
    Given:
        - A file entry with a name containing directory path components and no explicit file_name arg.
    When:
        - Calling upload_file_command.
    Then:
        - Verify that only the basename of the file name is used.
    """

    mocker.patch.object(
        demisto,
        "getFilePath",
        return_value={"path": "/tmp/testfile", "name": "/tmp/evil/../../../etc/passwd"},
    )

    mock_client = MagicMock()
    mock_client.get_table_name.return_value = "incident"
    mock_client.upload_file.return_value = {
        "result": {
            "file_name": "passwd",
            "download_link": "https://test.com/download",
            "sys_id": "abc123",
        }
    }

    # No file_name arg provided, so it should use basename from getFilePath
    args = {"id": "sys_id", "file_id": "entry_id", "ticket_type": "incident"}
    upload_file_command(mock_client, args)

    # Verify upload_file was called with the sanitized basename
    call_args = mock_client.upload_file.call_args[0]
    file_name_used = call_args[2]
    assert file_name_used == "passwd"
    assert os.path.basename(file_name_used) == file_name_used


def test_update_remote_system_with_entries_uses_basename(mocker):
    """
    Given:
        - A file entry with a name containing directory path components.
    When:
        - Calling update_remote_system_with_entries.
    Then:
        - Verify that only the basename of the file name is used when uploading.
    """
    mocker.patch.object(
        demisto,
        "getFilePath",
        return_value={"path": "/tmp/testfile", "name": "/tmp/evil/../../../etc/passwd"},
    )
    mocker.patch.object(demisto, "debug")

    mock_client = MagicMock()

    entries = [{"id": "entry_id", "type": 3, "tags": []}]
    params = {
        "file_tag_from_service_now": "FileFromServiceNow",
        "file_tag": "file",
    }
    update_remote_system_with_entries(mock_client, entries, params, "ticket_id", "incident")

    call_args = mock_client.upload_file.call_args[0]
    # The filename is basename (no path traversal) + "_mirrored_from_xsoar" suffix
    file_name_used = call_args[2]
    assert "passwd" in file_name_used
    assert "/" not in file_name_used
    assert ".." not in file_name_used
    assert os.path.basename(file_name_used) == file_name_used