diff --git a/tests/unit_tests/service/test_rest_api.py b/tests/unit_tests/service/test_rest_api.py index a2f46e668..1b15c7790 100644 --- a/tests/unit_tests/service/test_rest_api.py +++ b/tests/unit_tests/service/test_rest_api.py @@ -2,9 +2,8 @@ from collections.abc import Iterator from dataclasses import dataclass from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, PropertyMock, patch -import jwt import pytest from bluesky.protocols import Stoppable from fastapi import HTTPException, status @@ -27,7 +26,6 @@ cancel_active_task, get_device, get_plan, - pause_worker, resume_worker, submit_task, ) @@ -43,7 +41,7 @@ WorkerTask, ) from blueapi.service.runner import WorkerDispatcher -from blueapi.worker.event import WorkerState +from blueapi.worker.event import TaskStatusEnum, WorkerState from blueapi.worker.task import Task from blueapi.worker.task_worker import TrackableTask @@ -56,8 +54,78 @@ class MockCountModel(BaseModel): ... @pytest.fixture -def mock_runner() -> Mock: - return Mock(spec=WorkerDispatcher) +def mock_runner_data() -> Mock: + return Mock() + + +@pytest.fixture +def mock_runner(mock_runner_data: Mock) -> Mock: + runner = Mock(spec=WorkerDispatcher) + + def run(method, *args, **kwargs): + match method: + case interface.get_active_task: + running = run(interface.get_tasks_by_status, TaskStatusEnum.RUNNING) + return running[0] if running else None + case interface.get_task_by_id: + return {t.task_id: t for t in mock_runner_data.tasks}.get( + kwargs.get("task_id") or args[0] + ) + case interface.get_tasks: + return mock_runner_data.tasks + case interface.get_tasks_by_status: + status = kwargs.get("status") or args[0] + match status: + case TaskStatusEnum.RUNNING: + return [ + t + for t in mock_runner_data.tasks + if not t.is_pending and not t.is_complete + ] + case TaskStatusEnum.PENDING: + return [t for t in mock_runner_data.tasks if t.is_pending] + case TaskStatusEnum.COMPLETE: + return [t for t in mock_runner_data.tasks if t.is_complete] + case _: + return [] + + case interface.get_plans: + return mock_runner_data.plans + case interface.get_plan: + name = kwargs.get("name") or args[0] + plans = [p for p in mock_runner_data.plans if p.name == name] + if plans: + return plans[0] + raise KeyError(name) + case interface.get_devices: + return mock_runner_data.devices + case interface.get_device: + name = kwargs.get("name") or args[0] + devices = [d for d in mock_runner_data.devices if d.name == name] + if devices: + return devices[0] + raise KeyError(name) + case interface.get_oidc_config: + return mock_runner_data.oidc_config + case interface.get_worker_state: + return mock_runner_data.state + case interface.get_python_env: + return mock_runner_data.python_environment + case interface.submit_task: + return mock_runner_data.submit_task(*args, **kwargs) + case interface.begin_task: + return mock_runner_data.begin_task(*args, **kwargs) + case interface.clear_task: + return mock_runner_data.clear_task(*args, **kwargs) + case interface.cancel_active_task: + return mock_runner_data.cancel_active_task(*args, **kwargs) + case interface.pause_worker: + return mock_runner_data.pause_worker(*args, **kwargs) + case _: + raise ValueError("Unsupported method: " + method.__name__) + + runner.run.side_effect = run + return runner @pytest.fixture @@ -141,13 +209,13 @@ def stop(self, success: bool = True): def test_rest_config_with_cors_gets_plan( client_with_cors: TestClient, - mock_runner: Mock, + mock_runner_data: Mock, ): class MyModel(BaseModel): id: str plan = Plan(name="my-plan", model=MyModel) - mock_runner.run.return_value = [PlanModel.from_plan(plan)] + mock_runner_data.plans = [PlanModel.from_plan(plan)] response_get = client_with_cors.get("/plans") assert response_get.status_code == status.HTTP_200_OK @@ -155,7 +223,7 @@ class MyModel(BaseModel): def test_rest_config_with_cors( client_with_cors: TestClient, - mock_runner: Mock, + mock_runner_data: Mock, ): task = TaskRequest( name="my-plan", @@ -163,7 +231,7 @@ def test_rest_config_with_cors( instrument_session=FAKE_INSTRUMENT_SESSION, ) task_id = "f8424be3-203c-494e-b22f-219933b4fa67" - mock_runner.run.side_effect = [task_id] + mock_runner_data.submit_task.return_value = task_id # Allowed method response_post = client_with_cors.post( @@ -175,12 +243,12 @@ def test_rest_config_with_cors( assert response_post.headers["content-type"] == "application/json" -def test_get_plans(mock_runner: Mock, client: TestClient) -> None: +def test_get_plans(mock_runner_data: Mock, client: TestClient) -> None: class MyModel(BaseModel): id: str plan = Plan(name="my-plan", model=MyModel) - mock_runner.run.return_value = [PlanModel.from_plan(plan)] + mock_runner_data.plans = [PlanModel.from_plan(plan)] response = client.get("/plans") @@ -201,12 +269,14 @@ class MyModel(BaseModel): } -def test_get_plan_by_name(mock_runner: Mock, client: TestClient) -> None: +def test_get_plan_by_name( + mock_runner: Mock, mock_runner_data: Mock, client: TestClient +) -> None: class MyModel(BaseModel): id: str plan = Plan(name="my-plan", model=MyModel) - mock_runner.run.return_value = PlanModel.from_plan(plan) + mock_runner_data.plans = [PlanModel.from_plan(plan)] response = client.get("/plans/my-plan") @@ -224,17 +294,19 @@ class MyModel(BaseModel): } -def test_get_non_existent_plan_by_name(mock_runner: Mock, client: TestClient) -> None: - mock_runner.run.side_effect = KeyError("my-plan") +def test_get_non_existent_plan_by_name( + mock_runner_data: Mock, client: TestClient +) -> None: + mock_runner_data.plans = [] response = client.get("/plans/my-plan") assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json() == {"detail": "Item not found"} -def test_get_devices(mock_runner: Mock, client: TestClient) -> None: +def test_get_devices(mock_runner_data: Mock, client: TestClient) -> None: device = MinimalDevice("my-device") - mock_runner.run.return_value = [DeviceModel.from_device(device)] + mock_runner_data.devices = [DeviceModel.from_device(device)] response = client.get("/devices") @@ -249,10 +321,12 @@ def test_get_devices(mock_runner: Mock, client: TestClient) -> None: } -def test_get_device_by_name(mock_runner: Mock, client: TestClient) -> None: +def test_get_device_by_name( + mock_runner: Mock, mock_runner_data: Mock, client: TestClient +) -> None: device = MinimalDevice("my-device") - mock_runner.run.return_value = DeviceModel.from_device(device) + mock_runner_data.devices = [DeviceModel.from_device(device)] response = client.get("/devices/my-device") mock_runner.run.assert_called_once_with(get_device, "my-device") @@ -263,15 +337,15 @@ def test_get_device_by_name(mock_runner: Mock, client: TestClient) -> None: } -def test_get_non_existent_device_by_name(mock_runner: Mock, client: TestClient) -> None: - mock_runner.run.side_effect = KeyError("my-device") +def test_get_non_existent_device_by_name(mock_runner_data: Mock, client: TestClient): + mock_runner_data.devices = [] response = client.get("/devices/my-device") assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json() == {"detail": "Item not found"} -def test_create_task(mock_runner: Mock, client: TestClient) -> None: +def test_create_task(mock_runner: Mock, mock_runner_data: Mock, client: TestClient): task = TaskRequest( name="count", params={"detectors": ["x"]}, @@ -279,7 +353,7 @@ def test_create_task(mock_runner: Mock, client: TestClient) -> None: ) task_id = str(uuid.uuid4()) - mock_runner.run.side_effect = [task_id] + mock_runner_data.submit_task.return_value = task_id response = client.post("/tasks", json=task.model_dump()) @@ -306,6 +380,7 @@ def test_submit_task_requires_permission( def test_create_task_inserts_auth_metadata( mock_runner: Mock, + mock_runner_data: Mock, client_with_auth: TestClient, ) -> None: task = TaskRequest( @@ -316,8 +391,7 @@ def test_create_task_inserts_auth_metadata( client_with_auth.follow_redirects = False task_id = str(uuid.uuid4()) - # mock_runner.run.side_effect = [task_id] - mock_runner.run.return_value = [task_id] + mock_runner_data.submit_task.return_value = task_id client_with_auth.post("/tasks", json=task.model_dump()) @@ -356,8 +430,10 @@ def test_create_task_validation_error(mock_runner: Mock, client: TestClient) -> } -def test_put_plan_begins_task(client: TestClient) -> None: +def test_put_plan_begins_task(client: TestClient, mock_runner_data: Mock) -> None: task_id = "04cd9aa6-b902-414b-ae4b-49ea4200e957" + mock_runner_data.tasks = [TrackableTask(task_id=task_id, task=Task(name="foo"))] + mock_runner_data.begin_task.side_effect = lambda task, **kw: task resp = client.put("/worker/task", json={"task_id": task_id}) @@ -365,14 +441,19 @@ def test_put_plan_begins_task(client: TestClient) -> None: assert resp.json() == {"task_id": task_id} -def test_put_plan_fails_if_not_idle(mock_runner: Mock, client: TestClient) -> None: +def test_put_plan_fails_if_not_idle(mock_runner_data: Mock, client: TestClient) -> None: task_id_current = "260f7de3-b608-4cdc-a66c-257e95809792" task_id_new = "07e98d68-21b5-4ad7-ac34-08b2cb992d42" # Set to non idle - mock_runner.run.return_value = TrackableTask( - task=Task(name="none"), task_id=task_id_current, is_complete=False - ) + mock_runner_data.tasks = [ + TrackableTask( + task=Task(name="none"), + task_id=task_id_current, + is_pending=False, + is_complete=False, + ) + ] resp = client.put("/worker/task", json={"task_id": task_id_new}) @@ -380,8 +461,8 @@ def test_put_plan_fails_if_not_idle(mock_runner: Mock, client: TestClient) -> No assert resp.json() == {"detail": "Worker already active"} -def test_get_tasks(mock_runner: Mock, client: TestClient) -> None: - tasks = [ +def test_get_tasks(mock_runner_data: Mock, client: TestClient) -> None: + mock_runner_data.tasks = [ TrackableTask(task_id="0", task=Task(name="sleep", params={"time": 0.0})), TrackableTask( task_id="1", @@ -391,8 +472,6 @@ def test_get_tasks(mock_runner: Mock, client: TestClient) -> None: ), ] - mock_runner.run.return_value = tasks - response = client.get("/tasks") assert response.status_code == status.HTTP_200_OK @@ -428,8 +507,8 @@ def test_get_tasks(mock_runner: Mock, client: TestClient) -> None: } -def test_get_tasks_by_status(mock_runner: Mock, client: TestClient) -> None: - tasks = [ +def test_get_tasks_by_status(mock_runner_data: Mock, client: TestClient) -> None: + mock_runner_data.tasks = [ TrackableTask( task_id="3", task=Task(name="third_task"), @@ -438,9 +517,7 @@ def test_get_tasks_by_status(mock_runner: Mock, client: TestClient) -> None: ), ] - mock_runner.run.return_value = tasks - - response = client.get("/tasks", params={"task_status": "PENDING"}) + response = client.get("/tasks", params={"task_status": "COMPLETE"}) assert response.json() == { "tasks": [ { @@ -467,7 +544,7 @@ def test_get_tasks_by_status_invalid(client: TestClient) -> None: @pytest.mark.parametrize("admin,task_ids", [(True, ["foo", "bar"]), (False, ["foo"])]) def test_get_tasks_filters_by_user( - mock_runner: Mock, + mock_runner_data: Mock, client_with_opa: TestClient, access_token: str, mock_opa_client: Mock, @@ -475,7 +552,7 @@ def test_get_tasks_filters_by_user( task_ids: list[str], ): - mock_runner.run.return_value = [ + mock_runner_data.tasks = [ TrackableTask(task_id="foo", task=Task(name="f1", metadata={"user": "jd1"})), TrackableTask(task_id="bar", task=Task(name="f2", metadata={"user": "jd2"})), ] @@ -486,38 +563,43 @@ def test_get_tasks_filters_by_user( assert [t["task_id"] for t in tasks] == task_ids -def test_delete_submitted_task(mock_runner: Mock, client: TestClient) -> None: +def test_delete_submitted_task(mock_runner_data: Mock, client: TestClient) -> None: task_id = str(uuid.uuid4()) - mock_runner.run.return_value = task_id + mock_runner_data.tasks = [TrackableTask(task_id=task_id, task=Task(name="foo"))] + mock_runner_data.clear_task.return_value = task_id response = client.delete(f"/tasks/{task_id}") assert response.json() == {"task_id": f"{task_id}"} + mock_runner_data.clear_task.assert_called_once_with(task_id) def test_cant_delete_other_users_task( mock_runner: Mock, + mock_runner_data: Mock, client_with_opa: TestClient, access_token: str, mock_opa_client: Mock, ): mock_opa_client.admin.return_value = False - mock_runner.run.side_effect = lambda mth, *args: { - interface.get_task_by_id: TrackableTask( - task_id="bar", task=Task(name="t2", metadata={"user": "jd2"}) - ), - }[mth] + mock_runner_data.tasks = [ + TrackableTask( + task_id="bar", + is_pending=False, + task=Task(name="t2", metadata={"user": "jd2"}), + ) + ] client_with_opa.headers["Authorization"] = f"Bearer {access_token}" resp = client_with_opa.delete("/tasks/bar") # 404 to obfuscate whether task exists when inaccessible assert resp.status_code == 404 - - mock_runner.run.assert_called_once() + mock_runner_data.clear_task.assert_not_called() -def test_set_active_task(client: TestClient) -> None: +def test_set_active_task(client: TestClient, mock_runner_data: Mock) -> None: task_id = str(uuid.uuid4()) task = WorkerTask(task_id=task_id) + mock_runner_data.tasks = [TrackableTask(task_id=task_id, task=Task(name="foo"))] response = client.put("/worker/task", json=task.model_dump()) @@ -526,17 +608,19 @@ def test_set_active_task(client: TestClient) -> None: def test_set_active_task_active_task_complete( - mock_runner: Mock, client: TestClient + mock_runner_data: Mock, client: TestClient ) -> None: task_id = str(uuid.uuid4()) task = WorkerTask(task_id=task_id) - mock_runner.run.return_value = TrackableTask( - task_id="1", - task=Task(name="a_completed_task"), - is_complete=True, - is_pending=False, - ) + mock_runner_data.tasks = [ + TrackableTask( + task_id="1", + task=Task(name="a_completed_task"), + is_complete=True, + is_pending=False, + ) + ] response = client.put("/worker/task", json=task.model_dump()) @@ -545,17 +629,19 @@ def test_set_active_task_active_task_complete( def test_set_active_task_worker_already_running( - mock_runner: Mock, client: TestClient + mock_runner_data: Mock, client: TestClient ) -> None: task_id = str(uuid.uuid4()) task = WorkerTask(task_id=task_id) - mock_runner.run.return_value = TrackableTask( - task_id="1", - task=Task(name="a_running_task"), - is_complete=False, - is_pending=False, - ) + mock_runner_data.tasks = [ + TrackableTask( + task_id="1", + task=Task(name="a_running_task"), + is_complete=False, + is_pending=False, + ) + ] response = client.put("/worker/task", json=task.model_dump()) @@ -565,7 +651,7 @@ def test_set_active_task_worker_already_running( @pytest.mark.parametrize("admin,status", [(True, 200), (False, 404)]) def test_set_other_users_task_active( - mock_runner: Mock, + mock_runner_data: Mock, client_with_opa: TestClient, mock_opa_client: Mock, access_token: str, @@ -579,32 +665,31 @@ def test_set_other_users_task_active( client_with_opa.headers["Authorization"] = f"Bearer {access_token}" - mock_runner.run.side_effect = lambda mth, *a, **kw: { - interface.get_task_by_id: TrackableTask( - task_id="foo", task=Task(name="bar", metadata={"user": "jd2"}) - ), - interface.get_active_task: None, - interface.begin_task: None, - }[mth] + mock_runner_data.tasks = [ + TrackableTask(task_id=task_id, task=Task(name="foo", metadata={"user": "jd2"})) + ] resp = client_with_opa.put("/worker/task", json=task.model_dump()) + if status >= 400: + mock_runner_data.begin_task.assert_not_called() + assert resp.status_code == status -def test_get_task(mock_runner: Mock, client: TestClient): +def test_get_task(mock_runner_data: Mock, client: TestClient): task_id = str(uuid.uuid4()) - task = TrackableTask( - task_id=task_id, - task=Task( - name="third_task", - metadata={ - "foo": "bar", - }, - ), - ) - - mock_runner.run.return_value = task + mock_runner_data.tasks = [ + TrackableTask( + task_id=task_id, + task=Task( + name="third_task", + metadata={ + "foo": "bar", + }, + ), + ) + ] response = client.get(f"/tasks/{task_id}") assert response.json() == { @@ -626,7 +711,7 @@ def test_get_task(mock_runner: Mock, client: TestClient): @pytest.mark.parametrize("admin,status", [(True, 200), (False, 404)]) def test_get_other_users_task( - mock_runner: Mock, + mock_runner_data: Mock, client_with_opa: TestClient, mock_opa_client: Mock, access_token: str, @@ -634,25 +719,24 @@ def test_get_other_users_task( status: int, ): client_with_opa.headers["Authorization"] = f"Bearer {access_token}" - mock_runner.run.return_value = TrackableTask( - task_id="foo", task=Task(name="bar", metadata={"user": "jd2"}) - ) + mock_runner_data.tasks = [ + TrackableTask(task_id="foo", task=Task(name="bar", metadata={"user": "jd2"})) + ] mock_opa_client.admin.return_value = admin resp = client_with_opa.get("/tasks/foo") assert resp.status_code == status -def test_get_all_tasks(mock_runner: Mock, client: TestClient): +def test_get_all_tasks(mock_runner_data: Mock, client: TestClient): task_id = str(uuid.uuid4()) - tasks = [ + mock_runner_data.tasks = [ TrackableTask( task_id=task_id, task=Task(name="third_task"), ) ] - mock_runner.run.return_value = tasks response = client.get("/tasks") assert response.status_code == status.HTTP_200_OK assert response.json() == { @@ -674,58 +758,60 @@ def test_get_all_tasks(mock_runner: Mock, client: TestClient): } -def test_get_task_error(mock_runner: Mock, client: TestClient): +def test_get_task_error(mock_runner_data: Mock, client: TestClient): task_id = 567 - mock_runner.run.return_value = None + mock_runner_data.tasks = [] response = client.get(f"/tasks/{task_id}") assert response.json() == {"detail": "Item not found"} -def test_get_active_task(mock_runner: Mock, client: TestClient): +def test_get_active_task(mock_runner_data: Mock, client: TestClient): task_id = str(uuid.uuid4()) task = TrackableTask( task_id=task_id, task=Task(name="third_task"), + is_pending=False, + is_complete=False, ) - mock_runner.run.return_value = task + mock_runner_data.tasks = [task] response = client.get("/worker/task") assert response.json() == {"task_id": f"{task_id}"} -def test_get_active_task_none(mock_runner: Mock, client: TestClient): - mock_runner.run.return_value = None +def test_get_active_task_none(mock_runner_data: Mock, client: TestClient): + mock_runner_data.tasks = [] response = client.get("/worker/task") assert response.json() == {"task_id": None} -def test_get_state(mock_runner: Mock, client: TestClient): +def test_get_state(mock_runner_data: Mock, client: TestClient): state = WorkerState.SUSPENDING - mock_runner.run.return_value = state + mock_runner_data.state = state response = client.get("/worker/state") assert response.json() == state -def test_set_state_running_to_paused(mock_runner: Mock, client: TestClient): +def test_set_state_running_to_paused(mock_runner_data: Mock, client: TestClient): current_state = WorkerState.RUNNING final_state = WorkerState.PAUSED - mock_runner.run.side_effect = [ - current_state, + type(mock_runner_data).state = PropertyMock( + side_effect=[current_state, final_state] + ) + mock_runner_data.tasks = [ TrackableTask(task_id="foobar", task=Task(name="foo")), - None, - final_state, ] response = client.put( "/worker/state", json=StateChangeRequest(new_state=final_state).model_dump() ) - mock_runner.run.assert_any_call(pause_worker, False) + mock_runner_data.pause_worker.assert_called_once_with(False) assert response.status_code == status.HTTP_202_ACCEPTED assert response.json() == final_state @@ -829,7 +915,7 @@ def test_set_state_invalid_transition(mock_runner: Mock, client: TestClient): @pytest.mark.parametrize("admin,status", [(True, 202), (False, 403)]) def test_set_state_of_other_users_task( - mock_runner: Mock, + mock_runner_data: Mock, client_with_opa: TestClient, mock_opa_client: Mock, access_token: str, @@ -838,13 +924,15 @@ def test_set_state_of_other_users_task( ): mock_opa_client.admin.return_value = admin - mock_runner.run.side_effect = lambda mth, *a, **kw: { - interface.get_active_task: TrackableTask( - task_id="foo", task=Task(name="bar", metadata={"user": "jd2"}) + mock_runner_data.tasks = [ + TrackableTask( + task_id="foo", + is_pending=False, + task=Task(name="bar", metadata={"user": "jd2"}), ), - interface.get_worker_state: WorkerState.RUNNING, - interface.cancel_active_task: WorkerState.ABORTING, - }[mth] + ] + mock_runner_data.state = WorkerState.RUNNING + mock_runner_data.cancel_active_task.return_value = WorkerState.ABORTING client_with_opa.headers["Authorization"] = f"Bearer {access_token}" @@ -853,6 +941,8 @@ def test_set_state_of_other_users_task( json=StateChangeRequest(new_state=WorkerState.ABORTING).model_dump(), ) + if not admin: + mock_runner_data.cancel_active_task.assert_not_called() assert resp.status_code == status @@ -895,36 +985,37 @@ def test_subprocess_enabled_by_default(mp_pool_mock: MagicMock): main.teardown_runner() -def test_get_without_authentication(mock_runner: Mock, client: TestClient) -> None: - mock_runner.run.side_effect = jwt.PyJWTError - response = client.get("/devices/my-device") +def test_get_without_authentication(mock_runner: Mock, client_with_auth: TestClient): + del client_with_auth.headers["Authorization"] + response = client_with_auth.get("/devices/my-device") + mock_runner.run.assert_not_called() assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.json() == {"detail": "Not authenticated"} def test_oidc_config_not_found_when_auth_is_disabled( - mock_runner: Mock, client: TestClient + mock_runner_data: Mock, client: TestClient ): - mock_runner.run.return_value = None + mock_runner_data.oidc_config = None response = client.get("/config/oidc") assert response.status_code == status.HTTP_204_NO_CONTENT assert response.text == "" def test_get_oidc_config( - mock_runner: Mock, + mock_runner_data: Mock, oidc_config: OIDCConfig, mock_authn_server, client_with_auth: TestClient, ): - mock_runner.run.return_value = oidc_config + mock_runner_data.oidc_config = oidc_config response = client_with_auth.get("/config/oidc") assert response.status_code == status.HTTP_200_OK assert response.json() == oidc_config.model_dump() -def test_get_python_environment(mock_runner: Mock, client: TestClient): +def test_get_python_environment(mock_runner_data: Mock, client: TestClient): packages = PythonEnvironmentResponse( installed_packages=[ PackageInfo( @@ -936,7 +1027,7 @@ def test_get_python_environment(mock_runner: Mock, client: TestClient): ) ] ) - mock_runner.run.return_value = packages + mock_runner_data.python_environment = packages response = client.get("/python_environment") assert response.status_code == status.HTTP_200_OK assert response.json() == packages.model_dump() @@ -950,13 +1041,13 @@ def test_health_probe(client: TestClient): def test_logout( - mock_runner: Mock, + mock_runner_data: Mock, mock_authn_server, oidc_config: OIDCConfig, client_with_auth: TestClient, ): oidc_config.logout_redirect_endpoint = "/oauth2/sign_out/" - mock_runner.run.return_value = oidc_config + mock_runner_data.oidc_config = oidc_config client_with_auth.follow_redirects = False response = client_with_auth.get("/logout") assert response.status_code == status.HTTP_308_PERMANENT_REDIRECT @@ -979,16 +1070,16 @@ def test_docs_redirect( @pytest.mark.parametrize("has_oidc_config", [True, False]) def test_logout_when_oidc_config_invalid( has_oidc_config: bool, - mock_runner: Mock, + mock_runner_data: Mock, oidc_config: OIDCConfig, mock_authn_server, client_with_auth: TestClient, ): if has_oidc_config: oidc_config.logout_redirect_endpoint = "" - mock_runner.run.return_value = oidc_config + mock_runner_data.oidc_config = oidc_config else: - mock_runner.run.return_value = None + mock_runner_data.oidc_config = None response = client_with_auth.get("/logout") assert response.status_code == status.HTTP_205_RESET_CONTENT