import copy
from datetime import UTC, datetime
import demistomock as demisto
import pytest
from pytest_mock import MockerFixture
from requests_mock import Mocker as RequestsMock
from ArcherV2 import (
OCCURRED_FORMAT,
Client,
FilterConditionTypes,
construct_generic_filter_condition,
extract_from_xml,
fetch_incidents,
generate_field_contents,
generate_field_value,
get_errors_from_res,
get_fetch_time,
parser,
search_records_by_report_command,
search_records_soap_request,
upload_and_associate_command,
validate_xml_conditions,
)
from CommonServerPython import DemistoException
BASE_URL = "https://test.com/"
GET_TOKEN_SOAP = (
'' + ''
+ ' '
+ " TOKEN"
+ " "
+ " "
+ ""
)
XML_FOR_TEST = (
'' + ''
+ " "
+ ' '
+ " 6969"
+ " "
+ " "
+ ""
)
AUTH_LOGIN_RES = {"RequestedObject": {"SessionToken": "session-id"}, "IsSuccessful": True}
GET_LEVEL_RES = [{"IsSuccessful": True, "RequestedObject": {"Id": 123}}]
FIELD_DEFINITION_RES = [
{"IsSuccessful": True, "RequestedObject": {"Id": 1, "Type": 7, "Name": "External Links", "IsRequired": False}},
{
"IsSuccessful": True,
"RequestedObject": {"Id": 2, "Type": 1, "Name": "Device Name", "IsRequired": True, "RelatedValuesListId": 8},
},
]
GET_LEVELS_BY_APP = {
"level": 123,
"mapping": {
"1": {"Type": 7, "Name": "External Links", "FieldId": "1", "IsRequired": False, "RelatedValuesListId": None},
"2": {"Type": 1, "Name": "Device Name", "FieldId": "2", "IsRequired": True, "RelatedValuesListId": 8},
},
}
GET_FIElD_DEFINITION_RES = {
"RequestedObject": {"RelatedValuesListId": 62, "Type": 4},
"IsSuccessful": True,
"ValidationMessages": [],
}
VALUE_LIST_RES = {
"RequestedObject": {
"Children": [
{"Data": {"Id": 471, "Name": "Low", "IsSelectable": True}},
{"Data": {"Id": 472, "Name": "Medium", "IsSelectable": True}},
{"Data": {"Id": 473, "Name": "High", "IsSelectable": True}},
]
},
"IsSuccessful": True,
"ValidationMessages": [],
}
NESTED_VALUE_LIST_RES = {
"Links": [],
"RequestedObject": {
"Children": [
{
"Data": {
"Id": 83998,
"Name": "Corporate (Reportable)",
"IsSelectable": False,
},
"Children": [{"Data": {"Id": 88888, "Name": "level 2", "IsSelectable": False}, "Depth": 1}],
"Depth": 0,
},
{
"Data": {
"Id": 83999,
"Name": "Group & Other Non-Healthcare (Reportable)",
"IsSelectable": False,
"Generation": 0,
},
"Children": [
{
"Data": {
"Id": 84000,
"Name": "Group D&L, Run-off Businesses (Operating)",
"IsSelectable": False,
},
"Children": [],
"Depth": 1,
}
],
"Depth": 0,
},
{
"Data": {
"Id": 84001,
"Name": "Health Services (Reportable)",
"IsSelectable": False,
},
"Children": [
{
"Data": {
"Id": 84002,
"Name": "Pharmacy Operations (Operating)",
"IsSelectable": False,
"Generation": 1,
},
"Children": [
{
"Data": {
"Id": 84003,
"Name": "Cigna Home Delivery (Sub Segments)",
"IsSelectable": False,
},
"Children": [],
"Depth": 2,
},
{
"Data": {
"Id": 84004,
"Name": "ESI PBM (including Evicore) (Sub Segments)",
"IsSelectable": False,
},
"Children": [],
"Depth": 2,
},
],
"Depth": 1,
}
],
"Depth": 0,
},
{
"Data": {
"Id": 84005,
"Name": "Integrated Medical (Reportable)",
"IsSelectable": False,
},
"Children": [
{
"Data": {
"Id": 84006,
"Name": "Commercial (Operating)",
"IsSelectable": False,
},
"Children": [
{
"Data": {
"Id": 84007,
"Name": "Behavioral (Sub Segments)",
"IsSelectable": False,
},
"Children": [],
"Depth": 2,
},
],
"Depth": 1,
},
{
"Data": {
"Id": 84012,
"Name": "Government (Operating)",
"IsSelectable": False,
},
"Children": [
{
"Data": {
"Id": 84013,
"Name": "CareAllies (Sub Segments)",
"IsSelectable": False,
},
"Children": [],
"Depth": 2,
},
],
"Depth": 1,
},
],
"Depth": 0,
},
{
"Data": {
"Id": 107694,
"Name": "US Commercial",
"IsSelectable": False,
},
"Children": [],
"Depth": 0,
},
]
},
"IsSuccessful": "true",
"ValidationMessages": [],
}
VALUE_LIST_RES_FOR_SOURCE = {
"RequestedObject": {
"Children": [
{"Data": {"Id": 471, "Name": "ArcSight", "IsSelectable": True}},
{"Data": {"Id": 472, "Name": "Medium", "IsSelectable": True}},
{"Data": {"Id": 473, "Name": "High", "IsSelectable": True}},
]
},
"IsSuccessful": True,
"ValidationMessages": [],
}
VALUE_LIST_FIELD_DATA = {
"FieldId": 304,
"ValuesList": [
{"Id": 471, "Name": "Low", "IsSelectable": True, "Parent": "root", "Depth": None},
{"Id": 472, "Name": "Medium", "IsSelectable": True, "Parent": "root", "Depth": None},
{"Id": 473, "Name": "High", "IsSelectable": True, "Parent": "root", "Depth": None},
],
}
RES_WITH_ERRORS = {
"ValidationMessages": [
{"ResourcedMessage": "The Type field is a required field."},
{"ResourcedMessage": "The Device Name field is a required field."},
]
}
GET_RECORD_RES_failed = {"ValidationMessages": [{"ResourcedMessage": "No resource found."}]}
GET_RECORD_RES_SUCCESS = {
"Links": [],
"RequestedObject": {
"Id": 1010,
"LevelId": 123,
"FieldContents": {"2": {"Type": 1, "Value": "The device name", "FieldId": 2}},
},
"IsSuccessful": True,
"ValidationMessages": [],
}
INCIDENT_RECORD = {
"record": {"Id": "227602", "Status": "New", "Name": "Incident 01", "Date/Time Reported": "2018-03-26T10:03:32.243Z"},
"raw": {
"@contentId": "227602",
"@levelId": "67",
"@levelGuid": "b0c2d9a1-167c-4fee-ad91-4b4e7b098b4b",
"@moduleId": "75",
"@parentId": "0",
"Field": [
{
"@id": "302",
"@guid": "3ec0f462-4c17-4036-b0fa-2f04f3aba3d0",
"@type": "4",
"ListValues": {"ListValue": {"@id": "466", "@displayName": "New", "#text": "New"}},
},
{
"@id": "305",
"@guid": "9c5e3de1-299b-430f-998a-185ad86e2e79",
"@type": "3",
"@xmlConvertedValue": "2018-03-26T10:03:32.243Z",
"#text": "26/03/2018 06:03:32",
},
],
},
}
INCIDENT_RECORD_US_TZ = {
"record": {"Id": "227603", "Title": "Test", "created date": "2/25/2021 8:45:55 AM"},
"raw": {
"@contentId": "227603",
"@levelId": "67",
"@levelGuid": "b0c2d9a1-167c-4fee-ad91-4b4e7b098b4b",
"@moduleId": "75",
"@parentId": "0",
"Field": [
{"@id": "35339", "@guid": "9c5e3de1-299b-430f-998a-185ad86e2e79", "@type": "1", "#text": "Test"},
{
"@id": "53075",
"@guid": "9c5e3de1-299b-430f-998a-185ad86e2e80",
"@type": "21",
"@xmlConvertedValue": "2021-02-25T08:45:55.977Z",
"#text": "2/25/2021 8:45:55 AM",
},
],
},
}
SEARCH_RECORDS_RES = (
'' + ''
+ " "
+ ' '
+ " "
+ '<?xml version="1.0" encoding="utf-16"?><Records count="6"><Metadata><'
'FieldDefinitions><FieldDefinition id="2" name="Device Name" alias="Name_Full" /><'
'/FieldDefinitions></Metadata><LevelCounts><LevelCount id="37" count="6" /><'
'/LevelCounts><Record contentId="238756" levelId="37" moduleId="84" parentId="0"><Field id="2" guid='
'"9bc24614-2bc7-4849-a3a3-054729854ab4" type="1">DEVICE NAME</Field></Record></Records>'
+ " "
+ " "
+ " "
+ ""
)
GET_RESPONSE_NOT_SUCCESSFUL_JSON = {
"IsSuccessful": False,
"RequestedObject": None,
"ValidationMessages": [
{
"Reason": "Validation",
"Severity": 3,
"MessageKey": "ValidationMessageTemplates:LoginNotValid",
"Description": "",
"Location": -1,
"ErroredValue": None,
"Validator": "ArcherApi."
"Controllers.Security"
"Controller, ArcherApi, "
"Version=6.5.200.1045, "
"Culture=neutral, "
"PublicKeyToken=null",
"XmlData": None,
"ResourcedMessage": None,
}
],
}
GET_RESPONSE_SUCCESSFUL_JSON = {"IsSuccessful": True, "RequestedObject": {"SessionToken": "session-id"}}
SEARCH_RECORDS_BY_REPORT_RES = (
''
+ ""
+ " "
+ ' '
+ ' '
+ " "
+ ""
+ ""
+ ' '
+ ""
+ ''
+ ' 00.0 Introduction'
+ ' Information'
+ " "
+ ""
+ ""
)
MOCK_READABLE_SEARCH_RECORDS_BY_REPORT = (
"### Search records by report results\n|Id|Policy Name|Policy "
"Statement|\n|---|---|---|\n| 1720 | 00.0 Introduction | Information |\n"
)
MOCK_RESULTS_SEARCH_RECORDS_BY_REPORT = {
"Records": {
"@count": "18",
"Metadata": {
"FieldDefinitions": {
"FieldDefinition": [
{"@id": "1580", "@name": "Policy Name", "@alias": "Policy_Name"},
{"@id": "1583", "@name": "Policy Statement", "@alias": "Policy_Statement"},
]
}
},
"LevelCounts": {"LevelCount": {"@id": "3", "@count": "18"}},
"Record": {
"@contentId": "1720",
"@levelId": "3",
"@moduleId": "65",
"@parentId": "0",
"Field": [
{"@id": "1580", "@type": "1", "#text": "00.0 Introduction"},
{"@id": "1583", "@type": "1", "#text": "Information"},
],
},
}
}
GET_LEVEL_RES_2 = [
{
"RequestedObject": {"Type": 1, "Id": 1580, "LevelId": 3, "Name": "Policy Name", "Alias": "Policy_Name"},
"IsSuccessful": True,
},
{
"RequestedObject": {"Type": 1, "Id": 1583, "LevelId": 3, "Name": "Policy Statement", "Alias": "Policy_Statement"},
"IsSuccessful": True,
},
]
RES_DEPTH_0 = {
"FieldId": 304,
"ValuesList": [
{"Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{"Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{"Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{"Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{"Id": 107694, "Name": "US Commercial", "IsSelectable": False, "Parent": "root", "Depth": 0},
],
}
RES_DEPTH_1 = {
"FieldId": 304,
"ValuesList": [
{"Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{"Id": 88888, "Name": "level 2", "IsSelectable": False, "Parent": "Corporate (Reportable)", "Depth": 1},
{"Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{
"Id": 84000,
"Name": "Group D&L, Run-off Businesses (Operating)",
"IsSelectable": False,
"Parent": "Group & Other Non-Healthcare (Reportable)",
"Depth": 1,
},
{"Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{
"Id": 84002,
"Name": "Pharmacy Operations (Operating)",
"IsSelectable": False,
"Parent": "Health Services (Reportable)",
"Depth": 1,
},
{"Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{
"Id": 84006,
"Name": "Commercial (Operating)",
"IsSelectable": False,
"Parent": "Integrated Medical (Reportable)",
"Depth": 1,
},
{
"Id": 84012,
"Name": "Government (Operating)",
"IsSelectable": False,
"Parent": "Integrated Medical (Reportable)",
"Depth": 1,
},
{"Id": 107694, "Name": "US Commercial", "IsSelectable": False, "Parent": "root", "Depth": 0},
],
}
RES_DEPTH_2 = {
"FieldId": 304,
"ValuesList": [
{"Id": 83998, "Name": "Corporate (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{"Id": 88888, "Name": "level 2", "IsSelectable": False, "Parent": "Corporate (Reportable)", "Depth": 1},
{"Id": 83999, "Name": "Group & Other Non-Healthcare (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{
"Id": 84000,
"Name": "Group D&L, Run-off Businesses (Operating)",
"IsSelectable": False,
"Parent": "Group & Other Non-Healthcare (Reportable)",
"Depth": 1,
},
{"Id": 84001, "Name": "Health Services (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{
"Id": 84002,
"Name": "Pharmacy Operations (Operating)",
"IsSelectable": False,
"Parent": "Health Services (Reportable)",
"Depth": 1,
},
{
"Id": 84003,
"Name": "Cigna Home Delivery (Sub Segments)",
"IsSelectable": False,
"Parent": "Pharmacy Operations (Operating)",
"Depth": 2,
},
{
"Id": 84004,
"Name": "ESI PBM (including Evicore) (Sub Segments)",
"IsSelectable": False,
"Parent": "Pharmacy Operations (Operating)",
"Depth": 2,
},
{"Id": 84005, "Name": "Integrated Medical (Reportable)", "IsSelectable": False, "Parent": "root", "Depth": 0},
{
"Id": 84006,
"Name": "Commercial (Operating)",
"IsSelectable": False,
"Parent": "Integrated Medical (Reportable)",
"Depth": 1,
},
{"Id": 84007, "Name": "Behavioral (Sub Segments)", "IsSelectable": False, "Parent": "Commercial (Operating)", "Depth": 2},
{
"Id": 84012,
"Name": "Government (Operating)",
"IsSelectable": False,
"Parent": "Integrated Medical (Reportable)",
"Depth": 1,
},
{"Id": 84013, "Name": "CareAllies (Sub Segments)", "IsSelectable": False, "Parent": "Government (Operating)", "Depth": 2},
{"Id": 107694, "Name": "US Commercial", "IsSelectable": False, "Parent": "root", "Depth": 0},
],
}
class TestArcherV2:
def test_extract_from_xml(self):
field_id = extract_from_xml(XML_FOR_TEST, "Envelope.Body.GetValueListForField.fieldId")
assert field_id == "6969"
def test_get_level_by_app_id(self, requests_mock):
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.get(f"{BASE_URL}api/core/system/level/module/1", json=GET_LEVEL_RES)
requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/level/123", json=FIELD_DEFINITION_RES)
client = Client(BASE_URL, "", "", "", "", 400)
levels = client.get_level_by_app_id("1")
assert levels == GET_LEVELS_BY_APP
@pytest.mark.parametrize(
"requested_object, is_successful", [(GET_RESPONSE_NOT_SUCCESSFUL_JSON, False), (GET_RESPONSE_SUCCESSFUL_JSON, True)]
)
def test_update_session(self, mocker, requests_mock, requested_object, is_successful):
requests_mock.post(f"{BASE_URL}api/core/security/login", json=requested_object)
mocker.patch.object(demisto, "results")
client = Client(BASE_URL, "", "", "", "", 400)
if is_successful:
client.create_session()
assert demisto.results.call_count == 0
else:
with pytest.raises(SystemExit) as e:
# in case login wasn't successful, return_error will exit with a reason (for example, LoginNotValid)
# return_error reached
client.create_session()
assert e
def test_update_session_fail_parsing(self, mocker):
"""
Given:
an exception raised from _http_request who failed to pares json object
When:
- initiating session
Then:
- Raise exception with message to check the provided url
"""
mocker.patch.object(
Client,
"_http_request",
side_effect=DemistoException(
"Failed to parse json object from "
'response: b"
"
""
),
)
client = Client(BASE_URL, "", "", "", "", 400)
with pytest.raises(DemistoException) as e:
client.create_session()
assert "Check the given URL, it can be a redirect issue" in str(e.value)
@pytest.mark.parametrize(
"json_fields_values, expected_value",
[
pytest.param('{"Device Name":"Macbook\\Name\\"Test"}', 'Macbook\\Name"Test', id="Escaped double quote"),
pytest.param('{"Device Name":"Phone\\r\\n(Certified)"}', "Phone\r\n(Certified)", id="New line & carriage return"),
pytest.param('{"Device Name":"Employee\\A\\PC"}', "Employee\\A\\PC", id="Escaped Backslash"),
pytest.param('{"Device Name":"Laptop\\t#406"}', "Laptop\t#406", id="Tab"),
pytest.param('{"Device Name":"I \\u2764 Tech"}', "I \u2764 Tech", id="Unicode"),
pytest.param('{"Device Name":"Generic Tablet 2025"}', "Generic Tablet 2025", id="No special characters"),
pytest.param(r'{"Device Name":"Path: \\user\\123"}', r"Path: \user\123", id="Invalid unicode escape - user path"),
pytest.param(
r'{"Device Name":"C:\\users\\file.txt"}', r"C:\users\file.txt", id="Invalid unicode escape - Windows path"
),
pytest.param(r'{"Device Name":"Two:\\\\."}', r"Two:\\.", id="XSUP-59455 - Backslash before period"),
pytest.param(
r'{"Device Name":"Test of two double back-slashes followed by double-quote: \\". End of test."}',
r"Test of two double back-slashes followed by double-quote: \". End of test.",
id="XSUP-59455 Regression - Two backslashes before quote",
),
],
)
def test_generate_field_contents(self, json_fields_values: str, expected_value: str):
"""
Given:
- A string of fields values with special characters.
When:
- Loading a JSON string into a Python object.
Then:
- Return a valid object with the expected field value.
"""
client = Client(BASE_URL, "", "", "", "", 400)
field = generate_field_contents(client, json_fields_values, GET_LEVELS_BY_APP["mapping"], {"depth": 1})
assert field == {"2": {"Type": 1, "Value": expected_value, "FieldId": "2"}}
def test_get_errors_from_res(self):
errors = get_errors_from_res(RES_WITH_ERRORS)
assert errors == "The Type field is a required field.\nThe Device Name field is a required field."
def test_get_record_failed(self, requests_mock):
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.get(f"{BASE_URL}api/core/content/1010", json=GET_RECORD_RES_failed)
client = Client(BASE_URL, "", "", "", "", 400)
record, res, errors = client.get_record(75, 1010, {"depth": 1})
assert errors == "No resource found."
assert res
assert record == {}
def test_get_record_success(self, requests_mock):
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.get(f"{BASE_URL}api/core/content/1010", json=GET_RECORD_RES_SUCCESS)
requests_mock.get(f"{BASE_URL}api/core/system/level/module/1", json=GET_LEVEL_RES)
requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/level/123", json=FIELD_DEFINITION_RES)
client = Client(BASE_URL, "", "", "", "", 400)
record, res, errors = client.get_record(1, 1010, {"depth": 1})
assert errors is None
assert res
assert record == {"Device Name": "The device name", "Id": 1010}
def test_record_to_incident(self):
client = Client(BASE_URL, "", "", "", "", 400)
record = copy.deepcopy(INCIDENT_RECORD)
record["raw"]["Field"][1]["@xmlConvertedValue"] = "2018-03-26T10:03:00Z"
incident, incident_created_time = client.record_to_incident(record, 75, "305")
assert incident_created_time == datetime(2018, 3, 26, 10, 3, tzinfo=UTC)
assert incident["name"] == "RSA Archer Incident: 227602"
assert incident["occurred"] == "2018-03-26T10:03:00Z"
def test_search_records(self, requests_mock):
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.post(f"{BASE_URL}ws/general.asmx", text=GET_TOKEN_SOAP)
requests_mock.get(f"{BASE_URL}api/core/system/level/module/1", json=GET_LEVEL_RES)
requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/level/123", json=FIELD_DEFINITION_RES)
requests_mock.post(f"{BASE_URL}ws/search.asmx", text=SEARCH_RECORDS_RES)
client = Client(BASE_URL, "", "", "", "", 400)
records, raw_res = client.search_records(1, ["External Links", "Device Name"])
assert raw_res
assert len(records) == 1
assert records[0]["record"]["Id"] == "238756"
assert records[0]["record"]["Device Name"] == "DEVICE NAME"
@pytest.mark.parametrize(
"field_name, field_to_search_by_id, expected_condition",
[
pytest.param(
# Inputs ↓
"id_field_name",
"",
# Expected ↓
""
"Contains"
'field_id'
"1234"
"",
id="Generic text filter",
),
pytest.param(
# Inputs ↓
"id_field_name",
"id_field_name",
# Expected ↓
""
"5678"
"Equals"
"1234"
"",
id="Content filter by ID",
),
],
)
def test_search_records_soap_request(self, field_name: str, field_to_search_by_id: str, expected_condition: str):
"""
Given:
- Fields to search on records and id fields to search by ID.
When:
- Running search_records_soap_request to build the XML body.
Then:
- Ensure the correct condition is exist in the XML request body.
"""
xml_request = search_records_soap_request(
"token",
"app_id",
"display_fields",
"field_id",
field_name,
"1234",
field_to_search_by_id=field_to_search_by_id,
level_id="5678",
)
assert expected_condition in xml_request
@pytest.mark.parametrize(
"integration_context, expected_api_call_count",
[
pytest.param(
{"fieldValueList": {}},
1,
id="Cache miss (empty cache)",
),
pytest.param(
{"fieldValueList": {"304__1": VALUE_LIST_FIELD_DATA}},
0,
id="Cache hit (same id and depth)",
),
pytest.param(
{"fieldValueList": {"304__2": VALUE_LIST_FIELD_DATA}},
1,
id="Cache miss (same id but different depth)",
),
],
)
def test_get_field_value_list(
self,
mocker: MockerFixture,
requests_mock: RequestsMock,
integration_context: dict,
expected_api_call_count: int,
):
"""
Given:
- Integration context cache.
When:
- Running get_field_value_list.
Then:
- Ensure API requests are not performed if the matching data exists in the integration context.
- Ensure correct field value list is returned.
"""
field_id = 304
depth = 1
mocker.patch("ArcherV2.get_integration_context", return_value=integration_context)
login_request = requests_mock.post(
f"{BASE_URL}api/core/security/login",
json=AUTH_LOGIN_RES,
)
field_definition_request = requests_mock.get(
f"{BASE_URL}api/core/system/fielddefinition/{field_id}",
json=GET_FIElD_DEFINITION_RES,
)
value_list_request = requests_mock.get(
f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62",
json=VALUE_LIST_RES,
)
client = Client(BASE_URL, "", "", "", "", 400)
field_data = client.get_field_value_list(field_id, depth)
assert login_request.call_count == expected_api_call_count
assert field_definition_request.call_count == expected_api_call_count
assert value_list_request.call_count == expected_api_call_count
assert field_data == VALUE_LIST_FIELD_DATA
@pytest.mark.parametrize("args, expected_response", [(0, RES_DEPTH_0), (1, RES_DEPTH_1), (2, RES_DEPTH_2)])
def test_get_field_value_list_nested_response(self, requests_mock, args, expected_response):
cache = demisto.getIntegrationContext()
cache["fieldValueList"] = {}
demisto.setIntegrationContext(cache)
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/304", json=GET_FIElD_DEFINITION_RES)
requests_mock.get(f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=NESTED_VALUE_LIST_RES)
client = Client(BASE_URL, "", "", "", "", 400)
field_data = client.get_field_value_list(304, args)
assert field_data.get("FieldId") == expected_response.get("FieldId")
for expected, result in zip(expected_response.get("ValuesList"), field_data.get("ValuesList")):
assert expected == result
def test_generate_field_value_text_input(self):
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(client, "", {"Type": 1}, "Demisto", {"depth": 1})
assert field_key == "Value"
assert field_value == "Demisto"
def test_generate_field_value_values_list_input(self, requests_mock):
cache = demisto.getIntegrationContext()
cache["fieldValueList"] = {}
demisto.setIntegrationContext(cache)
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/304", json=GET_FIElD_DEFINITION_RES)
requests_mock.get(f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=VALUE_LIST_RES)
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(client, "", {"Type": 4, "FieldId": 304}, ["High"], 1)
assert field_key == "Value"
assert field_value == {"ValuesListIds": [473]}
def test_generate_field_external_link_input(self):
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(
client,
"",
{"Type": 7},
[{"value": "github", "link": "https://github.com"}, {"value": "google", "link": "https://google.com"}],
{"depth": 1},
)
assert field_key == "Value"
assert field_value == [{"Name": "github", "URL": "https://github.com"}, {"Name": "google", "URL": "https://google.com"}]
def test_generate_field_users_groups_input(self):
"""
Given:
Valid value from dictionary type under "fieldsToValues" argument
When:
- running archer-update-record
Then:
- assert fields are generated correctly
"""
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(client, "", {"Type": 8}, {"users": [20], "groups": [30]}, {"depth": 1})
assert field_key == "Value"
assert field_value == {"UserList": [{"ID": 20}], "GroupList": [{"ID": 30}]}
def test_generate_field_values_list_with_other(self, requests_mock, mocker):
"""
Given:
list values with "OtherText" from dictionary type under "fieldsToValues" argument
When:
- running archer-update-record
Then:
- assert fields are generated correctly
"""
mocker.patch.object(Client, "get_field_value_list", return_value={"ValuesList": [{"Name": "NA", "Id": 222}]})
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(
client, "", {"Type": 4, "FieldId": 1234}, {"ValuesList": ["NA"], "OtherText": "test"}, {"depth": 1}
)
assert field_key == "Value"
assert field_value == {"ValuesListIds": [222], "OtherText": "test"}
def test_generate_invalid_field_users_groups_input(self):
"""
Given:
Invalid value under "fieldsToValues" argument with type 8 (lists)
When:
- running archer-update-record
Then:
- Raise exception indicates that the value is not with the right format
"""
client = Client(BASE_URL, "", "", "", "", 400)
with pytest.raises(DemistoException) as e:
generate_field_value(client, "test", {"Type": 8}, "user1, user2", {"depth": 1})
assert (
'The value of the field: test must be a dictionary type and include a list under "users" key or '
'"groups" key e.g: {"Policy Owner":{"users":[20],"groups":[30]}}' in str(e.value)
)
@pytest.mark.parametrize(
"field_value, result", [([1, 2], [{"ContentID": 1}, {"ContentID": 2}]), (1234, [{"ContentID": 1234}])]
)
def test_generate_field_cross_reference_input(self, field_value, result):
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(client, "", {"Type": 9}, field_value, {"depth": 1})
assert field_key == "Value"
assert field_value == result
def test_generate_field_ip_address_input(self):
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(client, "", {"Type": 19}, "127.0.0.1", {"depth": 1})
assert field_key == "IpAddressBytes"
assert field_value == "127.0.0.1"
def test_generate_field_value(self, requests_mock):
"""
Given
- generate_field_value on Values List type
When
- the source is not a list
Then
- ensure generate_field_value will handle it
"""
cache = demisto.getIntegrationContext()
cache["fieldValueList"] = {}
demisto.setIntegrationContext(cache)
requests_mock.get(f"{BASE_URL}api/core/system/fielddefinition/16172", json=GET_FIElD_DEFINITION_RES)
requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
requests_mock.get(f"{BASE_URL}api/core/system/valueslistvalue/valueslist/62", json=VALUE_LIST_RES_FOR_SOURCE)
client = Client(BASE_URL, "", "", "", "", 400)
field_key, field_value = generate_field_value(
client,
"Source",
{"FieldId": "16172", "IsRequired": False, "Name": "Source", "RelatedValuesListId": 2092, "Type": 4},
"ArcSight",
1,
)
assert field_key == "Value"
assert field_value == {"ValuesListIds": [471]}
def test_record_to_incident_europe_time(self):
"""
Given:
record with european time (day first)
When:
fetching incidents
Then:
assert return dates are right
"""
client = Client(BASE_URL, "", "", "", "", 400)
incident = INCIDENT_RECORD.copy()
incident["raw"]["Field"][1]["@xmlConvertedValue"] = "2018-03-26T10:03:00Z"
incident["record"]["Date/Time Reported"] = "26/03/2018 10:03 AM"
incident, incident_created_time = client.record_to_incident(INCIDENT_RECORD, 75, "305")
assert incident_created_time == datetime(2018, 3, 26, 10, 3, tzinfo=UTC)
assert incident["occurred"] == "2018-03-26T10:03:00Z"
def test_record_to_incident_american_time(self):
"""
Given:
record with american time (month first)
When:
fetching incidents
Then:
assert return dates are right
"""
client = Client(BASE_URL, "", "", "", "", 400)
incident = INCIDENT_RECORD.copy()
incident["record"]["Date/Time Reported"] = "03/26/2018 10:03 AM"
incident["raw"]["Field"][1]["@xmlConvertedValue"] = "2018-03-26T10:03:00Z"
incident, incident_created_time = client.record_to_incident(INCIDENT_RECORD, 75, "305")
assert incident_created_time == datetime(2018, 3, 26, 10, 3, tzinfo=UTC)
assert incident["occurred"] == "2018-03-26T10:03:00Z"
def test_fetch_time_change(self, mocker):
"""
Given:
incident with date/time reported
european time (day first) - True or false
When:
Fetching incidents
Then:
Check that the new next fetch is greater than last_fetch
Check the wanted next_fetch is true
Assert occurred time
"""
client = Client(BASE_URL, "", "", "", "", 400)
date_time_reported = "2018-04-03T10:03:00.000Z"
params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"}
record = copy.deepcopy(INCIDENT_RECORD)
record["record"]["Date/Time Reported"] = date_time_reported
record["raw"]["Field"][1]["@xmlConvertedValue"] = date_time_reported
last_fetch = get_fetch_time({"last_fetch": "2018-03-01T10:03:00Z"}, params.get("fetch_time", "3 days"))
mocker.patch.object(client, "search_records", return_value=([record], {}))
incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305")
assert last_fetch < next_fetch
assert next_fetch == datetime(2018, 4, 3, 10, 3, tzinfo=UTC)
assert incidents[0]["occurred"] == date_time_reported
def test_two_fetches(self, mocker):
"""
Given:
2 incident with date/time reported
running two fetches.
When:
Fetching incidents
Then:
Check that the new next fetch is greater than last_fetch on both calls.
Check the wanted next_fetch is equals to the date in the incident in both calls.
Assert occurred time
"""
client = Client(BASE_URL, "", "", "", "", 400)
params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"}
record1, record2 = copy.deepcopy(INCIDENT_RECORD), copy.deepcopy(INCIDENT_RECORD)
record1["record"]["Date/Time Reported"] = "18/03/2020 10:30 AM"
record2["record"]["Date/Time Reported"] = "18/03/2020 03:30 PM"
record1["raw"]["Field"][1]["@xmlConvertedValue"] = "2020-03-18T10:30:00.000Z"
record2["raw"]["Field"][1]["@xmlConvertedValue"] = "2020-03-18T15:30:00.000Z"
last_fetch = parser("2020-18-03T09:00:00Z")
mocker.patch.object(client, "search_records", side_effect=[([record1], {}), ([record2], {})])
incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305")
assert last_fetch < next_fetch
assert next_fetch == datetime(2020, 3, 18, 10, 30, tzinfo=UTC)
assert incidents[0]["occurred"] == "2020-03-18T10:30:00.000Z"
incidents, next_fetch = fetch_incidents(client, params, next_fetch, "305")
assert last_fetch < next_fetch
assert next_fetch == datetime(2020, 3, 18, 15, 30, tzinfo=UTC)
assert incidents[0]["occurred"] == "2020-03-18T15:30:00.000Z"
def test_fetch_got_old_incident(self, mocker):
"""
Given:
last_fetch is newer than new incident
When:
Fetching incidents
Then:
Check that the next fetch is equals last fetch (no new incident)
Check that no incidents brought back
"""
client = Client(BASE_URL, "", "", "", "", 400)
date_time_reported = "2018-03-01T10:02:00.000Z"
params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"}
record = copy.deepcopy(INCIDENT_RECORD)
record["record"]["Date/Time Reported"] = date_time_reported
record["raw"]["Field"][1]["@xmlConvertedValue"] = date_time_reported
last_fetch = get_fetch_time({"last_fetch": "2018-03-01T10:03:00Z"}, params.get("fetch_time", "3 days"))
mocker.patch.object(client, "search_records", return_value=([record], {}))
incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305")
assert last_fetch == next_fetch
assert not incidents, "Should not get new incidents."
def test_fetch_got_exact_same_time(self, mocker):
"""
Given:
last_fetch is in the exact same time as the incident
When:
Fetching incidents
Then:
Check that the next fetch is equals last fetch (no new incident)
Check that no incidents brought back
"""
client = Client(BASE_URL, "", "", "", "", 400)
date_time_reported = "2018-03-01T10:02:00.000Z"
params = {"applicationId": "75", "applicationDateField": "Date/Time Reported"}
record = copy.deepcopy(INCIDENT_RECORD)
record["record"]["Date/Time Reported"] = date_time_reported
record["raw"]["Field"][1]["@xmlConvertedValue"] = date_time_reported
last_fetch = get_fetch_time({"last_fetch": date_time_reported}, params.get("fetch_time", "3 days"))
mocker.patch.object(client, "search_records", return_value=([record], {}))
incidents, next_fetch = fetch_incidents(client, params, last_fetch, "305")
assert last_fetch == next_fetch
assert not incidents, "Should not get new incidents."
@staticmethod
def test_fetch_blacklisted_date_filter():
"""
Given:
fetch_xml parameter with a forbidden DateComparisonFilterCondition
When:
Fetching incidents
Then:
Check that a ValueError is raised with the appropriate error message
"""
client = Client(BASE_URL, "", "", "", "", 400)
params = {
"applicationId": "75",
"applicationDateField": "Date/Time Reported",
"fetch_xml": (
""
"GreaterThan"
'7195'
"2023-06-04T13:08:43.433385Z"
"UTC Standard Time"
"TRUE"
""
),
}
from_time = datetime(2024, 12, 11)
expected_error_message = 'XML filter condition cannot contain the "DateComparisonFilterCondition" tag'
with pytest.raises(ValueError, match=expected_error_message):
fetch_incidents(client, params, from_time, "204")
def test_same_record_returned_in_two_fetches(self, mocker):
"""
Given:
- Same record returned in 2 fetch queries
When:
- Fetching incidents (2 iterations)
Then:
Check that the new next fetch is greater than last_fetch on both calls.
Check the wanted next_fetch is equals to the date in the incident in both calls.
Assert occurred time
"""
client = Client(BASE_URL, "", "", "", "", 400)
mocker.patch.object(client, "search_records", side_effect=[([INCIDENT_RECORD_US_TZ], {}), ([INCIDENT_RECORD_US_TZ], {})])
params = {"applicationId": "75", "applicationDateField": "created date"}
field_time_id = "53075"
first_fetch = parser("2021-02-24T08:45:55Z")
incidents, first_next_fetch = fetch_incidents(client, params, first_fetch, field_time_id)
assert first_fetch < first_next_fetch
assert first_next_fetch == datetime(2021, 2, 25, 8, 45, 55, 977000, tzinfo=UTC)
assert incidents[0]["occurred"] == "2021-02-25T08:45:55.977Z"
# first_next_fetch_dt simulates the set to last_run done in fetch-incidents
first_next_fetch_dt = parser(first_next_fetch.strftime(OCCURRED_FORMAT))
incidents, second_next_fetch = fetch_incidents(client, params, first_next_fetch_dt, field_time_id)
assert first_next_fetch == datetime(2021, 2, 25, 8, 45, 55, 977000, tzinfo=UTC)
assert not incidents
def test_search_records_by_report_command(self, mocker):
"""
Given:
- search_records_by_report_command command args
When:
- run search_records_by_report_command
Then:
- Verify response outputs
- verify response readable output
"""
mock_args = {"reportGuid": "id"}
client = Client(BASE_URL, "", "", "", "", 400)
mocker.patch.object(client, "do_soap_request", return_value=[SEARCH_RECORDS_BY_REPORT_RES, SEARCH_RECORDS_BY_REPORT_RES])
mocker.patch.object(client, "do_rest_request", return_value=GET_LEVEL_RES_2)
mocker.patch.object(demisto, "results")
search_records_by_report_command(client, mock_args)
assert demisto.results.call_args_list[0][0][0]["HumanReadable"] == MOCK_READABLE_SEARCH_RECORDS_BY_REPORT
assert demisto.results.call_args_list[0][0][0]["Contents"] == MOCK_RESULTS_SEARCH_RECORDS_BY_REPORT
@pytest.mark.parametrize(
"integration_context, is_login_expected, http_call_attempt_results",
[
({}, True, [{"status_code": 200, "json": {"res": "some_res"}}]),
({"session_id": "test_session_id"}, False, [{"status_code": 200, "json": {"res": "some_res"}}]),
({"session_id": "test_session_id"}, False, [{"status_code": 401}, {"status_code": 200, "json": {"res": "some_res"}}]),
(
{"session_id": "test_session_id"},
True,
[{"status_code": 401}, {"status_code": 401}, {"status_code": 200, "json": {"res": "some_res"}}],
),
],
)
def test_do_rest_request(self, mocker, requests_mock, integration_context, is_login_expected, http_call_attempt_results):
"""
Test for the do_rest_request function.
Given:
Case 1: Empty integration context (no cached session_id).
Case 2: Integration context with cached session_id.
Case 3: Integration context with cached session_id.
Case 4: Integration context with cached session_id.
When:
Case 1: rest API request succeed on first run with newly generated session_id.
Case 2: rest API request succeed on first run with cached the session_id.
Case 3: rest API request fails on first attempt and succeed on second run with the cached session_id.
Case 4: rest API request fails on two first attempts with the cached session_id and succeed on third run
after creating new session_id.
Then:
Case 1: Ensure new session_id was generated, and only one call to the search API was done (success).
Case 2: Ensure no new session_id was generated, and only one call to the search API was done (success).
Case 3: Ensure no new session_id was generated, and two calls to the search API were done (failure and
success).
Case 3: Ensure new session_id was generated, and three calls to the search API were done (failure, failure
and success).
"""
client = Client(BASE_URL, "", "", "", "", 400)
mocker.patch("ArcherV2.get_integration_context", return_value=integration_context)
login_mocker = requests_mock.post(f"{BASE_URL}api/core/security/login", json=AUTH_LOGIN_RES)
rest_mocker = requests_mock.get(f"{BASE_URL}test_requests", http_call_attempt_results)
dummy_response = client.do_rest_request("GET", "test_requests")
if is_login_expected:
assert login_mocker.called_once
else:
assert not login_mocker.called
assert rest_mocker.call_count == len(http_call_attempt_results)
assert dummy_response
@pytest.mark.parametrize(
"integration_context, is_new_token_expected, http_call_attempt_results",
[
({}, True, [{"status_code": 200, "text": SEARCH_RECORDS_RES}]),
({"token": "TOKEN"}, False, [{"status_code": 200, "text": SEARCH_RECORDS_RES}]),
({"token": "TOKEN"}, False, [{"status_code": 500}, {"status_code": 200, "text": SEARCH_RECORDS_RES}]),
(
{"token": "TOKEN"},
True,
[{"status_code": 500}, {"status_code": 500}, {"status_code": 200, "text": SEARCH_RECORDS_RES}],
),
],
)
def test_do_soap_request(self, mocker, requests_mock, integration_context, is_new_token_expected, http_call_attempt_results):
"""
Test for the do_soap_request function.
(we use the archer-search-records template as test case, but it doesn't really matter)
Given:
Case 1: Empty integration context (no cached token).
Case 2: Integration context with cached token.
Case 3: Integration context with cached token.
Case 4: Integration context with cached token.
When:
Case 1: Soap API request succeed on first run with newly generated the token.
Case 2: Soap API request succeed on first run with cached the token.
Case 3: Soap API request fails on first attempt and succeed on second run with the cached token.
Case 4: Soap API request fails on two first attempts and succeed on third run after creating new token.
Then:
Case 1: Ensure new token was generated, and only one call to the search API was done (success).
Case 2: Ensure no new token was generated, and only one call to the search API was done (success).
Case 3: Ensure no new token was generated, and two calls to the search API were done (failure and success).
Case 3: Ensure new token was generated, and three calls to the search API were done (failure, failure and
success).
"""
client = Client(BASE_URL, "", "", "", "", 400)
mocker.patch("ArcherV2.get_integration_context", return_value=integration_context)
new_token_mocker = requests_mock.post(f"{BASE_URL}ws/general.asmx", text=GET_TOKEN_SOAP)
soap_mocker = requests_mock.post(f"{BASE_URL}ws/search.asmx", http_call_attempt_results)
search_commands_args = {
"app_id": 1,
"display_fields": '1'
'2',
"field_id": "",
"field_name": "",
"field_to_search_by_id": "",
"numeric_operator": "",
"date_operator": "",
"search_value": "",
"max_results": 10,
"sort_type": "Ascending",
"level_id": 123,
}
client.do_soap_request("archer-search-records", **search_commands_args)
if is_new_token_expected:
assert new_token_mocker.called_once
else:
assert not new_token_mocker.called
assert soap_mocker.call_count == len(http_call_attempt_results)
def test_validate_xml_conditions_valid(self):
"""
Given:
- A string that is meant to represents a valid XML document.
When:
- Calling validate_xml_conditions.
Assert:
- Ensure no exception is raised.
"""
xml_conditions = (
""
"Equals"
'7'
"Dev"
""
""
"GreaterThan"
'8'
"25"
""
)
validate_xml_conditions(xml_conditions) # if exception raised, test would fail
@pytest.mark.parametrize(
"xml_document, blacklisted_tags, expected_error_message",
[
pytest.param(
# Inputs ↓
"false",
[],
# Expected ↓
"Invalid XML filter condition syntax",
id="Mismatched tags",
),
pytest.param(
# Inputs ↓
'5',
["ModuleCriteria"],
# Expected ↓
'XML filter condition cannot contain the "ModuleCriteria" tag',
id="Blacklisted tag",
),
],
)
def test_validate_xml_conditions_raise_exception(
self,
xml_document: str,
blacklisted_tags: list[str],
expected_error_message: str,
):
"""
Given:
- A malformed XML document and one that contains a forbidden XML tag.
When:
- Calling validate_xml_conditions.
Assert:
- Ensure a ValueError is raised with the correct error message.
"""
with pytest.raises(ValueError, match=expected_error_message):
validate_xml_conditions(xml_document, blacklisted_tags)
@pytest.mark.parametrize(
"condition_type, operator, field_name, field_id, search_value, expected_xml_condition",
[
pytest.param(
# Inputs ↓
FilterConditionTypes.date,
"GreaterThan",
"Last Updated",
"1234",
"2024-12-11T11:11:24.433385Z",
# Expected ↓
""
"GreaterThan"
'1234'
"2024-12-11T11:11:24.433385Z"
"",
id="Date greater than condition",
),
pytest.param(
# Inputs ↓
FilterConditionTypes.text,
"Contains",
"Incident Priority",
"456",
"High",
# Expected ↓
""
"Contains"
'456'
"High"
"",
id="Text contains condition",
),
],
)
def test_construct_generic_filter_condition(
self,
condition_type: FilterConditionTypes,
operator: str,
field_name: str,
field_id: str,
search_value: str,
expected_xml_condition: str,
):
"""
Given:
- A filter condition with a comparison operator on a given field.
When:
- Calling construct_generic_filter_condition.
Assert:
- Ensure a valid condition XML element with the correct sub-elements.
"""
xml_condition = construct_generic_filter_condition(
condition_type=condition_type,
operator=operator,
field_name=field_name,
field_id=field_id,
search_value=search_value,
)
assert xml_condition == expected_xml_condition
def test_upload_and_associate_command_record_has_attachments(self, mocker):
"""
Given: A record with existing attachments and multiple files to upload
When: The upload_and_associate_command is called
Then: Files are uploaded, associated with the record, and existing attachments are preserved
"""
associatedField = "Supporting Documentation"
client = Client(BASE_URL, "", "", "", "", 400)
mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123")
mock_update_record = mocker.patch("ArcherV2.update_record_command")
mock_get_record = mocker.patch.object(client, "get_record", return_value=({associatedField: ["456", "789"]}, "", ""))
args = {"applicationId": "app1", "contentId": "content1", "associatedField": associatedField, "entryId": "entry1, entry2"}
upload_and_associate_command(client, args)
assert mock_upload_file.call_count == 2
assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"})
assert mock_upload_file.call_args_list[1] == mocker.call(client, {"entryId": "entry2"})
mock_update_record.assert_called_once_with(
client,
{
"applicationId": "app1",
"contentId": "content1",
"associatedField": associatedField,
"entryId": "entry1, entry2",
"fieldsToValues": f'{{"{associatedField}": ["123", "123", "456", "789"]}}',
},
)
mock_get_record.assert_called_once_with("app1", "content1", 0)
def test_upload_and_associate_command_single_file(self, mocker):
"""
Given: A single file to upload and associate
When: The upload_and_associate_command is called
Then: The file is uploaded and associated with the record
"""
client = Client(BASE_URL, "", "", "", "", 400)
mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123")
mock_update_record = mocker.patch("ArcherV2.update_record_command")
mock_get_record = mocker.patch.object(client, "get_record", return_value=({"ID": "123"}, "", ""))
args = {"applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1"}
upload_and_associate_command(client, args)
assert mock_upload_file.call_count == 1
assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"})
mock_get_record.assert_called_once_with("app1", "content1", 0)
mock_update_record.assert_called_once_with(
client,
{
"applicationId": "app1",
"contentId": "content1",
"associatedField": "field1",
"entryId": "entry1",
"fieldsToValues": '{"field1": ["123"]}',
},
)
def test_upload_and_associate_command_record_has_no_attachments(self, mocker):
"""
Given: A record without existing attachments and multiple files to upload
When: The upload_and_associate_command is called
Then: Files are uploaded and associated with the record
"""
client = Client(BASE_URL, "", "", "", "", 400)
mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123")
mock_update_record = mocker.patch("ArcherV2.update_record_command")
mock_get_record = mocker.patch.object(client, "get_record", return_value=({"ID": "123"}, "", ""))
args = {"applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1, entry2"}
upload_and_associate_command(client, args)
assert mock_upload_file.call_count == 2
assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"})
assert mock_upload_file.call_args_list[1] == mocker.call(client, {"entryId": "entry2"})
mock_update_record.assert_called_once_with(
client,
{
"applicationId": "app1",
"contentId": "content1",
"associatedField": "field1",
"entryId": "entry1, entry2",
"fieldsToValues": '{"field1": ["123", "123"]}',
},
)
mock_get_record.assert_called_once_with("app1", "content1", 0)
def test_upload_and_associate_command_record_with_error(self, mocker):
"""
Given: An error occurs during record retrieval
When: The upload_and_associate_command is called
Then: Files are uploaded, association is attempted, and an error is returned
"""
client = Client(BASE_URL, "", "", "", "", 400)
mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123")
mock_update_record = mocker.patch("ArcherV2.update_record_command")
mock_get_record = mocker.patch.object(client, "get_record", return_value=({"ID": "123"}, "", "error"))
mock_error = mocker.patch("ArcherV2.return_error")
args = {"applicationId": "app1", "contentId": "content1", "associatedField": "field1", "entryId": "entry1, entry2"}
upload_and_associate_command(client, args)
assert mock_upload_file.call_count == 2
assert mock_upload_file.call_args_list[0] == mocker.call(client, {"entryId": "entry1"})
assert mock_upload_file.call_args_list[1] == mocker.call(client, {"entryId": "entry2"})
mock_update_record.assert_called_once_with(
client,
{
"applicationId": "app1",
"contentId": "content1",
"associatedField": "field1",
"entryId": "entry1, entry2",
"fieldsToValues": '{"field1": ["123", "123"]}',
},
)
mock_get_record.assert_called_once_with("app1", "content1", 0)
mock_error.assert_called_once_with("error")
def test_upload_and_associate_command_without_association(self, mocker):
"""
Given: A file to upload without association to a record
When: The upload_and_associate_command is called
Then: The file is uploaded without being associated to any record
"""
client = Client(BASE_URL, "", "", "", "", 400)
mock_upload_file = mocker.patch("ArcherV2.upload_file_command", return_value="123")
args = {"entryId": "entry1"}
upload_and_associate_command(client, args)
assert mock_upload_file.call_count == 1
mock_upload_file.assert_called_once_with(
client,
{
"entryId": "entry1",
},
)
def test_upload_and_associate_command_missing_args(self, mocker):
"""
Given: Incomplete arguments for upload and associate command
When: The upload_and_associate_command is called
Then: An exception is raised indicating missing required arguments
"""
client = Client(BASE_URL, "", "", "", "", 400)
# Test error when only applicationId is provided
args = {"applicationId": "app1", "entryId": "entry1"}
with pytest.raises(DemistoException) as e:
upload_and_associate_command(client, args)
assert str(e.value) == "Found arguments to associate an attachment to a record, but not all required arguments supplied"
# Test error when only contentId is provided
args = {"contentId": "content1", "entryId": "entry1"}
with pytest.raises(DemistoException) as e:
upload_and_associate_command(client, args)
assert str(e.value) == "Found arguments to associate an attachment to a record, but not all required arguments supplied"