import base64 import json import pytest from freezegun import freeze_time from iManageThreatManager import ( Client, get_events_command, fetch_events_command, validate_credentials_for_event_types, _add_fields_to_events, _deduplicate_events, _fetch_events_with_pagination, _update_next_run_state, BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS, DETECT_AND_PROTECT_ALERTS, EVENT_TYPE_CONFIG, ) from CommonServerPython import DemistoException BASE_URL = "https://test-instance.tm-cloudimanage.com" @pytest.fixture(autouse=True) def mock_content_client_init(mocker): """Mock ContentClient.__init__ to avoid httpx/anyio initialization in tests.""" mocker.patch("iManageThreatManager.ContentClient.__init__", return_value=None) @pytest.fixture def client(): """ Given: - Base URL and authentication credentials When: - Creating a Client instance Then: - Ensure the client is properly initialized """ c = Client( base_url=BASE_URL, verify=True, proxy=False, username="test_user", password="test_password", token="test_token", secret="test_secret", ) # Set attributes that ContentClient.__init__ would normally set c._base_url = BASE_URL c._verify = True return c class TestValidateCredentialsForEventTypes: """Tests for validate_credentials_for_event_types function""" def test_validate_credentials_behavior_analytics_success(self, client): """ Given: - Client with valid token and secret - Behavior Analytics event type When: - Calling validate_credentials_for_event_types Then: - Ensure no exception is raised """ validate_credentials_for_event_types(client, [BEHAVIOR_ANALYTICS]) def test_validate_credentials_addressable_alerts_success(self, client): """ Given: - Client with valid username and password - Addressable Alerts event type When: - Calling validate_credentials_for_event_types Then: - Ensure no exception is raised """ validate_credentials_for_event_types(client, [ADDRESSABLE_ALERTS]) def test_validate_credentials_missing_token_secret(self): """ Given: - Client without token and secret - Behavior Analytics event type When: - Calling validate_credentials_for_event_types Then: - Ensure DemistoException is raised """ client_no_token = Client(base_url=BASE_URL, verify=True, proxy=False, username="user", password="pass") with pytest.raises(DemistoException, match="Token and Secret"): validate_credentials_for_event_types(client_no_token, [BEHAVIOR_ANALYTICS]) def test_validate_credentials_missing_username_password(self): """ Given: - Client without username and password - Addressable Alerts event type When: - Calling validate_credentials_for_event_types Then: - Ensure DemistoException is raised """ client_no_creds = Client(base_url=BASE_URL, verify=True, proxy=False, token="token", secret="secret") with pytest.raises(DemistoException, match="Username and Password"): validate_credentials_for_event_types(client_no_creds, [ADDRESSABLE_ALERTS]) def test_validate_credentials_multiple_event_types(self, client): """ Given: - Client with all credentials - Multiple event types When: - Calling validate_credentials_for_event_types Then: - Ensure no exception is raised """ validate_credentials_for_event_types(client, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS, DETECT_AND_PROTECT_ALERTS]) @pytest.mark.parametrize( "client_kwargs, event_types, expected_errors", [ pytest.param( {"token": "token", "secret": "secret"}, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS, DETECT_AND_PROTECT_ALERTS], ["Addressable Alerts", "Detect And Protect Alerts"], id="token_only-missing_user_password_for_two_types", ), pytest.param( {"username": "user", "password": "pass"}, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS], ["Token and Secret"], id="user_only-missing_token_secret", ), pytest.param( {}, [BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS], ["Token and Secret", "Username and Password"], id="no_credentials-all_missing", ), ], ) def test_validate_credentials_mixed_partial(self, client_kwargs, event_types, expected_errors): """ Given: - Client with partial credentials - Multiple event types where some require missing credentials When: - Calling validate_credentials_for_event_types Then: - Ensure DemistoException is raised listing all missing credentials """ client = Client(base_url=BASE_URL, verify=True, proxy=False, **client_kwargs) with pytest.raises(DemistoException) as exc_info: validate_credentials_for_event_types(client, event_types) error_msg = str(exc_info.value) for expected in expected_errors: assert expected in error_msg class TestDeduplicateEvents: """Tests for _deduplicate_events function""" def test_deduplicate_events_no_duplicates(self): """ Given: - Events with unique IDs - Empty last_run_ids When: - Calling _deduplicate_events Then: - Ensure all events are returned """ events = [ {"id": "1", "alert_time": 1000}, {"id": "2", "alert_time": 900}, {"id": "3", "alert_time": 800}, ] result = _deduplicate_events(events, [], 700) assert len(result) == 3 def test_deduplicate_events_with_duplicates(self): """ Given: - Events with some duplicate IDs from last run - All events are newer than last_fetch_time When: - Calling _deduplicate_events Then: - Ensure all events are returned (newer events bypass ID check) """ events = [ {"id": "1", "alert_time": 1000}, {"id": "2", "alert_time": 900}, {"id": "3", "alert_time": 800}, ] last_run_ids = ["2"] # All events are newer than 700, so ID check is bypassed result = _deduplicate_events(events, last_run_ids, 700) assert len(result) == 3 def test_deduplicate_events_with_duplicates_same_time(self): """ Given: - Events at or before last_fetch_time - Some IDs are duplicates from last run When: - Calling _deduplicate_events Then: - Ensure duplicates are removed """ events = [ {"id": "1", "alert_time": 700}, {"id": "2", "alert_time": 700}, {"id": "3", "alert_time": 700}, ] last_run_ids = ["2"] result = _deduplicate_events(events, last_run_ids, 700) assert len(result) == 2 assert result[0]["id"] == "1" assert result[1]["id"] == "3" def test_deduplicate_events_newer_than_last_fetch(self): """ Given: - Events newer than last_fetch_time When: - Calling _deduplicate_events Then: - Ensure all newer events are returned without checking IDs """ events = [ {"id": "1", "alert_time": 1000}, {"id": "2", "alert_time": 900}, ] result = _deduplicate_events(events, ["1", "2"], 800) assert len(result) == 2 def test_deduplicate_events_without_ids(self): """ Given: - Events without ID field When: - Calling _deduplicate_events Then: - Ensure events are kept (cannot deduplicate) """ events = [ {"alert_time": 1000}, {"alert_time": 900}, ] result = _deduplicate_events(events, [], 700) assert len(result) == 2 class TestUpdateNextRunState: """Tests for _update_next_run_state function""" def test_no_new_events_keeps_old_state(self): """ Given: - Empty events list - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure old timestamp and IDs are preserved """ events = [] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert new_ids == ["id1", "id2"] def test_new_events_with_newer_timestamp_replaces_ids(self): """ Given: - Events with timestamp newer than last_fetch_time - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure timestamp is updated and old IDs are replaced with new ones """ events = [ {"id": "new1", "alert_time": 2000}, {"id": "new2", "alert_time": 2000}, {"id": "old1", "alert_time": 1500}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 2000 assert set(new_ids) == {"new1", "new2"} assert "id1" not in new_ids assert "id2" not in new_ids def test_new_events_with_same_timestamp_combines_ids(self): """ Given: - Events with same timestamp as last_fetch_time - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure timestamp stays same and old and new IDs are combined """ events = [ {"id": "new1", "alert_time": 1000}, {"id": "new2", "alert_time": 1000}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert set(new_ids) == {"id1", "id2", "new1", "new2"} def test_new_events_with_same_timestamp_avoids_duplicate_ids(self): """ Given: - Events with same timestamp as last_fetch_time - Some IDs overlap with previous run When: - Calling _update_next_run_state Then: - Ensure IDs are combined without duplicates """ events = [ {"id": "id1", "alert_time": 1000}, # Duplicate {"id": "new1", "alert_time": 1000}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert set(new_ids) == {"id1", "id2", "new1"} assert len(new_ids) == 3 # No duplicates def test_older_timestamp_combines_ids(self): """ Given: - Events with timestamp older than last_fetch_time (API doesn't filter by start_date) - Previous fetch time and IDs When: - Calling _update_next_run_state Then: - Ensure timestamp is preserved but IDs are combined to prevent duplicate fetches """ events = [ {"id": "old1", "alert_time": 500}, ] last_fetch_time = 1000 last_run_ids = ["id1", "id2"] new_time, new_ids = _update_next_run_state(events, last_fetch_time, last_run_ids) assert new_time == 1000 assert set(new_ids) == {"id1", "id2", "old1"} class TestAddFieldsToEvents: """Tests for _add_fields_to_events function""" def test_add_fields_to_events_with_update_time(self): """ Given: - Events with update_time - Source log type parameter When: - Calling _add_fields_to_events Then: - Ensure _time and _source_log_type fields are added with correct format """ events = [ {"update_time": 1609459200000, "id": "1"}, # 2021-01-01 00:00:00 UTC {"update_time": 1609545600000, "id": "2"}, # 2021-01-02 00:00:00 UTC ] _add_fields_to_events(events, "BehaviorAnalytics") assert "_time" in events[0] assert "_time" in events[1] assert events[0]["_time"] == "2021-01-01T00:00:00Z" assert events[1]["_time"] == "2021-01-02T00:00:00Z" assert events[0]["_source_log_type"] == "BehaviorAnalytics" assert events[1]["_source_log_type"] == "BehaviorAnalytics" def test_add_fields_to_events_entry_status_new(self): """ Given: - Events where update_time equals alert_time When: - Calling _add_fields_to_events Then: - Ensure _ENTRY_STATUS is set to 'new' """ events = [ {"update_time": 1609459200000, "alert_time": 1609459200000, "id": "1"}, ] _add_fields_to_events(events, "BehaviorAnalytics") assert events[0]["_ENTRY_STATUS"] == "new" def test_add_fields_to_events_entry_status_modified(self): """ Given: - Events where update_time is greater than alert_time When: - Calling _add_fields_to_events Then: - Ensure _ENTRY_STATUS is set to 'modified' """ events = [ {"update_time": 1609545600000, "alert_time": 1609459200000, "id": "1"}, ] _add_fields_to_events(events, "BehaviorAnalytics") assert events[0]["_ENTRY_STATUS"] == "modified" def test_add_fields_to_events_without_update_time(self): """ Given: - Events without update_time field - Source log type parameter When: - Calling _add_fields_to_events Then: - Ensure _time field is not added but _source_log_type is added """ events = [{"id": "1"}] _add_fields_to_events(events, "AddressableAlerts") assert events[0].get("_time") is None assert events[0]["_source_log_type"] == "AddressableAlerts" def test_add_fields_to_events_empty_list(self): """ Given: - Empty events list - Source log type parameter When: - Calling _add_fields_to_events Then: - Ensure no error is raised """ events = [] _add_fields_to_events(events, "BehaviorAnalytics") assert events == [] class TestFetchEventsWithPagination: """Tests for _fetch_events_with_pagination function""" def test_pagination_single_page(self, client, mocker): """ Given: - Limit of 50 events - API returns 30 events (less than page size) When: - Calling _fetch_events_with_pagination Then: - Ensure pagination stops after first page """ mocker.patch.object( client, "_fetch_alerts", return_value=[{"id": str(i), "alert_time": 1000 - i} for i in range(30)], ) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 50) assert len(events) == 30 def test_pagination_multiple_pages(self, client, mocker): """ Given: - Limit of 200 events When: - Calling _fetch_events_with_pagination Then: - Ensure _fetch_alerts is called 3 times with page_size: 90, 90, 20 """ # Mock _fetch_alerts to return dummy data mock_fetch = mocker.patch.object( client, "_fetch_alerts", side_effect=[ [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(90)], [{"id": f"2-{i}", "alert_time": 910 - i} for i in range(90)], [{"id": f"3-{i}", "alert_time": 820 - i} for i in range(90)], ], ) _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 200) assert mock_fetch.call_count == 3 # Verify page_size parameter in each call assert mock_fetch.call_args_list[0][0][3] == 90 # First call assert mock_fetch.call_args_list[1][0][3] == 90 # Second call assert mock_fetch.call_args_list[2][0][3] == 20 # Third call def test_pagination_with_deduplication_at_boundary(self, client, mocker): """ Given: - Events with same alert_time at page boundary When: - Calling _fetch_events_with_pagination Then: - Ensure duplicates at boundary are removed """ # First page: last 3 events have alert_time=910 page1 = [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(87)] page1.extend( [ {"id": "dup-1", "alert_time": 910}, {"id": "dup-2", "alert_time": 910}, {"id": "dup-3", "alert_time": 910}, ] ) # Second page: includes the same 3 duplicate events plus new ones page2 = [ {"id": "dup-1", "alert_time": 910}, {"id": "dup-2", "alert_time": 910}, {"id": "dup-3", "alert_time": 910}, ] page2.extend([{"id": f"2-{i}", "alert_time": 909 - i} for i in range(50)]) mocker.patch.object(client, "_fetch_alerts", side_effect=[page1, page2]) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 200) # Should have 90 from page1 + 50 new from page2 = 140 (3 duplicates removed) assert len(events) == 140 # Verify no duplicate IDs event_ids = [e["id"] for e in events] assert len(event_ids) == len(set(event_ids)) def test_pagination_boundary_all_identical_alert_time_and_id(self, client, mocker): """ Given: - All events at page boundary have identical alert_time and id When: - Calling _fetch_events_with_pagination Then: - Ensure no duplicate events are created """ # Page 1: all events at the boundary share the same alert_time page1 = [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(85)] page1.extend([{"id": f"boundary-{i}", "alert_time": 915} for i in range(5)]) # Page 2: starts with the same boundary events (duplicates) plus new events page2 = [{"id": f"boundary-{i}", "alert_time": 915} for i in range(5)] page2.extend([{"id": f"2-{i}", "alert_time": 914 - i} for i in range(30)]) mocker.patch.object(client, "_fetch_alerts", side_effect=[page1, page2]) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 200) # Verify no duplicate IDs event_ids = [e["id"] for e in events] assert len(event_ids) == len( set(event_ids) ), f"Found duplicate IDs: {[eid for eid in event_ids if event_ids.count(eid) > 1]}" # Should have 90 from page1 + 30 new from page2 = 120 (5 duplicates removed) assert len(events) == 120 def test_pagination_stops_when_limit_reached(self, client, mocker): """ Given: - Limit of 100 events When: - Calling _fetch_events_with_pagination Then: - Ensure pagination stops at limit with page_size: 90, 10 """ mock_fetch = mocker.patch.object( client, "_fetch_alerts", side_effect=[ [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(90)], # Page 1: 90 events [{"id": f"2-{i}", "alert_time": 910 - i} for i in range(10)], # Page 2: 10 events ], ) events = _fetch_events_with_pagination(client, BEHAVIOR_ANALYTICS, 500, 1000, 100) assert mock_fetch.call_count == 2 assert mock_fetch.call_args_list[0][0][3] == 90 # First call: page_size=90 assert mock_fetch.call_args_list[1][0][3] == 10 # Second call: page_size=10 (remaining) assert len(events) == 100 class TestClient: """Tests for Client class methods""" def test_get_access_token_from_token_secret(self, client, mocker): """ Given: - Valid token and secret When: - Calling get_access_token_from_token_secret Then: - Ensure access token is returned """ # Mock to bypass caching mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") mocker.patch.object(client, "_http_request", return_value={"access_token": "test_access_token"}) token = client.get_access_token_from_token_secret() assert token == "test_access_token" assert client._access_token == "test_access_token" def test_get_access_token_force_new(self, client, mocker): """ Given: - Client with existing cached token - force_new=True parameter When: - Calling get_access_token_from_token_secret with force_new=True Then: - Ensure new token is generated bypassing cache """ # Set existing token client._access_token = "old_token" mocker.patch.object(client, "_cache_token") mocker.patch.object(client, "_http_request", return_value={"access_token": "new_token"}) token = client.get_access_token_from_token_secret(force_new=True) assert token == "new_token" assert client._access_token == "new_token" def test_get_access_token_from_username_password(self, client, mocker): """ Given: - Valid username and password When: - Calling get_access_token_from_username_password Then: - Ensure access token is returned """ mocker.patch.object(client, "_http_request", return_value={"access_token": "test_user_access_token"}) token = client.get_access_token_from_username_password() assert token == "test_user_access_token" assert client._user_access_token == "test_user_access_token" def test_fetch_alerts_with_retry_on_429(self, client, mocker): """ Given: - API returns HTTP 429 (Too Many Requests) on first attempt - API succeeds on second attempt When: - Calling _fetch_alerts Then: - Ensure retry mechanism works and events are returned """ mocker.patch("time.sleep") # Mock sleep to speed up test mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") call_count = [0] def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": f"token_{call_count[0]}"} # Alert endpoint call_count[0] += 1 if call_count[0] == 1: raise DemistoException("Error in API call [429] - Too Many Requests") return {"results": [{"id": "1", "alert_time": 1000}]} mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) alerts = client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) assert len(alerts) == 1 assert call_count[0] == 2 # Should have made 2 attempts def test_fetch_alerts_with_retry_on_401(self, client, mocker): """ Given: - API returns HTTP 401 (Unauthorized - token expired) on first attempt - API succeeds on second attempt with new token When: - Calling _fetch_alerts Then: - Ensure token is regenerated and request succeeds """ mocker.patch("time.sleep") mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") call_count = [0] def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": f"token_{call_count[0]}"} # Alert endpoint call_count[0] += 1 if call_count[0] == 1: raise DemistoException("Error in API call [401] - Unauthorized") return {"results": [{"id": "1", "alert_time": 1000}]} mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) alerts = client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) assert len(alerts) == 1 assert call_count[0] == 2 def test_fetch_alerts_retry_exhausted(self, client, mocker, capfd): """ Given: - API returns HTTP 429 on all attempts When: - Calling _fetch_alerts Then: - Ensure exception is raised after max retries """ mocker.patch("time.sleep") mocker.patch.object(client, "_get_cached_token", return_value=None) mocker.patch.object(client, "_cache_token") def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": "token"} raise DemistoException("Error in API call [429] - Too Many Requests") mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) with capfd.disabled(), pytest.raises(DemistoException): client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) def test_fetch_alerts_no_retry_on_other_errors(self, client, mocker): """ Given: - API returns HTTP 500 (Server Error) When: - Calling _fetch_alerts Then: - Ensure no retry is attempted and exception is raised immediately """ mocker.patch.object(client, "_get_cached_token", return_value=None) def http_request_side_effect(**kwargs): url_suffix = kwargs.get("url_suffix", "") if "login" in url_suffix: return {"access_token": "token"} raise DemistoException("Error in API call [500] - Server Error") mocker.patch.object(client, "_http_request", side_effect=http_request_side_effect) with pytest.raises(DemistoException): client._fetch_alerts(BEHAVIOR_ANALYTICS, 500, 1000, 10) class TestJwtExtraction: """Tests for _extract_jwt_expiration edge cases""" @pytest.mark.parametrize( "jwt_token, expected_behavior", [ pytest.param( "only.two_parts", "default", id="malformed_jwt_wrong_number_of_parts", ), pytest.param( "a.b.c.d", "default", id="malformed_jwt_too_many_parts", ), pytest.param( "header.!!!invalid_base64!!!.signature", "default", id="invalid_base64_encoding", ), ], ) @freeze_time("2021-01-10T00:00:00Z") def test_extract_jwt_expiration_malformed(self, client, jwt_token, expected_behavior): """ Given: - A malformed JWT token (wrong parts count, invalid base64, or missing exp) When: - Calling _extract_jwt_expiration Then: - Ensure default 30-minute expiry is returned """ result = client._extract_jwt_expiration(jwt_token) # Default is current time + 1800 seconds (30 minutes) # Frozen time: 2021-01-10T00:00:00Z = 1610236800 expected_default = 1610236800 + 1800 assert result == expected_default @freeze_time("2021-01-10T00:00:00Z") def test_extract_jwt_expiration_missing_exp_field(self, client): """ Given: - A valid JWT structure but payload missing 'exp' field When: - Calling _extract_jwt_expiration Then: - Ensure default 30-minute expiry is returned """ # Create a valid JWT with no 'exp' field payload = base64.urlsafe_b64encode(json.dumps({"sub": "user123"}).encode()).decode().rstrip("=") jwt_token = f"header.{payload}.signature" result = client._extract_jwt_expiration(jwt_token) expected_default = 1610236800 + 1800 assert result == expected_default def test_extract_jwt_expiration_valid(self, client): """ Given: - A valid JWT with 'exp' field When: - Calling _extract_jwt_expiration Then: - Ensure the correct expiration timestamp is returned """ exp_time = 1610240400 # Some future timestamp payload = base64.urlsafe_b64encode(json.dumps({"exp": exp_time}).encode()).decode().rstrip("=") jwt_token = f"header.{payload}.signature" result = client._extract_jwt_expiration(jwt_token) assert result == exp_time class TestTokenCaching: """Tests for _cache_token and _get_cached_token edge cases""" @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_expired(self, client, mocker): """ Given: - A cached token that has already expired When: - Calling _get_cached_token Then: - Ensure None is returned (token is not used) """ # Frozen time: 1610236800. Token expired at 1610236700 (in the past) mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "expired_token", "api_token_expiry": 1610236700, }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result is None @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_expires_within_buffer(self, client, mocker): """ Given: - A cached token that expires within the 5-minute buffer (< 300 seconds from now) When: - Calling _get_cached_token Then: - Ensure None is returned (token is considered expired) """ # Frozen time: 1610236800. Token expires at 1610237000 (200 seconds from now, within 300s buffer) mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "almost_expired_token", "api_token_expiry": 1610237000, }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result is None @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_valid(self, client, mocker): """ Given: - A cached token that is still valid (expires well beyond the 5-minute buffer) When: - Calling _get_cached_token Then: - Ensure the cached token is returned """ # Frozen time: 1610236800. Token expires at 1610238800 (2000 seconds from now, well beyond 300s buffer) mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "valid_token", "api_token_expiry": 1610238800, }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result == "valid_token" @freeze_time("2021-01-10T00:00:00Z") def test_get_cached_token_non_integer_expiry(self, client, mocker): """ Given: - A cached token with a non-integer expiry value in integration context When: - Calling _get_cached_token Then: - Ensure None is returned (invalid expiry treated as expired) """ mocker.patch( "demistomock.getIntegrationContext", return_value={ "api_access_token": "some_token", "api_token_expiry": "not_an_integer", }, ) result = client._get_cached_token("api_access_token", "api_token_expiry") assert result is None def test_cache_token_empty_token(self, client, mocker): """ Given: - An empty token string When: - Calling _cache_token Then: - Ensure the token is not cached (setIntegrationContext is not called) """ mock_set_context = mocker.patch("demistomock.setIntegrationContext") mocker.patch("demistomock.getIntegrationContext", return_value={}) client._cache_token("", "api_access_token", "api_token_expiry") mock_set_context.assert_not_called() class TestTestModuleCommand: """Tests for test_module_command function""" @freeze_time("2021-01-10T00:00:00Z") def test_test_module_success_behavior_analytics(self, client, mocker): """ Given: - Valid client with token and secret - Behavior Analytics event type When: - Calling test_module_command Then: - Ensure 'ok' is returned """ from iManageThreatManager import test_module_command mocker.patch.object(client, "_fetch_alerts", return_value=[]) result = test_module_command(client, {}, [BEHAVIOR_ANALYTICS]) assert result == "ok" @freeze_time("2021-01-10T00:00:00Z") def test_test_module_success_addressable_alerts(self, client, mocker): """ Given: - Valid client with username and password - Addressable Alerts event type When: - Calling test_module_command Then: - Ensure 'ok' is returned """ from iManageThreatManager import test_module_command mocker.patch.object(client, "_fetch_alerts", return_value=[]) result = test_module_command(client, {}, [ADDRESSABLE_ALERTS]) assert result == "ok" def test_test_module_auth_error(self, client, mocker, capfd): """ Given: - Invalid credentials When: - Calling test_module_command Then: - Ensure authorization error message is returned """ from iManageThreatManager import test_module_command mocker.patch.object( client, "_fetch_alerts", side_effect=DemistoException("Error in API call [401] - Unauthorized"), ) with capfd.disabled(): result = test_module_command(client, {}, [BEHAVIOR_ANALYTICS]) assert "Authorization Error" in result @freeze_time("2021-01-10T00:00:00Z") def test_test_module_calls_fetch_alerts_without_retries(self, client, mocker): """ Given: - Valid client with credentials - Behavior Analytics event type When: - Calling test_module_command Then: - Ensure _fetch_alerts is called with enable_retries=False to avoid long retry delays that cause platform timeouts """ from iManageThreatManager import test_module_command mock_fetch = mocker.patch.object(client, "_fetch_alerts", return_value=[]) test_module_command(client, {}, [BEHAVIOR_ANALYTICS]) mock_fetch.assert_called_once() call_kwargs = mock_fetch.call_args.kwargs assert ( call_kwargs.get("enable_retries") is False ), "test_module_command must call _fetch_alerts with enable_retries=False to prevent timeout" class TestGetEventsCommand: """Tests for get_events_command function""" @freeze_time("2021-01-10T00:00:00Z") def test_get_events_command_behavior_analytics(self, client, mocker): """ Given: - Valid client and Behavior Analytics event type When: - Calling get_events_command Then: - Ensure events are returned """ mocker.patch.object( client, "_fetch_alerts", return_value=[{"id": "1", "alert_time": 1609459200000, "update_time": 1609459200000}], ) events, results = get_events_command(client, {"event_type": BEHAVIOR_ANALYTICS, "limit": "10"}) assert len(events) == 1 assert events[0]["id"] == "1" @freeze_time("2021-01-10T00:00:00Z") def test_get_events_command_with_pagination(self, client, mocker): """ Given: - Limit of 150 events - API returns events in multiple pages When: - Calling get_events_command Then: - Ensure all events are fetched via pagination """ page1 = [{"id": f"1-{i}", "alert_time": 1000 - i} for i in range(90)] page2 = [{"id": f"2-{i}", "alert_time": 910 - i} for i in range(60)] mocker.patch.object( client, "_fetch_alerts", side_effect=[page1, page2], ) events, results = get_events_command(client, {"event_type": BEHAVIOR_ANALYTICS, "limit": "150"}) assert len(events) == 150 class TestFetchEventsCommand: """Tests for fetch_events_command function""" @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_first_run(self, client, mocker): """ Given: - Empty last_run (first fetch) - Behavior Analytics event type When: - Calling fetch_events_command Then: - Ensure events are fetched and next_run is set correctly """ # Frozen time: 2021-01-10 00:00:00 UTC = 1610236800000 ms # First fetch lookback: 1 hour = 1610236800000 - 3600000 = 1610233200000 ms # Mock event must be AFTER 1610233200000 to be included mocker.patch.object( client, "_fetch_alerts", return_value=[{"id": "1", "alert_time": 1610235000000, "update_time": 1610235000000}], ) next_run, events = fetch_events_command( client=client, last_run={}, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=10 ) assert len(events) == 1 assert events[0]["_source_log_type"] == EVENT_TYPE_CONFIG[BEHAVIOR_ANALYTICS].source_log_type assert "last_fetch_BehaviorAnalytics" in next_run assert next_run["last_fetch_BehaviorAnalytics"] == 1610235000000 @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_with_pagination(self, client, mocker): """ Given: - max_events_per_type of 200 - API returns events in multiple pages When: - Calling fetch_events_command Then: - Ensure pagination is used to fetch all events """ page1 = [{"id": f"1-{i}", "alert_time": 1000 - i, "update_time": 1000 - i} for i in range(90)] page2 = [{"id": f"2-{i}", "alert_time": 910 - i, "update_time": 910 - i} for i in range(90)] page3 = [{"id": f"3-{i}", "alert_time": 820 - i, "update_time": 820 - i} for i in range(20)] mocker.patch.object( client, "_fetch_alerts", side_effect=[page1, page2, page3], ) next_run, events = fetch_events_command( client=client, last_run={}, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=200 ) assert len(events) == 200 @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_multiple_types(self, client, mocker): """ Given: - Multiple event types configured When: - Calling fetch_events_command Then: - Ensure events from all types are fetched """ behavior_events = [{"id": "1", "alert_time": 1609459200000, "update_time": 1609459200000}] addressable_events = [{"id": "2", "alert_time": 1609545600000, "update_time": 1609545600000}] mocker.patch.object( client, "_fetch_alerts", side_effect=[behavior_events, addressable_events], ) next_run, events = fetch_events_command( client=client, last_run={}, event_types=[BEHAVIOR_ANALYTICS, ADDRESSABLE_ALERTS], max_events_per_type=10 ) assert len(events) == 2 assert events[0]["_source_log_type"] == EVENT_TYPE_CONFIG[BEHAVIOR_ANALYTICS].source_log_type assert events[1]["_source_log_type"] == EVENT_TYPE_CONFIG[ADDRESSABLE_ALERTS].source_log_type @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_no_new_events(self, client, mocker): """ Given: - Last run with previous fetch time - No new events available When: - Calling fetch_events_command Then: - Ensure empty events list and updated next_run """ mocker.patch.object( client, "_fetch_alerts", return_value=[], ) last_run = {"last_fetch_BehaviorAnalytics": 1609459200000} next_run, events = fetch_events_command( client=client, last_run=last_run, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=10 ) assert len(events) == 0 assert "last_fetch_BehaviorAnalytics" in next_run @freeze_time("2021-01-10T00:00:00Z") def test_fetch_events_with_error(self, client, mocker, capfd): """ Given: - API error during fetch When: - Calling fetch_events_command Then: - Ensure error is handled and last_run is preserved """ mocker.patch.object( client, "_fetch_alerts", side_effect=DemistoException("Error in API call [500] - Server Error"), ) last_run = {"last_fetch_BehaviorAnalytics": 1609459200000} with capfd.disabled(): next_run, events = fetch_events_command( client=client, last_run=last_run, event_types=[BEHAVIOR_ANALYTICS], max_events_per_type=10 ) # Should preserve last_run on error assert next_run["last_fetch_BehaviorAnalytics"] == 1609459200000 assert len(events) == 0