Files
production-analytics/tests/test_postgres_material.py
T

129 lines
5.6 KiB
Python

import io
import sys
from datetime import UTC, datetime
from unittest.mock import MagicMock, Mock, patch
import pytest
from production_analytics.calculations.material_consumption import MaterialIntegrationState
from production_analytics.enlyze.exploration import ConfigurationError
from production_analytics.enlyze.gateway import EnlyzeProductionRun
from production_analytics.service.material_polling import MaterialPollResult
from production_analytics.service.material_runner import MaterialPollingRunner
from production_analytics.service.postgres_material import (
PostgresMaterialSnapshotWriter,
PostgresSettings,
)
ENV = dict(POSTGRES_HOST='localhost', POSTGRES_PORT='5432', POSTGRES_DB='analytics',
POSTGRES_USER='writer', POSTGRES_PASSWORD='secret-password')
NOW = datetime(2026, 1, 1, tzinfo=UTC)
@pytest.mark.parametrize('key', ENV)
@pytest.mark.parametrize('value', [None, '', ' ', '\x00'])
def test_required_settings(key, value):
environment = dict(ENV)
if value is None:
del environment[key]
else:
environment[key] = value
with pytest.raises(ConfigurationError, match=key) as error:
PostgresSettings.from_environment(environment)
assert 'secret-password' not in str(error.value)
@pytest.mark.parametrize('port', ['0', '-1', '65536', '1.5', 'secret-password'])
def test_invalid_port(port):
with pytest.raises(ConfigurationError, match='POSTGRES_PORT') as error:
PostgresSettings.from_environment(dict(ENV, POSTGRES_PORT=port))
assert str(error.value) == 'POSTGRES_PORT must be an integer from 1 to 65535'
def test_valid_settings_and_password_repr():
settings = PostgresSettings.from_environment(ENV)
assert (settings.host, settings.port, settings.dbname, settings.user) == (
'localhost', 5432, 'analytics', 'writer',
)
assert settings.password == 'secret-password'
assert settings.password not in repr(settings)
def test_parameterized_insert_and_connection_lifecycle():
driver = MagicMock()
connection = driver.connect.return_value.__enter__.return_value
writer = PostgresMaterialSnapshotWriter(PostgresSettings.from_environment(ENV))
fields = dict(timestamp=NOW, calculation_id='calc', machine_id='machine',
production_order=" 00842'; DROP TABLE x; -- ", run_id='run', consumption_kg=12.5)
with patch.dict(sys.modules, psycopg=driver):
writer.write(**fields)
sql, parameters = connection.execute.call_args.args
assert sql.count('%s') == 6
assert fields['production_order'] not in sql
assert parameters == tuple(fields.values())
assert 'ON CONFLICT' in sql
connection.execute.assert_called_once()
driver.connect.assert_called_once_with(
host='localhost', port=5432, dbname='analytics', user='writer',
password='secret-password', connect_timeout=10, options='-c statement_timeout=10000',
)
driver.connect.return_value.__exit__.assert_called_once_with(None, None, None)
def test_writer_failure_propagates_and_next_write_reconnects():
driver = MagicMock()
driver.connect.return_value.__enter__.return_value.execute.side_effect = [
RuntimeError('secret'), None,
]
writer = PostgresMaterialSnapshotWriter(PostgresSettings.from_environment(ENV))
fields = dict(timestamp=NOW, calculation_id='calc', machine_id='machine',
production_order='order', run_id='run', consumption_kg=0)
with patch.dict(sys.modules, psycopg=driver):
with pytest.raises(RuntimeError):
writer.write(**fields)
writer.write(**fields)
assert driver.connect.call_count == 2
assert driver.connect.return_value.__exit__.call_count == 2
@pytest.mark.parametrize('consumption', [0, 12.5])
def test_runner_snapshot_fields_and_none(consumption):
result = MaterialPollResult(
EnlyzeProductionRun('run', 'machine', None, ' 00842 ', NOW, None),
MaterialIntegrationState(consumption, 20),
)
writer = Mock()
output = io.StringIO()
MaterialPollingRunner(
Mock(poll_once=Mock(side_effect=[None, result])), machine_id='machine',
calculation_id='configured-calculation', snapshot_writer=writer,
poll_interval_seconds=10, clock=lambda: NOW,
sleep=Mock(side_effect=[None, KeyboardInterrupt]), stdout=output,
).run()
writer.write.assert_called_once_with(
timestamp=NOW, calculation_id='configured-calculation', machine_id='machine',
production_order=' 00842 ', run_id='run', consumption_kg=consumption,
)
assert 'no open Production Run' in output.getvalue()
assert f'consumption_kg={consumption:.9f}' in output.getvalue()
def test_snapshot_failure_reports_safely_and_continues():
result = MaterialPollResult(
EnlyzeProductionRun('run', 'machine', None, 'order', NOW, None),
MaterialIntegrationState(12.5, 20),
)
service = Mock(poll_once=Mock(return_value=result))
writer = Mock(write=Mock(side_effect=[RuntimeError('secret-password arbitrary SQL'), None]))
output, errors = io.StringIO(), io.StringIO()
MaterialPollingRunner(
service, machine_id='machine', calculation_id='calc', snapshot_writer=writer,
poll_interval_seconds=10, clock=lambda: NOW,
sleep=Mock(side_effect=[None, KeyboardInterrupt]), stdout=output, stderr=errors,
).run()
assert service.poll_once.call_count == writer.write.call_count == 2
assert errors.getvalue().count('PostgreSQL snapshot write failed') == 1
assert 'secret-password' not in errors.getvalue()
assert 'arbitrary SQL' not in errors.getvalue()
assert output.getvalue().count('consumption_kg=12.500000000') == 2