from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import unquote
import freezegun
import pytest
from PATHelpdeskAdvanced import (
DATETIME_FORMAT,
Client,
DemistoException,
Field,
Filter,
PaginateArgs,
convert_response_dates,
demisto,
json,
list_groups_command,
list_ticket_attachments_command,
list_users_command,
paginate,
pat_table_to_markdown,
)
@pytest.mark.parametrize(
"demisto_name, expected_demisto_name, expected_hda_name",
[
("incident_id", "incident_id", "IncidentID"),
("unread_email_html", "unread_email_html", "UnReadEmailHTML"),
("task", "task", "Task"),
("unread", "unread", "UnRead"),
("user_id_html", "user_id_html", "UserIDHTML"),
],
)
def test_field(demisto_name, expected_demisto_name, expected_hda_name):
"""
Given a Demisto name, when initializing an instance of MyClass,
then the demisto_name attribute should be set to the given Demisto name,
and the hda_name attribute should be set to the expected HDA name.
Args:
demisto_name (str): The Demisto name.
expected_demisto_name (str): The expected value of the demisto_name attribute.
expected_hda_name (str): The expected value of the hda_name attribute.
Returns:
None
"""
field = Field(demisto_name)
assert field.demisto_name == expected_demisto_name
assert field.hda_name == expected_hda_name
def test_converts_date_fields():
"""
Given a response dict with date fields
When convert_response_dates is called
Then date fields are converted to datetime, and others are untouched
"""
EPOCH_2023_INT = 1693573200000
EPOCH_2022_INT = 1641042000000
STR_2023 = (datetime.fromtimestamp(EPOCH_2023_INT / 1000, tz=timezone.utc)).strftime(DATETIME_FORMAT) # noqa: UP017
STR_2022 = (datetime.fromtimestamp(EPOCH_2022_INT / 1000, tz=timezone.utc)).strftime(DATETIME_FORMAT) # noqa: UP017
raw = {
"Date1": f"/Date({EPOCH_2023_INT})/",
"Date2": [f"/Date({EPOCH_2023_INT})/", f"/Date({EPOCH_2022_INT})/", "🕒"],
"other": [EPOCH_2023_INT, STR_2023],
}
result = convert_response_dates(raw)
assert result["Date1"] == STR_2023
assert result["Date2"] == [STR_2023, STR_2022, "🕒"]
assert result["other"] == raw["other"]
@pytest.fixture
def mocked_client(mocker):
"""Given a mocked Client instance"""
return mocker.Mock()
class TestCommands:
@classmethod
def test_create_ticket(cls, mocked_client):
from PATHelpdeskAdvanced import create_ticket_command
"""
Given a client instance and arguments for creating a ticket
When create_ticket_command is called with the client and args
Then it should return a CommandResults instance with the expected outputs
"""
args = {"subject": "Test ticket", "problem": "Something went wrong"}
mocked_client.create_ticket.return_value = {
"id": 123,
"data": {
"ObjectDescription": "Test ticket",
"ObjectEntity": "ticket",
"Solution": "Fixed the issue",
"TicketClassificationId": 1,
"IsNew": True,
"ExpirationDate": "/Date(1609459200000)/",
"FirstUpdateUserId": 1,
"OwnerUserId": 2,
"Date": "/Date(1609459200000)/",
"AssignedUserId": 2,
},
}
result = create_ticket_command(mocked_client, args)
assert result.outputs == {
"id": 123,
"data": {
"ObjectDescription": "Test ticket",
"ObjectEntity": "ticket",
"Solution": "Fixed the issue",
"TicketClassificationId": 1,
"IsNew": True,
"ExpirationDate": "2021-01-01T00:00:00Z",
"FirstUpdateUserId": 1,
"OwnerUserId": 2,
"Date": "2021-01-01T00:00:00Z",
"AssignedUserId": 2,
},
}
assert result.raw_response == mocked_client.create_ticket.return_value
assert result.readable_output == (
"### Ticket Created\n|Object Description|Object Entity|Solution|Is New|Expiration Date|Date|\n"
"|---|---|---|---|---|---|\n| "
"Test ticket | ticket | Fixed the issue | true | 2021-01-01T00:00:00Z | 2021-01-01T00:00:00Z |\n"
)
@classmethod
def test_get_ticket_history(cls, mocked_client):
from PATHelpdeskAdvanced import get_ticket_history_command
"""
Given a client instance and arguments for getting a ticket's history
When get_ticket_history is called with the client and args
Then it should return a CommandResults instance with the expected outputs
"""
TICKET_ID = "dummy_ticket_id"
history = [
{
"AccountID": "",
"Attachments": None,
"AutEmailCounter": 0,
"ContactID": "",
"Data": {
"Comment": "testing",
"From": "Solved",
"FromID": "S6",
"To": "In Progress",
"ToID": "S2",
},
"ExternalAction": None,
"FullName": "John Doe",
"HistoryID": 5665556,
"OperationDescription": "Status change",
"OperationTypeID": 20,
"UpdateDate": "2023-10-04T07:45:35Z",
"UserID": "S00000C",
"Username": "username",
}
]
mocked_client.get_ticket_history.return_value = history
result = get_ticket_history_command(mocked_client, {"ticket_id": TICKET_ID})
assert result.outputs_prefix == "HelpdeskAdvanced.TicketHistory"
assert result.outputs == [value | {"TicketID": TICKET_ID} for value in history]
assert result.raw_response == history
assert result.readable_output == (
"### Ticket History: dummy_ticket_id\n|Aut Email Counter|Data|Full Name|History ID|Operation Description|"
"Operation Type ID|Update Date|User ID|Username|Ticket ID|\n|---|---|---|---|---|---|---|---|---|---|\n|"
" | ***Comment***: testing
***From***: Solved
***FromID***: S6
***To***: In Progress
***ToID***:"
" S2 | John Doe | | Status change | | 2023-10-04T07:45:35Z | S00000C | username | dummy_ticket_id |\n"
)
@classmethod
def test_list_tickets(cls, mocked_client):
"""
Given a client instance
When list_tickets_command is called
Then it should return CommandResults with expected outputs
"""
from PATHelpdeskAdvanced import list_tickets_command
data = [
{
"ExpirationDate": "/Date(1111111111111)/",
"Subject": "Support Request",
"Solution": "Solution text redacted",
"LocationID": "XXX",
"Solicits": None,
"Date": "/Date(1111111111111)/",
"FirstUpdateUserID": "AA",
"BilledTokens": 0.0,
"ServiceID": "XXX",
"TicketTypeID": "XX",
"Problem": "Problem description redacted",
"SiteUnRead": None,
"ClosureDate": "/Date(1111111111111)/",
"ContactID": "XXX",
"SupplierID": None,
"CustomerContractID": None,
"EstimatedTaskStartDate": None,
"OwnerUserID": "AA",
"Score": 0,
"LastExpirationDate": "/Date(1111111111111)/",
"KnownIssue": False,
"MailBoxID": "XXX",
"NextExpirationID": "XXX",
"TicketClassificationID": "XXX",
"CalendarID": None,
"UrgencyID": "XX",
"TaskEffort": None,
"ProblemHTML": "Problem description redacted",
"AccountID": "XXX",
"NextExpirationDate": "/Date(1111111111111)/",
"AssetID": "XXX",
"ID": "10000002C",
"SolutionHTML": "Solution text redacted",
"EstimatedTaskDuration": 0,
"LanguageID": 5,
},
{
"ExpirationDate": "/Date(1111111111111)/",
"Subject": "On-site Support",
"Solution": "Ticket correctly created through template.",
"LocationID": "XXX",
"Solicits": None,
"Date": "/Date(1111111111111)/",
"FirstUpdateUserID": "AA",
"BilledTokens": 0.0,
"ServiceID": "XXX",
"TicketTypeID": "XX",
"Problem": "Problem description redacted",
"SiteUnRead": 0,
"ClosureDate": "/Date(1111111111111)/",
"ContactID": "XXX",
"SupplierID": None,
"CustomerContractID": None,
"EstimatedTaskStartDate": None,
"OwnerUserID": "XXX",
"Score": 0,
"LastExpirationDate": "/Date(1111111111111)/",
"KnownIssue": False,
"MailBoxID": "XXX",
"NextExpirationID": None,
"TicketClassificationID": "XXX",
"CalendarID": None,
"UrgencyID": None,
"TaskEffort": None,
"ProblemHTML": "Problem description redacted",
"AccountID": "XXX",
"NextExpirationDate": "/Date(1111111111111)/",
"AssetID": None,
"ID": "10000003C",
"SolutionHTML": "Solution text redacted",
"EstimatedTaskDuration": 0,
},
]
mocked_client.list_tickets.return_value = {"data": data}
result = list_tickets_command(mocked_client, {})
assert result.outputs == [
{
"ExpirationDate": "2005-03-18T01:58:31Z",
"Subject": "Support Request",
"Solution": "Solution text redacted",
"LocationID": "XXX",
"Solicits": None,
"Date": "2005-03-18T01:58:31Z",
"FirstUpdateUserID": "AA",
"BilledTokens": 0.0,
"ServiceID": "XXX",
"TicketTypeID": "XX",
"Problem": "Problem description redacted",
"SiteUnRead": None,
"ClosureDate": "2005-03-18T01:58:31Z",
"ContactID": "XXX",
"SupplierID": None,
"CustomerContractID": None,
"EstimatedTaskStartDate": None,
"OwnerUserID": "AA",
"Score": 0,
"LastExpirationDate": "2005-03-18T01:58:31Z",
"KnownIssue": False,
"MailBoxID": "XXX",
"NextExpirationID": "XXX",
"TicketClassificationID": "XXX",
"CalendarID": None,
"UrgencyID": "XX",
"TaskEffort": None,
"ProblemHTML": "Problem description redacted",
"AccountID": "XXX",
"NextExpirationDate": "2005-03-18T01:58:31Z",
"AssetID": "XXX",
"ID": "10000002C",
"SolutionHTML": "Solution text redacted",
"EstimatedTaskDuration": 0,
"LanguageID": 5,
},
{
"ExpirationDate": "2005-03-18T01:58:31Z",
"Subject": "On-site Support",
"Solution": "Ticket correctly created through template.",
"LocationID": "XXX",
"Solicits": None,
"Date": "2005-03-18T01:58:31Z",
"FirstUpdateUserID": "AA",
"BilledTokens": 0.0,
"ServiceID": "XXX",
"TicketTypeID": "XX",
"Problem": "Problem description redacted",
"SiteUnRead": 0,
"ClosureDate": "2005-03-18T01:58:31Z",
"ContactID": "XXX",
"SupplierID": None,
"CustomerContractID": None,
"EstimatedTaskStartDate": None,
"OwnerUserID": "XXX",
"Score": 0,
"LastExpirationDate": "2005-03-18T01:58:31Z",
"KnownIssue": False,
"MailBoxID": "XXX",
"NextExpirationID": None,
"TicketClassificationID": "XXX",
"CalendarID": None,
"UrgencyID": None,
"TaskEffort": None,
"ProblemHTML": "Problem description redacted",
"AccountID": "XXX",
"NextExpirationDate": "2005-03-18T01:58:31Z",
"AssetID": None,
"ID": "10000003C",
"SolutionHTML": "Solution text redacted",
"EstimatedTaskDuration": 0,
},
]
assert result.outputs_prefix == "HelpdeskAdvanced.Ticket"
assert result.outputs_key_field == "ID"
assert result.raw_response == mocked_client.list_tickets.return_value
assert (
result.readable_output
== "\n".join(
(
"### Tickets",
"|Ticket ID|Subject|Solution|Date|Service ID|Problem|Contact ID|Owner User ID|Account ID|",
"|---|---|---|---|---|---|---|---|---|",
"| 10000002C | Support Request | Solution text redacted | 2005-03-18T01:58:31Z | XXX |" # continued next line
" Problem description redacted | XXX | AA | XXX |",
"| 10000003C | On-site Support | Ticket correctly created through template." # continued next line
" | 2005-03-18T01:58:31Z | XXX | Problem description redacted | XXX | XXX | XXX |",
"",
)
)
)
class TestFilter:
def test_filter_parse(self):
"""
Tests parsing a list of filter condition strings into Filter objects.
Given A list of filter condition strings
When Filter.parse_list() is called on the conditions
Then The expected Filter objects are returned
"""
assert Filter.parse_list(
(
'"id" eq "123"',
'"name" lt "john"',
'"nullvalue" ne null',
'"notanullvalue" ne "none"', # quotes make it a string, not null
)
) == [
Filter(key="id", operator="eq", value="123"),
Filter(key="name", operator="lt", value="john"),
Filter(key="nullvalue", operator="ne", value=None),
Filter(key="notanullvalue", operator="ne", value="none"),
]
def test_dumps(self):
"""
Tests dumping a list of Filter objects to a JSON string.
Given:
A list of Filter objects with different key, operator and value.
When:
Filter.dumps_list() is called on the list of Filter objects.
Then:
The JSON string returned should match the expected format.
"""
assert Filter.dumps_list(
(
Filter(key="id", operator="eq", value="123"),
Filter(key="name", operator="lt", value="john"),
Filter(key="nullvalue", operator="ne", value=None),
Filter(key="notanullvalue", operator="ne", value="none"),
)
) == json.dumps(
[
{"property": "id", "op": "eq", "value": "123"},
{"property": "name", "op": "lt", "value": "john"},
{"property": "nullvalue", "op": "ne", "value": None},
{"property": "notanullvalue", "op": "ne", "value": "none"},
]
)
def test_parse_invalid(self):
"""
Given an invalid filter condition string
When parse_filter_conditions is called with a list containing the invalid string
Then it should raise a DemistoException
"""
with pytest.raises(DemistoException):
Filter.parse_list('"id" eq 123') # missing quotes around value
class TestClient:
base_url = "https://example.com"
@staticmethod
def dummy_client() -> Client:
return Client(
base_url=TestClient.base_url,
username="test",
password="test",
verify=False,
proxy=False,
)
@classmethod
def logged_in_client(cls, requests_mock_object):
cls.mock_login_response(requests_mock_object)
return cls.dummy_client()
@staticmethod
def mock_login_response(requests_mock_object):
requests_mock_object.post(
"https://example.com/Authentication/LoginEx?username=test&password=test",
json={
"refreshToken": "refresh_token",
"requestToken": "request_token",
"expiresIn": 3300,
"success": True,
},
)
requests_mock_object.post(
"https://example.com/Authentication/RefreshToken?token=refresh_token",
json={
"refreshToken": "refresh_token",
"requestToken": "request_token",
"expiresIn": 3300,
"success": True,
},
)
@classmethod
def test_client_login(cls, mocker, requests_mock) -> None:
"""
Given an expired refresh token in integration context
When _reuse_or_create_token is called
Then it should log in using username/password to get a new token
"""
# Mock expired token
cls.mock_login_response(requests_mock)
mocker.patch.object(
demisto,
"getIntegrationContext",
return_value={},
)
client = TestClient.dummy_client()
assert client.token.refresh_token == "refresh_token"
@classmethod
def test_expired_token(cls, mocker, requests_mock) -> None:
"""
Given an expired refresh token in integration context
When _reuse_or_create_token is called
Then it should log in using username/password to get a new token
"""
# Mock expired token
requests_mock.post(
"https://example.com/Authentication/LoginEx?username=test&password=test",
json={
"refreshToken": "new_refresh_token",
"requestToken": "new_request_token",
"expiresIn": 3300,
"success": True,
},
)
mocker.patch.object(
demisto,
"getIntegrationContext",
return_value={},
)
client = TestClient.dummy_client()
assert client.token.refresh_token == "new_refresh_token"
@classmethod
@freezegun.freeze_time("2023-01-01 12:00:00")
def test_reuse_valid_token(cls, mocker, requests_mock):
"""
Given a valid refresh token in integration context
When _reuse_or_create_token is called
Then it should reuse the existing valid token
"""
# Mock valid token
mocker.patch.object(
demisto,
"getIntegrationContext",
return_value={
"refresh_token": "previous_refresh_token",
"expires_in": 3600,
"token_expiry_utc": datetime(2023, 1, 1, 12, 30).isoformat(),
},
)
requests_mock.post(
"https://example.com/Authentication/RefreshToken?token=previous_refresh_token",
json={
"refreshToken": "new_refresh_token",
"requestToken": "new_request_token",
"expiresIn": 3300,
"success": True,
},
)
client = TestClient.dummy_client()
# Assert valid token reused
assert client.token.refresh_token == "new_refresh_token"
assert client.token.request_token == "new_request_token"
@classmethod
def test_add_ticket_attachment(cls, mocker, requests_mock):
"""
Given a client
When calling add_ticket_attachment with two files
Then make sure they are properly sent in the request
NOTE: This tests the method, not the _command, as the method has non-trivial logic.
"""
from PATHelpdeskAdvanced import Path
client = cls.logged_in_client(requests_mock)
mocker.patch.object(
demisto,
"getFilePath",
return_value={"path": "test.txt", "name": "test.txt"},
)
mocker.patch.object(Path, "open", return_value="mock file contents")
mocked_request = requests_mock.post("https://example.com/Ticket/UploadNewAttachment", json={"success": True})
client.add_ticket_attachment(["1", "2"], ticket_id="ticket_id")
assert mocked_request.called_once
stringified_request = str(mocked_request.request_history[0]._request.body)
for i in (1, 2):
assert f'name="TicketAttachment_{i}"; filename="test.txt"\\r\\n\\r\\nmock file contents' in stringified_request
@classmethod
def test_list_groups(cls, requests_mock):
"""
Given a client and a mocked response
When calling the list_groups_command with provided arguments
Then test that the request is properly made, and the resuilt is properly returned
"""
mocked_request = requests_mock.post(
f"{cls.base_url}/WSC/Projection",
json={
"data": [
{
"Description": "dummy group 1",
"ID": "G001",
"ObjectTypeID": "65",
},
],
"success": True,
},
)
result = list_groups_command(
client=cls.logged_in_client(requests_mock),
args={"group_id": "G001", "limit": "1", "page": "3", "page_size": "2"},
)
assert mocked_request.called_once
assert unquote(mocked_request.request_history[0].query) == (
"entity=usergroup&columnexpressions=['id',+'description',+'objecttypeid']&"
"columnnames=['id',+'description',+'objecttypeid']&start=6&limit=1&"
'filter=[{"property":+"id",+"op":+"eq",+"value":+"g001"}]'
)
assert result.readable_output == (
"### PAT HelpDeskAdvanced Groups\n|Group ID|Description|Object Type ID|"
"\n|---|---|---|\n| G001 | dummy group 1 | 65 |\n"
)
assert result.outputs == [{"ID": "G001", "Description": "dummy group 1", "ObjectTypeID": "65"}]
@classmethod
def test_list_users(cls, requests_mock):
"""
Given a client and a mocked response
When calling the list_users_command provided arguments
Then test that the request is properly made, and the resuilt is properly returned
"""
mocked_request = requests_mock.post(
f"{cls.base_url}/WSC/Projection",
json={
"data": [
{
"User.Phone": None,
"ID": "U001",
"User.FirstName": "HDA",
"User.LastName": "Cortex XSOAR",
"User.EMail": None,
}
],
"success": True,
},
)
result = list_users_command(
client=cls.logged_in_client(requests_mock),
args={"user_id": "U001", "limit": "1", "page": "3", "page_size": "2"},
)
assert mocked_request.called_once
assert unquote(mocked_request.request_history[0].query) == (
"entity=users&columnexpressions=['id',+'user.firstname',+'user.lastname',+'user.email',"
"+'user.phone',+'user.mobile']&columnnames=['id',+'user.firstname',+'user.lastname',+'user.email',"
'+\'user.phone\',+\'user.mobile\']&start=6&limit=1&filter=[{"property":+"id",+"op":+"eq",+"value":+"u001"}]'
)
assert result.readable_output == (
"### PAT HelpDeskAdvanced Users\n|Phone|ID|First Name|Last Name|E Mail|"
"\n|---|---|---|---|---|\n| | U001 | HDA | Cortex XSOAR | |\n"
)
assert result.outputs == [
{
"Phone": None,
"ID": "U001",
"FirstName": "HDA",
"LastName": "Cortex XSOAR",
"EMail": None,
}
]
@classmethod
def test_list_ticket_attachments(cls, requests_mock):
"""
Given a client and a mocked response
When calling the list_users_command provided arguments
Then test that the request is properly made, and the resuilt is properly returned
"""
mocked_request = requests_mock.post(
f"{cls.base_url}/WSC/List",
json={
"data": [
{
"ObjectDescription": "Untitled document.pdf",
"ObjectEntity": "Attachment",
"LastUpdateUserID": "",
"IsNew": False,
"KBSize": 0.76,
"ObjectTypeID": "DEFAULT",
"ID": "A12345C",
"UniqueID": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaaaaaa",
"Note": "",
"FileName": "Untitled document.pdf",
"FirstUpdate": None,
"TicketID": "1234567C",
"ContentType": "application/pdf",
"OwnerUserID": "",
"ParentObject": "Incident",
"ParentObjectID": "1234567C",
"Site": None,
"EmailID": "",
"BlobID": "A12345C",
"FirstUpdateUserID": "S44444C",
"RemoteID": "",
"LastUpdate": "2023-08-02T00:00:00Z",
"Description": "Untitled document.pdf",
}
],
"total": 5,
"result": {
"code": "0",
"subcode": None,
"parameters": None,
"desc": "",
},
"success": True,
"requestToken": "token",
},
)
result = list_ticket_attachments_command(
client=cls.logged_in_client(requests_mock),
args={"ticket_id": "A12345C", "limit": "1"},
)
assert mocked_request.called_once
assert unquote(mocked_request.request_history[0].query) == (
'entity=attachments&start=0&limit=1&filter=[{"property":+"parentobject",+"op":+"eq",+"value":+"ticket"},'
'+{"property":+"parentobjectid",+"op":+"eq",+"value":+"a12345c"}]'
)
assert result.readable_output == (
"### Attachments of A12345C\n|File Name|Last Update|Description|Object Description|First Update User ID|Object Entity"
"|Content Type|\n|---|---|---|---|---|---|---|\n| Untitled document.pdf "
"| 2023-08-02T00:00:00Z | Untitled document.pdf"
" | Untitled document.pdf | S44444C | Attachment | application/pdf |\n"
)
assert result.outputs == [
{
"ObjectDescription": "Untitled document.pdf",
"ObjectEntity": "Attachment",
"LastUpdateUserID": "",
"IsNew": False,
"KBSize": 0.76,
"ObjectTypeID": "DEFAULT",
"ID": "A12345C",
"UniqueID": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaaaaaa",
"Note": "",
"FileName": "Untitled document.pdf",
"FirstUpdate": None,
"TicketID": "1234567C",
"ContentType": "application/pdf",
"OwnerUserID": "",
"ParentObject": "Incident",
"ParentObjectID": "1234567C",
"Site": None,
"EmailID": "",
"BlobID": "A12345C",
"FirstUpdateUserID": "S44444C",
"RemoteID": "",
"LastUpdate": "2023-08-02T00:00:00Z",
"Description": "Untitled document.pdf",
}
]
@classmethod
def test_non_json_response(cls, requests_mock):
client = cls.logged_in_client(requests_mock)
requests_mock.post(
"https://example.com/WSC/Projection?entity=UserGroup&columnExpressions=%5B%27ID%27"
"%2C+%27Description%27%2C+%27ObjectTypeID"
"%27%5D&columnNames=%5B%27ID%27%2C+%27Description%27%2C+%27ObjectTypeID%27%5D&start=0&limit=1",
text="surprise",
)
with pytest.raises(ValueError, match="API returned non-JSON response: surprise"):
client.list_groups(limit=1)
@classmethod
@pytest.mark.parametrize(
"response_file,expected_error_message",
(
pytest.param(
"response_401.html",
"Server Error. 401 - Unauthorized: Access is denied due to invalid credentials",
id="401",
),
pytest.param(
"response_invalid_request.html",
"A server error has occurred. Please, contact portal administrator.",
id="invalid response",
),
),
)
def test_error_response(cls, requests_mock, response_file: str, expected_error_message: str):
"""
Given a client and an invalid response
When calling a method that simulates these responses
Then make sure the response is parsed well
"""
client = cls.logged_in_client(requests_mock)
requests_mock.post(
"https://example.com/WSC/Projection?entity=UserGroup&columnExpressions=%5B%27ID%27%2C+%27Description%27%2C+%"
"27ObjectTypeID%27%5D&columnNames=%5B%27ID%27%2C+%27Description%27%2C+%27ObjectTypeID%27%5D&start=0&limit=1",
text=Path(f"test_data/{response_file}").read_text(),
)
with pytest.raises(DemistoException, match=expected_error_message):
client.list_groups(limit=1)
class TestPaginate:
@staticmethod
def test_paginate_with_limit_only():
"""
Given limit is provided
When paginate is called with only limit
Then it returns start as 0 and provided limit
"""
assert paginate(limit=10) == PaginateArgs(start=0, limit=10)
@staticmethod
def test_paginate_with_page_and_page_size():
"""
Given limit is provided
When paginate is called with paging arguments
Then it returns start as 0 and provided limit
"""
assert paginate(page=2, page_size=3, limit=3) == PaginateArgs(start=6, limit=3)
@staticmethod
def test_paginate_with_invalid_args():
"""
Given only one of page or page_size is provided
When paginate is called with invalid args
Then it raises an appropriate error
"""
with pytest.raises(ValueError):
paginate(page=1, limit="None")
with pytest.raises(KeyError):
paginate(page_size=10)
@staticmethod
def test_paginate_with_non_int_args():
"""
Given page and page_size are non-integer values
When paginate is called with non-int args
Then it raises a DemistoException
"""
with pytest.raises(ValueError):
paginate(page="a", page_size="b", limit=3)
class TestPatToMarkdown:
@staticmethod
def test_pat_table_to_markdown_with_no_fields():
"""
Given a title, output dict, and no fields
When pat_table_to_markdown is called
Then it returns markdown string of the full output
"""
title = "Test Title"
output = {"key1": "value1", "key2": "value2"}
assert pat_table_to_markdown(title, output, None) == "### Test Title\n|Key 1|Key 2|\n|---|---|\n| value1 | value2 |\n"
@staticmethod
def test_pat_table_to_markdown_with_fields():
"""
Given title, output, and specified fields
When pat_table_to_markdown is called
Then it returns markdown string of filtered outputs
"""
title = "Test Title"
output = {"Field1": "1", "Field2": "2", "Field3": "3"}
fields = (Field("field1"), Field("field2"))
assert pat_table_to_markdown(title, output, fields) == "### Test Title\n|Field 1|Field 2|\n|---|---|\n| 1 | 2 |\n"
@staticmethod
def test_pat_table_to_markdown_with_field_replacements():
"""
Given title, output dict, fields, and field replacements
When pat_table_to_markdown is called with field_replacements
Then it returns markdown string with replaced field names
"""
title = "Test Title"
replace_me = Field("replace_me")
untouched = Field("untouched")
replaced = Field("replaced")
output = {replace_me.hda_name: "1", untouched.hda_name: "2"}
assert (
pat_table_to_markdown(title, output, fields=None, field_replacements={replace_me: replaced})
== "### Test Title\n|Replaced|Untouched|\n|---|---|\n| 1 | 2 |\n"
)