import json from datetime import datetime, timedelta import pytest from freezegun import freeze_time from WorkdayEventCollector import DATE_FORMAT, DEFAULT_MAX_FETCH, MAX_PAGE_SIZE, Client def util_load_json(path): with open(path, encoding="utf-8") as f: return json.loads(f.read()) class TestFetchActivity: @pytest.fixture(autouse=True) def create_client(self, mocker): self.base_url = "https://test.com" self.tenant_name = "test" self.token_url = f"{self.base_url}/ccx/oauth2/{self.tenant_name}/token" mocker.patch.object(Client, "get_access_token", return_value="1234") self.client = Client( base_url=self.base_url, refresh_token="refresh_token", client_id="test12", client_secret="test_sec1234", token_url=self.token_url, verify=False, proxy=False, headers={"Accept": "application/json", "Content-Type": "application/json"}, max_fetch=25000, ) @staticmethod def create_response_by_limit(from_date, to_date, offset, user_activity_entry_count=False, limit=1): single_response = util_load_json("test_data/single_loggings_response.json") return [single_response.copy() for i in range(limit)] @staticmethod def create_response_with_duplicates(request_time, limit, number_of_different_time, id_to_start_from): """ Creates response with different requestTime and ids. Args: request_time: request time to start from. limit: number of responses number_of_different_time: number of responses with different requestTime id_to_start_from: id to start from """ single_response = util_load_json("test_data/single_loggings_response.json") request_time_date_time = datetime.strptime(request_time, DATE_FORMAT) output = [] def create_single(single_response, time, id, output): single_response["requestTime"] = time single_response["taskId"] = str(id) output.append(single_response) id += 1 return output, id for _i in range(limit - number_of_different_time): output, id_to_start_from = create_single(single_response.copy(), request_time, id_to_start_from, output) for _i in range(limit - number_of_different_time, limit): new_time = datetime.strftime(request_time_date_time + timedelta(seconds=10), DATE_FORMAT) output, id_to_start_from = create_single(single_response.copy(), new_time, id_to_start_from, output) return output @pytest.mark.parametrize("loggings_to_fetch", [1, 4, 6], ids=["Single", "Part", "All"]) def test_get_max_fetch_activity_logging(self, loggings_to_fetch, requests_mock, mocker): """ Given: number of logging to fetch. When: running get activity logging command or fetch. Then: return the correct number of loggings. """ from WorkdayEventCollector import get_max_fetch_activity_logging mocker.patch.object(Client, "get_activity_logging_request", side_effect=self.create_response_by_limit) res = get_max_fetch_activity_logging( client=self.client, logging_to_fetch=loggings_to_fetch, from_date="2023-04-15T07:00:00.000Z", to_date="2023-04-16T07:00:00.000Z", ) assert len(res) == loggings_to_fetch def test_get_max_fetch_activity_logging_paginates_above_page_size(self, mocker): """ Given: a max_fetch (logging_to_fetch) larger than the per-call page cap (3 * MAX_PAGE_SIZE). When: running get_max_fetch_activity_logging. Then: the client is called multiple times (pagination), each call is capped at MAX_PAGE_SIZE, the offset advances by the number of events returned, and all events are collected. """ from WorkdayEventCollector import get_max_fetch_activity_logging logging_to_fetch = 3 * MAX_PAGE_SIZE def response_by_requested_limit(from_date, to_date, offset, limit): # Workday returns up to `limit` events (page cap enforced by caller at MAX_PAGE_SIZE). single_response = util_load_json("test_data/single_loggings_response.json") return [single_response.copy() for _ in range(limit)] request_mock = mocker.patch.object(Client, "get_activity_logging_request", side_effect=response_by_requested_limit) res = get_max_fetch_activity_logging( client=self.client, logging_to_fetch=logging_to_fetch, from_date="2023-04-15T07:00:00.000Z", to_date="2023-04-16T07:00:00.000Z", ) # logging_to_fetch / MAX_PAGE_SIZE => exactly 3 paginated API calls. assert request_mock.call_count == 3 # Every call must be capped at the per-call page limit. assert [call.kwargs["limit"] for call in request_mock.call_args_list] == [MAX_PAGE_SIZE, MAX_PAGE_SIZE, MAX_PAGE_SIZE] # Offset must advance by the number of events collected so far. assert [call.kwargs["offset"] for call in request_mock.call_args_list] == [0, MAX_PAGE_SIZE, 2 * MAX_PAGE_SIZE] # All events are accumulated across the pages. assert len(res) == logging_to_fetch def test_get_max_fetch_activity_logging_stops_on_empty_page(self, mocker): """ Given: a large max_fetch but the source runs out of events mid-pagination. When: running get_max_fetch_activity_logging. Then: pagination stops as soon as an empty page is returned (no infinite loop), and only the available events are collected. """ from WorkdayEventCollector import get_max_fetch_activity_logging single_response = util_load_json("test_data/single_loggings_response.json") first_page = [single_response.copy() for _ in range(MAX_PAGE_SIZE)] second_page = [single_response.copy() for _ in range(MAX_PAGE_SIZE // 2)] request_mock = mocker.patch.object( Client, "get_activity_logging_request", side_effect=[first_page, second_page, []], ) res = get_max_fetch_activity_logging( client=self.client, logging_to_fetch=3 * MAX_PAGE_SIZE, from_date="2023-04-15T07:00:00.000Z", to_date="2023-04-16T07:00:00.000Z", ) available_events = MAX_PAGE_SIZE + MAX_PAGE_SIZE // 2 # Third call returns empty -> loop breaks. Only the available events are collected. assert request_mock.call_count == 3 assert [call.kwargs["offset"] for call in request_mock.call_args_list] == [0, MAX_PAGE_SIZE, available_events] assert len(res) == available_events def test_resolve_max_fetch_uses_provided_value(self): """ Given: a max_fetch value is provided in the params. When: running resolve_max_fetch. Then: the provided value is returned (as an int). """ from WorkdayEventCollector import resolve_max_fetch assert resolve_max_fetch({"max_fetch": "500"}) == 500 @pytest.mark.parametrize("params", [{}, {"max_fetch": None}, {"max_fetch": ""}], ids=["missing", "none", "empty"]) def test_resolve_max_fetch_falls_back_to_default(self, params): """ Given: max_fetch is missing, None, or empty in the params. When: running resolve_max_fetch. Then: DEFAULT_MAX_FETCH is returned. """ from WorkdayEventCollector import resolve_max_fetch assert resolve_max_fetch(params) == DEFAULT_MAX_FETCH DUPLICATED_ACTIVITY_LOGGINGS = [ (("2023-04-15T07:00:00Z", 5, 2, 0), 5, {}), (("2023-04-15T07:00:00Z", 5, 1, 0), 4, {"last_log": 0}), ] @pytest.mark.parametrize("args, len_of_activity_loggings, last_run", DUPLICATED_ACTIVITY_LOGGINGS) def test_remove_duplicated_activity_logging(self, args, len_of_activity_loggings, last_run): """ Given: responses with potential duplications from last fetch. When: running fetch command. Then: return last responses with the latest requestTime to check if there are duplications. """ from WorkdayEventCollector import remove_duplications loggings = self.create_response_with_duplicates(*args) if "last_log" in last_run: last_run["last_log"] = loggings[last_run.get("last_log")] activity_loggings = remove_duplications(loggings, last_run) assert len(activity_loggings) == len_of_activity_loggings def test_remove_milliseconds_from_time_of_logging(self): """ Given: loggings with time string with milliseconds When: Fetching loggings from Workday Then: Remove the milliseconds. """ from WorkdayEventCollector import remove_milliseconds_from_time_of_logging activity_logging: dict = util_load_json("test_data/single_loggings_response.json") requests_time = "2023-04-24T07:00:00.123Z" final_time = "2023-04-24T07:00:00Z" activity_logging["requestTime"] = requests_time assert remove_milliseconds_from_time_of_logging(activity_logging) == final_time def test_get_activity_logging_command(self, mocker): """ Given: params to run get_activity_logging_command When: running the command Then: Accurate response and readable output is returned. """ from WorkdayEventCollector import get_activity_logging_command mocker.patch.object(Client, "get_activity_logging_request", side_effect=self.create_response_by_limit) activity_loggings, res = get_activity_logging_command( client=self.client, from_date="2023-04-15T07:00:00Z", to_date="2023-04-16T07:00:00Z", limit=4, offset=0 ) assert len(activity_loggings) == 4 assert "Activity Logging List" in res.readable_output @freeze_time("2023-04-15 08:00:00") def test_fetch_activity_logging(self, mocker): """ Tests the fetch_events function Given: - first_fetch_time When: - Running the 'fetch_activity_logging' function. Then: - Validates that the function generates the correct API requests with the expected parameters. - Validates that the function returns the expected events and next_run timestamps. """ from WorkdayEventCollector import fetch_activity_logging first_fetch_time = datetime.strptime("2023-04-12T07:00:00Z", DATE_FORMAT) fetched_events = util_load_json("test_data/fetch_activity_loggings.json") http_responses = mocker.patch.object( Client, "get_activity_logging_request", side_effect=[ fetched_events.get("fetch_loggings_before"), fetched_events.get("fetch_loggings"), ], ) activity_loggings, new_last_run = fetch_activity_logging( self.client, last_run={}, first_fetch=first_fetch_time, max_fetch=3 ) assert http_responses.call_args_list[0][1] == { "limit": 3, "offset": 0, "from_date": "2023-04-12T07:00:00Z", "to_date": "2023-04-15T08:00:00Z", } assert http_responses.call_args_list[1][1] == { "limit": 2, "offset": 1, "from_date": "2023-04-12T07:00:00Z", "to_date": "2023-04-15T08:00:00Z", } assert activity_loggings == fetched_events.get("fetched_events") assert new_last_run.get("last_fetch_time") == "2023-04-15T07:00:00Z" assert new_last_run.get("last_log").get("taskId") == "3" # assert no new results when given the last_run: fetched_events = util_load_json("test_data/fetch_activity_loggings.json") http_responses = mocker.patch.object( Client, "get_activity_logging_request", side_effect=[fetched_events.get("fetch_loggings"), []] ) activity_loggings, new_last_run = fetch_activity_logging( self.client, last_run=new_last_run, first_fetch=first_fetch_time, max_fetch=3 ) assert http_responses.call_args_list[0][1] == { "limit": 3, "offset": 0, "from_date": "2023-04-15T07:00:00Z", "to_date": "2023-04-15T08:00:00Z", } assert activity_loggings == [] assert new_last_run.get("last_fetch_time") == "2023-04-15T07:00:00Z" assert new_last_run.get("last_log").get("taskId") == "3" @pytest.mark.parametrize("max_fetch, instance_returned", [(6000, 1), (15000, 2), (60000, 6)]) def test_instance_returned_request(self, mocker, max_fetch, instance_returned): self.client.max_fetch = max_fetch http_request = mocker.patch.object(Client, "http_request") self.client.get_activity_logging_request(from_date="2023-04-15T07:00:00Z", to_date="2023-04-15T08:00:00Z") params_sent = http_request.call_args_list[0][1].get("params", {}) assert params_sent.get("instancesReturned") == instance_returned