"""E2E scenarios — 6. Client certificates and OPC security (MinIO + opc_certificates). See ``e2e/scenarios.md``.""" import pytest import pytest_asyncio from e2e.helpers import ( insert_prediction, load_scenario_input, notifications, readings, stays_false, wait_until, write_status, ) pytestmark = [pytest.mark.integration, pytest.mark.asyncio] TIMESTAMP = '2026-10-13T12:00:00-03:00' SECURED = {'mode': 'SignAndEncrypt', 'policy': 'Basic256Sha256'} @pytest_asyncio.fixture async def secured_server(opc_servers): """plant-s: only Basic256Sha256 SignAndEncrypt endpoints, accepts any client certificate.""" return await opc_servers('plant-s', {'Temperature': 20.0, 'Setpoint': 0.0}, secured=True) def secured_slot(server, certificate, **security): slot = load_scenario_input('slot_secured.json', {'plant-s': server}) slot['plant-s']['certificate'] = certificate slot['plant-s']['security'] = {**SECURED, **security} return slot def secured_connector(server, certificate_id): return { 'connector_id': '7', 'type': 'opc_ua', 'name': 'plant-s', 'url': server.url, 'status': 'Active', 'subscription_period_ms': 100, 'security': {**SECURED, 'client_certificate_id': certificate_id}, } def write_command(): return load_scenario_input( 'write_command.json', server_id='7', model_id='2', timestamp=TIMESTAMP ) # 6.1 Secured connections --------------------------------------------------------------- async def test_scenario_6_1_1_secured_read_with_certificate_from_minio( secured_server, client_certificate, pods, put_slot, mongo_db, cert_cache_dir ): certificate = await client_certificate(7) put_slot(1, secured_slot(secured_server, certificate)) pod = await pods('pod-a') await pod.tick() await secured_server.set('Temperature', 21.0) await wait_until(lambda: readings(mongo_db, 'raw_secured', value=21.0)) assert sorted(p.name for p in (cert_cache_dir / '7').iterdir()) == ['cert.der', 'key.pem'] assert notifications(mongo_db, 'OPC_CONNECTION_ERROR') == [] async def test_scenario_6_1_2_secured_write_resolved_from_connectors_and_certificates( secured_server, client_certificate, pods, mongo_db, postgres_engine, publish_command ): await client_certificate(7) mongo_db['connectors'].insert_one(secured_connector(secured_server, 7)) insert_prediction(postgres_engine, 2, TIMESTAMP, 'pending') await pods('pod-a') await publish_command(write_command()) await wait_until(lambda: write_status(postgres_engine, 2, TIMESTAMP) == 'ok') assert (await secured_server.read('Setpoint')).Value.Value == 1.25 # 6.2 Failures -------------------------------------------------------------------------- async def test_scenario_6_2_1_certificate_missing_in_minio( secured_server, client_certificate, pods, put_slot, mongo_db, cert_cache_dir ): certificate = await client_certificate(7, upload=False) put_slot(1, secured_slot(secured_server, certificate)) pod = await pods('pod-a') await pod.tick() assert notifications(mongo_db, 'OPC_CONNECTION_ERROR_plant-s') assert pod.manager.opc_managers == {} assert not list((cert_cache_dir / '7').glob('*.der')) assert await stays_false(lambda: readings(mongo_db, 'raw_secured')) async def test_scenario_6_2_2_unsupported_security_policy( secured_server, client_certificate, pods, put_slot, mongo_db ): certificate = await client_certificate(7) put_slot(1, secured_slot(secured_server, certificate, policy='Basic999')) pod = await pods('pod-a') await pod.tick() [error] = notifications(mongo_db, 'OPC_CONNECTION_ERROR_plant-s') assert 'Unsupported OPC security' in error['message'] assert pod.manager.opc_managers == {} async def test_scenario_6_2_3_no_certificate_cannot_reach_a_secured_only_server( secured_server, pods, put_slot, mongo_db ): put_slot(1, secured_slot(secured_server, None)) pod = await pods('pod-a') await pod.tick() assert notifications(mongo_db, 'OPC_CONNECTION_ERROR_plant-s') assert await stays_false(lambda: readings(mongo_db, 'raw_secured')) async def test_scenario_6_2_4_inactive_certificate_write_fails( secured_server, client_certificate, pods, mongo_db, postgres_engine, publish_command ): await client_certificate(7, status='revoked') mongo_db['connectors'].insert_one(secured_connector(secured_server, 7)) insert_prediction(postgres_engine, 2, TIMESTAMP, 'pending') await pods('pod-a') await publish_command(write_command()) await wait_until(lambda: write_status(postgres_engine, 2, TIMESTAMP) == 'error') assert secured_server.writes == [] # 6.3 Rotation -------------------------------------------------------------------------- async def test_scenario_6_3_1_certificate_rotation_reconnects_with_the_new_one( secured_server, client_certificate, pods, put_slot, mongo_db, cert_cache_dir ): first = await client_certificate(7) put_slot(1, secured_slot(secured_server, first)) pod = await pods('pod-a') await pod.tick() await wait_until(lambda: readings(mongo_db, 'raw_secured')) first_client = pod.manager.pool._connections['7'][0] second = await client_certificate(8) put_slot(1, secured_slot(secured_server, second)) await pod.tick() assert pod.manager.pool._connections['7'][0] is not first_client assert pod.manager.pool.config_for('7')['certificate']['certificate_id'] == 8 assert (cert_cache_dir / '8' / 'cert.der').is_file() await secured_server.set('Temperature', 22.0) await wait_until(lambda: readings(mongo_db, 'raw_secured', value=22.0)) await wait_until(lambda: secured_server.session_count() == 1)