import json import unittest from typing import Any from unittest.mock import patch import pytest from CommonServerPython import DemistoException from freezegun import freeze_time from WorkdaySignOnEventCollector import ( Client, convert_to_json, fetch_sign_on_events_command, fetch_sign_on_logs, fletcher16, generate_pseudo_id, get_from_time, get_sign_on_events_command, main, VENDOR, PRODUCT, get_api_version, process_and_filter_events, ) def test_get_from_time() -> None: """ Given: - A time duration in seconds (3600 seconds or 1 hour ago). When: - The function `get_from_time` is called to convert this duration to a UTC datetime string. Then: - Ensure that the returned value is a string. - Validate that the string ends with 'Z', indicating it's in UTC format. """ # Given: A time duration of 3600 seconds (or 1 hour) ago. seconds_ago = 3600 # 1 hour ago # When: Calling the function to convert this to a UTC datetime string. result: Any = get_from_time(seconds_ago) # Then: Validate the type and format of the returned value. assert isinstance(result, str) assert result.endswith("Z") # Check if it's in the right format def test_fletcher16() -> None: """ Given: - Two types of byte strings, one containing the word 'test' and another being empty. When: - The function `fletcher16` is called to calculate the checksum for these byte strings. Then: - Ensure that the checksum calculated for the byte string 'test' matches the expected value of 22976. - Validate that the checksum for an empty byte string is 0. """ # Given: A byte string containing the word 'test'. data = b"test" # When: Calling `fletcher16` to calculate the checksum. result: Any = fletcher16(data) # Then: Validate that the checksum matches the expected value. expected = 22976 assert result == expected # Given: An empty byte string. data = b"" # When: Calling `fletcher16` to calculate the checksum. result = fletcher16(data) # Then: Validate that the checksum for an empty byte string is 0. expected = 0 assert result == expected def test_generate_pseudo_id() -> None: """ Given: - Four different event dictionaries: 1. A valid event dictionary with known values. 2. An empty event dictionary. 3. An event dictionary missing the "Signon_DateTime" key. 4. A large event dictionary. When: - Calling `generate_pseudo_id` to calculate a unique ID based on the event dictionary. Then: - For the first case, ensure that the unique ID matches the expected value. - For the second and third cases, ensure that an exception is raised. - For the fourth case, ensure the function can handle large dictionaries without errors. """ # Given: A valid event dictionary with known values. event1 = { "Short_Session_ID": "12345", "User_Name": "ABC123", "Successful": 1, "Signon_DateTime": "2023-09-04T07:47:57.460-07:00", } # When: Calling `generate_pseudo_id` to calculate the unique ID. event1_str: str = json.dumps(event1, sort_keys=True) expected_checksum1: Any = fletcher16(event1_str.encode()) expected_unique_id1: str = f"{expected_checksum1}_{event1['Signon_DateTime']}" result1: str = generate_pseudo_id(event1) # Then: Validate that the unique ID matches the expected value. assert result1 == expected_unique_id1 # Given: An empty event dictionary. event2 = {} # When & Then: Calling `generate_pseudo_id` and expecting an exception. try: generate_pseudo_id(event2) except DemistoException as e: assert ( str(e) == "While calculating the pseudo ID for an event, an event without a Signon_DateTime was " "found.\nError: 'Signon_DateTime'" ) else: raise AssertionError("Expected DemistoException but did not get one") # Given: An event dictionary missing the "Signon_DateTime" key. event3 = { "Short_Session_ID": "12345", "User_Name": "ABC123", "Successful": 1, } # When & Then: Calling `generate_pseudo_id` and expecting an exception. try: generate_pseudo_id(event3) except DemistoException: pass else: raise AssertionError("Expected DemistoException but did not get one") # Given: A large event dictionary. event4 = {str(i): i for i in range(10000)} # Create a large dictionary event4["Signon_DateTime"] = "2023-09-04T07:47:57.460-07:00" # Add a Signon_DateTime key # When & Then: Calling `generate_pseudo_id` to check if the function can handle it. assert generate_pseudo_id(event4) def test_process_and_filter_events() -> None: """ Given: - A list of two valid sign-on events that differ by 1 second in their "Signon_DateTime". - An initial time ("from_time") that matches the "Signon_DateTime" of one of the events. - An empty set of pseudo_ids from the previous run. When: - Calling the `process_and_filter_events` function to filter out duplicates and process events for the next iteration. Then: - The list of non-duplicate events should match the original list of events. - The set of pseudo_ids for the next iteration should contain two elements. - Each event in the list of non-duplicates should have an additional "_time" key that matches its "Signon_DateTime". """ # Given: A list of two valid sign-on events and other initial conditions events = [ { "Short_Session_ID": "12345", "User_Name": "ABC6789", "Successful": 1, "Signon_DateTime": "2023-09-04T07:47:57.460-07:00", }, { "Short_Session_ID": "12346", "User_Name": "ABC6790", "Successful": 1, "Signon_DateTime": "2023-09-04T07:47:57.460-07:00", }, ] from_time: str = "2021-09-01T12:00:00Z" previous_run_pseudo_ids: set[Any] = set() # Assume no previous checksums for simplicity # When: Calling the function to test non_duplicates, pseudo_ids_for_next_iteration = process_and_filter_events(events, from_time, previous_run_pseudo_ids) # Then: Validate the function's output assert non_duplicates == events # Check if the list of non-duplicates is as expected assert len(pseudo_ids_for_next_iteration) == 2 # Check if the set of pseudo_ids for next iteration is updated # Check if '_time' key is added to each event for event in non_duplicates: assert "_time" in event assert event["_time"] == event["Signon_DateTime"] def test_convert_to_json() -> None: """ Given: - A sample XML response string containing a single 'Workday_Account_Signon' entry with a 'Signon_DateTime'. When: - Calling the 'convert_to_json' function to convert the XML data to a Python dictionary. Then: - The function should return two Python dictionaries. - The first dictionary should represent the entire XML structure. - The second dictionary should contain just the 'Workday_Account_Signon' entries. - Both dictionaries should correctly reflect the 'Signon_DateTime' from the original XML. """ # Given: Test with XML data (this is a simplified version for the sake of the test) xml_response = """ 2023-09-04T07:47:57.460-07:00 """ # When: Calling the function to test raw_json_response, account_signon_data = convert_to_json(xml_response) # Then: Check if the converted data matches the expected structure assert ( raw_json_response["Envelope"]["Body"]["Get_Workday_Account_Signons_Response"]["Response_Data"]["Workday_Account_Signon"][ 0 ]["Signon_DateTime"] == "2023-09-04T07:47:57.460-07:00" ) assert account_signon_data["Workday_Account_Signon"][0]["Signon_DateTime"] == "2023-09-04T07:47:57.460-07:00" def test_generate_workday_account_signons_body() -> None: """ Given: - A Client object initialized with a base URL, verification settings, a tenant name, and login credentials. - Parameters specifying the page, count, and time range for fetching Workday sign-on events. When: - Calling the 'generate_workday_account_signons_body' method on the Client object to generate the SOAP request body. Then: - The returned SOAP request body should contain all the specified parameters. - The body should also contain the username and password for authentication. """ # Given: Initialize a Client object with sample data mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": "test_user", "password": "test_pass"}, "insecure": True, } client = Client(params=mock_params) # When: Generate the SOAP request body body = client.generate_workday_account_signons_body( page=1, count=10, to_time="2021-09-01T12:00:00Z", from_time="2021-09-01T11:00:00Z", ) # Then: Verify that the SOAP request body contains all the specified parameters assert "1" in body assert "10" in body assert "2021-09-01T11:00:00Z" in body assert "2021-09-01T12:00:00Z" in body assert "test_user" in body assert ( 'test_pass' # noqa:E501 in body ) def test_generate_test_payload() -> None: """ Given: - A Client object initialized with a base URL, verification settings, a tenant name, and login credentials. - Parameters specifying the time range for fetching Workday sign-on events for the test payload. When: - Calling the 'generate_test_payload' method on the Client object to generate a SOAP request payload for testing. Then: - The returned SOAP request payload should contain all the specified parameters. - The payload should also contain the username and password for authentication. """ # Given: Initialize a Client object with sample data mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": "test_user", "password": "test_pass"}, "insecure": True, } client = Client(params=mock_params) # When: Generate the SOAP request payload for testing payload = client.generate_test_payload(from_time="2021-09-01T11:00:00Z", to_time="2021-09-01T12:00:00Z") # Then: Verify that the SOAP request payload contains all the specified parameters assert "1" in payload assert "1" in payload assert "2021-09-01T11:00:00Z" in payload assert "2021-09-01T12:00:00Z" in payload assert "test_user" in payload assert ( 'test_pass' # noqa:E501 in payload ) @pytest.mark.parametrize( "api_version, from_time, to_time, expected_version", [ # Given valid api_version and time range ("v42.1", "2021-09-01T11:00:00Z", "2021-09-01T12:00:00Z", "v42.1"), # Given default api_version and time range ("v40.0", "2021-09-01T11:00:00Z", "2021-09-01T12:00:00Z", "v40.0"), # Given valid api_version and different time range ("v50.0", "2021-10-01T11:00:00Z", "2021-10-01T12:00:00Z", "v50.0"), # Given invalid api_version should fallback to default ("invalid", "2021-09-01T11:00:00Z", "2021-09-01T12:00:00Z", "v40.0"), ], ) def test_generate_test_payload_with_version(api_version, from_time, to_time, expected_version) -> None: """ Given: - A Client object initialized with a base URL, verification settings, a tenant name, login credentials, and an API version - Parameters specifying the time range for fetching Workday sign-on events for the test payload. When: - Calling the 'generate_test_payload' method on the Client object to generate a SOAP request payload for testing. Then: - The returned SOAP request payload should contain all the specified parameters, including the correct API version. - The payload should also contain the username and password for authentication. """ # Given: Initialize a Client object with sample data mock_params = { "base_url": "https://something.test", # disable-secrets-detection "verify_certificate": True, "proxy": False, "tenant_name": "test_tenant", "credentials": { "identifier": "test_user", "password": "test_pass", }, "api_version": api_version, } client = Client(params=mock_params) # When: Generate the SOAP request payload for testing payload = client.generate_test_payload(from_time=from_time, to_time=to_time) # Then: Verify that the SOAP request payload contains all the specified parameters assert "1" in payload assert "1" in payload assert f"{from_time}" in payload assert f"{to_time}" in payload assert "test_user" in payload assert ( 'test_pass' # noqa:E501 in payload ) # Verify that the SOAP request payload contains the correct API version assert ( f'' in payload ) def test_convert_to_json_valid_input() -> None: """ Given: - An XML-formatted response string from the Workday API, containing sign-on event data. When: - Calling the 'convert_to_json' function to convert the XML response to JSON format. Then: - The function should return two JSON objects: one containing the full JSON-converted data, and another containing only the sign-on event data. - Both JSON objects should be properly formatted and contain the expected data fields. """ # Given: An XML-formatted response string from the Workday API response = """ 2021-09-01T11:00:00Z """ # When: Converting the XML to JSON full_json, account_signon_data = convert_to_json(response) # Then: Validate the full_json data structure envelope = full_json.get("Envelope", {}) body = envelope.get("Body", {}) response = body.get("Get_Workday_Account_Signons_Response", {}) response_data = response.get("Response_Data", {}) workday_account_signons = response_data.get("Workday_Account_Signon", []) # Assertions for full_json assert isinstance(workday_account_signons, list), "workday_account_signons is not a list" assert workday_account_signons, "workday_account_signons is empty" assert workday_account_signons[0].get("Signon_DateTime") == "2021-09-01T11:00:00Z" # Then: Validate the account_signon_data structure workday_account_signons_data = account_signon_data.get("Workday_Account_Signon", []) # Assertions for account_signon_data assert workday_account_signons_data assert workday_account_signons_data[0].get("Signon_DateTime") == "2021-09-01T11:00:00Z" class TestFetchSignOnLogs(unittest.TestCase): def setUp(self) -> None: """ Given: - A Client object with mock URL, tenant, username, and password. When: - Setting up each unit test case. Then: - The Client object should be initialized and ready for testing. """ mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": "mock_user", "password": "mock_pass"}, "insecure": True, } self.client = Client(params=mock_params) @patch.object(Client, "retrieve_events") def test_fetch_sign_on_logs_single_page(self, mock_retrieve_events) -> None: """ Given: - A mock Client object with a retrieve_events method that returns a sample response. - The sample response contains a single Workday sign-on event. When: - Calling the fetch_sign_on_logs function to fetch sign-on logs. Then: - The function should return a list of events. - The length of the list should be 1. - The event in the list should have the User_Name "John". """ # Given: Sample data to be returned by the mock mock_response = ( { "Workday_Account_Signon": [ { "Signon_DateTime": "2021-09-01T11:00:00Z", "User_Name": "John", "Short_Session_ID": "123456", "Successful": 1, } ] }, 1, ) # Setup: Configure the mock to return the sample data mock_retrieve_events.return_value = mock_response # When: Fetching sign-on logs events = fetch_sign_on_logs(self.client, 10, "2021-09-01T00:00:00Z", "2021-09-02T00:00:00Z") # Then: Validate the function's return value assert len(events) == 1 assert events[0]["User_Name"] == "John" class TestGetSignOnEventsCommand(unittest.TestCase): def test_get_sign_on_events_command(self) -> None: """ Given: - A Client object with mock settings. - A patch for the fetch_sign_on_logs function to return a mock event. - The mock event has details such as Signon_DateTime, User_Name, Short_Session_ID, and Successful status. When: - Calling the get_sign_on_events_command function to get sign-on events between two date-time ranges. Then: - The function should return a list of events and results. - The length of the list should be 1. - The event in the list should have the User_Name "John" and _time "2021-09-01T11:00:00Z". - The readable_output of the results should start with "### Sign On Events List:". """ # Given: Sample data to be returned by the mock mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": "mock_user", "password": "mock_pass"}, "insecure": True, } mock_events = [ { "Signon_DateTime": "2023-09-04T07:47:57.460-07:00", "User_Name": "John", "Short_Session_ID": "123456", "Successful": 1, "_time": "2021-09-01T11:00:00Z", # This is added by the process_events function } ] # Setup: Use patch to mock the fetch_sign_on_logs function with patch("WorkdaySignOnEventCollector.fetch_sign_on_logs", return_value=mock_events): client = Client(params=mock_params) # When: Calling the get_sign_on_events_command events, results = get_sign_on_events_command(client, "2021-09-01T00:00:00Z", "2021-09-02T00:00:00Z", 10) # Then: Validate the function's return value assert len(events) == 1 assert events[0]["User_Name"] == "John" assert events[0]["_time"] == "2023-09-04T07:47:57.460-07:00" assert results.readable_output.startswith("### Sign On Events List:") @freeze_time("2023-09-04T00:00:00.000-07:00") def test_fetch_sign_on_events_command_single_page() -> None: """ Given: - A Client object with mock settings. - A patch for the Client's retrieve_events method to return a mock event. - A patch for demisto.getLastRun function to return a mock last_run dictionary. - The mock event has details such as Signon_DateTime, User_Name, Short_Session_ID, and Successful status. - The mock last_run dictionary contains last_fetch_time and previous_run_pseudo_ids. When: - Calling the fetch_sign_on_events_command function to fetch sign-on events. Then: - The function should return a list of events and a new_last_run dictionary. - The length of the list should be 1. - The event in the list should have the User_Name "John" and _time "2021-09-01T11:00:00Z". - The new_last_run dictionary should have last_fetch_time updated to "2021-09-01T11:00:00Z". """ # Given: Sample data to be returned by the mock mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": "mock_user", "password": "mock_pass"}, "insecure": True, } mock_events = [ { "Signon_DateTime": "2023-09-04T07:47:57.460-07:00", "User_Name": "John", "Short_Session_ID": "123456", "Successful": 1, "_time": "2023-09-04T07:47:57.460-07:00", # This is added by the process_events function } ] # Setup: Mock the client's retrieve_events method and demisto.getLastRun function mock_retrieve_response = ({"Workday_Account_Signon": mock_events}, 1) mock_last_run = { "last_fetch_time": "2023-09-04T07:47:57.460-07:00", "previous_run_pseudo_ids": set(), } # When: Calling the fetch_sign_on_events_command with ( patch.object(Client, "retrieve_events", return_value=mock_retrieve_response), patch("demistomock.getLastRun", return_value=mock_last_run), ): client = Client(params=mock_params) events, new_last_run = fetch_sign_on_events_command(client, 10, mock_last_run) # Then: Validate the function's return value assert len(events) == 1 assert events[0]["User_Name"] == "John" assert events[0]["_time"] == "2023-09-04T07:47:57.460-07:00" assert new_last_run["last_fetch_time"] == "2023-09-04T07:47:57.460-07:00" def test_main_fetch_events() -> None: """ Given: - A set of mock parameters for the client. - Mock functions for demisto's getLastRun, setLastRun, and params. - Mock for the fetch_sign_on_events_command function to return mock events and new last_run data. - Mock for the send_events_to_xsiam function. When: - The main function is called and the command is 'fetch-events'. Then: - Ensure that fetch_sign_on_events_command is called with the correct arguments. - Ensure that send_events_to_xsiam is called with the mock events. - Ensure that setLastRun is called to update the last_run data. """ # Given: Mock parameters and last run data mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": "TestUser", "password": "testpass"}, "insecure": True, } # Mocking demisto.command to return 'fetch-events' with ( patch("demistomock.command", return_value="fetch-events"), patch("demistomock.getLastRun", return_value={"some": "data"}), patch("demistomock.setLastRun") as mock_set_last_run, patch("demistomock.params", return_value=mock_params), patch("WorkdaySignOnEventCollector.Client") as mock_client, patch("WorkdaySignOnEventCollector.fetch_sign_on_events_command") as mock_fetch_sign_on_events_command, patch("WorkdaySignOnEventCollector.send_events_to_xsiam") as mock_send_events_to_xsiam, ): # Mocking the output of fetch_sign_on_events_command mock_events = [{"event": "data"}] mock_new_last_run = {"new": "data"} mock_fetch_sign_on_events_command.return_value = ( mock_events, mock_new_last_run, ) # When: Calling the main function main() # Then: Validate the function calls and arguments mock_fetch_sign_on_events_command.assert_called_with( client=mock_client.return_value, max_fetch=10000, last_run={"some": "data"}, ) mock_send_events_to_xsiam.assert_called_with(mock_events, vendor=VENDOR, product=PRODUCT) mock_set_last_run.assert_called_with(mock_new_last_run) @pytest.mark.parametrize( "username, escaped_username, password, escaped_password", [ ("username&", "username&", "pass&", "pass&"), ("username>", "username>", "pass>", "pass>"), ("username<", "username<", "pass<", "pass<"), ("username", "username", "pass", "pass"), ], ) def test_escaping_user_name(username, escaped_username, password, escaped_password): """ Given: A Client object initialized with a base URL, verification settings, a tenant name, and login credentials. In the first 3 cases the credentials contains a special character that needs to be escaped, and the last case checks that in a case of a credentials without special characters, they don't change. When: Creating a new Workday Sign Ons client. Then: Check that the credentials are escaped correctly. """ mock_params = { "tenant_name": "TestTenant", "max_fetch": "10000", "base_url": "https://testurl.com", "credentials": {"identifier": username, "password": password}, "insecure": True, } client = Client(params=mock_params) assert client.username == escaped_username assert client.password == escaped_password @pytest.mark.parametrize( "params, default, expected", [ # Given valid api_version, when get_api_version is called, then it returns the valid api_version ({"api_version": "v42.1"}, "v40.0", "v42.1"), # Given invalid api_version, when get_api_version is called, then it returns the default value ({"api_version": "42.1"}, "v40.0", "v40.0"), # Given missing api_version, when get_api_version is called, then it returns the default value ({}, "v40.0", "v40.0"), # Given invalid api_version with custom default, when get_api_version is called, then it returns the custom default value ({"api_version": "invalid_format"}, "v50.0", "v50.0"), # Given another valid api_version, when get_api_version is called, then it returns the valid api_version ({"api_version": "v50.0"}, "v40.0", "v50.0"), ], ) def test_get_api_version(params, default, expected): """ Test get_api_version with various inputs using parameterization. Given: Different sets of params and default values. When: get_api_version is called with these params. Then: It should return the expected result. """ # When result = get_api_version(params, default) # Then assert result == expected