1717from uuid import uuid4
1818
1919import pytest
20+ import socketio
2021import sqlalchemy as sa
2122from aiohttp .test_utils import TestClient
2223from aioresponses import aioresponses
2930 FileMetaDataGet ,
3031 PresignedLink ,
3132)
33+ from models_library .api_schemas_webserver .projects_nodes import NodeGetIdle
3234from models_library .generics import Envelope
35+ from models_library .projects_nodes import Node , NodeShareStatus
3336from models_library .projects_nodes_io import NodeID
3437from models_library .services_resources import (
3538 DEFAULT_SINGLE_SERVICE_NAME ,
3841)
3942from models_library .utils .fastapi_encoders import jsonable_encoder
4043from pydantic import NonNegativeFloat , NonNegativeInt , TypeAdapter
44+ from pytest_mock import MockerFixture
4145from pytest_simcore .helpers .assert_checks import assert_status
4246from pytest_simcore .helpers .monkeypatch_envs import setenvs_from_dict
4347from pytest_simcore .helpers .webserver_parametrizations import (
5458 _ProjectNodePreview ,
5559)
5660from simcore_service_webserver .projects .models import ProjectDict
61+ from simcore_service_webserver .socketio .messages import SOCKET_IO_NODE_UPDATED_EVENT
5762from tenacity import (
5863 AsyncRetrying ,
5964 RetryError ,
@@ -412,7 +417,9 @@ class _RunningServices:
412417 running_services_uuids : list [str ] = field (default_factory = list )
413418
414419 def num_services (
415- self , * args , ** kwargs # noqa: ARG002
420+ self ,
421+ * args ,
422+ ** kwargs , # noqa: ARG002
416423 ) -> list [DynamicServiceGet ]:
417424 return [
418425 DynamicServiceGet .model_validate (
@@ -512,7 +519,7 @@ async def test_create_node_does_not_start_dynamic_node_if_there_are_already_too_
512519 "service_version" : faker .numerify ("%.#.#" ),
513520 "service_id" : None ,
514521 }
515- response = await client .post (f"{ url } " , json = body )
522+ response = await client .post (f"{ url } " , json = body )
516523 await assert_status (response , expected .created )
517524 mocked_dynamic_services_interface [
518525 "dynamic_scheduler.api.run_dynamic_service"
@@ -543,7 +550,9 @@ class _RunninServices:
543550 running_services_uuids : list [str ] = field (default_factory = list )
544551
545552 async def num_services (
546- self , * args , ** kwargs # noqa: ARG002
553+ self ,
554+ * args ,
555+ ** kwargs , # noqa: ARG002
547556 ) -> list [dict [str , Any ]]:
548557 return [
549558 {"service_uuid" : service_uuid }
@@ -632,7 +641,7 @@ async def test_create_node_does_start_dynamic_node_if_max_num_set_to_0(
632641 "service_version" : faker .numerify ("%.#.#" ),
633642 "service_id" : None ,
634643 }
635- response = await client .post (f"{ url } " , json = body )
644+ response = await client .post (f"{ url } " , json = body )
636645 await assert_status (response , expected .created )
637646 mocked_dynamic_services_interface [
638647 "dynamic_scheduler.api.run_dynamic_service"
@@ -765,6 +774,21 @@ async def test_delete_node(
765774 assert node_id not in workbench
766775
767776
777+ @pytest .fixture
778+ async def socket_io_node_updated_mock (
779+ mocker : MockerFixture ,
780+ client : TestClient ,
781+ logged_user ,
782+ create_socketio_connection : Callable [
783+ [str | None , TestClient | None ], Awaitable [tuple [socketio .AsyncClient , str ]]
784+ ],
785+ ) -> mock .Mock :
786+ socket_io_conn , _ = await create_socketio_connection (None , client )
787+ mock_node_updated_handler = mocker .MagicMock ()
788+ socket_io_conn .on (SOCKET_IO_NODE_UPDATED_EVENT , handler = mock_node_updated_handler )
789+ return mock_node_updated_handler
790+
791+
768792@pytest .mark .parametrize (* standard_role_response (), ids = str )
769793async def test_start_node (
770794 client : TestClient ,
@@ -783,7 +807,8 @@ async def test_start_node(
783807 all_service_uuids = list (project ["workbench" ])
784808 # start the node, shall work as expected
785809 url = client .app .router ["start_node" ].url_for (
786- project_id = project ["uuid" ], node_id = choice (all_service_uuids ) # noqa: S311
810+ project_id = project ["uuid" ],
811+ node_id = choice (all_service_uuids ), # noqa: S311
787812 )
788813 response = await client .post (f"{ url } " )
789814 data , error = await assert_status (
@@ -804,6 +829,97 @@ async def test_start_node(
804829 ].assert_not_called ()
805830
806831
832+ @pytest .mark .parametrize (* standard_user_role ())
833+ async def test_start_stop_node_sends_node_updated_socketio_event (
834+ client : TestClient ,
835+ logged_user : dict [str , Any ],
836+ socket_io_node_updated_mock : mock .Mock ,
837+ user_project_with_num_dynamic_services : Callable [[int ], Awaitable [ProjectDict ]],
838+ expected : ExpectedResponse ,
839+ mocked_dynamic_services_interface : dict [str , mock .MagicMock ],
840+ mock_catalog_api : dict [str , mock .Mock ],
841+ faker : Faker ,
842+ max_amount_of_auto_started_dyn_services : int ,
843+ mocker : MockerFixture ,
844+ ):
845+ assert client .app
846+ project = await user_project_with_num_dynamic_services (
847+ max_amount_of_auto_started_dyn_services or faker .pyint (min_value = 3 )
848+ )
849+ all_service_uuids = list (project ["workbench" ])
850+ # start the node, shall work as expected
851+ chosen_node_id = choice (all_service_uuids ) # noqa: S311
852+ url = client .app .router ["start_node" ].url_for (
853+ project_id = project ["uuid" ], node_id = chosen_node_id
854+ )
855+
856+ # simulate that the dynamic service is running
857+ mocked_dynamic_services_interface [
858+ "dynamic_scheduler.api.get_dynamic_service"
859+ ].return_value = DynamicServiceGet .model_validate (
860+ DynamicServiceGet .model_json_schema ()["examples" ][0 ]
861+ )
862+
863+ response = await client .post (f"{ url } " )
864+ await assert_status (response , expected .no_content )
865+ mocked_dynamic_services_interface [
866+ "dynamic_scheduler.api.run_dynamic_service"
867+ ].assert_called_once ()
868+
869+ socket_io_node_updated_mock .assert_called_once ()
870+ message = socket_io_node_updated_mock .call_args [0 ][0 ]
871+ assert "data" in message
872+ assert "project_id" in message
873+ assert "node_id" in message
874+ assert message ["project_id" ] == project ["uuid" ]
875+ assert message ["node_id" ] == chosen_node_id
876+ received_node = Node .model_validate (message ["data" ])
877+ assert received_node .state
878+ assert received_node .state .lock_state
879+ assert received_node .state .lock_state .locked is True
880+ assert received_node .state .lock_state .current_user_groupids == [
881+ logged_user ["primary_gid" ]
882+ ]
883+ assert received_node .state .lock_state .status is NodeShareStatus .OPENED
884+ socket_io_node_updated_mock .reset_mock ()
885+
886+ # now stop the node
887+ url = client .app .router ["stop_node" ].url_for (
888+ project_id = project ["uuid" ], node_id = chosen_node_id
889+ )
890+ # simulate that the dynamic service is idle
891+ mocked_dynamic_services_interface [
892+ "dynamic_scheduler.api.get_dynamic_service"
893+ ].return_value = NodeGetIdle .model_validate (
894+ NodeGetIdle .model_json_schema ()["examples" ][0 ]
895+ )
896+ response = await client .post (f"{ url } " )
897+ await assert_status (response , expected .accepted )
898+ async for attempt in AsyncRetrying (
899+ retry = retry_if_exception_type (AssertionError ),
900+ stop = stop_after_delay (5 ),
901+ wait = wait_fixed (0.1 ),
902+ reraise = True ,
903+ ):
904+ with attempt :
905+ mocked_dynamic_services_interface [
906+ "dynamic_scheduler.api.stop_dynamic_service"
907+ ].assert_called_once ()
908+ socket_io_node_updated_mock .assert_called_once ()
909+ message = socket_io_node_updated_mock .call_args [0 ][0 ]
910+ assert "data" in message
911+ assert "project_id" in message
912+ assert "node_id" in message
913+ assert message ["project_id" ] == project ["uuid" ]
914+ assert message ["node_id" ] == chosen_node_id
915+ received_node = Node .model_validate (message ["data" ])
916+ assert received_node .state
917+ assert received_node .state .lock_state
918+ assert received_node .state .lock_state .locked is False
919+ assert received_node .state .lock_state .current_user_groupids is None
920+ assert received_node .state .lock_state .status is None
921+
922+
807923@pytest .mark .parametrize (* standard_user_role ())
808924async def test_start_node_raises_if_dynamic_services_limit_attained (
809925 client : TestClient ,
@@ -827,7 +943,8 @@ async def test_start_node_raises_if_dynamic_services_limit_attained(
827943 ]
828944 # start the node, shall work as expected
829945 url = client .app .router ["start_node" ].url_for (
830- project_id = project ["uuid" ], node_id = choice (all_service_uuids ) # noqa: S311
946+ project_id = project ["uuid" ],
947+ node_id = choice (all_service_uuids ), # noqa: S311
831948 )
832949 response = await client .post (f"{ url } " )
833950 data , error = await assert_status (
@@ -862,7 +979,8 @@ async def test_start_node_starts_dynamic_service_if_max_number_of_services_set_t
862979 ]
863980 # start the node, shall work as expected
864981 url = client .app .router ["start_node" ].url_for (
865- project_id = project ["uuid" ], node_id = choice (all_service_uuids ) # noqa: S311
982+ project_id = project ["uuid" ],
983+ node_id = choice (all_service_uuids ), # noqa: S311
866984 )
867985 response = await client .post (f"{ url } " )
868986 data , error = await assert_status (
@@ -895,7 +1013,8 @@ async def test_start_node_raises_if_called_with_wrong_data(
8951013
8961014 # start the node, with wrong project
8971015 url = client .app .router ["start_node" ].url_for (
898- project_id = faker .uuid4 (), node_id = choice (all_service_uuids ) # noqa: S311
1016+ project_id = faker .uuid4 (),
1017+ node_id = choice (all_service_uuids ), # noqa: S311
8991018 )
9001019 response = await client .post (f"{ url } " )
9011020 data , error = await assert_status (
@@ -943,7 +1062,8 @@ async def test_stop_node(
9431062 all_service_uuids = list (project ["workbench" ])
9441063 # start the node, shall work as expected
9451064 url = client .app .router ["stop_node" ].url_for (
946- project_id = project ["uuid" ], node_id = choice (all_service_uuids ) # noqa: S311
1065+ project_id = project ["uuid" ],
1066+ node_id = choice (all_service_uuids ), # noqa: S311
9471067 )
9481068 response = await client .post (f"{ url } " )
9491069 _ , error = await assert_status (
0 commit comments