from unittest.mock import AsyncMock, MagicMock import grpc import pytest from ingestor.adapters.grpc_server import GrpcServer, ReadTagServicer from ingestor.proto.opc_gateway_pb2 import ReadTagRequest def make_request(server_id='srv1', addr='ns=2;i=5'): return ReadTagRequest(server_id=server_id, addr=addr, data_type='float') @pytest.mark.asyncio async def test_read_tag_maps_full_result_onto_response(): active_read = AsyncMock() active_read.read_now.return_value = { 'value': 1.5, 'opc_status_code': 'Good', 'source_timestamp': '2026-07-15T10:00:00-03:00', 'server_timestamp': '2026-07-15T10:00:01-03:00', 'persisted': False, } servicer = ReadTagServicer(active_read, MagicMock()) response = await servicer.ReadTag(make_request(), AsyncMock()) assert response.value == '1.5' assert response.opc_status_code == 'Good' assert response.source_timestamp == '2026-07-15T10:00:00-03:00' assert response.server_timestamp == '2026-07-15T10:00:01-03:00' assert response.persisted is False @pytest.mark.asyncio async def test_read_tag_none_timestamps_become_empty_string(): active_read = AsyncMock() active_read.read_now.return_value = { 'value': 1.5, 'opc_status_code': None, 'source_timestamp': None, 'server_timestamp': None, 'persisted': False, } servicer = ReadTagServicer(active_read, MagicMock()) response = await servicer.ReadTag(make_request(), AsyncMock()) assert response.opc_status_code == '' assert response.source_timestamp == '' assert response.server_timestamp == '' @pytest.mark.asyncio async def test_read_tag_aborts_with_internal_on_engine_failure(): active_read = AsyncMock() active_read.read_now.side_effect = RuntimeError('boom') logger = MagicMock() servicer = ReadTagServicer(active_read, logger) context = AsyncMock() context.abort.side_effect = grpc.RpcError('aborted') request = make_request() with pytest.raises(grpc.RpcError): await servicer.ReadTag(request, context) context.abort.assert_awaited_once() args = context.abort.call_args.args assert args[0] == grpc.StatusCode.INTERNAL logger.error.assert_called_once() @pytest.mark.asyncio async def test_read_tag_uses_slot_lookup_names_when_provided(): active_read = AsyncMock() active_read.read_now.return_value = { 'value': 1, 'opc_status_code': 'Good', 'source_timestamp': None, 'server_timestamp': None, 'persisted': True, } names = [{'collection': 'c1', 'name': 't1'}] slot_lookup = MagicMock(return_value=names) servicer = ReadTagServicer(active_read, MagicMock(), slot_lookup=slot_lookup) await servicer.ReadTag(make_request(server_id='srv1', addr='ns=2;i=5'), AsyncMock()) slot_lookup.assert_called_once_with('srv1', 'ns=2;i=5') active_read.read_now.assert_awaited_once_with('srv1', 'ns=2;i=5', names=names) @pytest.mark.asyncio async def test_grpc_server_stop_before_start_does_not_raise(): server = GrpcServer(active_read=AsyncMock(), logger=MagicMock()) await server.stop() # no raise