Files
sientia-dataops-opc-ingestor/tests/unit/connection/test_certs.py
vitor-aignosi 97bf2fd748
Some checks failed
Quality gate / quality-gate (push) Has been cancelled
SIENTIAPDE-1973
SIENTIAPDE-1973: Refactor unit tests for improved readability, primarily by splitting chained assertions into individual statements and minor fixture import adjustments.
2026-10-29 13:33:25 -03:00

162 lines
5.8 KiB
Python

import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest
from ingestor.connection.certs import CertificateNotConfiguredError, CertProvider, LazyMinio
def certificate(certificate_id=1, name='sientia-platform', ext='pem', bucket='opc-certificates'):
"""A client certificate as the slot v2 carries it."""
return {
'certificate_id': certificate_id,
'application_uri': 'urn:sientia:opc-ingestor',
'cert': {'bucket': bucket, 'object_name': f'client/{name}/cert.{ext}'},
'private_key': {'bucket': bucket, 'object_name': f'client/{name}/key.{ext}'},
}
def _provider(tmp_path, data=b'pem'):
minio = MagicMock()
minio.download_file = AsyncMock(return_value=data)
return CertProvider(minio=minio, cache_dir=str(tmp_path)), minio
@pytest.mark.asyncio
async def test_fetch_downloads_the_objects_the_certificate_names(tmp_path):
minio = AsyncMock()
minio.download_file.side_effect = [b'cert-bytes', b'key-bytes']
provider = CertProvider(minio, cache_dir=str(tmp_path))
cert, key = await provider.fetch(certificate(7))
assert (cert, key) == (str(tmp_path / '7' / 'cert.pem'), str(tmp_path / '7' / 'key.pem'))
assert [c.args for c in minio.download_file.await_args_list] == [
('client/sientia-platform/cert.pem', 'opc-certificates', {}),
('client/sientia-platform/key.pem', 'opc-certificates', {}),
]
assert (tmp_path / '7' / 'cert.pem').read_bytes() == b'cert-bytes'
assert (tmp_path / '7' / 'key.pem').read_bytes() == b'key-bytes'
@pytest.mark.asyncio
async def test_local_files_keep_the_object_extension(tmp_path):
"""asyncua picks the format (.pem / .der) by the extension"""
provider, _minio = _provider(tmp_path)
cert, key = await provider.fetch(certificate(ext='der'))
assert cert.endswith('cert.der')
assert key.endswith('key.der')
@pytest.mark.asyncio
@pytest.mark.parametrize(
'broken',
[
pytest.param({'cert': None}, id='no-cert'),
pytest.param({'private_key': {'bucket': 'b'}}, id='key-without-object'),
pytest.param({'certificate_id': None}, id='no-id'),
pytest.param({'certificate_id': '../etc'}, id='id-not-an-integer'),
],
)
async def test_fetch_of_an_incomplete_certificate_raises_clear_error(tmp_path, broken):
provider, minio = _provider(tmp_path)
incomplete = {**certificate(), **broken}
with pytest.raises(CertificateNotConfiguredError, match='opc_certificates|certificate_id'):
await provider.fetch(incomplete)
minio.download_file.assert_not_awaited()
@pytest.mark.asyncio
async def test_fetch_is_cached_per_certificate_until_invalidated(tmp_path):
provider, minio = _provider(tmp_path)
first = await provider.fetch(certificate(1))
assert await provider.fetch(certificate(1)) == first
assert minio.download_file.await_count == 2 # cert + key, once
other = await provider.fetch(certificate(2, name='other'))
assert other != first
assert minio.download_file.await_count == 4
provider.invalidate(1)
await provider.fetch(certificate(1))
await provider.fetch(certificate(2, name='other')) # still cached
assert minio.download_file.await_count == 6
@pytest.mark.asyncio
async def test_a_replaced_certificate_is_downloaded_again(tmp_path):
"""Same certificate_id, other objects in the slot: the cache does not serve the old files"""
provider, minio = _provider(tmp_path)
await provider.fetch(certificate(1, name='old'))
await provider.fetch(certificate(1, name='new'))
assert minio.download_file.await_count == 4
assert minio.download_file.await_args_list[-1].args[0] == 'client/new/key.pem'
@pytest.mark.asyncio
async def test_concurrent_fetches_download_once(tmp_path):
provider, minio = _provider(tmp_path)
results = await asyncio.gather(*(provider.fetch(certificate()) for _ in range(5)))
assert len(set(results)) == 1
assert minio.download_file.await_count == 2
@pytest.mark.asyncio
async def test_download_replaces_file_atomically(tmp_path):
provider, _minio = _provider(tmp_path, data=b'new')
(tmp_path / '1').mkdir()
(tmp_path / '1' / 'cert.pem').write_bytes(b'old')
cert_path, _ = await provider.fetch(certificate(1))
assert (tmp_path / '1' / 'cert.pem').read_bytes() == b'new'
assert cert_path == str(tmp_path / '1' / 'cert.pem')
assert not list(tmp_path.glob('**/*.tmp'))
@pytest.mark.asyncio
async def test_lazy_minio_builds_one_client_per_bucket_on_first_download():
clients = {'a': AsyncMock(), 'b': AsyncMock()}
for client in clients.values():
client.download_file.return_value = b'bytes'
factory = MagicMock(side_effect=lambda bucket: clients[bucket])
minio = LazyMinio(factory)
factory.assert_not_called() # no network at construction
assert await minio.download_file('x.pem', 'a', {}) == b'bytes'
await minio.download_file('y.pem', 'a', {})
await minio.download_file('z.pem', 'b', {})
assert [c.args for c in factory.call_args_list] == [('a',), ('b',)]
assert clients['a'].download_file.await_count == 2
clients['b'].download_file.assert_awaited_once_with('z.pem', 'b', {})
def test_build_cert_provider_is_lazy_and_uses_config(monkeypatch):
from ingestor.config import GatewayConfig
from ingestor.connection import certs
minio_cls = MagicMock()
monkeypatch.setattr(certs, 'MinioRepository', minio_cls)
provider = certs.build_cert_provider(
config=GatewayConfig(cert_cache_dir='certs-cache'),
logger=MagicMock(),
notification_handler=MagicMock(),
metrics_controller=MagicMock(),
)
assert provider.cache_dir == 'certs-cache'
assert isinstance(provider.minio, certs.LazyMinio)
minio_cls.assert_not_called() # MinIO is only reached when a cert is needed
provider.minio._factory('opc-certificates')
assert minio_cls.call_args.kwargs['bucket'] == 'opc-certificates'