Files
vitor-aignosi ecb055eefb SIENTIAPDE-1973
SIENTIAPDE-1973: Implement per-connector OPC client certificates and remove legacy read engine.
This update overhauls certificate management, transitioning from a single platform certificate to individual client certificates defined in the slot configuration. The CertProvider now downloads certificates from MinIO based on certificate_id, applying dynamic security policies (mode, policy) from the slot. The legacy READ_ENGINE=legacy option and OpcManager have been entirely removed, standardizing on the ServerReader with strict slot v2 enforcement. Write command resolution (ServerConfigResolver) now consistently uses connectors joined with opc_certificates. The CERT_BUCKET environment variable is no longer used.
2026-10-29 12:39:21 -03:00

132 lines
4.0 KiB
Python

import pytest
from ingestor.io.slots import CONNECTION_FIELDS, parse_slot
SECURITY = {'mode': 'SignAndEncrypt', 'policy': 'Basic256Sha256'}
CERTIFICATE = {
'certificate_id': 1,
'application_uri': 'urn:sientia:opc-ingestor',
'cert': {'bucket': 'opc-certificates', 'object_name': 'client/p/cert.pem'},
'private_key': {'bucket': 'opc-certificates', 'object_name': 'client/p/key.pem'},
}
NAME = {'collection': 'c', 'tag_name': 't', 'model_name': 'm', 'model_id': '1'}
RAW = {
'server1': {
'server_id': '1',
'name': 'server1',
'url': 'opc.tcp://h:4840',
'subscription_period_ms': 1000,
'security': SECURITY,
'certificate': CERTIFICATE,
'tags': {
'ns=2;i=2': {
'frequency': '15000',
'active_read': {'enabled': True, 'cycle_multiple': 3},
'names': [
{
'collection': 'raw_x',
'tag_name': 'Counter',
'model_name': 'm1',
'model_id': '1',
},
{
'collection': 'raw_y',
'tag_name': 'Contador',
'model_name': 'm2',
'model_id': '2',
},
],
}
},
}
}
def _server(**fields):
server = {
'server_id': '1',
'url': 'u',
'security': {'mode': 'None', 'policy': 'None'},
'certificate': None,
'tags': {'ns=2;i=1': {'frequency': '1000', 'names': [NAME]}},
}
return {'s': {**server, **fields}}
def test_parse_slot_multi_name():
slot = parse_slot(RAW)
tag = slot['server1']['tags']['ns=2;i=2']
assert len(tag['names']) == 2
assert tag['active_read']['cycle_multiple'] == 3
def test_parse_slot_keeps_security_and_certificate():
server = parse_slot(RAW)['server1']
assert server['security'] == SECURITY
assert server['certificate'] == CERTIFICATE
assert {field: server[field] for field in CONNECTION_FIELDS} == {
'url': 'opc.tcp://h:4840',
'subscription_period_ms': 1000,
'security': SECURITY,
'certificate': CERTIFICATE,
}
def test_parse_slot_without_certificate_key_means_no_certificate():
raw = _server()
del raw['s']['certificate']
server = parse_slot(raw)['s']
assert server['certificate'] is None
assert server['name'] == 's' # falls back to the slot key
def test_parse_slot_defaults_active_read_disabled():
tag = parse_slot(_server())['s']['tags']['ns=2;i=1']
assert tag['active_read'] == {'enabled': False}
@pytest.mark.parametrize(
'raw',
[
pytest.param(_server(tags={'ns=2;i=1': {'frequency': '1000'}}), id='tag-without-names'),
pytest.param(
_server(tags={'ns=2;i=1': {'frequency': '1', 'names': [{'collection': 'c'}]}}),
id='name-without-tag-name',
),
pytest.param(
_server(
tags={
'ns=2;i=1': {
'frequency': '1',
'names': [{'collection': 'c', 'tag_name': 't'}],
}
}
),
id='name-without-model-id',
),
pytest.param(_server(security=None), id='no-security'),
# Slot v1 (an Orchestrator before SIENTIAPDE-1974): no longer accepted
pytest.param(
{
'kepserver': {
'server_id': '1',
'url': 'opc.tcp://kepserver:49320',
'server_uri': 'urn:sientia:opc-ingestor',
'cert_path': '/app/certs/opc-ingestor-cert.pem',
'tags': {
'ns=2;s=A': {'tag_name': 'A', 'frequency': 30000, 'topics': ['raw_x']}
},
}
},
id='slot-v1',
),
],
)
def test_parse_slot_rejects_invalid_slots(raw):
with pytest.raises((KeyError, ValueError)):
parse_slot(raw)