PATHelpdeskAdvanced

Improve the effectiveness of your service provision and resources, and the quality of your IT department.

Utilities · PAT Helpdesk Advanced

Details

IDPATHelpdeskAdvanced
ProviderPATServicesLTD
CategoryUtilities
From Version6.9.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Improve the effectiveness of your service provision and resources, and the quality of your IT department.
This integration was integrated and tested with version 11.2.3 of PATHelpdeskAdvanced.

Configure PAT HelpdeskAdvanced in Cortex

Parameter Required
PAT Helpdesk URL True
Username True
Password True
Trust any certificate (not secure) False
Use system proxy settings False

Commands

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

hda-create-ticket


Create a new ticket.

Base Command

hda-create-ticket

Input

Argument Name Description Required
object_type_id The object type ID for the ticket. Required
ticket_status_id The status ID for the ticket. Use hda-list-ticket-statuses to get a list of status IDs. Required
ticket_priority_id The priority ID for the ticket. Use hda-list-ticket-priorities to get a list of priority IDs. Required
object_description The description of the object. Optional
ticket_classification_id The classification ID for the ticket. Optional
ticket_type_id The type ID for the ticket. Optional
contact_id The contact ID for the ticket. Optional
subject The subject for the ticket. Optional
problem The problem description for the ticket. Optional
site The site for the ticket. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Ticket.AccountID string The account ID of the ticket.
HelpdeskAdvanced.Ticket.AssetCategoryID string The asset category ID of the ticket.
HelpdeskAdvanced.Ticket.AssetID string The asset ID of the ticket.
HelpdeskAdvanced.Ticket.AssignedUserGroupID string The assigned user group ID of the ticket.
HelpdeskAdvanced.Ticket.AssignedUserID string The assigned user ID of the ticket.
HelpdeskAdvanced.Ticket.AssignedUserOrGroupID string The assigned user or group ID of the ticket.
HelpdeskAdvanced.Ticket.BilledTokens string The billed tokens of the ticket.
HelpdeskAdvanced.Ticket.BusinessFunctionID string The business function ID of the ticket.
HelpdeskAdvanced.Ticket.C12 string The C12 field of the ticket.
HelpdeskAdvanced.Ticket.C13 string The C13 field of the ticket.
HelpdeskAdvanced.Ticket.C134C string The C134C field of the ticket.
HelpdeskAdvanced.Ticket.C14 string The C14 field of the ticket.
HelpdeskAdvanced.Ticket.C15 string The C15 field of the ticket.
HelpdeskAdvanced.Ticket.CalendarID string The calendar ID of the ticket.
HelpdeskAdvanced.Ticket.ClosedByUserID string The ID of the user who closed the ticket.
HelpdeskAdvanced.Ticket.ClosureDate string The closure date of the ticket.
HelpdeskAdvanced.Ticket.ContactID string The contact ID of the ticket.
HelpdeskAdvanced.Ticket.CostCenterID string The cost center ID of the ticket.
HelpdeskAdvanced.Ticket.CustomerContractID string The customer contract ID of the ticket.
HelpdeskAdvanced.Ticket.Date date The date of the ticket.
HelpdeskAdvanced.Ticket.DefaultSolutionID string The default solution ID of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskDuration string The estimated task duration of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskStartDate string The estimated task start date of the ticket.
HelpdeskAdvanced.Ticket.ExpirationDate date The expiration date of the ticket.
HelpdeskAdvanced.Ticket.FirstUpdateUserID string The ID of the user who first updated the ticket.
HelpdeskAdvanced.Ticket.FullText string The full text of the ticket.
HelpdeskAdvanced.Ticket.ID string The ID of the ticket.
HelpdeskAdvanced.Ticket.ImpactID string The impact ID of the ticket.
HelpdeskAdvanced.Ticket.IsNew boolean Whether the ticket is new or not.
HelpdeskAdvanced.Ticket.KnownIssue string The known issue field of the ticket.
HelpdeskAdvanced.Ticket.LanguageID string The language ID of the ticket.
HelpdeskAdvanced.Ticket.LastExpirationDate date The last expiration date of the ticket.
HelpdeskAdvanced.Ticket.LastExpirationID string The last expiration ID of the ticket.
HelpdeskAdvanced.Ticket.LocationID string The location ID of the ticket.
HelpdeskAdvanced.Ticket.MailBoxID string The mailbox ID of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationDate string The next expiration date of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationID string The next expiration ID of the ticket.
HelpdeskAdvanced.Ticket.ObjectDescription string The object description of the ticket.
HelpdeskAdvanced.Ticket.ObjectEntity string The object entity of the ticket.
HelpdeskAdvanced.Ticket.ObjectTypeID string The object type ID of the ticket.
HelpdeskAdvanced.Ticket.OwnerUserGroupID string The owner user group ID of the ticket.
HelpdeskAdvanced.Ticket.OwnerUserID string The owner user ID of the ticket.
HelpdeskAdvanced.Ticket.ParentRecurringTicketID string The parent recurring ticket ID of the ticket.
HelpdeskAdvanced.Ticket.ParentTicketID string The parent ticket ID of the ticket.
HelpdeskAdvanced.Ticket.Problem string The problem field of the ticket.
HelpdeskAdvanced.Ticket.ProblemHTML string The problem field in HTML format of the ticket.
HelpdeskAdvanced.Ticket.RemoteID string The remote ID of the ticket.
HelpdeskAdvanced.Ticket.Score string The score of the ticket.
HelpdeskAdvanced.Ticket.ServiceID string The service ID of the ticket.
HelpdeskAdvanced.Ticket.Site string The site of the ticket.
HelpdeskAdvanced.Ticket.SiteUnRead string Whether the ticket is unread for the site.
HelpdeskAdvanced.Ticket.Solicits string Whether the ticket solicits feedback.
HelpdeskAdvanced.Ticket.SolutionHTML string The solution in HTML format of the ticket.
HelpdeskAdvanced.Ticket.SourceMailBoxID string The source mailbox ID of the ticket.
HelpdeskAdvanced.Ticket.Subject string The subject of the ticket.
HelpdeskAdvanced.Ticket.SupplierID string The supplier ID of the ticket.
HelpdeskAdvanced.Ticket.Tag string The tag of the ticket.
HelpdeskAdvanced.Ticket.TaskEffort string The task effort of the ticket.
HelpdeskAdvanced.Ticket.TemplateTicketID string The template ticket ID of the ticket.
HelpdeskAdvanced.Ticket.TicketClassificationID string The ticket classification ID of the ticket.
HelpdeskAdvanced.Ticket.TicketCode string The ticket code of the ticket.
HelpdeskAdvanced.Ticket.TicketPriorityID string The ticket priority ID of the ticket.
HelpdeskAdvanced.Ticket.TicketSolutionID string The ticket solution ID of the ticket.
HelpdeskAdvanced.Ticket.TicketSourceID string The ticket source ID of the ticket.
HelpdeskAdvanced.Ticket.TicketStatusID string The ticket status ID of the ticket.
HelpdeskAdvanced.Ticket.TicketTypeID string The ticket type ID of the ticket.
HelpdeskAdvanced.Ticket.UnRead string Whether the ticket is unread.
HelpdeskAdvanced.Ticket.UrgencyID string The urgency ID of the ticket.
HelpdeskAdvanced.Ticket.UserID string The user ID of the ticket.

Command example

!hda-create-ticket object_type_id=T2 ticket_priority_id=P4 ticket_status_id=S3

Context Example

{
    "HelpdeskAdvanced": {
        "Ticket": {
            "data": {
                "AccountID": "",
                "AssetCategoryID": "",
                "AssetID": "",
                "AssignedUserGroupID": "",
                "AssignedUserID": "S00000C",
                "AssignedUserOrGroupID": "S00000C",
                "BilledTokens": 0,
                "BusinessFunctionID": "",
                "C12": null,
                "C13": null,
                "C134C": null,
                "C14": null,
                "C15": null,
                "CalendarID": "",
                "ClosedByUserID": "",
                "ClosureDate": null,
                "ContactID": "",
                "CostCenterID": "",
                "CustomerContractID": "",
                "Date": "2023-10-04T07:45:09Z",
                "DefaultSolutionID": "",
                "EstimatedTaskDuration": 0,
                "EstimatedTaskStartDate": null,
                "ExpirationDate": "2023-10-04T15:45:00Z",
                "FirstUpdateUserID": "S00000C",
                "FullText": "",
                "ID": "12345678C",
                "ImpactID": "",
                "IsNew": false,
                "KnownIssue": false,
                "LanguageID": "0",
                "LastExpirationDate": "2998-12-31T23:00:00Z",
                "LastExpirationID": "",
                "LocationID": "",
                "MailBoxID": "DEF_MBOX",
                "NextExpirationDate": "2998-12-31T23:00:00Z",
                "NextExpirationID": "",
                "ObjectDescription": "12345678C: ",
                "ObjectEntity": "Incident",
                "ObjectTypeID": "T2",
                "OwnerUserGroupID": "G66C",
                "OwnerUserID": "S00000C",
                "ParentRecurringTicketID": "",
                "ParentTicketID": "",
                "Problem": "",
                "ProblemHTML": "",
                "RemoteID": "",
                "Score": 0,
                "ServiceID": "",
                "Site": null,
                "SiteUnRead": true,
                "Solicits": 0,
                "SolutionHTML": "",
                "SourceMailBoxID": "DEFAULT",
                "Subject": "",
                "SupplierID": "",
                "Tag": "",
                "TaskEffort": 0,
                "TemplateTicketID": "",
                "TicketClassificationID": "INC",
                "TicketCode": "",
                "TicketPriorityID": "P4",
                "TicketSolutionID": "",
                "TicketSourceID": "",
                "TicketStatusID": "S3",
                "TicketTypeID": "T2",
                "UnRead": true,
                "UrgencyID": "",
                "UserID": ""
            },
            "requestToken": "token",
            "result": {
                "code": "0",
                "desc": "",
                "parameters": null,
                "subcode": null
            },
            "success": true,
            "total": 1
        }
    }
}

Human Readable Output

Ticket Created

Object Description Object Entity Ticket Classification ID Is New Expiration Date First Update User ID Owner User ID Date Assigned User ID
12345678C: Incident INC false 2023-10-04T15:45:00Z S00000C S00000C 2023-10-04T07:45:09Z S00000C

hda-list-tickets


List tickets.

Base Command

hda-list-tickets

Input

Argument Name Description Required
ticket_id The ID of the ticket to retrieve. Optional
filter Filters to apply to the ticket list. Optional
page The page number to retrieve. Optional
page_size The number of tickets per page. Optional
limit The maximum number of tickets to return. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Ticket.AccountID string The account ID of the ticket.
HelpdeskAdvanced.Ticket.AssetID string The asset ID of the ticket.
HelpdeskAdvanced.Ticket.BilledTokens string The billed tokens of the ticket.
HelpdeskAdvanced.Ticket.CalendarID string The calendar ID of the ticket.
HelpdeskAdvanced.Ticket.ClosureDate string The closure date of the ticket.
HelpdeskAdvanced.Ticket.ContactID string The contact ID of the ticket.
HelpdeskAdvanced.Ticket.CustomerContractID string The customer contract ID of the ticket.
HelpdeskAdvanced.Ticket.Date string The date of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskDuration string The estimated task duration of the ticket.
HelpdeskAdvanced.Ticket.EstimatedTaskStartDate string The estimated task start date of the ticket.
HelpdeskAdvanced.Ticket.ExpirationDate string The expiration date of the ticket.
HelpdeskAdvanced.Ticket.FirstUpdateUserID string The ID of the user who first updated the ticket.
HelpdeskAdvanced.Ticket.ID string The ID of the ticket.
HelpdeskAdvanced.Ticket.KnownIssue string The known issue field of the ticket.
HelpdeskAdvanced.Ticket.LanguageID string The language ID of the ticket.
HelpdeskAdvanced.Ticket.LastExpirationDate string The last expiration date of the ticket.
HelpdeskAdvanced.Ticket.LocationID string The location ID of the ticket.
HelpdeskAdvanced.Ticket.MailBoxID string The mailbox ID of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationDate string The next expiration date of the ticket.
HelpdeskAdvanced.Ticket.NextExpirationID string The next expiration ID of the ticket.
HelpdeskAdvanced.Ticket.OwnerUserID string The owner user ID of the ticket.
HelpdeskAdvanced.Ticket.Problem string The problem field of the ticket.
HelpdeskAdvanced.Ticket.ProblemHTML string The problem field in HTML format of the ticket.
HelpdeskAdvanced.Ticket.Score string The score of the ticket.
HelpdeskAdvanced.Ticket.ServiceID string The service ID of the ticket.
HelpdeskAdvanced.Ticket.SiteUnRead string Whether the ticket is unread for the site.
HelpdeskAdvanced.Ticket.Solicits string Whether the ticket solicits feedback.
HelpdeskAdvanced.Ticket.Solution string The solution of the ticket.
HelpdeskAdvanced.Ticket.SolutionHTML string The solution in HTML format of the ticket.
HelpdeskAdvanced.Ticket.Subject string The subject of the ticket.
HelpdeskAdvanced.Ticket.SupplierID string The supplier ID of the ticket.
HelpdeskAdvanced.Ticket.TaskEffort string The task effort of the ticket.
HelpdeskAdvanced.Ticket.TicketClassificationID string The ticket classification ID of the ticket.
HelpdeskAdvanced.Ticket.TicketTypeID string The ticket type ID of the ticket.
HelpdeskAdvanced.Ticket.UrgencyID string The urgency ID of the ticket.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "Ticket": [
            {
                "AccountID": "C00C",
                "AssetID": null,
                "BilledTokens": 0,
                "CalendarID": null,
                "ClosureDate": "2019-04-28T07:04:19Z",
                "ContactID": "S24007C",
                "CustomerContractID": null,
                "Date": "2019-04-25T07:03:48Z",
                "EstimatedTaskDuration": 0,
                "EstimatedTaskStartDate": null,
                "ExpirationDate": "2019-04-28T15:03:00Z",
                "FirstUpdateUserID": "S00000C",
                "ID": "10000000C",
                "KnownIssue": false,
                "LanguageID": 5,
                "LastExpirationDate": "2019-04-29T10:00:00Z",
                "LocationID": "F3C",
                "MailBoxID": "DEF_MBOX",
                "NextExpirationDate": "2019-04-29T10:00:00Z",
                "NextExpirationID": "244C",
                "OwnerUserID": "S00000C",
                "Problem": "Problem",
                "ProblemHTML": "<p>Problem</p>",
                "Score": 0,
                "ServiceID": "S000X",
                "SiteUnRead": null,
                "Solicits": null,
                "Solution": null,
                "SolutionHTML": null,
                "Subject": "Problem",
                "SupplierID": null,
                "TaskEffort": null,
                "TicketClassificationID": "INC",
                "TicketTypeID": "T4",
                "UrgencyID": "U2"
            }
        ]
    }
}

Human Readable Output

Tickets

Ticket ID Subject Solution Date Service ID Problem Contact ID Owner User ID Account ID
10000000C Problem   2019-04-25T07:03:48Z S000X Problem S24007C S00000C C00C

hda-add-ticket-attachment


Add an attachment to a ticket.

Base Command

hda-add-ticket-attachment

Input

Argument Name Description Required
ticket_id The ticket ID to retrieve details for. Required
entry_id The file entry ID to attach. Required

Context Output

There is no context output for this command.

hda-list-ticket-attachments


List attachments for a ticket.

Base Command

hda-list-ticket-attachments

Input

Argument Name Description Required
ticket_id The ticket ID to retrieve details for. Required
limit The maximum number of tickets to return. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Ticket.Attachment.BlobID string The blob ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.ContentType string The content type of the attachment.
HelpdeskAdvanced.Ticket.Attachment.Description string The description of the attachment.
HelpdeskAdvanced.Ticket.Attachment.EmailID string The email ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.FileName string The file name of the attachment.
HelpdeskAdvanced.Ticket.Attachment.FirstUpdate string The date and time when the attachment was first updated.
HelpdeskAdvanced.Ticket.Attachment.FirstUpdateUserID string The ID of the user who first uploaded the attachment.
HelpdeskAdvanced.Ticket.Attachment.ID string The ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.IsNew string Indicates whether the attachment is new.
HelpdeskAdvanced.Ticket.Attachment.KBSize string The size of the attachment in KB.
HelpdeskAdvanced.Ticket.Attachment.LastUpdate string The date and time when the attachment was last updated.
HelpdeskAdvanced.Ticket.Attachment.LastUpdateUserID string The ID of the user who last updated the attachment.
HelpdeskAdvanced.Ticket.Attachment.Note string Any additional notes about the attachment.
HelpdeskAdvanced.Ticket.Attachment.ObjectDescription string The description of the object the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.ObjectEntity string The object entity of the attachment.
HelpdeskAdvanced.Ticket.Attachment.ObjectTypeID string The object type ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.OwnerUserID string The owner user ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.ParentObject string The parent object the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.ParentObjectID string The ID of the parent object the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.RemoteID string The remote ID of the attachment.
HelpdeskAdvanced.Ticket.Attachment.Site string The site ID the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.TicketID string The ticket ID the attachment is associated with.
HelpdeskAdvanced.Ticket.Attachment.UniqueID string A unique ID for the attachment.

Command example

!hda-list-ticket-attachments ticket_id=A0000001C

Context Example

{
    "HelpdeskAdvanced": {
        "Ticket": {
            "Attachment": [
                {
                    "BlobID": "A0000000C",
                    "ContentType": "text/plain",
                    "Description": "test.txt",
                    "EmailID": "",
                    "FileName": "test.txt",
                    "FirstUpdate": null,
                    "FirstUpdateUserID": "S00000C",
                    "ID": "A0000000C",
                    "IsNew": false,
                    "KBSize": 0,
                    "LastUpdate": "2023-09-29T08:30:04Z",
                    "LastUpdateUserID": "",
                    "Note": "",
                    "ObjectDescription": "test.txt",
                    "ObjectEntity": "Attachment",
                    "ObjectTypeID": "DEFAULT",
                    "OwnerUserID": "",
                    "ParentObject": "Incident",
                    "ParentObjectID": "A0000001C",
                    "RemoteID": "",
                    "Site": null,
                    "TicketID": "A0000001C",
                    "UniqueID": "00000000-0000-0000-0000-000000000000"
                }
            ]
        }
    }
}

Human Readable Output

Attachments of A0000001C

File Name Last Update Description Object Description First Update User ID Object Entity Content Type
test.txt 2023-09-29T08:30:04Z test.txt test.txt S00000C Attachment text/plain

hda-add-ticket-comment


Add a comment to a ticket.

Base Command

hda-add-ticket-comment

Input

Argument Name Description Required
ticket_id The ticket ID to retrieve details for. Required
comment The comment text. Required
site_visible Whether the ticket is visible to the customer site. Possible values are: True, False. Default is False. Required

Context Output

There is no context output for this command.

Command example

!hda-add-ticket-comment comment="here be comment" site_visible=True ticket_id=A0000001C

Human Readable Output

Comment was succesfully added to A0000001C

hda-list-ticket-statuses


List ticket statuses.

Base Command

hda-list-ticket-statuses

Input

Argument Name Description Required
limit The maximum number of statuses to return. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.TicketStatus.ID Number The status ID.
HelpdeskAdvanced.TicketStatus.Name String The status name.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "TicketStatus": [
            {
                "Description": "New",
                "ID": "S1"
            },
            {
                "Description": "Other",
                "ID": "S10"
            }
        ]
    }
}

Human Readable Output

Results

Description ID
New S1
Other S10

hda-change-ticket-status


Change the status of a ticket.

Base Command

hda-change-ticket-status

Input

Argument Name Description Required
ticket_id The ID of the ticket to change status for. Required
status_id The ID of the status to change the ticket to. Required
note An optional note to add with the status change. Optional

Context Output

There is no context output for this command.

Command example

!hda-change-ticket-status status_id=S2 ticket_id=1111111111C note=testing debug-mode=true

Human Readable Output

Changed status of ticket 1111111111C to S2 successfully.

hda-list-ticket-priorities


List ticket priorities.

Base Command

hda-list-ticket-priorities

Input

Argument Name Description Required
ticket_id The ID of the ticket to change status for. Required

Context Output

Path Type Description
HelpdeskAdvanced.TicketPriority.ID Number The priority ID.
HelpdeskAdvanced.TicketPriority.Name String The priority name.

hda-get-ticket-history


Get ticket history.

Base Command

hda-get-ticket-history

Input

Argument Name Description Required
ticket_id The ID of the ticket to change status for. Required

Context Output

Path Type Description
HelpdeskAdvanced.TicketHistory.TicketID string The ticket ID associated with the history entry.
HelpdeskAdvanced.TicketHistory.AccountID string The account ID of the ticket.
HelpdeskAdvanced.TicketHistory.Attachments string List of attachments for the ticket.
HelpdeskAdvanced.TicketHistory.AutEmailCounter string The AUT email counter.
HelpdeskAdvanced.TicketHistory.ContactID string The contact ID of the ticket.
HelpdeskAdvanced.TicketHistory.Data.Comment string The comment text.
HelpdeskAdvanced.TicketHistory.Data.Date string The date of the history entry.
HelpdeskAdvanced.TicketHistory.Data.From string The user who made the change.
HelpdeskAdvanced.TicketHistory.Data.FromID string The ID of the user who made the change.
HelpdeskAdvanced.TicketHistory.Data.Problem string The problem associated with the history entry.
HelpdeskAdvanced.TicketHistory.Data.To string The user/team the ticket was assigned to.
HelpdeskAdvanced.TicketHistory.Data.ToID string The ID of the user/team the ticket was assigned to.
HelpdeskAdvanced.TicketHistory.ExternalAction string The external action associated with the history entry.
HelpdeskAdvanced.TicketHistory.FullName string The full name of the ticket.
HelpdeskAdvanced.TicketHistory.HistoryID string The ID of the history entry.
HelpdeskAdvanced.TicketHistory.OperationDescription string The description of the operation.
HelpdeskAdvanced.TicketHistory.OperationTypeID string The type ID of the operation.
HelpdeskAdvanced.TicketHistory.UpdateDate date The date the ticket was last updated.
HelpdeskAdvanced.TicketHistory.UserID string The user ID of the ticket.
HelpdeskAdvanced.TicketHistory.Username string The username associated with the ticket.

Command example

!hda-get-ticket-history ticket_id=1111111C

Context Example

{
    "HelpdeskAdvanced": {
        "TicketHistory": {
            "AccountID": "",
            "Attachments": null,
            "TicketID": "1111111C",
            "AutEmailCounter": 0,
            "ContactID": "",
            "Data": {
                "Comment": "testing",
                "From": "Solved",
                "FromID": "S6",
                "To": "In Progress",
                "ToID": "S2"
            },
            "ExternalAction": false,
            "FullName": "John Doe",
            "HistoryID": 5667526,
            "OperationDescription": "Status change",
            "OperationTypeID": 20,
            "UpdateDate": "2023-10-04T07:45:35Z",
            "UserID": "S00000C",
            "Username": "username"
        }
    }
}

Human Readable Output

Ticket History: 1111111C

Update Date Ticket ID Operation Type ID Operation Description Full Name
2023-10-04T07:45:35Z 1111111C   Status change John Doe

hda-list-users


List users.

Base Command

hda-list-users

Input

Argument Name Description Required
user_id The ID of the user to retrieve details for. Optional
page The page number. Optional
page_size The number of users to return per page. Optional
limit The maximum number of users to return per page. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.User.ID string The user ID.
HelpdeskAdvanced.User.Email string The user’s email address.
HelpdeskAdvanced.User.FirstName string The user’s first name.
HelpdeskAdvanced.User.LastName string The user’s last name.
HelpdeskAdvanced.User.Phone string The user’s phone number.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "User": [
            {
                "EMail": "Guest@example.com",
                "FirstName": null,
                "ID": "#GUEST#",
                "LastName": "Guest",
                "Phone": null
            }
        ]
    }
}

Human Readable Output

PAT HelpDeskAdvanced Users

Phone ID First Name Last Name E Mail
  #GUEST#   Guest Guest@example.com

hda-list-groups


List groups.

Base Command

hda-list-groups

Input

Argument Name Description Required
group_id The ID of the group to retrieve details for. Optional
page The page number. Optional
page_size The number of groups to return per page. Optional
limit The maximum number of groups to return per page. Default is 50. Optional

Context Output

Path Type Description
HelpdeskAdvanced.Group.ID string The group ID.
HelpdeskAdvanced.Group.ObjectTypeID string The object type ID of the group.
HelpdeskAdvanced.Group.Description string The group description.

Command example


#### Context Example

```json
{
    "HelpdeskAdvanced": {
        "Group": [
            {
                "Description": "Example",
                "ID": "G000C",
                "ObjectTypeID": "65"
            }
        ]
    }
}

Human Readable Output

PAT HelpDeskAdvanced Groups

Group ID Description Object Type ID
G000C Example 65

Configuration parameters

  • base_url — PAT Helpdesk URL (required)
  • credentials — Username (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (11)

  • hda-add-ticket-attachment

    Add an attachment to a ticket.

  • hda-add-ticket-comment

    Add a comment to a ticket.

  • hda-change-ticket-status

    Change the status of a ticket.

  • hda-create-ticket

    Create a new ticket.

  • hda-get-ticket-history

    Get ticket history.

  • hda-list-groups

    List groups.

  • hda-list-ticket-attachments

    List attachments for a ticket.

  • hda-list-ticket-priorities

    List ticket priorities.

  • hda-list-ticket-statuses

    List ticket statuses.

  • hda-list-tickets

    List tickets.

  • hda-list-users

    List users.

from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import unquote

import freezegun
import pytest
from PATHelpdeskAdvanced import (
    DATETIME_FORMAT,
    Client,
    DemistoException,
    Field,
    Filter,
    PaginateArgs,
    convert_response_dates,
    demisto,
    json,
    list_groups_command,
    list_ticket_attachments_command,
    list_users_command,
    paginate,
    pat_table_to_markdown,
)


@pytest.mark.parametrize(
    "demisto_name, expected_demisto_name, expected_hda_name",
    [
        ("incident_id", "incident_id", "IncidentID"),
        ("unread_email_html", "unread_email_html", "UnReadEmailHTML"),
        ("task", "task", "Task"),
        ("unread", "unread", "UnRead"),
        ("user_id_html", "user_id_html", "UserIDHTML"),
    ],
)
def test_field(demisto_name, expected_demisto_name, expected_hda_name):
    """
    Given a Demisto name, when initializing an instance of MyClass,
    then the demisto_name attribute should be set to the given Demisto name,
    and the hda_name attribute should be set to the expected HDA name.

    Args:
        demisto_name (str): The Demisto name.
        expected_demisto_name (str): The expected value of the demisto_name attribute.
        expected_hda_name (str): The expected value of the hda_name attribute.

    Returns:
        None
    """
    field = Field(demisto_name)
    assert field.demisto_name == expected_demisto_name
    assert field.hda_name == expected_hda_name


def test_converts_date_fields():
    """
    Given a response dict with date fields
    When convert_response_dates is called
    Then date fields are converted to datetime, and others are untouched
    """
    EPOCH_2023_INT = 1693573200000
    EPOCH_2022_INT = 1641042000000

    STR_2023 = (datetime.fromtimestamp(EPOCH_2023_INT / 1000, tz=timezone.utc)).strftime(DATETIME_FORMAT)  # noqa: UP017
    STR_2022 = (datetime.fromtimestamp(EPOCH_2022_INT / 1000, tz=timezone.utc)).strftime(DATETIME_FORMAT)  # noqa: UP017

    raw = {
        "Date1": f"/Date({EPOCH_2023_INT})/",
        "Date2": [f"/Date({EPOCH_2023_INT})/", f"/Date({EPOCH_2022_INT})/", "🕒"],
        "other": [EPOCH_2023_INT, STR_2023],
    }

    result = convert_response_dates(raw)

    assert result["Date1"] == STR_2023
    assert result["Date2"] == [STR_2023, STR_2022, "🕒"]
    assert result["other"] == raw["other"]


@pytest.fixture
def mocked_client(mocker):
    """Given a mocked Client instance"""
    return mocker.Mock()


class TestCommands:
    @classmethod
    def test_create_ticket(cls, mocked_client):
        from PATHelpdeskAdvanced import create_ticket_command

        """
        Given a client instance and arguments for creating a ticket
        When create_ticket_command is called with the client and args
        Then it should return a CommandResults instance with the expected outputs
        """
        args = {"subject": "Test ticket", "problem": "Something went wrong"}
        mocked_client.create_ticket.return_value = {
            "id": 123,
            "data": {
                "ObjectDescription": "Test ticket",
                "ObjectEntity": "ticket",
                "Solution": "Fixed the issue",
                "TicketClassificationId": 1,
                "IsNew": True,
                "ExpirationDate": "/Date(1609459200000)/",
                "FirstUpdateUserId": 1,
                "OwnerUserId": 2,
                "Date": "/Date(1609459200000)/",
                "AssignedUserId": 2,
            },
        }

        result = create_ticket_command(mocked_client, args)
        assert result.outputs == {
            "id": 123,
            "data": {
                "ObjectDescription": "Test ticket",
                "ObjectEntity": "ticket",
                "Solution": "Fixed the issue",
                "TicketClassificationId": 1,
                "IsNew": True,
                "ExpirationDate": "2021-01-01T00:00:00Z",
                "FirstUpdateUserId": 1,
                "OwnerUserId": 2,
                "Date": "2021-01-01T00:00:00Z",
                "AssignedUserId": 2,
            },
        }
        assert result.raw_response == mocked_client.create_ticket.return_value
        assert result.readable_output == (
            "### Ticket Created\n|Object Description|Object Entity|Solution|Is New|Expiration Date|Date|\n"
            "|---|---|---|---|---|---|\n| "
            "Test ticket | ticket | Fixed the issue | true | 2021-01-01T00:00:00Z | 2021-01-01T00:00:00Z |\n"
        )

    @classmethod
    def test_get_ticket_history(cls, mocked_client):
        from PATHelpdeskAdvanced import get_ticket_history_command

        """
        Given a client instance and arguments for getting a ticket's history
        When get_ticket_history is called with the client and args
        Then it should return a CommandResults instance with the expected outputs
        """
        TICKET_ID = "dummy_ticket_id"
        history = [
            {
                "AccountID": "",
                "Attachments": None,
                "AutEmailCounter": 0,
                "ContactID": "",
                "Data": {
                    "Comment": "testing",
                    "From": "Solved",
                    "FromID": "S6",
                    "To": "In Progress",
                    "ToID": "S2",
                },
                "ExternalAction": None,
                "FullName": "John Doe",
                "HistoryID": 5665556,
                "OperationDescription": "Status change",
                "OperationTypeID": 20,
                "UpdateDate": "2023-10-04T07:45:35Z",
                "UserID": "S00000C",
                "Username": "username",
            }
        ]
        mocked_client.get_ticket_history.return_value = history

        result = get_ticket_history_command(mocked_client, {"ticket_id": TICKET_ID})
        assert result.outputs_prefix == "HelpdeskAdvanced.TicketHistory"
        assert result.outputs == [value | {"TicketID": TICKET_ID} for value in history]
        assert result.raw_response == history
        assert result.readable_output == (
            "### Ticket History: dummy_ticket_id\n|Aut Email Counter|Data|Full Name|History ID|Operation Description|"
            "Operation Type ID|Update Date|User ID|Username|Ticket ID|\n|---|---|---|---|---|---|---|---|---|---|\n|"
            "  | ***Comment***: testing<br>***From***: Solved<br>***FromID***: S6<br>***To***: In Progress<br>***ToID***:"
            " S2 | John Doe |  | Status change |  | 2023-10-04T07:45:35Z | S00000C | username | dummy_ticket_id |\n"
        )

    @classmethod
    def test_list_tickets(cls, mocked_client):
        """
        Given a client instance
        When list_tickets_command is called
        Then it should return CommandResults with expected outputs
        """
        from PATHelpdeskAdvanced import list_tickets_command

        data = [
            {
                "ExpirationDate": "/Date(1111111111111)/",
                "Subject": "Support Request",
                "Solution": "Solution text redacted",
                "LocationID": "XXX",
                "Solicits": None,
                "Date": "/Date(1111111111111)/",
                "FirstUpdateUserID": "AA",
                "BilledTokens": 0.0,
                "ServiceID": "XXX",
                "TicketTypeID": "XX",
                "Problem": "Problem description redacted",
                "SiteUnRead": None,
                "ClosureDate": "/Date(1111111111111)/",
                "ContactID": "XXX",
                "SupplierID": None,
                "CustomerContractID": None,
                "EstimatedTaskStartDate": None,
                "OwnerUserID": "AA",
                "Score": 0,
                "LastExpirationDate": "/Date(1111111111111)/",
                "KnownIssue": False,
                "MailBoxID": "XXX",
                "NextExpirationID": "XXX",
                "TicketClassificationID": "XXX",
                "CalendarID": None,
                "UrgencyID": "XX",
                "TaskEffort": None,
                "ProblemHTML": "Problem description redacted",
                "AccountID": "XXX",
                "NextExpirationDate": "/Date(1111111111111)/",
                "AssetID": "XXX",
                "ID": "10000002C",
                "SolutionHTML": "Solution text redacted",
                "EstimatedTaskDuration": 0,
                "LanguageID": 5,
            },
            {
                "ExpirationDate": "/Date(1111111111111)/",
                "Subject": "On-site Support",
                "Solution": "Ticket correctly created through template.",
                "LocationID": "XXX",
                "Solicits": None,
                "Date": "/Date(1111111111111)/",
                "FirstUpdateUserID": "AA",
                "BilledTokens": 0.0,
                "ServiceID": "XXX",
                "TicketTypeID": "XX",
                "Problem": "Problem description redacted",
                "SiteUnRead": 0,
                "ClosureDate": "/Date(1111111111111)/",
                "ContactID": "XXX",
                "SupplierID": None,
                "CustomerContractID": None,
                "EstimatedTaskStartDate": None,
                "OwnerUserID": "XXX",
                "Score": 0,
                "LastExpirationDate": "/Date(1111111111111)/",
                "KnownIssue": False,
                "MailBoxID": "XXX",
                "NextExpirationID": None,
                "TicketClassificationID": "XXX",
                "CalendarID": None,
                "UrgencyID": None,
                "TaskEffort": None,
                "ProblemHTML": "Problem description redacted",
                "AccountID": "XXX",
                "NextExpirationDate": "/Date(1111111111111)/",
                "AssetID": None,
                "ID": "10000003C",
                "SolutionHTML": "Solution text redacted",
                "EstimatedTaskDuration": 0,
            },
        ]
        mocked_client.list_tickets.return_value = {"data": data}

        result = list_tickets_command(mocked_client, {})

        assert result.outputs == [
            {
                "ExpirationDate": "2005-03-18T01:58:31Z",
                "Subject": "Support Request",
                "Solution": "Solution text redacted",
                "LocationID": "XXX",
                "Solicits": None,
                "Date": "2005-03-18T01:58:31Z",
                "FirstUpdateUserID": "AA",
                "BilledTokens": 0.0,
                "ServiceID": "XXX",
                "TicketTypeID": "XX",
                "Problem": "Problem description redacted",
                "SiteUnRead": None,
                "ClosureDate": "2005-03-18T01:58:31Z",
                "ContactID": "XXX",
                "SupplierID": None,
                "CustomerContractID": None,
                "EstimatedTaskStartDate": None,
                "OwnerUserID": "AA",
                "Score": 0,
                "LastExpirationDate": "2005-03-18T01:58:31Z",
                "KnownIssue": False,
                "MailBoxID": "XXX",
                "NextExpirationID": "XXX",
                "TicketClassificationID": "XXX",
                "CalendarID": None,
                "UrgencyID": "XX",
                "TaskEffort": None,
                "ProblemHTML": "Problem description redacted",
                "AccountID": "XXX",
                "NextExpirationDate": "2005-03-18T01:58:31Z",
                "AssetID": "XXX",
                "ID": "10000002C",
                "SolutionHTML": "Solution text redacted",
                "EstimatedTaskDuration": 0,
                "LanguageID": 5,
            },
            {
                "ExpirationDate": "2005-03-18T01:58:31Z",
                "Subject": "On-site Support",
                "Solution": "Ticket correctly created through template.",
                "LocationID": "XXX",
                "Solicits": None,
                "Date": "2005-03-18T01:58:31Z",
                "FirstUpdateUserID": "AA",
                "BilledTokens": 0.0,
                "ServiceID": "XXX",
                "TicketTypeID": "XX",
                "Problem": "Problem description redacted",
                "SiteUnRead": 0,
                "ClosureDate": "2005-03-18T01:58:31Z",
                "ContactID": "XXX",
                "SupplierID": None,
                "CustomerContractID": None,
                "EstimatedTaskStartDate": None,
                "OwnerUserID": "XXX",
                "Score": 0,
                "LastExpirationDate": "2005-03-18T01:58:31Z",
                "KnownIssue": False,
                "MailBoxID": "XXX",
                "NextExpirationID": None,
                "TicketClassificationID": "XXX",
                "CalendarID": None,
                "UrgencyID": None,
                "TaskEffort": None,
                "ProblemHTML": "Problem description redacted",
                "AccountID": "XXX",
                "NextExpirationDate": "2005-03-18T01:58:31Z",
                "AssetID": None,
                "ID": "10000003C",
                "SolutionHTML": "Solution text redacted",
                "EstimatedTaskDuration": 0,
            },
        ]
        assert result.outputs_prefix == "HelpdeskAdvanced.Ticket"
        assert result.outputs_key_field == "ID"
        assert result.raw_response == mocked_client.list_tickets.return_value
        assert (
            result.readable_output
            == "\n".join(
                (
                    "### Tickets",
                    "|Ticket ID|Subject|Solution|Date|Service ID|Problem|Contact ID|Owner User ID|Account ID|",
                    "|---|---|---|---|---|---|---|---|---|",
                    "| 10000002C | Support Request | Solution text redacted | 2005-03-18T01:58:31Z | XXX |"  # continued next line
                    " Problem description redacted | XXX | AA | XXX |",
                    "| 10000003C | On-site Support | Ticket correctly created through template."  # continued next line
                    " | 2005-03-18T01:58:31Z | XXX | Problem description redacted | XXX | XXX | XXX |",
                    "",
                )
            )
        )


class TestFilter:
    def test_filter_parse(self):
        """
        Tests parsing a list of filter condition strings into Filter objects.

        Given A list of filter condition strings
        When Filter.parse_list() is called on the conditions
        Then The expected Filter objects are returned
        """

        assert Filter.parse_list(
            (
                '"id" eq "123"',
                '"name" lt "john"',
                '"nullvalue" ne null',
                '"notanullvalue" ne "none"',  # quotes make it a string, not null
            )
        ) == [
            Filter(key="id", operator="eq", value="123"),
            Filter(key="name", operator="lt", value="john"),
            Filter(key="nullvalue", operator="ne", value=None),
            Filter(key="notanullvalue", operator="ne", value="none"),
        ]

    def test_dumps(self):
        """
        Tests dumping a list of Filter objects to a JSON string.

        Given:
            A list of Filter objects with different key, operator and value.

        When:
            Filter.dumps_list() is called on the list of Filter objects.

        Then:
            The JSON string returned should match the expected format.
        """

        assert Filter.dumps_list(
            (
                Filter(key="id", operator="eq", value="123"),
                Filter(key="name", operator="lt", value="john"),
                Filter(key="nullvalue", operator="ne", value=None),
                Filter(key="notanullvalue", operator="ne", value="none"),
            )
        ) == json.dumps(
            [
                {"property": "id", "op": "eq", "value": "123"},
                {"property": "name", "op": "lt", "value": "john"},
                {"property": "nullvalue", "op": "ne", "value": None},
                {"property": "notanullvalue", "op": "ne", "value": "none"},
            ]
        )

    def test_parse_invalid(self):
        """
        Given an invalid filter condition string
        When parse_filter_conditions is called with a list containing the invalid string
        Then it should raise a DemistoException
        """

        with pytest.raises(DemistoException):
            Filter.parse_list('"id" eq 123')  # missing quotes around value


class TestClient:
    base_url = "https://example.com"

    @staticmethod
    def dummy_client() -> Client:
        return Client(
            base_url=TestClient.base_url,
            username="test",
            password="test",
            verify=False,
            proxy=False,
        )

    @classmethod
    def logged_in_client(cls, requests_mock_object):
        cls.mock_login_response(requests_mock_object)
        return cls.dummy_client()

    @staticmethod
    def mock_login_response(requests_mock_object):
        requests_mock_object.post(
            "https://example.com/Authentication/LoginEx?username=test&password=test",
            json={
                "refreshToken": "refresh_token",
                "requestToken": "request_token",
                "expiresIn": 3300,
                "success": True,
            },
        )
        requests_mock_object.post(
            "https://example.com/Authentication/RefreshToken?token=refresh_token",
            json={
                "refreshToken": "refresh_token",
                "requestToken": "request_token",
                "expiresIn": 3300,
                "success": True,
            },
        )

    @classmethod
    def test_client_login(cls, mocker, requests_mock) -> None:
        """
        Given an expired refresh token in integration context
        When _reuse_or_create_token is called
        Then it should log in using username/password to get a new token
        """
        # Mock expired token
        cls.mock_login_response(requests_mock)

        mocker.patch.object(
            demisto,
            "getIntegrationContext",
            return_value={},
        )
        client = TestClient.dummy_client()
        assert client.token.refresh_token == "refresh_token"

    @classmethod
    def test_expired_token(cls, mocker, requests_mock) -> None:
        """
        Given an expired refresh token in integration context
        When _reuse_or_create_token is called
        Then it should log in using username/password to get a new token
        """
        # Mock expired token
        requests_mock.post(
            "https://example.com/Authentication/LoginEx?username=test&password=test",
            json={
                "refreshToken": "new_refresh_token",
                "requestToken": "new_request_token",
                "expiresIn": 3300,
                "success": True,
            },
        )

        mocker.patch.object(
            demisto,
            "getIntegrationContext",
            return_value={},
        )
        client = TestClient.dummy_client()
        assert client.token.refresh_token == "new_refresh_token"

    @classmethod
    @freezegun.freeze_time("2023-01-01 12:00:00")
    def test_reuse_valid_token(cls, mocker, requests_mock):
        """
        Given a valid refresh token in integration context
        When _reuse_or_create_token is called
        Then it should reuse the existing valid token
        """

        # Mock valid token
        mocker.patch.object(
            demisto,
            "getIntegrationContext",
            return_value={
                "refresh_token": "previous_refresh_token",
                "expires_in": 3600,
                "token_expiry_utc": datetime(2023, 1, 1, 12, 30).isoformat(),
            },
        )
        requests_mock.post(
            "https://example.com/Authentication/RefreshToken?token=previous_refresh_token",
            json={
                "refreshToken": "new_refresh_token",
                "requestToken": "new_request_token",
                "expiresIn": 3300,
                "success": True,
            },
        )
        client = TestClient.dummy_client()

        # Assert valid token reused
        assert client.token.refresh_token == "new_refresh_token"
        assert client.token.request_token == "new_request_token"

    @classmethod
    def test_add_ticket_attachment(cls, mocker, requests_mock):
        """
        Given a client
        When calling add_ticket_attachment with two files
        Then make sure they are properly sent in the request

        NOTE: This tests the method, not the _command, as the method has non-trivial logic.
        """
        from PATHelpdeskAdvanced import Path

        client = cls.logged_in_client(requests_mock)
        mocker.patch.object(
            demisto,
            "getFilePath",
            return_value={"path": "test.txt", "name": "test.txt"},
        )
        mocker.patch.object(Path, "open", return_value="mock file contents")
        mocked_request = requests_mock.post("https://example.com/Ticket/UploadNewAttachment", json={"success": True})
        client.add_ticket_attachment(["1", "2"], ticket_id="ticket_id")

        assert mocked_request.called_once
        stringified_request = str(mocked_request.request_history[0]._request.body)
        for i in (1, 2):
            assert f'name="TicketAttachment_{i}"; filename="test.txt"\\r\\n\\r\\nmock file contents' in stringified_request

    @classmethod
    def test_list_groups(cls, requests_mock):
        """
        Given   a client and a mocked response
        When    calling the list_groups_command with provided arguments
        Then    test that the request is properly made, and the resuilt is properly returned
        """
        mocked_request = requests_mock.post(
            f"{cls.base_url}/WSC/Projection",
            json={
                "data": [
                    {
                        "Description": "dummy group 1",
                        "ID": "G001",
                        "ObjectTypeID": "65",
                    },
                ],
                "success": True,
            },
        )

        result = list_groups_command(
            client=cls.logged_in_client(requests_mock),
            args={"group_id": "G001", "limit": "1", "page": "3", "page_size": "2"},
        )

        assert mocked_request.called_once
        assert unquote(mocked_request.request_history[0].query) == (
            "entity=usergroup&columnexpressions=['id',+'description',+'objecttypeid']&"
            "columnnames=['id',+'description',+'objecttypeid']&start=6&limit=1&"
            'filter=[{"property":+"id",+"op":+"eq",+"value":+"g001"}]'
        )
        assert result.readable_output == (
            "### PAT HelpDeskAdvanced Groups\n|Group ID|Description|Object Type ID|"
            "\n|---|---|---|\n| G001 | dummy group 1 | 65 |\n"
        )
        assert result.outputs == [{"ID": "G001", "Description": "dummy group 1", "ObjectTypeID": "65"}]

    @classmethod
    def test_list_users(cls, requests_mock):
        """
        Given   a client and a mocked response
        When    calling the list_users_command provided arguments
        Then    test that the request is properly made, and the resuilt is properly returned
        """
        mocked_request = requests_mock.post(
            f"{cls.base_url}/WSC/Projection",
            json={
                "data": [
                    {
                        "User.Phone": None,
                        "ID": "U001",
                        "User.FirstName": "HDA",
                        "User.LastName": "Cortex XSOAR",
                        "User.EMail": None,
                    }
                ],
                "success": True,
            },
        )

        result = list_users_command(
            client=cls.logged_in_client(requests_mock),
            args={"user_id": "U001", "limit": "1", "page": "3", "page_size": "2"},
        )

        assert mocked_request.called_once
        assert unquote(mocked_request.request_history[0].query) == (
            "entity=users&columnexpressions=['id',+'user.firstname',+'user.lastname',+'user.email',"
            "+'user.phone',+'user.mobile']&columnnames=['id',+'user.firstname',+'user.lastname',+'user.email',"
            '+\'user.phone\',+\'user.mobile\']&start=6&limit=1&filter=[{"property":+"id",+"op":+"eq",+"value":+"u001"}]'
        )
        assert result.readable_output == (
            "### PAT HelpDeskAdvanced Users\n|Phone|ID|First Name|Last Name|E Mail|"
            "\n|---|---|---|---|---|\n|  | U001 | HDA | Cortex XSOAR |  |\n"
        )
        assert result.outputs == [
            {
                "Phone": None,
                "ID": "U001",
                "FirstName": "HDA",
                "LastName": "Cortex XSOAR",
                "EMail": None,
            }
        ]

    @classmethod
    def test_list_ticket_attachments(cls, requests_mock):
        """
        Given   a client and a mocked response
        When    calling the list_users_command provided arguments
        Then    test that the request is properly made, and the resuilt is properly returned
        """
        mocked_request = requests_mock.post(
            f"{cls.base_url}/WSC/List",
            json={
                "data": [
                    {
                        "ObjectDescription": "Untitled document.pdf",
                        "ObjectEntity": "Attachment",
                        "LastUpdateUserID": "",
                        "IsNew": False,
                        "KBSize": 0.76,
                        "ObjectTypeID": "DEFAULT",
                        "ID": "A12345C",
                        "UniqueID": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaaaaaa",
                        "Note": "",
                        "FileName": "Untitled document.pdf",
                        "FirstUpdate": None,
                        "TicketID": "1234567C",
                        "ContentType": "application/pdf",
                        "OwnerUserID": "",
                        "ParentObject": "Incident",
                        "ParentObjectID": "1234567C",
                        "Site": None,
                        "EmailID": "",
                        "BlobID": "A12345C",
                        "FirstUpdateUserID": "S44444C",
                        "RemoteID": "",
                        "LastUpdate": "2023-08-02T00:00:00Z",
                        "Description": "Untitled document.pdf",
                    }
                ],
                "total": 5,
                "result": {
                    "code": "0",
                    "subcode": None,
                    "parameters": None,
                    "desc": "",
                },
                "success": True,
                "requestToken": "token",
            },
        )

        result = list_ticket_attachments_command(
            client=cls.logged_in_client(requests_mock),
            args={"ticket_id": "A12345C", "limit": "1"},
        )

        assert mocked_request.called_once
        assert unquote(mocked_request.request_history[0].query) == (
            'entity=attachments&start=0&limit=1&filter=[{"property":+"parentobject",+"op":+"eq",+"value":+"ticket"},'
            '+{"property":+"parentobjectid",+"op":+"eq",+"value":+"a12345c"}]'
        )
        assert result.readable_output == (
            "### Attachments of A12345C\n|File Name|Last Update|Description|Object Description|First Update User ID|Object Entity"
            "|Content Type|\n|---|---|---|---|---|---|---|\n| Untitled document.pdf "
            "| 2023-08-02T00:00:00Z | Untitled document.pdf"
            " | Untitled document.pdf | S44444C | Attachment | application/pdf |\n"
        )
        assert result.outputs == [
            {
                "ObjectDescription": "Untitled document.pdf",
                "ObjectEntity": "Attachment",
                "LastUpdateUserID": "",
                "IsNew": False,
                "KBSize": 0.76,
                "ObjectTypeID": "DEFAULT",
                "ID": "A12345C",
                "UniqueID": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaaaaaa",
                "Note": "",
                "FileName": "Untitled document.pdf",
                "FirstUpdate": None,
                "TicketID": "1234567C",
                "ContentType": "application/pdf",
                "OwnerUserID": "",
                "ParentObject": "Incident",
                "ParentObjectID": "1234567C",
                "Site": None,
                "EmailID": "",
                "BlobID": "A12345C",
                "FirstUpdateUserID": "S44444C",
                "RemoteID": "",
                "LastUpdate": "2023-08-02T00:00:00Z",
                "Description": "Untitled document.pdf",
            }
        ]

    @classmethod
    def test_non_json_response(cls, requests_mock):
        client = cls.logged_in_client(requests_mock)
        requests_mock.post(
            "https://example.com/WSC/Projection?entity=UserGroup&columnExpressions=%5B%27ID%27"
            "%2C+%27Description%27%2C+%27ObjectTypeID"
            "%27%5D&columnNames=%5B%27ID%27%2C+%27Description%27%2C+%27ObjectTypeID%27%5D&start=0&limit=1",
            text="surprise",
        )
        with pytest.raises(ValueError, match="API returned non-JSON response: surprise"):
            client.list_groups(limit=1)

    @classmethod
    @pytest.mark.parametrize(
        "response_file,expected_error_message",
        (
            pytest.param(
                "response_401.html",
                "Server Error. 401 - Unauthorized: Access is denied due to invalid credentials",
                id="401",
            ),
            pytest.param(
                "response_invalid_request.html",
                "A server error has occurred. Please, contact portal administrator.",
                id="invalid response",
            ),
        ),
    )
    def test_error_response(cls, requests_mock, response_file: str, expected_error_message: str):
        """
        Given   a client and an invalid response
        When    calling a method that simulates these responses
        Then    make sure the response is parsed well
        """
        client = cls.logged_in_client(requests_mock)

        requests_mock.post(
            "https://example.com/WSC/Projection?entity=UserGroup&columnExpressions=%5B%27ID%27%2C+%27Description%27%2C+%"
            "27ObjectTypeID%27%5D&columnNames=%5B%27ID%27%2C+%27Description%27%2C+%27ObjectTypeID%27%5D&start=0&limit=1",
            text=Path(f"test_data/{response_file}").read_text(),
        )
        with pytest.raises(DemistoException, match=expected_error_message):
            client.list_groups(limit=1)


class TestPaginate:
    @staticmethod
    def test_paginate_with_limit_only():
        """
        Given limit is provided
        When paginate is called with only limit
        Then it returns start as 0 and provided limit
        """
        assert paginate(limit=10) == PaginateArgs(start=0, limit=10)

    @staticmethod
    def test_paginate_with_page_and_page_size():
        """
        Given limit is provided
        When paginate is called with paging arguments
        Then it returns start as 0 and provided limit
        """
        assert paginate(page=2, page_size=3, limit=3) == PaginateArgs(start=6, limit=3)

    @staticmethod
    def test_paginate_with_invalid_args():
        """
        Given only one of page or page_size is provided
        When paginate is called with invalid args
        Then it raises an appropriate error
        """
        with pytest.raises(ValueError):
            paginate(page=1, limit="None")

        with pytest.raises(KeyError):
            paginate(page_size=10)

    @staticmethod
    def test_paginate_with_non_int_args():
        """
        Given page and page_size are non-integer values
        When paginate is called with non-int args
        Then it raises a DemistoException
        """
        with pytest.raises(ValueError):
            paginate(page="a", page_size="b", limit=3)


class TestPatToMarkdown:
    @staticmethod
    def test_pat_table_to_markdown_with_no_fields():
        """
        Given a title, output dict, and no fields
        When pat_table_to_markdown is called
        Then it returns markdown string of the full output
        """
        title = "Test Title"
        output = {"key1": "value1", "key2": "value2"}
        assert pat_table_to_markdown(title, output, None) == "### Test Title\n|Key 1|Key 2|\n|---|---|\n| value1 | value2 |\n"

    @staticmethod
    def test_pat_table_to_markdown_with_fields():
        """
        Given title, output, and specified fields
        When pat_table_to_markdown is called
        Then it returns markdown string of filtered outputs
        """
        title = "Test Title"
        output = {"Field1": "1", "Field2": "2", "Field3": "3"}
        fields = (Field("field1"), Field("field2"))

        assert pat_table_to_markdown(title, output, fields) == "### Test Title\n|Field 1|Field 2|\n|---|---|\n| 1 | 2 |\n"

    @staticmethod
    def test_pat_table_to_markdown_with_field_replacements():
        """
        Given title, output dict, fields, and field replacements
        When pat_table_to_markdown is called with field_replacements
        Then it returns markdown string with replaced field names
        """
        title = "Test Title"
        replace_me = Field("replace_me")
        untouched = Field("untouched")
        replaced = Field("replaced")
        output = {replace_me.hda_name: "1", untouched.hda_name: "2"}

        assert (
            pat_table_to_markdown(title, output, fields=None, field_replacements={replace_me: replaced})
            == "### Test Title\n|Replaced|Untouched|\n|---|---|\n| 1 | 2 |\n"
        )