AtlassianJiraServiceManagement

Use this integration to manage Jira objects and attach files to Jira objects from Cortex XSOAR.

IT Services · Atlassian Jira Service Management

Details

IDAtlassianJiraServiceManagement
ProviderAtlassian
CategoryIT Services
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Use this integration to manage Jira objects and attach files to Jira objects from Cortex XSOAR.
This integration was integrated and tested with version 5.4.15 of AtlassianJiraServiceManagement.

Configure Atlassian Jira Service Management in Cortex

Parameter Required
Server URL True
API Token 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.

jira-asset-object-schema-list


List all object schemas.

Base Command

jira-asset-object-schema-list

Input

Argument Name Description Required
limit The maximum amount of schemas to retrieve. Cannot be used alongside the page and page_size arguments. Optional
all_results Whether to retrieve all object schemas with no limit. Possible values are: true, false. Optional

Context Output

Path Type Description
JiraAsset.Schema.ID Number The ID of the object schema.
JiraAsset.Schema.Name String The name of the object schema.
JiraAsset.Schema.Key String The key of the object schema.
JiraAsset.Schema.Status String The status of the object schema.
JiraAsset.Schema.Created Date The date in which this object schema was created.
JiraAsset.Schema.Updated Date The date in which this object schema was updated.
JiraAsset.Schema.ObjectCount Number The number of objects in the schema.
JiraAsset.Schema.ObjectTypeCount Number The number of different object types in the schema.

jira-asset-object-type-list


List all object types.

Base Command

jira-asset-object-type-list

Input

Argument Name Description Required
schema_id The schema from which the command will retrieve object types. Values can be received by running the jira-asset-object-schema-list command. Required
exclude Object types with that name will be excluded from the results. Optional
limit The maximum amount of object types to retrieve. Optional
all_results Whether to retrieve all object types with no limit. Possible values are: true, false. Optional

Context Output

Path Type Description
JiraAsset.ObjectType.ID Number The ID of the object type.
JiraAsset.ObjectType.Name String The name of the object type.
JiraAsset.ObjectType.Type Number The type of the object type.
JiraAsset.ObjectType.Position Number The position of the object type among other object types with the same level in the schema hierarchy.
JiraAsset.ObjectType.Created Date The date the object type was created.
JiraAsset.ObjectType.Updated Date The date the object type was last updated.
JiraAsset.ObjectType.ObjectCount Number The amount of objects listed under this object type.
JiraAsset.ObjectType.ObjectSchemaId Number The ID of the schema this object type is listed under.
JiraAsset.ObjectType.Inherited Boolean Whether this object type inherits from a parent object type.
JiraAsset.ObjectType.AbstractObjectType Boolean Is this object type abstract (can only be inherited).
JiraAsset.ObjectType.ParentObjectTypeInherited Boolean Whether this object type inherits from a parent object type.

jira-asset-object-type-attribute-list


List all attributes for an object type.

Base Command

jira-asset-object-type-attribute-list

Input

Argument Name Description Required
object_type_id The object type for which the command will retrieve attributes. Values can be received by running the jira-asset-object-type-list command. Required
is_editable Whether to only return attributes that can be edited. Possible values are: true, false. Optional
order_by_name Whether to sort the results by the attribute name. Possible values are: true, false. Optional
query Query to filter on available object type attributes. Filter attributes that start with the query. Optional
include_value_exist Should the response only include attributes where attribute values exists. Possible values are: true, false. Optional
exclude_parent_attributes Should the response exclude parent attributes. Possible values are: true, false. Optional
include_children Should the response include child attributes. Possible values are: true, false. Optional
order_by_required Should the response be ordered by the number of required attributes. Possible values are: true, false. Optional
limit The maximum amount of object types to retrieve. Optional
all_results Whether to retrieve all object types with no limit. Possible values are: true, false. Optional

Context Output

Path Type Description
JiraAsset.Attribute.ID Number The ID of the attribute.
JiraAsset.Attribute.Name String The name of the attribute.
JiraAsset.Attribute.Label Boolean Whether this attribute is used as the label for objects of that type.
JiraAsset.Attribute.Type String The type of the attribute (Default, Reference, User, Project etc.).
JiraAsset.Attribute.DefaultType.id Number ID for the sub-type of the default type.
JiraAsset.Attribute.DefaultType.name String Name for the sub-type of the default type.
JiraAsset.Attribute.Editable Boolean Whether this attribute is editable.
JiraAsset.Attribute.System Boolean Whether this attribute is a system attribute or a custom one.
JiraAsset.Attribute.Sortable Boolean Can the objects be sorted by that attribute.
JiraAsset.Attribute.Summable Boolean Can this attribute be summarized.
JiraAsset.Attribute.Indexed Boolean Is this attribute indexed.
JiraAsset.Attribute.MinimumCardinality Number The minimum amount of elements this attribute should populate.
JiraAsset.Attribute.MaximumCardinality Number The maximum amount of elements this attribute should populate.
JiraAsset.Attribute.Removable Boolean Whether this attribute can be removed from the object.
JiraAsset.Attribute.Hidden Boolean Whether this attribute is a hidden attribute.
JiraAsset.Attribute.IncludeChildObjectTypes Boolean Whether this attribute includes child object types.
JiraAsset.Attribute.UniqueAttribute Boolean Whether this attribute is unique to its object.
JiraAsset.Attribute.Options String Options for the attributes.
JiraAsset.Attribute.Position Number The position of the attribute in relation to other object attributes.

jira-asset-object-create


Create a new object of the specified object type ID. Either attributes or attributes_json must be provided.

Base Command

jira-asset-object-create

Input

Argument Name Description Required
object_type_id The object type for which a new object will be created. Required
attributes Provide this field if attributes_json is not provided. A key-value map of object attributes. The structure of the field is: { “attributeId1”: [ “value1”, “value2”] , “attributeId2”:[“value1”, “value2”] }. You can run the jira-asset-object-type-attribute-list command to retrieve the list of available attributes. Optional
attributes_json Provide this field if attributes is not provided. A JSON string of object attributes to be added to the new object. Take a look at https://docs.atlassian.com/assets/REST/10.7.0/#object-createObject for the structure of this field. You can use the jira-asset-object-type-attribute-list command to retrieve the list of available attributes. Optional

Context Output

Path Type Description
JiraAsset.Object.ID Number ID of the newly created object.
JiraAsset.Object.Label String The value of the attribute that is marked as Label for this object type.
JiraAsset.Object.ObjectKey String The auto-generated object key.
JiraAsset.Object.Avatar.objectId Number ID of the newly created object.
JiraAsset.Object.Created Date The date in which this object was created.
JiraAsset.Object.Updated Date The date in which this object was last updated.
JiraAsset.Object.HasAvatar Boolean Does this object have an Avatar.
JiraAsset.Object.Timestamp Date Epoch version of the object’s creation date.
JiraAsset.Object.Links.self String The link to view this object in the Jira web app.
JiraAsset.Object.Name String The name of the newly created object.

jira-asset-object-update


Updates an existing object of specified object type ID. Either attributes or attributes_json must be provided.

Base Command

jira-asset-object-update

Input

Argument Name Description Required
object_id ID of the object that will be updated. Required
attributes Provide this field if attributes_json is not provided. A key-value map of object attributes. The structure of the field is: { “attributeId1”: [ “value1”, “value2”] , “attributeId2”:[“value1”, “value2”] }. You can use the jira-asset-object-type-attribute-list command to retrieve the list of available attributes. Optional
attributes_json Provide this field if attributes is not provided. A JSON string of object attributes to be added to the new object. Take a look at https://docs.atlassian.com/assets/REST/10.7.0/#object-createObject for the structure of this field. You can use the jira-asset-object-type-attribute-list command to retrieve the list of available attributes. Optional

Context Output

There is no context output for this command.

jira-asset-object-delete


Deletes an existing object of the specified object ID.

Base Command

jira-asset-object-delete

Input

Argument Name Description Required
object_id ID of the object that will be deleted. Required

Context Output

There is no context output for this command.

jira-asset-object-get


Retrieves the object with the specified object ID.

Base Command

jira-asset-object-get

Input

Argument Name Description Required
object_id ID of the object that will be retrieved. Required

Context Output

Path Type Description
JiraAsset.Object.ID Number ID of the retrieved object.
JiraAsset.Object.Label String The value of the attribute that is marked as Label for this object type.
JiraAsset.Object.ObjectKey String The auto-generated object key.
JiraAsset.Object.Created Date The date in which this object was created.
JiraAsset.Object.Updated Date The date in which this object was last updated.
JiraAsset.Object.HasAvatar Boolean Does this object have an Avatar.
JiraAsset.Object.Timestamp Date Epoch version of the object’s creation date.
JiraAsset.Object.ExtendedInfo.openIssuesExists Boolean Does this object appear in any open issues.
JiraAsset.Object.ExtendedInfo.attachmentsExists Boolean Does this object have any attachments.
JiraAsset.Object.Links.self String The link to view this object in the Jira web app.
JiraAsset.Object.Name String The name of the object.

jira-asset-object-search


Searches for objects of the specified object type.

Base Command

jira-asset-object-search

Input

Argument Name Description Required
ql_query An aql query to search objects by. View https://support.atlassian.com/jira-service-management-cloud/docs/use-assets-query-language-aql/ for further details. Required
include_attributes Whether to include the attributes structure in the response. Possible values are: true, false. Optional
page Page number. Optional
page_size Use this argument or limit, but not both. Size of the page. Defaults to 50. Optional
limit Use this argument or a combination of page and page_size, but not both. Limit the number of entries returned by the command. Optional

Context Output

Path Type Description
JiraAsset.Object.ID Number ID of the retrieved object.
JiraAsset.Object.Label String The value of the attribute that is marked as Label for this object type.
JiraAsset.Object.ObjectKey String The auto-generated object key.
JiraAsset.Object.Created Date The date in which this object was created.
JiraAsset.Object.Updated Date The date in which this object was last updated.
JiraAsset.Object.HasAvatar Boolean Does this object have an Avatar.
JiraAsset.Object.Timestamp Date Epoch version of the object’s creation date.
JiraAsset.Object.Attributes.id Number The ID of the attribute.
JiraAsset.Object.Attributes.objectTypeAttributeId Number The ID of the attribute, not relative to the object type.
JiraAsset.Object.Attributes.objectAttributeValues.value String The value of the specific attribute for this object.
JiraAsset.Object.Attributes.objectAttributeValues.referencedType Boolean Whether this attribute is referenced by other types.
JiraAsset.Object.Attributes.objectAttributeValues.displayValue String The display value for this attribute.
JiraAsset.Object.Attributes.objectAttributeValues.searchValue String The search value for this attribute.
JiraAsset.Object.Attributes.objectId Number The ID of the object this attribute is assigned to.
JiraAsset.Object.Links.self String The link to view this object in the Jira web app.
JiraAsset.Object.Name String The name of the object.

jira-asset-attribute-json-create


Utility command used to create a JSON file with all attributes of the specified object type. All that is left is to fill in the values of each attribute.

Base Command

jira-asset-attribute-json-create

Input

Argument Name Description Required
object_type_id ID of the object type that attributes JSON will be created for. You could use the jira-asset-object-type-list command to find object types. Required
is_editable Whether to fetch only editable attributes. Possible values are: true, false. Optional
is_required Whether to fetch only required attributes. Possible values are: true, false. Optional

Context Output

Path Type Description
InfoFile.Name string File name.
InfoFile.EntryID string The entry ID of the report.
InfoFile.Size number File size.
InfoFile.Type string File type, e.g., “PE”.
InfoFile.Info string Basic information of the file.

jira-asset-comment-create


Creates a comment for the specified object.

Base Command

jira-asset-comment-create

Input

Argument Name Description Required
object_id ID of the object that the comment will be added to. You could use the jira-asset-object-search command to find objects. Required
comment Body of the comment that will be created. Required

Context Output

Path Type Description
JiraAsset.Comment.Created Date The date in which this comment was created.
JiraAsset.Comment.Updated Date The date in which this object was last updated.
JiraAsset.Comment.Id Number The ID of the newly created comment.
JiraAsset.Comment.Actor.AvatarUrl String URL to the avatar of the author of the comment.
JiraAsset.Comment.Actor.DisplayName String Display name of the author of the comment.
JiraAsset.Comment.Actor.Name String Name of the author of the comment.
JiraAsset.Comment.Actor.Key String Key of the author of the comment.
JiraAsset.Comment.Actor.RenderedLink String A link to the author’s Jira profile.
JiraAsset.Comment.Actor.IsDeleted Boolean Is the author of the comment deleted.
JiraAsset.Comment.Role Number The role of the author of the comment.
JiraAsset.Comment.Comment String The body of the comment that was newly added.
JiraAsset.Comment.CommentOutput String The body of the comment that was newly added.
JiraAsset.Comment.ObjectId Number The ID of the object the new comment was added to.
JiraAsset.Comment.CanEdit Boolean Whether the comment is editable.
JiraAsset.Comment.CanDelete Boolean Whether the comment can be deleted.

jira-asset-connected-ticket-list


Returns a list of all connected tickets for the specified object.

Base Command

jira-asset-connected-ticket-list

Input

Argument Name Description Required
object_id ID of the object for which connected tickets will be fetched. Required

Context Output

Path Type Description
JiraAsset.ConnectedTicket.Tickets.Key String Key for the connected ticket.
JiraAsset.ConnectedTicket.Tickets.ID Number The ID of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Reporter String The user who reported the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Created Date The date in which this connected ticket was created.
JiraAsset.ConnectedTicket.Tickets.Updated Date The date in which this connected ticket was last updated.
JiraAsset.ConnectedTicket.Tickets.Title String Title of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Status.Name String The name of the status of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Status.Colorname String The color of the connected tikcet.
JiraAsset.ConnectedTicket.Tickets.Type.Name String The type of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Type.Description String The description of the type of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Type.Iconurl String The URL to the icon representing the type of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Priority.Name String Priority of the connected ticket.
JiraAsset.ConnectedTicket.Tickets.Priority.Iconurl String The URL to the icon representing the priority of the connected ticket.
JiraAsset.ConnectedTicket.Allticketsquery String The query used to fetch the connected tickets.

jira-asset-comment-list


Returns a list of comments for the specified object.

Base Command

jira-asset-comment-list

Input

Argument Name Description Required
object_id ID of the object that comments will be retrieved for. You could use the jira-asset-object-search command to find objects. Required

Context Output

Path Type Description
JiraAsset.Comment.Created Date The date in which this comment was created.
JiraAsset.Comment.Updated Date The date in which this object was last updated.
JiraAsset.Comment.ID Number The ID of the newly created comment.
JiraAsset.Comment.Role Number The role of the author of the comment.
JiraAsset.Comment.Comment String The body of the comment that was newly added.
JiraAsset.Comment.CommentOutput String The body of the comment that was newly added.
JiraAsset.Comment.ObjectId Number The ID of the object the new comment was added to.
JiraAsset.Comment.CanEdit Boolean Whether the comment is editable.
JiraAsset.Comment.CanDelete unknown Whether the comment can be deleted.

jira-asset-attachment-add


Uploads a file attachment to the specified object.

Base Command

jira-asset-attachment-add

Input

Argument Name Description Required
object_id ID of the object that the file will be attached to. Required
entry_id ID of the file to upload. Required

Context Output

Path Type Description
JiraAsset.Attachment.ID Number ID of the attachment.
JiraAsset.Attachment.Author String Author of the attachment.
JiraAsset.Attachment.MimeType String The file’s MIME type.
JiraAsset.Attachment.Filename String The file’s name.
JiraAsset.Attachment.Filesize String The size of the file.
JiraAsset.Attachment.Created Date The date in which this attachment was created.
JiraAsset.Attachment.Comment String The comment attached to the attachment, if there is one.
JiraAsset.Attachment.CommentOutput String The comment output of the attachment, if there is one.
JiraAsset.Attachment.Url String URL to the attachment.

jira-asset-attachment-list


Returns a list of attachments for the specified object. You can also download the files in a zipped format.

Base Command

jira-asset-attachment-list

Input

Argument Name Description Required
object_id ID of the object that attachments will be fetched for. Required
download_file Whether to download the attachments for this object in a zipped format. Possible values are: true, false. Optional

Context Output

Path Type Description
JiraAsset.Attachment.ID Number ID of the attachment.
JiraAsset.Attachment.Author String Author of the attachment.
JiraAsset.Attachment.MimeType String The file’s MIME type.
JiraAsset.Attachment.Filename String The file’s name.
JiraAsset.Attachment.Filesize String The size of the file.
JiraAsset.Attachment.Created Date The date in which this attachment was created.
JiraAsset.Attachment.Comment String The comment attached to the attachment, if there is one.
JiraAsset.Attachment.CommentOutput String The comment output of the attachment, if there is one.
JiraAsset.Attachment.Url String URL to the attachment.

jira-asset-attachment-remove


Removes an attachment from a specific object.

Base Command

jira-asset-attachment-remove

Input

Argument Name Description Required
id ID of the attachment that will be deleted. Required

Context Output

Path Type Description
JiraAsset.Attachment.ID Number ID of the attachment that was deleted.
JiraAsset.Attachment.Author String Author of the attachment.
JiraAsset.Attachment.MimeType String The file’s MIME type.
JiraAsset.Attachment.Filename String The file’s name.
JiraAsset.Attachment.Filesize String The size of the file.
JiraAsset.Attachment.Created Date The date in which this attachment was created.
JiraAsset.Attachment.Comment String The comment attached to the attachment, if there is one.
JiraAsset.Attachment.CommentOutput String The comment output of the attachment, if there is one.
JiraAsset.Attachment.Url String URL to the attachment.

Configuration parameters

  • url — Server URL (required)
  • credentials — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (15)

  • jira-asset-attachment-add

    Uploads a file attachment to the specified object.

  • jira-asset-attachment-list

    Returns a list of attachments for the specified object. You can also download the files in a zipped format.

  • jira-asset-attachment-remove

    Removes an attachment from a specific object.

  • jira-asset-attribute-json-create

    Utility command used to create a JSON file with all attributes of the specified object type. All that is left is to fill in the values of each attribute.

  • jira-asset-comment-create

    Creates a comment for the specified object.

  • jira-asset-comment-list

    Returns a list of comments for the specified object.

  • jira-asset-connected-ticket-list

    Returns a list of all connected tickets for the specified object.

  • jira-asset-object-create

    Create a new object of the specified object type ID. Either attributes or attributes_json must be provided.

  • jira-asset-object-delete

    Deletes an existing object of the specified object ID.

  • jira-asset-object-get

    Retrieves the object with the specified object ID.

  • jira-asset-object-schema-list

    List all object schemas.

  • jira-asset-object-search

    Searches for objects of the specified object type.

  • jira-asset-object-type-attribute-list

    List all attributes for an object type.

  • jira-asset-object-type-list

    List all object types.

  • jira-asset-object-update

    Updates an existing object of specified object type ID. Either attributes or attributes_json must be provided.

import json
import os
from typing import Any
from unittest.mock import Mock

import AtlassianJiraServiceManagement as JSM
import demistomock as demisto
import pytest
from CommonServerPython import CommandResults
from pytest_mock import MockerFixture


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


command_test_data = util_load_json("./test_data/command_test_data.json")

client = JSM.Client(base_url="https://url.com", verify=False, proxy=False, api_key="key123")


def test_convert_keys_to_pascal_empty_list():
    """
    Given:
        - An empty list of objects to convert to pascal case.
    When:
        - Calling the convert_keys_to_pascal function.
    Then:
        - The function should return an empty list.
    """
    objects = []
    key_mapping = None
    expected_result = []
    result = JSM.convert_keys_to_pascal(objects, key_mapping)
    assert result == expected_result


def test_convert_keys_to_pascal_list_with_keys():
    """
    Given:
        - A list of objects with keys to convert to pascal case.
    When:
        - Calling the convert_keys_to_pascal function.
    Then:
        - The function should return the list with keys converted to pascal case.
    """
    objects = [
        {"key1": "value1", "key2": "value2"},
        {"key3": "value3", "key4": "value4"},
    ]
    key_mapping = None
    expected_result = [
        {"Key1": "value1", "Key2": "value2"},
        {"Key3": "value3", "Key4": "value4"},
    ]
    result = JSM.convert_keys_to_pascal(objects, key_mapping)
    assert result == expected_result


def test_convert_keys_to_pascal_list_with_keys_and_mapping():
    """
    Given:
        - A list of objects with keys to convert to pascal case.
        - A key mapping to apply to the keys.
    When:
        - Calling the convert_keys_to_pascal function.
    Then:
        - The function should return the list with keys converted to pascal case
          and mapping applied where specified.
    """
    objects = [
        {"key1": "value1", "key2": "value2"},
        {"key3": "value3", "key4": "value4"},
    ]
    key_mapping = {"key1": "MappedKey1", "key4": "MappedKey4"}
    expected_result = [
        {"MappedKey1": "value1", "Key2": "value2"},
        {"Key3": "value3", "MappedKey4": "value4"},
    ]
    result = JSM.convert_keys_to_pascal(objects, key_mapping)
    assert result == expected_result


def test_get_object_readable_outputs():
    objects = [
        {"objectType": {"name": "typeA"}, "avatar": "avatarA", "id": 123, "key": "value"},
        {"objectType": {"name": "typeB"}, "avatar": "avatarB", "id": 456, "key": "value"},
    ]

    expected_output = [{"ID": 123, "Key": "value", "Type": "typeA"}, {"ID": 456, "Key": "value", "Type": "typeB"}]
    assert JSM.get_object_readable_outputs(objects) == expected_output


def test_clean_object_attributes_with_type_and_default_type():
    """
    Given:
        - An object with 'id', 'name', 'type', and 'defaultType' fields.
    When:
        - Cleaning the object attributes to match a certain format.
    Then:
        - The cleaned attributes should match the expected format with 'type' replaced by its string representation and
        keys in PascalCase.
    """
    attributes = [{"id": 1, "name": "Test", "type": 0, "defaultType": {"name": "Text"}, "objectType": {"id": 1}}]
    expected = [{"id": 1, "name": "Test", "type": "Text", "defaultType": {"name": "Text"}}]
    result = JSM.clean_object_attributes(attributes)
    assert result == expected


def test_clean_object_attributes_with_type():
    """
    Given:
        - An object with 'id', 'value', and 'type' fields.
    When:
        - Cleaning the object attributes to match a certain format.
    Then:
        - The cleaned attributes should match the expected format with 'type' replaced by its string representation and
        keys in PascalCase.
    """
    attributes = [{"id": 2, "value": "Hello", "type": 1}]
    expected = [{"id": 2, "value": "Hello", "type": "Object Reference"}]
    result = JSM.clean_object_attributes(attributes)
    assert result == expected


def test_clean_object_attributes_empty():
    """
    Given:
        - An empty list of object attributes.
    When:
        - Cleaning the object attributes to match a certain format.
    Then:
        - The function should return an empty list as no attributes are present to clean.
    """
    attributes = []
    expected = []
    result = JSM.clean_object_attributes(attributes)
    assert result == expected


def test_convert_attributes_multiple_values():
    """
    Given:
        - A dictionary with one attribute having multiple values.
    When:
        - The convert_attributes function is called with the dictionary.
    Then:
        - The function should return a list with a dictionary containing 'objectTypeAttributeId' set to the attribute ID
         and 'objectAttributeValues' as a list of dictionaries with 'value' keys.
    """
    attributes = {"attr1": ["value1", "value2"]}
    expected = [{"objectTypeAttributeId": "attr1", "objectAttributeValues": [{"value": "value1"}, {"value": "value2"}]}]
    result = JSM.convert_attributes(attributes)
    assert result == expected


def test_convert_attributes_single_value():
    """
    Given:
        - A dictionary with one attribute having a single value.
    When:
        - The convert_attributes function is called with the dictionary.
    Then:
        - The function should return a list with a dictionary containing 'objectTypeAttributeId' set to the attribute ID
         and 'objectAttributeValues' as a list with a single dictionary with the 'value' key.
    """
    attributes = {"attr2": ["hello"]}
    expected = [{"objectTypeAttributeId": "attr2", "objectAttributeValues": [{"value": "hello"}]}]
    result = JSM.convert_attributes(attributes)
    assert result == expected


def test_convert_attributes_empty():
    """
    Given:
        - An empty dictionary of attributes.
    When:
        - The convert_attributes function is called with the dictionary.
    Then:
        - The function should return an empty list as there are no attributes to convert.
    """
    attributes = {}
    expected = []
    result = JSM.convert_attributes(attributes)
    assert result == expected


def test_convert_attributes_empty_values():
    """
    Given: A dictionary with an attribute ID and an empty list of values
    When: The convert_attributes function is called with the dictionary
    Then: The function should return a list with a dictionary containing an empty list for objectAttributeValues
    """
    attributes = {"attr4": []}
    expected = [{"objectTypeAttributeId": "attr4", "objectAttributeValues": []}]
    result = JSM.convert_attributes(attributes)
    assert result == expected


def test_get_attributes_json_data_with_attributes():
    """
    Given: An object type ID and a string representation of attributes
    When: The get_attributes_json_data function is called with the object type ID and attributes
    Then: The function should return a dictionary with the object type ID and converted attributes
    """
    object_type_id = "test_object_type"
    attributes = {"attr1": ["value1", "value2"], "attr2": ["hello"]}
    expected = {
        "objectTypeId": object_type_id,
        "attributes": [
            {"objectTypeAttributeId": "attr1", "objectAttributeValues": [{"value": "value1"}, {"value": "value2"}]},
            {"objectTypeAttributeId": "attr2", "objectAttributeValues": [{"value": "hello"}]},
        ],
    }
    result = JSM.get_attributes_json_data(object_type_id, attributes=attributes)
    assert result == expected


def test_get_attributes_json_data_with_attributes_json():
    """
    Given: An object type ID and a JSON string representation of attributes
    When: The get_attributes_json_data function is called with the object type ID and attributes_json
    Then: The function should return a dictionary with the object type ID and converted attributes
    """
    object_type_id = "test_object_type"
    attributes_json = (
        "{"
        '"attributes": ['
        '{"objectTypeAttributeId": "attr1", "objectAttributeValues": [{"value": "value1"},{"value": "value2"}]},'
        '{"objectTypeAttributeId": "attr2", "objectAttributeValues": [{"value": "hello"}]}]}'
    )
    expected = {
        "objectTypeId": object_type_id,
        "attributes": [
            {"objectTypeAttributeId": "attr1", "objectAttributeValues": [{"value": "value1"}, {"value": "value2"}]},
            {"objectTypeAttributeId": "attr2", "objectAttributeValues": [{"value": "hello"}]},
        ],
    }
    result = JSM.get_attributes_json_data(object_type_id, attributes_json=attributes_json)
    assert result == expected


def test_get_attributes_json_data_no_input():
    """
    Given: An object type ID, but no attributes or attributes_json
    When: The get_attributes_json_data function is called without providing attributes or attributes_json
    Then: The function should raise a ValueError
    """
    object_type_id = "test_object_type"
    with pytest.raises(ValueError):
        JSM.get_attributes_json_data(object_type_id)


def test_get_attributes_json_data_both_inputs():
    """
    Given: An object type ID, and both attributes and attributes_json
    When: The get_attributes_json_data function is called with both attributes and attributes_json
    Then: The function should raise a ValueError
    """
    object_type_id = "test_object_type"
    attributes = '{"attr1": ["value1", "value2"]}'
    attributes_json = '{"attributes": [{"attr2": ["hello"]}]}'
    with pytest.raises(ValueError):
        JSM.get_attributes_json_data(object_type_id, attributes=attributes, attributes_json=attributes_json)


def test_parse_object_results_basic():
    """
    Given: An object with multiple fields, but no object type.
    When: The parse_object_results function is called with the object.
    Then: The function should return a dictionary with the object ID and the object name.
    """
    res = {"id": 123, "name": "TestObject"}
    expected_output = {"outputs": [{"id": 123, "name": "TestObject"}], "objectId": 123}
    assert JSM.parse_object_results(res) == expected_output


def test_parse_object_results_with_object_type():
    """
    Given: An object with multiple fields and an object type.
    When: The parse_object_results function is called with the object.
    Then: The function should return a dict with the object ID and name converted to PascalCase but delete the object type.
    """
    res = {"id": 123, "name": "TestObject", "objectType": "TestType"}
    expected_output = {"outputs": [{"id": 123, "name": "TestObject"}], "objectId": 123}
    assert JSM.parse_object_results(res) == expected_output


def test_parse_object_results_empty():
    """
    Given: An empty object.
    When: The parse_object_results function is called with the object.
    Then: The function should return a dictionary with an empty dictionary for outputs and no ID.
    """
    res = {}
    expected_output = {"outputs": [{}], "objectId": None}
    assert JSM.parse_object_results(res) == expected_output


def test_jira_asset_object_schema_list_command_with_limit(mocker: MockerFixture):
    """
    Given:
        - An args dict with a 'limit' argument.
    When:
        - Calling the jira_asset_object_schema_list_command with the args dict.
    Then:
        - The command returns a limited number of results as specified by the 'limit' argument.
    """
    args = {"limit": "2"}
    expected_len = 2
    mocked_return_value = command_test_data["object_schema_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_schema_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_schema_list_command(client, args)
    mocked_client.assert_called()
    assert len(command_results.outputs) == expected_len


def test_jira_asset_object_schema_list_command_all_results(mocker: MockerFixture):
    """
    Given:
        - An args dict with 'all_results' set to 'true'.
    When:
        - Calling the jira_asset_object_schema_list_command with the args dict.
    Then:
        - The command returns all results, ignoring any 'limit' argument.
    """
    args = {"all_results": "true"}
    expected_len = 3
    mocked_return_value = command_test_data["object_schema_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_schema_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_schema_list_command(client, args)
    mocked_client.assert_called()
    assert len(command_results.outputs) == expected_len


def test_jira_asset_object_schema_list_command_all_results_with_limit(mocker: MockerFixture):
    """
    Given:
        - An args dict with 'all_results' set to 'true' and a 'limit' argument.
    When:
        - Calling the jira_asset_object_schema_list_command with the args dict.
    Then:
        - The command returns all results, taking into account the 'limit' only if 'all_results' is false.
    """
    args = {"all_results": "true", "limit": 2}
    expected_len = 3
    mocked_return_value = command_test_data["object_schema_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_schema_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_schema_list_command(client, args)
    mocked_client.assert_called()
    assert len(command_results.outputs) == expected_len


def test_jira_asset_object_type_list_command_with_limit_2(mocker: MockerFixture):
    """
    Given:
        - An arguments dictionary with 'limit' set to '2' and 'schema_id' set to '1'.
    When:
        - Calling the jira_asset_object_type_list_command with the given arguments.
    Then:
        - The command should return exactly 2 results as specified by the 'limit' argument.
    """
    args = {"limit": "2", "schema_id": "1"}
    expected_len = 2
    mocked_return_value = command_test_data["object_type_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_object_type_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_type_list_command(client, args)
    mocked_client.assert_called_with("1", None)
    assert len(command_results.outputs) == expected_len


def test_jira_asset_object_type_list_command_with_limit_4(mocker: MockerFixture):
    """
    Given:
        - An arguments dictionary with 'limit' set to '4' and 'schema_id' set to '1'.
    When:
        - Calling the jira_asset_object_type_list_command with the given arguments.
    Then:
        - The command should return exactly 4 results as specified by the 'limit' argument.
    """
    args = {"limit": "4", "schema_id": "1"}
    expected_len = 4
    mocked_return_value = command_test_data["object_type_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_object_type_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_type_list_command(client, args)
    mocked_client.assert_called_with("1", None)
    assert len(command_results.outputs) == expected_len


def test_jira_asset_object_type_list_command_all_results(mocker: MockerFixture):
    """
    Given:
        - An arguments dictionary with 'all_results' set to 'true' and 'schema_id' set to '1'.
    When:
        - Calling the jira_asset_object_type_list_command with the given arguments.
    Then:
        - The command should return all available results, ignoring the 'limit' argument.
    """
    args = {"all_results": "true", "schema_id": "1"}
    expected_len = 5
    mocked_return_value = command_test_data["object_type_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_object_type_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_type_list_command(client, args)
    mocked_client.assert_called_with("1", None)
    assert len(command_results.outputs) == expected_len


def test_jira_asset_object_type_list_command_all_results_with_limit(mocker: MockerFixture):
    """
    Given:
        - An arguments dictionary with 'all_results' set to 'true', a 'limit' of 2, and 'schema_id' set to '1'.
    When:
        - Calling the jira_asset_object_type_list_command with the given arguments.
    Then:
        - The command should return all available results, as 'all_results' takes precedence over the 'limit'.
    """
    args = {"all_results": "true", "limit": 2, "schema_id": "1"}
    expected_len = 5
    mocked_return_value = command_test_data["object_type_list"]["response"]
    mocked_client = mocker.patch.object(client, "get_object_type_list", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_type_list_command(client, args)
    mocked_client.assert_called_with("1", None)
    assert len(command_results.outputs) == expected_len


@pytest.mark.parametrize(
    "args, expected_len",
    [
        ({"limit": "2", "object_type_id": "1"}, 2),
        ({"limit": "4", "object_type_id": "1"}, 4),
        ({"all_results": "true", "object_type_id": "1"}, 6),
        ({"all_results": "true", "limit": 2, "object_type_id": "1"}, 6),
    ],
)
def test_jira_asset_object_type_attribute_list_command(mocker: MockerFixture, args: dict[str, Any], expected_len: int):
    """
    Given: An args dict with limit and/or all_results
    When: Calling the jira_asset_object_type_list_command with the args dict
    Then: The command returns results, taking into account the limit, only if all_results is false
    """
    mocked_return_value = command_test_data["object_type_attributes"]["response"]
    mocked_client = mocker.patch.object(client, "get_object_type_attributes", return_value=mocked_return_value)
    command_results = JSM.jira_asset_object_type_attribute_list_command(client, args)
    assert len(command_results.outputs) == expected_len
    mocked_client.assert_called_with(
        object_type_id=args["object_type_id"],
        order_by_name=False,
        query=None,
        include_value_exist=False,
        exclude_parent_attributes=False,
        include_children=False,
        order_by_required=False,
    )


def test_jira_asset_object_create_command(mocker: MockerFixture):
    """
    Given: An object_type_id and a string of attributes
    When: The object_create command is called with said arguments
    Then: The client's create_object function is called with said arguments in the format that the
            get_attributes_json_data function returns
    """
    object_type_id = "1"
    attributes = {"1": ["value1"], "2": ["value1", "value2"]}
    attributes_json_data = JSM.get_attributes_json_data(object_type_id, attributes)
    mocked_client = mocker.patch.object(client, "create_object", return_value=command_test_data["object_create"]["response"])
    JSM.jira_asset_object_create_command(client, {"object_type_id": object_type_id, "attributes": attributes})
    mocked_client.assert_called_with(attributes_json_data)


def test_jira_asset_object_update_command(mocker: MockerFixture):
    """
    Given: An object_id and a string of attributes
    When: The object_update command is called with said arguments
    Then: The client's update_object function is called with said arguments in the format that the
            get_attributes_json_data function returns
    """
    object_id = "1"
    object_type_id = "3"
    attributes = {"1": ["value1"], "2": ["value1", "value2"]}
    attributes_json_data = JSM.get_attributes_json_data(object_type_id, attributes)
    mocker.patch.object(client, "get_object", return_value=command_test_data["get_object"]["response"])
    mocked_update_object = mocker.patch.object(
        client, "update_object", return_value=command_test_data["object_update"]["response"]
    )
    JSM.jira_asset_object_update_command(client, {"object_id": object_id, "attributes": attributes})
    mocked_update_object.assert_called_with(object_id, attributes_json_data)


def test_jira_asset_object_delete_command(mocker: MockerFixture):
    """
    Given: An object id
    When: The jira_asset_object_delete_command function is called
    Then: The client's delete_object function is called with said object id
    """
    object_id = "1"
    mocked_client = mocker.patch.object(client, "delete_object", return_value={})
    JSM.jira_asset_object_delete_command(client, {"object_id": object_id})
    mocked_client.assert_called_with(object_id)


def test_jira_asset_object_delete_non_exising_object(mocker: MockerFixture):
    """
    Given: A non-existing object id
    When: The client's delete_object function returns None
    Then: The jira_asset_object_delete_command function returns a specific hr response
    """
    object_id = "-1"
    mock_delete = mocker.patch.object(client, "delete_object", return_value=None)
    command_result = JSM.jira_asset_object_delete_command(client, {"object_id": object_id})
    mock_delete.assert_called_with(object_id)
    assert command_result.readable_output == f"Object with id: {object_id} does not exist"


def test_jira_object_get_command(mocker: MockerFixture):
    """
    Given: An object id
    When: The jira_asset_object_get_command function is called
    Then: The client's get_object function is called with said object id
    """
    object_id = "1"
    mocked_client = mocker.patch.object(client, "get_object", return_value=command_test_data["get_object"]["response"])
    JSM.jira_asset_object_get_command(client, {"object_id": object_id})
    mocked_client.assert_called_with(object_id)


def test_jira_asset_object_get_non_exising_object(mocker: MockerFixture):
    """
    Given: A non-existing object id
    When: The client's get_object function returns None
    Then: The jira_asset_object_get_command function returns a specific hr response
    """
    object_id = "-1"
    mock_delete = mocker.patch.object(client, "get_object", return_value=None)
    command_result = JSM.jira_asset_object_get_command(client, {"object_id": object_id})
    mock_delete.assert_called_with(object_id)
    assert command_result.readable_output == f"Object with id: {object_id} does not exist"


def test_jira_asset_object_search_command(mocker: MockerFixture):
    """
    Given: A ql_query
    When: The jira_asset_object_search_command function is called
    Then: The client's search_objects function is called with said ql_query, with the default values for the rest of the arguments
    """
    ql_query = "objectType = SW_engineer"
    mocked_client = mocker.patch.object(client, "search_objects", return_value=command_test_data["search_objects"]["response"])
    JSM.jira_asset_object_search_command(client, {"ql_query": ql_query})
    mocked_client.assert_called_with(ql_query, False, 1, 50, None)


def test_jira_asset_object_search_command_no_results(mocker: MockerFixture):
    """
    Given: A ql_query that matches no objects (API returns a response without 'objectEntries' key,
           e.g. a 404 error body such as {"errorMessages": [...], "errors": {}}).
    When: The jira_asset_object_search_command function is called.
    Then: The command returns a human-readable "No objects found." message instead of raising a KeyError.
    """
    ql_query = '"Hostname" in ("NONEXISTENT")'
    mocker.patch.object(client, "search_objects", return_value={"errorMessages": ["No resource found"], "errors": {}})
    result = JSM.jira_asset_object_search_command(client, {"ql_query": ql_query})
    assert result.readable_output == "No objects found."


def test_jira_asset_object_search_command_empty_object_entries(mocker: MockerFixture):
    """
    Given: A ql_query that matches no objects (API returns {"objectEntries": [], "total": 0}).
    When: The jira_asset_object_search_command function is called.
    Then: The command returns a human-readable "No objects found." message.
    """
    ql_query = '"Hostname" in ("NONEXISTENT")'
    mocker.patch.object(client, "search_objects", return_value={"objectEntries": [], "total": 0})
    result = JSM.jira_asset_object_search_command(client, {"ql_query": ql_query})
    assert result.readable_output == "No objects found."


@pytest.mark.parametrize(
    "args, expected_len", [({"object_type_id": "1"}, 6), ({"object_type_id": "1", "is_required": "true"}, 4)]
)
def test_jira_asset_attribute_json_create_command(mocker: MockerFixture, args, expected_len):
    """
    Given: An args dictionary with the object_type_id and an optional is_required argument
    When: The jira_asset_attribute_json_create_command function is called
    Then: The client's get_object_type_attributes function is called with the right parameters and returns only required objects
            if the is_required argument is set to true
    """
    object_type_id = "1"
    mocked_attributes_response = command_test_data["object_type_attributes"]["response"]
    mock_attributes_call = mocker.patch.object(client, "get_object_type_attributes", return_value=mocked_attributes_response)
    command_results = JSM.jira_asset_attribute_json_create_command(client, args)
    mock_attributes_call.assert_called_with(object_type_id=object_type_id, is_editable=False)
    _, command_results = command_results
    os.remove(f"1_{_.get('FileID')}")
    attributes = json.loads(command_results.readable_output).get("attributes")
    assert len(attributes) == expected_len


def test_jira_asset_comment_create_command(mocker: MockerFixture):
    """
    Given: An object id and a comment
    When: The jira_asset_comment_create_command function is called
    Then: The client's create_comment function is called with the object_id and the comment
    """
    object_id = "1"
    comment = "comment body"
    mocked_client = mocker.patch.object(client, "create_comment", return_value={"id": 1})
    JSM.jira_asset_comment_create_command(client, {"object_id": object_id, "comment": comment})
    mocked_client.assert_called_with(object_id, comment)


def test_jira_asset_comment_list_command(mocker: MockerFixture):
    """
    Given: An object id
    When: The jira_asset_comment_list_command function is called
    Then: The client's get_comment_list function is called with the object_id
    """
    object_id = "1"
    mocked_client = mocker.patch.object(client, "get_comment_list", return_value=[{"id": 1}])
    JSM.jira_asset_comment_list_command(client, {"object_id": object_id})
    mocked_client.assert_called_with(object_id)


def test_jira_asset_connected_ticket_list_command(mocker: MockerFixture):
    """
    Given: An object id
    When: The jira_asset_connected_ticket_list_command function is called
    Then: The client's get_object_connected_tickets function is called with the object_id
    """
    object_id = "1"
    mocked_client = mocker.patch.object(client, "get_object_connected_tickets", return_value={"tickets": [{"id": 1}]})
    JSM.jira_asset_connected_ticket_list_command(client, {"object_id": object_id})
    mocked_client.assert_called_with(object_id)


def test_jira_asset_attachment_add_command(mocker: MockerFixture):
    object_id = "1"
    entry_id = None
    mocker.patch.object(demisto, "getFilePath", return_value={"path": "test_data/test_file.txt"})
    mocked_client = mocker.patch.object(client, "send_file", return_value=[{"id": 1}])
    JSM.jira_asset_attachment_add_command(client, {"object_id": object_id, "entry_id": entry_id})
    mocked_client.assert_called_with(object_id=object_id, file_path="test_data/test_file.txt")


def test_jira_asset_attachment_list_command_no_download(mocker: MockerFixture):
    object_id = "1"
    path = f"/attachments/object/{object_id}"
    mocked_client = mocker.patch.object(client, "get_object_attachment_list", return_value=[{"id": 1}])
    JSM.jira_asset_attachment_list_command(client, {"object_id": object_id, "download_file": "false"})
    mocked_client.assert_called_with(path)


def test_jira_asset_attachment_list_command_with_download(mocker: MockerFixture):
    object_id = "1"
    file_list = command_test_data["attachment_list"]["response"]
    path = f"/attachments/object/{object_id}"
    mocked_client = mocker.patch.object(client, "get_object_attachment_list", return_value=file_list)
    mocker.patch.object(client, "get_file", return_value=Mock(content=b"file content"))
    mocker.patch("os.remove")
    mocker.patch("builtins.open", mocker.mock_open())
    mocker.patch("zipfile.ZipFile")
    result = JSM.jira_asset_attachment_list_command(client, {"object_id": object_id, "download_file": "true"})

    mocked_client.assert_called_with(path)
    assert isinstance(result, list)
    assert isinstance(result[0], dict)  # fileResult returns a dict
    assert result[0]["File"] == "ObjectAttachments.zip"
    assert isinstance(result[1], CommandResults)


def test_jira_asset_attachment_remove_command(mocker: MockerFixture):
    file_id = "1"
    mocked_client = mocker.patch.object(client, "remove_file", return_value={"id": 1})
    JSM.jira_asset_attachment_remove_command(client, {"id": file_id})
    mocked_client.assert_called_with(file_id)