diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2e95f8f..c41cc5b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Release History =============== +dev - +------------------ + +* Updated requirements files to have current dependencies +* Added global option to switch off error on duplicate +* Limited duplicate detection to successful previous transmissions only +* Giving random name to duplicate messages + + 1.2.0 - 2020-04-12 ------------------ diff --git a/docs/detailed-guide/configuration.rst b/docs/detailed-guide/configuration.rst index 39e5992..0066799 100644 --- a/docs/detailed-guide/configuration.rst +++ b/docs/detailed-guide/configuration.rst @@ -34,6 +34,11 @@ The available settings along with their usage is described below: | MAX_ARCH_DAYS | 30 | Number of days files and messages are kept in | | | | storage. | +------------------------+----------------------------+------------------------------------------------+ +| ERROR_ON_DUPLICATE | True | When set to true, duplicate messages are | +| | | handled as error with negative MDN. | +| | | When set to false, duplicates are handled as | +| | | successful transmissions. | ++------------------------+----------------------------+------------------------------------------------+ The Data Directory diff --git a/pyas2/settings.py b/pyas2/settings.py index 1977b56..a2bee6c 100644 --- a/pyas2/settings.py +++ b/pyas2/settings.py @@ -20,3 +20,6 @@ # Max number of days worth of messages to be saved in archive MAX_ARCH_DAYS = APP_SETTINGS.get("MAX_ARCH_DAYS", 30) + +# Send positive MDN when duplicate message is received +ERROR_ON_DUPLICATE = APP_SETTINGS.get("ERROR_ON_DUPLICATE", True) diff --git a/pyas2/tests/test_advanced.py b/pyas2/tests/test_advanced.py index bb1044e..64e945c 100644 --- a/pyas2/tests/test_advanced.py +++ b/pyas2/tests/test_advanced.py @@ -257,10 +257,89 @@ def test_duplicate_error(self, mock_request): # Make sure out message was created self.assertEqual(in_message.status, "E") out_message = Message.objects.get( - message_id=in_message.message_id + "_duplicate", direction="IN" + message_id__startswith=in_message.message_id + "_duplicate_", direction="IN" ) self.assertEqual(out_message.status, "E") + # send it a third time to cause another duplicate error + in_message.send_message(as2message.headers, as2message.content) + + # Make sure out message was created + self.assertEqual(in_message.status, "E") + out_messages = Message.objects.filter( + message_id__startswith=in_message.message_id + "_duplicate_", direction="IN" + ) + for out_message in out_messages: + self.assertEqual(out_message.status, "E") + + @mock.patch("requests.post") + def test_duplicate_success(self, mock_request): + with override_settings(PYAS2={"ERROR_ON_DUPLICATE": False}): + importlib.reload(settings) + partner = Partner.objects.create( + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + signature="sha1", + signature_cert=self.server_crt, + encryption="tripledes_192_cbc", + encryption_cert=self.server_crt, + mdn=True, + mdn_mode="SYNC", + mdn_sign="sha1", + ) + + # Send the message once + as2message = As2Message( + sender=self.organization.as2org, receiver=partner.as2partner + ) + as2message.build( + self.payload, + filename="testmessage.edi", + subject=partner.subject, + content_type=partner.content_type, + ) + in_message, _ = Message.objects.create_from_as2message( + as2message=as2message, payload=self.payload, direction="OUT", status="P" + ) + + mock_request.side_effect = SendMessageMock(self.client) + in_message.send_message(as2message.headers, as2message.content) + + # Check the status of the message + self.assertEqual(in_message.status, "S") + out_message = Message.objects.get( + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") + + # send it again to, should not cause duplicate error + in_message.send_message(as2message.headers, as2message.content) + + # Make sure out message was created + self.assertEqual(in_message.status, "S") + out_message = Message.objects.get( + message_id__startswith=in_message.message_id + "_duplicate_", + direction="IN", + ) + self.assertEqual(out_message.status, "S") + + # send it again to, should not cause duplicate error, and no create error + in_message.send_message(as2message.headers, as2message.content) + + # Make sure out message was created + self.assertEqual(in_message.status, "S") + out_messages = Message.objects.filter( + message_id__startswith=in_message.message_id + "_duplicate_", + direction="IN", + ) + for out_message in out_messages: + self.assertEqual(out_message.status, "S") + + with override_settings(PYAS2={"ERROR_ON_DUPLICATE": True}): + importlib.reload(settings) + self.assertEqual(settings.ERROR_ON_DUPLICATE, True) + def test_org_missing_error(self): # Create the client partner and send the command partner = Partner.objects.create( diff --git a/pyas2/views.py b/pyas2/views.py index cf08805..47cc6b5 100644 --- a/pyas2/views.py +++ b/pyas2/views.py @@ -12,6 +12,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import FormView +from django.utils.crypto import get_random_string from pyas2lib import Message as As2Message from pyas2lib import Mdn as As2Mdn from pyas2lib.exceptions import DuplicateDocument @@ -25,6 +26,7 @@ from pyas2.utils import run_post_receive from pyas2.utils import run_post_send from pyas2.forms import SendAs2MessageForm +from pyas2 import settings logger = logging.getLogger("pyas2") @@ -47,7 +49,19 @@ def find_message(message_id, partner_id): return message.as2message @staticmethod - def check_message_exists(message_id, partner_id): + def check_success_message_exists(message_id, partner_id): + """ Check if the message already exists in the system """ + if settings.ERROR_ON_DUPLICATE: + return Message.objects.filter( + message_id=message_id, + partner_id=partner_id.strip(), + status__in=("S", "P"), + ).exists() + else: + return False + + @staticmethod + def check_same_message_exists(message_id, partner_id): """ Check if the message already exists in the system """ return Message.objects.filter( message_id=message_id, partner_id=partner_id.strip() @@ -125,7 +139,7 @@ def post(self, request, *args, **kwargs): request_body, self.find_organization, self.find_partner, - self.check_message_exists, + self.check_success_message_exists, ) logger.info( @@ -135,8 +149,14 @@ def post(self, request, *args, **kwargs): ) # In case of duplicates update message id - if isinstance(exception[0], DuplicateDocument): - as2message.message_id += "_duplicate" + if isinstance(exception[0], DuplicateDocument) or ( + not settings.ERROR_ON_DUPLICATE + and self.check_same_message_exists( + message_id=as2message.message_id, + partner_id=as2message.sender.as2_name, + ) + ): + as2message.message_id += "_duplicate_" + get_random_string(5) # Create the Message and MDN objects message, full_fn = Message.objects.create_from_as2message(