Some checks failed
Quality gate / quality-gate (push) Has been cancelled
SIENTIAPDE-1973: Refactor unit tests for improved readability, primarily by splitting chained assertions into individual statements and minor fixture import adjustments.
162 lines
5.8 KiB
Python
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'
|