def test_write_throws_error_on_bad_params(self): with self.assertRaises(ValueError): log.IntegrationAdaptorsLogger('SYS')._format_and_write( "message", {}, "", logging.INFO) with self.assertRaises(ValueError): log.IntegrationAdaptorsLogger('SYS')._format_and_write( "message", {}, None, logging.INFO)
def test_dictionary_with_non_string_values(self): input_dict = {'EasyKey': False} expected_output = {'EasyKey': 'EasyKey=False'} output = log.IntegrationAdaptorsLogger('SYS')._format_values_in_map( input_dict) self.assertEqual(output, expected_output)
def test_log_levels(self): logger = log.IntegrationAdaptorsLogger('SYS') logger._format_and_write = MagicMock() with self.subTest('INFO'): logger.info('100', '{yes}', {'yes': 'no'}) logger._format_and_write.assert_called_with( '{yes}', {'yes': 'no'}, '100', logging.INFO) with self.subTest('AUDIT'): logger.audit('100', '{yes}', {'yes': 'no'}) logger._format_and_write.assert_called_with( '{yes}', {'yes': 'no'}, '100', log.AUDIT) with self.subTest('WARNING'): logger.warning('100', '{yes}', {'yes': 'no'}) logger._format_and_write.assert_called_with( '{yes}', {'yes': 'no'}, '100', logging.WARNING) with self.subTest('ERROR'): logger.error('100', '{yes}', {'yes': 'no'}) logger._format_and_write.assert_called_with( '{yes}', {'yes': 'no'}, '100', logging.ERROR) with self.subTest('CRITICAL'): logger.critical('100', '{yes}', {'yes': 'no'}) logger._format_and_write.assert_called_with( '{yes}', {'yes': 'no'}, '100', logging.CRITICAL)
def test_empty_values(self, mock_stdout): log.configure_logging() logger = log.IntegrationAdaptorsLogger('SYS') with self.subTest('Empty info log'): logger.info('100', 'I can still log info strings without values!') output = mock_stdout.getvalue() self.assertIn('I can still log info strings without values!', output) with self.subTest('Empty audit log'): logger.audit('100', 'I can still log audit strings without values!') output = mock_stdout.getvalue() self.assertIn('I can still log audit strings without values!', output) with self.subTest('Empty warning log'): logger.warning('100', 'I can still log warning strings without values!') output = mock_stdout.getvalue() self.assertIn('I can still log warning strings without values!', output) with self.subTest('Empty error log'): logger.error('100', 'I can still log error strings without values!') output = mock_stdout.getvalue() self.assertIn('I can still log error strings without values!', output) with self.subTest('Empty Critical log'): logger.critical( '100', 'I can still log critical strings without values!') output = mock_stdout.getvalue() self.assertIn('I can still log critical strings without values!', output)
def test_log_threshold(self, mock_stdout): config.config['LOG_LEVEL'] = 'AUDIT' log.configure_logging() log.IntegrationAdaptorsLogger('TES').info('100', 'Test message') output = mock_stdout.getvalue() self.assertEqual("", output)
def test_format_and_write(self, mock_std): log.configure_logging() log.message_id.set('10') log.correlation_id.set('5') log.inbound_message_id.set('8') log.IntegrationAdaptorsLogger('SYS')._format_and_write( message='{yes} {no} {maybe}', values={ 'yes': 'one', 'no': 'two', 'maybe': 'three' }, level=logging.INFO, process_key_num='100') output = mock_std.getvalue() # Check that each value has spaces self.assertIn(' CorrelationId=5 ', output) self.assertIn(' LogLevel=INFO ', output) self.assertIn(' ProcessKey=SYS100', output) self.assertIn(' RequestId=10 ', output) self.assertIn(' maybe=three ', output) self.assertIn(' yes=one ', output) self.assertIn(f' pid={os.getpid()}', output) self.assertIn('InboundMessageId=8', output)
def test_correct_time_format(self, mock_stdout): log.configure_logging() logger = log.IntegrationAdaptorsLogger('SYS') logger.info('100', 'I can still log info strings without values!') output = mock_stdout.getvalue() time_value = output.split('[')[1].split(']')[0] time.strptime(time_value, '%Y-%m-%dT%H:%M:%S.%fZ')
def test_name_can_be_empty(self, mock_stdout): mock_stdout.truncate(0) log.configure_logging() log.IntegrationAdaptorsLogger('SYS').info('%s %s', 'yes', 'no') log_entry = LogEntry(mock_stdout.getvalue()) self.assertEqual('SYS', log_entry.name)
def test_log_level_threshold(self, mock_stdout): mock_stdout.truncate(0) config.config['LOG_LEVEL'] = 'CRITICAL' log.configure_logging() log.IntegrationAdaptorsLogger('TES').info('Test message') config.config['LOG_LEVEL'] = 'INFO' output = mock_stdout.getvalue() self.assertEqual("", output)
def test_custom_audit_level(self, mock_stdout): log.configure_logging() log.correlation_id.set('2') log.IntegrationAdaptorsLogger('TES') \ .audit('100', '{There Will Be No Spaces Today}', {'There Will Be No Spaces Today': 'wow qwe'}) output = mock_stdout.getvalue() self.assertIn('CorrelationId=2', output) self.assertIn('ThereWillBeNoSpacesToday="wow qwe"', output) self.assertIn('LogLevel=AUDIT', output) self.assertIn('ProcessKey=TES100', output)
def test_interaction_id_context_var_should_be_logged_when_set( self, mock_stdout): # Arrange log.configure_logging() log.interaction_id.set('GP_101') logger = log.IntegrationAdaptorsLogger('TES') # Act logger.audit('100', 'This log message should have interaction id ') output = mock_stdout.getvalue() # Assert self.assertIn('InteractionId=GP_101', output)
def test_dictionary_formatting(self): # Tests both removing the spaces and surrounding values with quotes if needed input_dict = { 'Key With Space': 'value with space', # Needs quotes 'EasyKey': 'EasyValue', # Doesn't need quotes } expected_output = { 'Key With Space': 'KeyWithSpace="value with space"', 'EasyKey': 'EasyKey=EasyValue' } output = log.IntegrationAdaptorsLogger('SYS')._format_values_in_map( input_dict) self.assertEqual(output, expected_output)
def test_custom_audit_level(self, mock_stdout): mock_stdout.truncate(0) log.configure_logging() mdc.correlation_id.set('2') log.IntegrationAdaptorsLogger('TES').audit( 'Some audit message with %s %s', 'additional', 'parameters') output = mock_stdout.getvalue() log_entry = LogEntry(output) self.assertEqual('2', log_entry.correlation_id) self.assertEqual('AUDIT', log_entry.level) self.assertIn('Some audit message with additional parameters', log_entry.message)
def test_message_format_and_log_entry_parts(self, mock_stdout): mock_stdout.truncate(0) log.configure_logging('TEST') mdc.correlation_id.set('15') log.IntegrationAdaptorsLogger('SYS').info('%s %s', 'yes', 'no') log_entry = LogEntry(mock_stdout.getvalue()) self.assertEqual('15', log_entry.correlation_id) self.assertEqual('TEST.SYS', log_entry.name) self.assertEqual('INFO', log_entry.level) self.assertEqual('yes no', log_entry.message) time.strptime(log_entry.time, '%Y-%m-%dT%H:%M:%S.%f')
def test_format_and_write_empty_vals(self, mock_std): log.configure_logging() log.IntegrationAdaptorsLogger('SYS')._format_and_write( message='{yes} {no} {maybe}', values={ 'yes': 'one', 'no': 'two', 'maybe': 'three' }, level=logging.INFO, process_key_num='100') output = mock_std.getvalue() self.assertIn(' LogLevel=INFO ', output) self.assertIn(' ProcessKey=SYS100', output) self.assertIn(' maybe=three ', output) self.assertIn(f' pid={os.getpid()}', output) self.assertIn(' yes=one ', output) self.assertNotIn('CorrelationId=', output) self.assertNotIn('RequestId=', output)
"""Module for Proton specific queue adaptor functionality. """ import json from typing import Dict, Any import proton.handlers import proton.reactor import tornado.ioloop import comms.queue_adaptor import utilities.integration_adaptors_logger as log import utilities.message_utilities logger = log.IntegrationAdaptorsLogger('PROTON_QUEUE') class MessageSendingError(RuntimeError): """An error occurred whilst sending a message to the Message Queue""" pass class EarlyDisconnectError(RuntimeError): """The connection to the Message Queue ended before sending of the message had been done.""" pass class ProtonQueueAdaptor(comms.queue_adaptor.QueueAdaptor): """Proton implementation of a queue adaptor.""" def __init__(self, **kwargs) -> None: """ Construct a Proton implementation of a :class:`QueueAdaptor <comms.queue_adaptor.QueueAdaptor>`.
from typing import Dict from tornado import httpclient from utilities import integration_adaptors_logger as log logger = log.IntegrationAdaptorsLogger("COMMON_HTTPS") class CommonHttps(object): @staticmethod async def make_request(url: str, method: str, headers: Dict[str, str], body: str, client_cert: str = None, client_key: str = None, ca_certs: str = None, validate_cert: bool = True, http_proxy_host: str = None, http_proxy_port: int = None, raise_error_response: bool = True): """Send a HTTPS request and return it's response. :param url: A string containing the endpoint to send the request to. :param method: A string containing the HTTP method to send the request as. :param headers: A dictionary containing key value pairs for the details of the HTTP header. :param body: A string containing the message to send to the endpoint. :param client_cert: A string containing the full path of the client certificate file. :param client_key: A string containing the full path of the client private key file. :param ca_certs: A string containing the full path of the certificate authority certificate file. :param validate_cert: Whether the server's certificate should be validated or not.
"""This module defines the component used to orchestrate the retrieval and caching of routing and reliability information for a remote MHS.""" from typing import Dict from utilities import integration_adaptors_logger as log import lookup.cache_adaptor as cache_adaptor import lookup.sds_client as sds_client logger = log.IntegrationAdaptorsLogger('SRL_ATTRIBUTE_LOOKUP') class MHSAttributeLookup(object): """A tool that allows the routing and reliability information for a remote MHS to be retrieved.""" def __init__(self, client: sds_client.SDSClient, cache: cache_adaptor.CacheAdaptor): """ :param client The SDS client to use when retrieving remote MHS details. :param cache: The cache adaptor to use to cache remote MHS details. """ if not client: raise ValueError('sds client required') if not cache: raise ValueError('No cache supplied') self.cache = cache self.sds_client = client async def retrieve_mhs_attributes(self, ods_code, interaction_id) -> Dict: """Obtains the attributes of the MHS registered for the given ODS code and interaction ID. These details will
"""This module contains the client used to make requests to SDS.""" import asyncio from typing import Dict, List import ldap3 import ldap3.core.exceptions as ldap_exceptions from utilities import integration_adaptors_logger as log import lookup.sds_exception as sds_exception logger = log.IntegrationAdaptorsLogger('SRL_CLIENT') MHS_OBJECT_CLASS = "nhsMhs" AS_OBJECT_CLASS = "nhsAs" MHS_PARTY_KEY = 'nhsMHSPartyKey' MHS_ASID = 'uniqueIdentifier' mhs_attributes = [ 'nhsEPInteractionType', 'nhsIDCode', 'nhsMhsCPAId', 'nhsMHSEndPoint', 'nhsMhsFQDN', 'nhsMHsIN', 'nhsMHSIsAuthenticated', 'nhsMHSPartyKey', 'nhsMHsSN', 'nhsMhsSvcIA', 'nhsProductKey', 'uniqueIdentifier', 'nhsMHSAckRequested', 'nhsMHSActor', 'nhsMHSDuplicateElimination', 'nhsMHSPersistDuration', 'nhsMHSRetries', 'nhsMHSRetryInterval', 'nhsMHSSyncReplyMode' ] class SDSClient(object): """A client that can be used to query SDS.""" def __init__(self,
from __future__ import annotations import asyncio from typing import Callable, Awaitable from utilities import integration_adaptors_logger logger = integration_adaptors_logger.IntegrationAdaptorsLogger('RETRIABLE_ACTION') class RetriableAction(object): """Responsible for retrying an action a configurable number of times with a configurable delay""" def __init__(self, action: Callable[[], Awaitable[object]], retries: int, delay: float): """ :param action: The action to be retried. :param retries: The number of times to retry the action if it fails. The initial attempt will always occur. :param delay: The delay (in seconds) between retries. """ self.action = action self.retries = retries self.delay = delay self.retriable_exception_check = lambda exception: True self.success_check = lambda result: True logger.info("0001", "Configuring a retriable action with {action}, {retries} and {delay}", {"action": action, "retries": retries, "delay": delay}) def with_success_check(self, success_check: Callable[[object], bool]) -> RetriableAction: """Set a callable that can be used to determine whether the result of the action was a success.
"""The Summary Care Record endpoint""" import json from typing import Dict, Optional import tornado.web import tornado.ioloop import utilities.integration_adaptors_logger as log from utilities import message_utilities from message_handling import message_forwarder as mh from builder.pystache_message_builder import MessageGenerationError from message_handling.message_forwarder import MessageSendingError logger = log.IntegrationAdaptorsLogger('SCR_ENDPOINT') class SummaryCareRecord(tornado.web.RequestHandler): """ The SummaryCareRecord endpoint - provides the entrypoint for inbound ** Note: It is current expected behavior of the system that verification of the inputs is performed ** by the Pystache library as a part of the message generation """ def initialize(self, forwarder: mh.MessageForwarder) -> None: """ SummaryCareRecord endpoint class :param forwarder: A message forwarder, parsed messages are pushed to this object for processing :return: """ self.forwarder = forwarder async def post(self): """
XSLT_DIR = 'mhs_common/messages/xslt' SOAP_HEADER_XSLT = 'soap_header.xslt' SOAP_BODY_XSLT = 'soap_body.xslt' FROM_ASID = "from_asid" TO_ASID = "to_asid" SERVICE = "service" ACTION = "action" MESSAGE_ID = 'message_id' TIMESTAMP = 'timestamp' MESSAGE = 'hl7_message' REQUIRED_SOAP_ELEMENTS = [FROM_ASID, TO_ASID, MESSAGE_ID, SERVICE, ACTION, MESSAGE] SOAP_TEMPLATE = "soap_request" logger = log.IntegrationAdaptorsLogger('SOAP_ENVELOPE') soap_header_transformer_path = str(Path(ROOT_DIR) / XSLT_DIR / SOAP_HEADER_XSLT) soap_body_transformer_path = str(Path(ROOT_DIR) / XSLT_DIR / SOAP_BODY_XSLT) class SoapEnvelope(envelope.Envelope): """An envelope that contains a message to be sent synchronously to a remote MHS.""" def __init__(self, message_dictionary: Dict[str, Union[str, bool]]): """Create a new SoapEnvelope that populates the message with the provided dictionary. :param message_dictionary: The dictionary of values to use when populating the template. """ super().__init__(SOAP_TEMPLATE, message_dictionary)
import definitions import tornado.httpserver import tornado.ioloop import tornado.web import utilities.config as config import utilities.integration_adaptors_logger as log from comms import proton_queue_adaptor from mhs_common import workflow from mhs_common.configuration import configuration_manager from mhs_common.request import healthcheck_handler from mhs_common.state import persistence_adaptor, dynamo_persistence_adaptor from utilities import secrets, certs import inbound.request.handler as async_request_handler logger = log.IntegrationAdaptorsLogger('INBOUND_MAIN') ASYNC_TIMEOUT = 30 def initialise_workflows() -> Dict[str, workflow.CommonWorkflow]: """Initialise the workflows :return: The workflows that can be used to handle messages. """ queue_adaptor = proton_queue_adaptor.ProtonQueueAdaptor( host=config.get_config('INBOUND_QUEUE_URL'), username=secrets.get_secret_config('INBOUND_QUEUE_USERNAME'), password=secrets.get_secret_config('INBOUND_QUEUE_PASSWORD')) raw_queue_adaptor = proton_queue_adaptor.ProtonQueueAdaptor( host=config.get_config('INBOUND_RAW_QUEUE_URL'),
"""This module defines the outbound transmission component.""" from ssl import SSLError from typing import Dict from comms.common_https import CommonHttps from mhs_common.retry import retriable_action from mhs_common.transmission import transmission_adaptor from tornado import httpclient from utilities import integration_adaptors_logger as log logger = log.IntegrationAdaptorsLogger("OUTBOUND_TRANSMISSION") class OutboundTransmissionError(Exception): pass class OutboundTransmission(transmission_adaptor.TransmissionAdaptor): errors_not_to_retry = (httpclient.HTTPClientError, SSLError) """A component that sends HTTP requests to a remote MHS.""" def __init__(self, client_cert: str, client_key: str, ca_certs: str, max_retries: int, retry_delay: int, http_proxy_host: str = None, http_proxy_port: int = None): """Create a new OutboundTransmission that loads certificates from the specified directory.
"""The main entry point for the summary care record service""" import tornado.ioloop import tornado.web import utilities.integration_adaptors_logger as log from scr import gp_summary_upload from utilities import config, secrets, certs import definitions from endpoints import summary_care_record from message_handling import message_forwarder, message_sender logger = log.IntegrationAdaptorsLogger('SCR-WEB') def build_app(): interactions = { 'SCR_GP_SUMMARY_UPLOAD': gp_summary_upload.GpSummaryUpload() } address = config.get_config('MHS_ADDRESS') certificates = certs.Certs.create_certs_files(definitions.ROOT_DIR, ca_certs=secrets.get_secret_config('MHS_CA_CERTS', default=None)) sender = message_sender.MessageSender(address, ca_certs=certificates.ca_certs_path) forwarder = message_forwarder.MessageForwarder(interactions, sender) app = tornado.web.Application([(r"/", summary_care_record.SummaryCareRecord, dict(forwarder=forwarder))]) return app def main():
from tornado import httpclient from utilities import timing from utilities.date_utilities import DateUtilities from mhs_common import workflow from mhs_common.errors import ebxml_handler from mhs_common.errors.soap_handler import handle_soap_error from mhs_common.messages.ebxml_error_envelope import EbxmlErrorEnvelope from mhs_common.messages.soap_fault_envelope import SOAPFault from mhs_common.routing import routing_reliability from mhs_common.state import persistence_adaptor from mhs_common.state import work_description as wd from mhs_common.transmission import transmission_adaptor from mhs_common.workflow import common_asynchronous logger = log.IntegrationAdaptorsLogger('ASYNC_RELIABLE_WORKFLOW') class AsynchronousReliableWorkflow( common_asynchronous.CommonAsynchronousWorkflow): """Handles the workflow for the asynchronous reliable messaging pattern.""" def __init__( self, party_key: str = None, persistence_store: persistence_adaptor.PersistenceAdaptor = None, transmission: transmission_adaptor.TransmissionAdaptor = None, queue_adaptor: queue_adaptor.QueueAdaptor = None, inbound_queue_max_retries: int = None, inbound_queue_retry_delay: int = None, max_request_size: int = None, persistence_store_max_retries: int = None,
import tornado.web from utilities import timing, integration_adaptors_logger as log from lookup import routing_reliability logger = log.IntegrationAdaptorsLogger('SRL_RELIABILITY_REQUEST_HANDLER') class ReliabilityRequestHandler(tornado.web.RequestHandler): """A handler for requests to obtain reliability information.""" def initialize(self, routing: routing_reliability.RoutingAndReliability) -> None: """Initialise this request handler with the provided configuration values. :param routing: The routing and reliability component to use to look up values in SDS. """ self.routing = routing @timing.time_request async def get(self): org_code = self.get_query_argument("org-code") service_id = self.get_query_argument("service-id") logger.info( "001", "Looking up reliability information. {org_code}, {service_id}", { "org_code": org_code, "service_id": service_id }) reliability_info = await self.routing.get_reliability( org_code, service_id)
"""This module defines the envelope used to wrap asynchronous messages to be sent to a remote MHS.""" import copy from typing import Dict, Tuple, Any, Optional, NamedTuple from xml.etree.ElementTree import Element import utilities.message_utilities as message_utilities from utilities import integration_adaptors_logger as log from mhs_common.messages import envelope logger = log.IntegrationAdaptorsLogger('COMMON_EBXML_ENVELOPE') TEMPLATES_DIR = "data/templates" FROM_PARTY_ID = "from_party_id" TO_PARTY_ID = "to_party_id" CPA_ID = "cpa_id" CONVERSATION_ID = 'conversation_id' SERVICE = "service" ACTION = "action" MESSAGE_ID = 'message_id' TIMESTAMP = 'timestamp' RECEIVED_MESSAGE_ID = "received_message_id" ERROR_CODE = "error_code" SEVERITY = "severity" DESCRIPTION = "description" EBXML_NAMESPACE = "eb" SOAP_NAMESPACE = "SOAP" XLINK_NAMESPACE = "xlink"
from comms import queue_adaptor from tornado import httpclient from utilities import timing from mhs_common import workflow from mhs_common.errors import ebxml_handler from mhs_common.errors.soap_handler import handle_soap_error from mhs_common.messages.ebxml_error_envelope import EbxmlErrorEnvelope from mhs_common.messages.soap_fault_envelope import SOAPFault from mhs_common.routing import routing_reliability from persistence import persistence_adaptor from mhs_common.state import work_description as wd from mhs_common.transmission import transmission_adaptor from mhs_common.workflow import common_asynchronous logger = log.IntegrationAdaptorsLogger(__name__) class AsynchronousExpressWorkflow(common_asynchronous.CommonAsynchronousWorkflow): """Handles the workflow for the asynchronous express messaging pattern.""" def __init__(self, party_key: str = None, persistence_store: persistence_adaptor.PersistenceAdaptor = None, transmission: transmission_adaptor.TransmissionAdaptor = None, queue_adaptor: queue_adaptor.QueueAdaptor = None, max_request_size: int = None, routing: routing_reliability.RoutingAndReliability = None): super().__init__(party_key, persistence_store, transmission, queue_adaptor, max_request_size, routing) self.workflow_specific_interaction_details = dict(duplicate_elimination=False, ack_requested=False, ack_soap_actor="urn:oasis:names:tc:ebxml-msg:actor:toPartyMSH",
def test_undefined_log_ref_throws_error(self): with self.assertRaises(ValueError): log.IntegrationAdaptorsLogger('')