Example #1
0
    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)
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
 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)
Example #5
0
    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)
Example #6
0
    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)
Example #7
0
    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)
Example #10
0
    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)
Example #11
0
    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)
Example #12
0
    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)
Example #13
0
    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')
Example #15
0
    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.
Example #18
0
"""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
Example #19
0
"""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.
Example #21
0
"""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)
Example #23
0
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():
Example #26
0
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",
Example #30
0
 def test_undefined_log_ref_throws_error(self):
     with self.assertRaises(ValueError):
         log.IntegrationAdaptorsLogger('')