From 50ab7f9f3354a4db18043cfec22555f7af2760ec Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 5 Apr 2020 11:07:16 +0530 Subject: [PATCH 1/9] bump version of pyas2lib --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 663f108..b42a54a 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ README = f.read() install_requires = [ - 'pyas2lib==1.2.2', + 'pyas2lib==1.3.0', 'django>=2.1.9', 'requests' ] From 5571a03b5a2fd1437a3f88e8b7ae823701e2b9e1 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 5 Apr 2020 11:07:46 +0530 Subject: [PATCH 2/9] fix tox config and some bugs in the test cases --- .coveragerc | 5 ++++- pyas2/tests/test_advanced.py | 5 +++-- pyas2/tests/test_basic.py | 18 +++++++----------- pyas2/views.py | 2 +- requirements/base.txt | 2 +- requirements/test.txt | 3 +-- requirements/tox.txt | 3 ++- tox.ini | 18 ++++++++---------- 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/.coveragerc b/.coveragerc index f7e4d72..46db6d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,4 +8,7 @@ omit = pyas2/admin.py # Omit the template tags - pyas2/templatetags/pyas2.py \ No newline at end of file + pyas2/templatetags/pyas2.py + + # Omit tests + pyas2/tests/* \ No newline at end of file diff --git a/pyas2/tests/test_advanced.py b/pyas2/tests/test_advanced.py index bd0ef72..582751f 100644 --- a/pyas2/tests/test_advanced.py +++ b/pyas2/tests/test_advanced.py @@ -1,9 +1,10 @@ -import mock import os +from pathlib import Path +from unittest import mock + from django.core import management from django.test import Client from django.test import TestCase -from pathlib import Path from pyas2lib import Message as As2Message from pyas2 import settings diff --git a/pyas2/tests/test_basic.py b/pyas2/tests/test_basic.py index 8985b64..f3d3755 100644 --- a/pyas2/tests/test_basic.py +++ b/pyas2/tests/test_basic.py @@ -1,20 +1,16 @@ -from __future__ import unicode_literals +import os +from email.parser import HeaderParser +from unittest import mock + from django.test import TestCase, Client +from requests import Response + from pyas2.models import PrivateKey, PublicCertificate, Organization, Partner, \ Message, Mdn from pyas2 import settings from pyas2lib.as2 import Message as As2Message -from email.parser import HeaderParser -from requests import Response -try: - from itertools import izip as zip -except ImportError: # will be 3.x series - pass -import mock -import os -TEST_DIR = os.path.join((os.path.dirname( - os.path.abspath(__file__))), 'fixtures') +TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), 'fixtures') class BasicServerClientTestCase(TestCase): diff --git a/pyas2/views.py b/pyas2/views.py index e617e48..e41a927 100644 --- a/pyas2/views.py +++ b/pyas2/views.py @@ -6,7 +6,7 @@ from django.shortcuts import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.urls import reverse_lazy from django.views import View from django.views.decorators.csrf import csrf_exempt diff --git a/requirements/base.txt b/requirements/base.txt index 8c284de..f9a6492 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,3 +1,3 @@ -pyas2lib==1.2.2 +pyas2lib==1.3.0 requests django>=2.1.9 diff --git a/requirements/test.txt b/requirements/test.txt index 510f3a0..cfd73f1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,4 @@ pytest pytest-cov pytest-django -coverage -mock +pytest-mock diff --git a/requirements/tox.txt b/requirements/tox.txt index a8693c6..88588c1 100644 --- a/requirements/tox.txt +++ b/requirements/tox.txt @@ -1,2 +1,3 @@ --r base.txt +pyas2lib==1.3.0 +requests -r test.txt diff --git a/tox.ini b/tox.ini index 54ccb1a..b8ad032 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,18 @@ [tox] envlist = - {py27,py34,py35,py36}-django{111} - {py27}-django{110} - {py36}-django{20} + {py36,py37,py38}-django{30} + {py37}-django{22} [testenv] basepython = - py27: python2.7 - py34: python3.4 - py35: python3.5 py36: python3.6 + py37: python3.7 + py38: python3.8 deps = -r{toxinidir}/requirements/tox.txt - # {py27}-django19: Django>=1.9,<1.10 - {py27}-django110: Django>=1.10,<1.11 - {py27,py34,py35,py36}-django111: Django>=1.11 - {py36}-django20: Django>=2.0 + {py37,py38}-django30: django==3.0.5 + py36-django30: django==3.0.5 + py36-django30: dataclasses + {py37}-django22: django==2.2.10 setenv = PYTHONPATH = {toxinidir} From b3af42f7cb5faaba1c36ef23638f53f1521c57ae Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 11 Apr 2020 17:21:40 +0530 Subject: [PATCH 3/9] use django storage framework for interacting with file system --- example/settings.py | 12 +++++++ pyas2/management/commands/sendas2bulk.py | 36 +++++++++++++++------ pyas2/management/commands/sendas2message.py | 13 +++----- pyas2/models.py | 12 +++++-- pyas2/settings.py | 11 +++---- pyas2/tests/test_advanced.py | 8 ++--- pyas2/tests/test_basic.py | 3 +- pyas2/views.py | 2 +- 8 files changed, 62 insertions(+), 35 deletions(-) diff --git a/example/settings.py b/example/settings.py index 93879cc..0f971b6 100644 --- a/example/settings.py +++ b/example/settings.py @@ -11,6 +11,11 @@ """ import os +import environ + +# reading .env file +env = environ.Env() +environ.Env.read_env() # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -121,3 +126,10 @@ STATIC_URL = '/static/' +if env.bool("USE_S3_FILE_STORAGE", False): + DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') + AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') + AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME') + AWS_LOCATION = 'pyas2_data' + AWS_DEFAULT_ACL = None diff --git a/pyas2/management/commands/sendas2bulk.py b/pyas2/management/commands/sendas2bulk.py index f3123bf..b0ff180 100644 --- a/pyas2/management/commands/sendas2bulk.py +++ b/pyas2/management/commands/sendas2bulk.py @@ -2,6 +2,7 @@ import os from django.core.management import call_command from django.core.management.base import BaseCommand +from django.core.files.storage import default_storage from pyas2 import settings from pyas2.models import Organization @@ -16,15 +17,32 @@ def handle(self, *args, **options): self.stdout.write('Process files in the outbox directory for ' 'partner "%s".' % partner.as2_name) for org in Organization.objects.all(): - outbox_folder = os.path.join( - settings.DATA_DIR, 'messages', partner.as2_name, - 'outbox', org.as2_name) - if not os.path.isdir(outbox_folder): - os.makedirs(outbox_folder) - for pend_file in glob.glob(outbox_folder + '/*'): + if settings.DATA_DIR: + outbox_folder = os.path.join( + settings.DATA_DIR, 'messages', partner.as2_name, + 'outbox', org.as2_name) + else: + outbox_folder = os.path.join( + 'messages', partner.as2_name, 'outbox', org.as2_name) + + # Check of the directory exists and if not create it + try: + _, pending_files = default_storage.listdir(outbox_folder) + except FileNotFoundError: + pending_files = [] + os.makedirs(default_storage.path(outbox_folder)) + + # For each file found call send message to send it to the server + pending_files = filter(lambda x: x != ".", pending_files) + for pending_file in pending_files: + pending_file = os.path.join(outbox_folder, pending_file) self.stdout.write( 'Sending file "%s" from organization "%s" to partner ' - '"%s".' % (pend_file, org.as2_name, partner.as2_name)) + '"%s".' % (pending_file, org.as2_name, partner.as2_name)) call_command( - 'sendas2message', org.as2_name, partner.as2_name, - os.path.join(outbox_folder, pend_file), delete=True) + 'sendas2message', + org.as2_name, + partner.as2_name, + pending_file, + delete=True + ) diff --git a/pyas2/management/commands/sendas2message.py b/pyas2/management/commands/sendas2message.py index e696758..e87fa66 100644 --- a/pyas2/management/commands/sendas2message.py +++ b/pyas2/management/commands/sendas2message.py @@ -3,6 +3,7 @@ from django.core.management.base import BaseCommand from django.core.management.base import CommandError +from django.core.files.storage import default_storage from pyas2lib import Message as AS2Message from pyas2.models import Message @@ -44,18 +45,14 @@ def handle(self, *args, **options): raise CommandError( f'Partner "{options["partner_as2name"]}" does not exist') - # Check if file exists and we have the right permissions - if not os.path.isfile(options['path_to_payload']): + # Check if file exists + if not default_storage.exists(options['path_to_payload']): raise CommandError( f'Payload at location "{options["path_to_payload"]}" does not exist.') - if options['delete'] and not os.access(options['path_to_payload'], os.W_OK): - raise CommandError( - f'Insufficient file permission for payload {options["path_to_payload"]}.') - # Build and send the AS2 message original_filename = os.path.basename(options['path_to_payload']) - with open(options['path_to_payload'], 'rb') as in_file: + with default_storage.open(options['path_to_payload'], 'rb') as in_file: payload = in_file.read() as2message = AS2Message(sender=org.as2org, receiver=partner.as2partner) as2message.build( @@ -75,4 +72,4 @@ def handle(self, *args, **options): # Delete original file if option is set if options['delete']: - os.remove(options['path_to_payload']) + default_storage.delete(options['path_to_payload']) diff --git a/pyas2/models.py b/pyas2/models.py index 9a07838..b854cc8 100644 --- a/pyas2/models.py +++ b/pyas2/models.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- import logging import os +import posixpath import traceback from email.parser import HeaderParser from uuid import uuid4 import requests from django.core.files.base import ContentFile +from django.core.files.storage import default_storage from django.db import models from django.utils import timezone from django.utils.translation import ugettext as _ @@ -21,7 +23,6 @@ from pyas2 import settings from pyas2.utils import run_post_send -from pyas2.utils import store_file logger = logging.getLogger('pyas2') @@ -281,10 +282,15 @@ def create_from_as2message(self, as2message, payload, direction, status, filenam # Save the payload to the inbox folder full_filename = None if direction == 'IN' and status == 'S': - folder = os.path.join(settings.DATA_DIR, 'messages', organization, 'inbox', partner) + if settings.DATA_DIR: + dirname = os.path.join( + settings.DATA_DIR, 'messages', organization, 'inbox', partner) + else: + dirname = os.path.join('messages', organization, 'inbox', partner) if not message.partner.keep_filename or not filename: filename = f'{message.message_id}.msg' - full_filename = store_file(folder, filename, payload) + full_filename = default_storage.generate_filename(posixpath.join(dirname, filename)) + default_storage.save(name=full_filename, content=ContentFile(payload)) return message, full_filename diff --git a/pyas2/settings.py b/pyas2/settings.py index df026e8..252555b 100644 --- a/pyas2/settings.py +++ b/pyas2/settings.py @@ -1,16 +1,13 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from django.conf import settings import os +from django.conf import settings + APP_SETTINGS = getattr(settings, 'PYAS2', {}) # Get the root directory for saving messages -if APP_SETTINGS.get('DATA_DIR') \ - and os.path.isdir(APP_SETTINGS['DATA_DIR']): +DATA_DIR = None +if APP_SETTINGS.get('DATA_DIR') and os.path.isdir(APP_SETTINGS['DATA_DIR']): DATA_DIR = APP_SETTINGS['DATA_DIR'] -else: - DATA_DIR = settings.MEDIA_ROOT or settings.BASE_DIR # Max number of times to retry failed sends MAX_RETRIES = APP_SETTINGS.get('MAX_RETRIES', 5) diff --git a/pyas2/tests/test_advanced.py b/pyas2/tests/test_advanced.py index 582751f..d76ffdc 100644 --- a/pyas2/tests/test_advanced.py +++ b/pyas2/tests/test_advanced.py @@ -79,8 +79,7 @@ def setUp(self): @classmethod def tearDownClass(cls): # remove all files in the inbox folders - inbox = os.path.join( - settings.DATA_DIR, 'messages', 'as2server', 'inbox', 'as2client') + inbox = os.path.join('messages', 'as2server', 'inbox', 'as2client') try: files = os.listdir(inbox) except OSError: @@ -177,7 +176,7 @@ def test_post_receive_command(self): # Check that the command got executed touch_file = os.path.join( - TEST_DIR, '%s.msg.received' % in_message.message_id) + TEST_DIR, '%s.msg.received' % in_message.message_id.replace("@", "")) self.assertTrue(os.path.exists(touch_file)) os.remove(touch_file) @@ -426,8 +425,7 @@ def test_sendmessage_command(self): def test_sendbulk_command(self): """ Test the command for sending all files in the outbox folder """ # Create a file for testing - outbox_dir = os.path.join( - settings.DATA_DIR, 'messages', 'as2client', 'outbox', 'as2server') + outbox_dir = os.path.join('messages', 'as2client', 'outbox', 'as2server') try: os.makedirs(outbox_dir) except FileExistsError: diff --git a/pyas2/tests/test_basic.py b/pyas2/tests/test_basic.py index f3d3755..10ea022 100644 --- a/pyas2/tests/test_basic.py +++ b/pyas2/tests/test_basic.py @@ -7,7 +7,6 @@ from pyas2.models import PrivateKey, PublicCertificate, Organization, Partner, \ Message, Mdn -from pyas2 import settings from pyas2lib.as2 import Message as As2Message TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), 'fixtures') @@ -73,7 +72,7 @@ def setUpTestData(cls): def tearDown(self): # remove all files in the inbox folders inbox = os.path.join( - settings.DATA_DIR, 'messages', 'as2server', 'inbox', 'as2client') + 'messages', 'as2server', 'inbox', 'as2client') for the_file in os.listdir(inbox): file_path = os.path.join(inbox, the_file) if os.path.isfile(file_path): diff --git a/pyas2/views.py b/pyas2/views.py index e41a927..bc9a1db 100644 --- a/pyas2/views.py +++ b/pyas2/views.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- import logging import os + from django.contrib import messages from django.shortcuts import Http404 from django.shortcuts import HttpResponse From 7112d2a1bebcb3cf322e27948c8a57b4d0b0ff9b Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 11 Apr 2020 17:22:05 +0530 Subject: [PATCH 4/9] add black and pylama checks to the test command --- .travis.yml | 5 +++-- requirements/test.txt | 11 +++++++---- setup.cfg | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 264c6bf..b7daf7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,12 @@ language: python python: - '3.6' - '3.7' + - '3.8' install: - python setup.py install - - pip install pytest-cov pytest-django mock + - pip install -r requirements/test.txt script: - - pytest --cov-report term --cov-config .coveragerc --cov=pyas2 + - pytest --cov-report term --cov-config .coveragerc --cov=pyas2 --black --pylama pyas2 after_success: - pip install codecov - codecov diff --git a/requirements/test.txt b/requirements/test.txt index cfd73f1..2f7c935 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,4 +1,7 @@ -pytest -pytest-cov -pytest-django -pytest-mock +pytest==5.4.1 +pytest-cov==2.8.1 +pytest-django==3.9.0 +pytest-mock==3.0.0 +pylama==7.7.1 +pytest-black==0.3.8 +django-environ==0.4.5 diff --git a/setup.cfg b/setup.cfg index 2a9acf1..ef1867e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,23 @@ [bdist_wheel] universal = 1 + +[pylama:pycodestyle] +max_line_length = 100 + +[pylama:pylint] +max_line_length = 100 +ignore = E1101,R0902,R0903,W1203,C0103 + +[pylama:pydocstyle] +convention = numpy +ignore = D202 + +[pylama:pep8] +max_line_length = 100 + +[pylama] +format = pep8 +skip = venv/*,.tox/* +linters= pycodestyle,pydocstyle,pyflakes,pylint,pep8 +ignore = D203,D212,E231 + From cf997d22b56e592986b96592f2508eb3f4156b07 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sat, 11 Apr 2020 17:23:44 +0530 Subject: [PATCH 5/9] run the black formatter on pyas2 --- pyas2/__init__.py | 6 +- pyas2/admin.py | 163 ++++--- pyas2/apps.py | 4 +- pyas2/forms.py | 138 +++--- pyas2/management/commands/manageas2server.py | 115 ++--- pyas2/management/commands/sendas2bulk.py | 26 +- pyas2/management/commands/sendas2message.py | 50 +-- pyas2/migrations/0001_initial.py | 442 ++++++++++++++++--- pyas2/migrations/0002_auto_20190603_1329.py | 40 +- pyas2/models.py | 420 +++++++++++------- pyas2/settings.py | 15 +- pyas2/templatetags/pyas2.py | 2 +- pyas2/tests/test_advanced.py | 387 ++++++++-------- pyas2/tests/test_basic.py | 387 ++++++++-------- pyas2/urls.py | 13 +- pyas2/utils.py | 35 +- pyas2/views.py | 167 ++++--- requirements/test.txt | 1 + 18 files changed, 1468 insertions(+), 943 deletions(-) diff --git a/pyas2/__init__.py b/pyas2/__init__.py index 1e3a535..e079bc2 100644 --- a/pyas2/__init__.py +++ b/pyas2/__init__.py @@ -1,8 +1,8 @@ # Set the version -__version__ = '1.1.1' +__version__ = "1.1.1" -default_app_config = 'pyas2.apps.Pyas2Config' +default_app_config = "pyas2.apps.Pyas2Config" __all__ = [ - 'default_app_config', + "default_app_config", ] diff --git a/pyas2/admin.py b/pyas2/admin.py index 0f3e444..b462ab5 100644 --- a/pyas2/admin.py +++ b/pyas2/admin.py @@ -20,13 +20,13 @@ @admin.register(PrivateKey) class PrivateKeyAdmin(admin.ModelAdmin): form = PrivateKeyForm - list_display = ('name', 'valid_from', 'valid_to', 'serial_number', 'download_key') + list_display = ("name", "valid_from", "valid_to", "serial_number", "download_key") def download_key(self, obj): - download_url = reverse_lazy('download-file', - args=['private_key', obj.id]) - return format_html('Click to Download', - download_url) + download_url = reverse_lazy("download-file", args=["private_key", obj.id]) + return format_html( + 'Click to Download', download_url + ) download_key.allow_tags = True download_key.short_description = "Key File" @@ -35,13 +35,13 @@ def download_key(self, obj): @admin.register(PublicCertificate) class PublicCertificateAdmin(admin.ModelAdmin): form = PublicCertificateForm - list_display = ('name', 'valid_from', 'valid_to', 'serial_number', 'download_cert') + list_display = ("name", "valid_from", "valid_to", "serial_number", "download_cert") def download_cert(self, obj): - download_url = reverse_lazy('download-file', - args=['public_cert', obj.id]) - return format_html('Click to Download', - download_url) + download_url = reverse_lazy("download-file", args=["public_cert", obj.id]) + return format_html( + 'Click to Download', download_url + ) download_cert.allow_tags = True download_cert.short_description = "Certificate File" @@ -50,68 +50,119 @@ def download_cert(self, obj): @admin.register(Partner) class PartnerAdmin(admin.ModelAdmin): form = PartnerForm - list_display = ['name', 'as2_name', 'target_url', 'encryption', - 'encryption_cert', 'signature', 'signature_cert', - 'mdn', 'mdn_mode'] - list_filter = ('name', 'as2_name') + list_display = [ + "name", + "as2_name", + "target_url", + "encryption", + "encryption_cert", + "signature", + "signature_cert", + "mdn", + "mdn_mode", + ] + list_filter = ("name", "as2_name") fieldsets = ( - (None, { - 'fields': ( - 'name', 'as2_name', 'email_address', 'target_url', - 'subject', 'content_type', 'confirmation_message') - }), - ('Http Authentication', { - 'classes': ('collapse', 'wide'), - 'fields': ('http_auth', 'http_auth_user', 'http_auth_pass', 'https_verify_ssl') - }), - ('Security Settings', { - 'classes': ('collapse', 'wide'), - 'fields': ('compress', 'encryption', 'encryption_cert', 'signature', - 'signature_cert') - }), - ('MDN Settings', { - 'classes': ('collapse', 'wide'), - 'fields': ('mdn', 'mdn_mode', 'mdn_sign') - }), - ('Advanced Settings', { - 'classes': ('collapse', 'wide'), - 'fields': ('keep_filename', 'cmd_send', 'cmd_receive') - }), + ( + None, + { + "fields": ( + "name", + "as2_name", + "email_address", + "target_url", + "subject", + "content_type", + "confirmation_message", + ) + }, + ), + ( + "Http Authentication", + { + "classes": ("collapse", "wide"), + "fields": ( + "http_auth", + "http_auth_user", + "http_auth_pass", + "https_verify_ssl", + ), + }, + ), + ( + "Security Settings", + { + "classes": ("collapse", "wide"), + "fields": ( + "compress", + "encryption", + "encryption_cert", + "signature", + "signature_cert", + ), + }, + ), + ( + "MDN Settings", + { + "classes": ("collapse", "wide"), + "fields": ("mdn", "mdn_mode", "mdn_sign"), + }, + ), + ( + "Advanced Settings", + { + "classes": ("collapse", "wide"), + "fields": ("keep_filename", "cmd_send", "cmd_receive"), + }, + ), ) - actions = ['send_message'] + actions = ["send_message"] def send_message(self, request, queryset): partner = queryset.first() return HttpResponseRedirect( - reverse_lazy('as2-send') + '?partner_id=%s' % partner.as2_name) + reverse_lazy("as2-send") + "?partner_id=%s" % partner.as2_name + ) send_message.short_description = "Send a message to the selected partner" @admin.register(Organization) class OrganizationAdmin(admin.ModelAdmin): - list_display = ['name', 'as2_name'] - list_filter = ('name', 'as2_name') + list_display = ["name", "as2_name"] + list_filter = ("name", "as2_name") @admin.register(Message) class MessageAdmin(admin.ModelAdmin): - def has_add_permission(self, request): return False - search_fields = ('message_id', 'payload') + search_fields = ("message_id", "payload") - list_filter = ('direction', 'status', 'organization__as2_name', 'partner__as2_name') + list_filter = ("direction", "status", "organization__as2_name", "partner__as2_name") - list_display = ['message_id', 'timestamp', 'status', 'direction', - 'organization', 'partner', 'compressed', 'encrypted', - 'signed', 'download_file', 'mdn_url'] + list_display = [ + "message_id", + "timestamp", + "status", + "direction", + "organization", + "partner", + "compressed", + "encrypted", + "signed", + "download_file", + "mdn_url", + ] def mdn_url(self, obj): - if hasattr(obj, 'mdn'): + if hasattr(obj, "mdn"): view_url = reverse_lazy( - f'admin:{Mdn._meta.app_label}_{Mdn._meta.model_name}_change', args=[obj.mdn.id]) + f"admin:{Mdn._meta.app_label}_{Mdn._meta.model_name}_change", + args=[obj.mdn.id], + ) return format_html('{}', view_url, obj.mdn.mdn_id) mdn_url.allow_tags = True @@ -119,8 +170,10 @@ def mdn_url(self, obj): def download_file(self, obj): if obj.payload: - view_url = reverse_lazy('download-file', args=['message_payload', obj.id]) - return format_html('{}', view_url, os.path.basename(obj.payload.name)) + view_url = reverse_lazy("download-file", args=["message_payload", obj.id]) + return format_html( + '{}', view_url, os.path.basename(obj.payload.name) + ) download_file.allow_tags = True download_file.short_description = "Payload" @@ -128,10 +181,12 @@ def download_file(self, obj): @admin.register(Mdn) class MdnAdmin(admin.ModelAdmin): - def has_add_permission(self, request): return False - search_fields = ('mdn_id', 'message_id',) - list_display = ('mdn_id', 'message', 'timestamp', 'status') - list_filter = ('status',) + search_fields = ( + "mdn_id", + "message_id", + ) + list_display = ("mdn_id", "message", "timestamp", "status") + list_filter = ("status",) diff --git a/pyas2/apps.py b/pyas2/apps.py index ef70d63..b173b44 100644 --- a/pyas2/apps.py +++ b/pyas2/apps.py @@ -3,8 +3,8 @@ class Pyas2Config(AppConfig): - name = 'pyas2' - verbose_name = 'pyAS2 File Transfer Server' + name = "pyas2" + verbose_name = "pyAS2 File Transfer Server" def ready(self): super(Pyas2Config, self).ready() diff --git a/pyas2/forms.py b/pyas2/forms.py index 1b4b475..067660d 100644 --- a/pyas2/forms.py +++ b/pyas2/forms.py @@ -12,42 +12,47 @@ class PartnerForm(forms.ModelForm): - def clean(self): cleaned_data = super(PartnerForm, self).clean() # If http auth is set and credentials are missing raise error - if cleaned_data.get('http_auth'): - if not cleaned_data.get('http_auth_user'): + if cleaned_data.get("http_auth"): + if not cleaned_data.get("http_auth_user"): raise forms.ValidationError( - _('HTTP username is mandatory when HTTP authentication ' - 'is enabled')) - if not cleaned_data.get('http_auth_pass'): - self._errors['http_auth_pass'] = self.error_class( - _('HTTP password is mandatory when HTTP authentication ' - 'is enabled')) + _( + "HTTP username is mandatory when HTTP authentication " + "is enabled" + ) + ) + if not cleaned_data.get("http_auth_pass"): + self._errors["http_auth_pass"] = self.error_class( + _( + "HTTP password is mandatory when HTTP authentication " + "is enabled" + ) + ) # if encryption is set and no cert is mentioned set error - if cleaned_data.get('encryption') and \ - not cleaned_data.get('encryption_cert'): + if cleaned_data.get("encryption") and not cleaned_data.get("encryption_cert"): raise forms.ValidationError( - _('Encryption Key is mandatory when message encryption is set')) + _("Encryption Key is mandatory when message encryption is set") + ) # if signature is set and no cert is mentioned set error - if cleaned_data.get('signature') and \ - not cleaned_data.get('signature_cert'): + if cleaned_data.get("signature") and not cleaned_data.get("signature_cert"): raise forms.ValidationError( - _('Signature Key is required when message signature is set')) + _("Signature Key is required when message signature is set") + ) # if mdn is set then the mode must also be set - if cleaned_data.get('mdn') and not cleaned_data.get('mdn_mode'): - raise forms.ValidationError(_('MDN Mode needs to be specified')) + if cleaned_data.get("mdn") and not cleaned_data.get("mdn_mode"): + raise forms.ValidationError(_("MDN Mode needs to be specified")) # if the mdn signature is set then the signature cert must be set - if cleaned_data.get('mdn_sign') and \ - not cleaned_data.get('signature_cert'): + if cleaned_data.get("mdn_sign") and not cleaned_data.get("signature_cert"): raise forms.ValidationError( - _('Signature Key is mandatory when signed mdn is requested')) + _("Signature Key is mandatory when signed mdn is requested") + ) return cleaned_data @@ -60,29 +65,31 @@ class PrivateKeyForm(forms.ModelForm): key_file = forms.FileField() def clean_key_file(self): - key_file = self.cleaned_data['key_file'] + key_file = self.cleaned_data["key_file"] ext = os.path.splitext(key_file.name)[1] - valid_extensions = ['.pem', '.p12', '.pfx'] + valid_extensions = [".pem", ".p12", ".pfx"] if not ext.lower() in valid_extensions: - raise forms.ValidationError(_( - 'Unsupported key format, supported formats ' - 'include %s.') % ', '.join(valid_extensions)) + raise forms.ValidationError( + _("Unsupported key format, supported formats " "include %s.") + % ", ".join(valid_extensions) + ) return key_file def clean(self): cleaned_data = super(PrivateKeyForm, self).clean() - key_file = cleaned_data.get('key_file') + key_file = cleaned_data.get("key_file") if key_file: - cleaned_data['key_filename'] = key_file.name - cleaned_data['key_file'] = key_file.read() + cleaned_data["key_filename"] = key_file.name + cleaned_data["key_file"] = key_file.read() try: - As2Organization.load_key(cleaned_data['key_file'], - cleaned_data['key_pass']) + As2Organization.load_key( + cleaned_data["key_file"], cleaned_data["key_pass"] + ) except AS2Exception as e: raise forms.ValidationError(e.args[0]) @@ -90,69 +97,74 @@ def clean(self): def save(self, commit=True): instance = super(PrivateKeyForm, self).save(commit=False) - instance.name = self.cleaned_data['key_filename'] - instance.key = self.cleaned_data['key_file'] + instance.name = self.cleaned_data["key_filename"] + instance.key = self.cleaned_data["key_file"] if commit: instance.save() return instance class Meta: model = PrivateKey - fields = ['key_file', 'key_pass'] + fields = ["key_file", "key_pass"] widgets = { - 'key_pass': forms.PasswordInput(), + "key_pass": forms.PasswordInput(), } class PublicCertificateForm(forms.ModelForm): - cert_file = forms.FileField(label='Certificate File') - cert_ca_file = forms.FileField(label='Certificate CA File', required=False) + cert_file = forms.FileField(label="Certificate File") + cert_ca_file = forms.FileField(label="Certificate CA File", required=False) def clean_cert_file(self): - cert_file = self.cleaned_data['cert_file'] + cert_file = self.cleaned_data["cert_file"] ext = os.path.splitext(cert_file.name)[1] - valid_extensions = ['.pem', '.der', '.cer'] + valid_extensions = [".pem", ".der", ".cer"] if not ext.lower() in valid_extensions: - raise forms.ValidationError(_( - 'Unsupported certificate format, supported formats ' - 'include %s.') % ', '.join(valid_extensions)) + raise forms.ValidationError( + _("Unsupported certificate format, supported formats " "include %s.") + % ", ".join(valid_extensions) + ) return cert_file def clean_cert_ca_file(self): - cert_ca_file = self.cleaned_data['cert_ca_file'] + cert_ca_file = self.cleaned_data["cert_ca_file"] if cert_ca_file: ext = os.path.splitext(cert_ca_file.name)[1] - valid_extensions = ['.pem', '.der', '.cer', '.ca'] + valid_extensions = [".pem", ".der", ".cer", ".ca"] if not ext.lower() in valid_extensions: - raise forms.ValidationError(_( - 'Unsupported certificate format, supported formats ' - 'include %s.') % ', '.join(valid_extensions)) + raise forms.ValidationError( + _( + "Unsupported certificate format, supported formats " + "include %s." + ) + % ", ".join(valid_extensions) + ) return cert_ca_file def clean(self): cleaned_data = super(PublicCertificateForm, self).clean() - cert_file = cleaned_data.get('cert_file') - cert_ca_file = cleaned_data.get('cert_ca_file', '') + cert_file = cleaned_data.get("cert_file") + cert_ca_file = cleaned_data.get("cert_ca_file", "") - if cert_file and cert_ca_file != '': - cleaned_data['cert_filename'] = cert_file.name - cleaned_data['cert_file'] = cert_file.read() + if cert_file and cert_ca_file != "": + cleaned_data["cert_filename"] = cert_file.name + cleaned_data["cert_file"] = cert_file.read() if cert_ca_file: - cleaned_data['cert_ca_file'] = cert_ca_file.read() + cleaned_data["cert_ca_file"] = cert_ca_file.read() try: partner = As2Partner( - 'partner', - verify_cert=cleaned_data['cert_file'], - verify_cert_ca=cleaned_data['cert_ca_file'], - validate_certs=cleaned_data['verify_cert'] + "partner", + verify_cert=cleaned_data["cert_file"], + verify_cert_ca=cleaned_data["cert_ca_file"], + validate_certs=cleaned_data["verify_cert"], ) partner.load_verify_cert() except AS2Exception as e: @@ -162,11 +174,11 @@ def clean(self): def save(self, commit=True): instance = super(PublicCertificateForm, self).save(commit=False) - instance.name = self.cleaned_data['cert_filename'] - instance.certificate = self.cleaned_data['cert_file'] + instance.name = self.cleaned_data["cert_filename"] + instance.certificate = self.cleaned_data["cert_file"] - if self.cleaned_data['cert_ca_file']: - instance.certificate_ca = self.cleaned_data['cert_ca_file'] + if self.cleaned_data["cert_ca_file"]: + instance.certificate_ca = self.cleaned_data["cert_ca_file"] if commit: instance.save() @@ -174,12 +186,12 @@ def save(self, commit=True): class Meta: model = PublicCertificate - fields = ['cert_file', 'cert_ca_file', 'verify_cert'] + fields = ["cert_file", "cert_ca_file", "verify_cert"] class SendAs2MessageForm(forms.Form): organization = forms.ModelChoiceField( - queryset=Organization.objects.all(), empty_label=None) + queryset=Organization.objects.all(), empty_label=None + ) partner = forms.ModelChoiceField(queryset=Partner.objects.all()) file = forms.FileField() - diff --git a/pyas2/management/commands/manageas2server.py b/pyas2/management/commands/manageas2server.py index 23dcb38..e478676 100644 --- a/pyas2/management/commands/manageas2server.py +++ b/pyas2/management/commands/manageas2server.py @@ -11,41 +11,43 @@ class Command(BaseCommand): - help = 'Command to manage the as2 server, includes options to cleanup, ' \ - 'handle async mdns and message retries' + help = ( + "Command to manage the as2 server, includes options to cleanup, " + "handle async mdns and message retries" + ) def add_arguments(self, parser): parser.add_argument( - '--clean', - action='store_true', - dest='clean', + "--clean", + action="store_true", + dest="clean", default=False, - help='Cleans up all the old messages and archived files.' + help="Cleans up all the old messages and archived files.", ) parser.add_argument( - '--retry', - action='store_true', - dest='retry', + "--retry", + action="store_true", + dest="retry", default=False, - help='Retrying all failed outbound communications.' + help="Retrying all failed outbound communications.", ) parser.add_argument( - '--async-mdns', - action='store_true', - dest='async_mdns', + "--async-mdns", + action="store_true", + dest="async_mdns", default=False, - help='Handle sending and receiving of Asynchronous MDNs.' + help="Handle sending and receiving of Asynchronous MDNs.", ) def handle(self, *args, **options): - if options['retry']: - self.stdout.write('Retrying all failed outbound messages') + if options["retry"]: + self.stdout.write("Retrying all failed outbound messages") # Get the list of all messages with status retry - failed_msgs = Message.objects.filter(status='R', direction='OUT') + failed_msgs = Message.objects.filter(status="R", direction="OUT") for failed_msg in failed_msgs: @@ -57,35 +59,35 @@ def handle(self, *args, **options): # if max retries has exceeded then mark message status as error if failed_msg.retries > settings.MAX_RETRIES: - failed_msg.status = 'E' + failed_msg.status = "E" failed_msg.save() continue self.stdout.write( - 'Retry send the message with ID %s' % failed_msg.message_id) + "Retry send the message with ID %s" % failed_msg.message_id + ) # Build and resend the AS2 message as2message = AS2Message( sender=failed_msg.organization.as2org, - receiver=failed_msg.partner.as2partner) + receiver=failed_msg.partner.as2partner, + ) as2message.build( failed_msg.payload.read(), filename=os.path.basename(failed_msg.payload.name), subject=failed_msg.partner.subject, - content_type=failed_msg.partner.content_type + content_type=failed_msg.partner.content_type, ) - failed_msg.send_message( - as2message.headers, as2message.content) + failed_msg.send_message(as2message.headers, as2message.content) - self.stdout.write( - 'Processed all failed outbound messages') + self.stdout.write("Processed all failed outbound messages") - if options['async_mdns']: + if options["async_mdns"]: # First part of script sends asynchronous MDNs for inbound messages # received from partners fetch all the pending asynchronous # MDN objects - self.stdout.write('Sending all pending asynchronous MDNs') - in_pending_mdns = Mdn.objects.filter(status='P') + self.stdout.write("Sending all pending asynchronous MDNs") + in_pending_mdns = Mdn.objects.filter(status="P") for pending_mdn in in_pending_mdns: # Parse the MDN headers from text @@ -94,20 +96,28 @@ def handle(self, *args, **options): try: # Set http basic auth if enabled in the partner profile auth = None - if pending_mdn.message.partner and pending_mdn.message.partner.http_auth: - auth = (pending_mdn.message.partner.http_auth_user, - pending_mdn.message.partner.http_auth_pass) + if ( + pending_mdn.message.partner + and pending_mdn.message.partner.http_auth + ): + auth = ( + pending_mdn.message.partner.http_auth_user, + pending_mdn.message.partner.http_auth_pass, + ) # Post the MDN message to the url provided on the # original as2 message requests.post( - pending_mdn.return_url, auth=auth, + pending_mdn.return_url, + auth=auth, headers=dict(mdn_headers.items()), - data=pending_mdn.payload.read()) - pending_mdn.status = 'S' + data=pending_mdn.payload.read(), + ) + pending_mdn.status = "S" except requests.exceptions.RequestException as e: - self.stdout.write('Failed to send MDN "%s", error: %s' %( - pending_mdn.mdn_id, e)) + self.stdout.write( + 'Failed to send MDN "%s", error: %s' % (pending_mdn.mdn_id, e) + ) finally: pending_mdn.save() @@ -115,32 +125,35 @@ def handle(self, *args, **options): # messages to partners self.stdout.write( 'Checking messages waiting for MDNs for more than "%s" ' - 'minutes.' % settings.ASYNC_MDN_WAIT) + "minutes." % settings.ASYNC_MDN_WAIT + ) # Find all messages waiting MDNs for more than the set async m # dn wait time - time_threshold = timezone.now() - \ - timedelta(minutes=settings.ASYNC_MDN_WAIT) + time_threshold = timezone.now() - timedelta(minutes=settings.ASYNC_MDN_WAIT) out_pending_msgs = Message.objects.filter( - status='P', direction='OUT', timestamp__lt=time_threshold) + status="P", direction="OUT", timestamp__lt=time_threshold + ) # Mark these messages as erred for pending_msg in out_pending_msgs: - pending_msg.status = 'E' - pending_msg.detailed_status = \ - 'Failed to receive asynchronous MDN within the ' \ - 'threshold limit.' + pending_msg.status = "E" + pending_msg.detailed_status = ( + "Failed to receive asynchronous MDN within the " "threshold limit." + ) pending_msg.save() - self.stdout.write(u'Successfully processed all pending mdns.') + self.stdout.write(u"Successfully processed all pending mdns.") - if options['clean']: - self.stdout.write(u'Cleanup maintenance process started') + if options["clean"]: + self.stdout.write(u"Cleanup maintenance process started") max_archive_dt = timezone.now() - timedelta(settings.MAX_ARCH_DAYS) self.stdout.write( - 'Delete all messages older than %s' % settings.MAX_ARCH_DAYS) - old_message = Message.objects.filter( - timestamp__lt=max_archive_dt).order_by('timestamp') + "Delete all messages older than %s" % settings.MAX_ARCH_DAYS + ) + old_message = Message.objects.filter(timestamp__lt=max_archive_dt).order_by( + "timestamp" + ) for message in old_message: message.payload.delete() @@ -153,4 +166,4 @@ def handle(self, *args, **options): except Mdn.DoesNotExist: pass message.delete() - self.stdout.write('Cleanup maintenance process completed') + self.stdout.write("Cleanup maintenance process completed") diff --git a/pyas2/management/commands/sendas2bulk.py b/pyas2/management/commands/sendas2bulk.py index b0ff180..69945fa 100644 --- a/pyas2/management/commands/sendas2bulk.py +++ b/pyas2/management/commands/sendas2bulk.py @@ -10,20 +10,27 @@ class Command(BaseCommand): - help = 'Command for sending all pending messages in the outbox folders' + help = "Command for sending all pending messages in the outbox folders" def handle(self, *args, **options): for partner in Partner.objects.all(): - self.stdout.write('Process files in the outbox directory for ' - 'partner "%s".' % partner.as2_name) + self.stdout.write( + "Process files in the outbox directory for " + 'partner "%s".' % partner.as2_name + ) for org in Organization.objects.all(): if settings.DATA_DIR: outbox_folder = os.path.join( - settings.DATA_DIR, 'messages', partner.as2_name, - 'outbox', org.as2_name) + settings.DATA_DIR, + "messages", + partner.as2_name, + "outbox", + org.as2_name, + ) else: outbox_folder = os.path.join( - 'messages', partner.as2_name, 'outbox', org.as2_name) + "messages", partner.as2_name, "outbox", org.as2_name + ) # Check of the directory exists and if not create it try: @@ -38,11 +45,12 @@ def handle(self, *args, **options): pending_file = os.path.join(outbox_folder, pending_file) self.stdout.write( 'Sending file "%s" from organization "%s" to partner ' - '"%s".' % (pending_file, org.as2_name, partner.as2_name)) + '"%s".' % (pending_file, org.as2_name, partner.as2_name) + ) call_command( - 'sendas2message', + "sendas2message", org.as2_name, partner.as2_name, pending_file, - delete=True + delete=True, ) diff --git a/pyas2/management/commands/sendas2message.py b/pyas2/management/commands/sendas2message.py index e87fa66..f92acb4 100644 --- a/pyas2/management/commands/sendas2message.py +++ b/pyas2/management/commands/sendas2message.py @@ -10,66 +10,66 @@ from pyas2.models import Organization from pyas2.models import Partner -logger = logging.getLogger('pyas2') +logger = logging.getLogger("pyas2") class Command(BaseCommand): - help = 'Send an as2 message to your trading partner' - args = '' + help = "Send an as2 message to your trading partner" + args = "" def add_arguments(self, parser): - parser.add_argument('org_as2name', type=str) - parser.add_argument('partner_as2name', type=str) - parser.add_argument('path_to_payload', type=str) + parser.add_argument("org_as2name", type=str) + parser.add_argument("partner_as2name", type=str) + parser.add_argument("path_to_payload", type=str) parser.add_argument( - '--delete', - action='store_true', - dest='delete', + "--delete", + action="store_true", + dest="delete", default=False, - help='Delete source file after processing' + help="Delete source file after processing", ) def handle(self, *args, **options): # Check if organization and partner exists try: - org = Organization.objects.get( - as2_name=options['org_as2name']) + org = Organization.objects.get(as2_name=options["org_as2name"]) except Organization.DoesNotExist: raise CommandError( - f'Organization "{options["org_as2name"]}" does not exist') + f'Organization "{options["org_as2name"]}" does not exist' + ) try: - partner = Partner.objects.get(as2_name=options['partner_as2name']) + partner = Partner.objects.get(as2_name=options["partner_as2name"]) except Partner.DoesNotExist: - raise CommandError( - f'Partner "{options["partner_as2name"]}" does not exist') + raise CommandError(f'Partner "{options["partner_as2name"]}" does not exist') # Check if file exists - if not default_storage.exists(options['path_to_payload']): + if not default_storage.exists(options["path_to_payload"]): raise CommandError( - f'Payload at location "{options["path_to_payload"]}" does not exist.') + f'Payload at location "{options["path_to_payload"]}" does not exist.' + ) # Build and send the AS2 message - original_filename = os.path.basename(options['path_to_payload']) - with default_storage.open(options['path_to_payload'], 'rb') as in_file: + original_filename = os.path.basename(options["path_to_payload"]) + with default_storage.open(options["path_to_payload"], "rb") as in_file: payload = in_file.read() as2message = AS2Message(sender=org.as2org, receiver=partner.as2partner) as2message.build( payload, filename=original_filename, subject=partner.subject, - content_type=partner.content_type + content_type=partner.content_type, ) message, _ = Message.objects.create_from_as2message( as2message=as2message, payload=payload, filename=original_filename, - direction='OUT', - status='P' + direction="OUT", + status="P", ) message.send_message(as2message.headers, as2message.content) # Delete original file if option is set - if options['delete']: - default_storage.delete(options['path_to_payload']) + if options["delete"]: + default_storage.delete(options["path_to_payload"]) diff --git a/pyas2/migrations/0001_initial.py b/pyas2/migrations/0001_initial.py index 3cafac8..bf733a0 100644 --- a/pyas2/migrations/0001_initial.py +++ b/pyas2/migrations/0001_initial.py @@ -9,102 +9,400 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='PrivateKey', + name="PrivateKey", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('key', models.BinaryField()), - ('key_pass', models.CharField(max_length=100, verbose_name='Private Key Password')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("key", models.BinaryField()), + ( + "key_pass", + models.CharField( + max_length=100, verbose_name="Private Key Password" + ), + ), ], ), migrations.CreateModel( - name='PublicCertificate', + name="PublicCertificate", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('certificate', models.BinaryField()), - ('certificate_ca', models.BinaryField(blank=True, null=True, verbose_name='Local CA Store')), - ('verify_cert', models.BooleanField(default=True, help_text='Uncheck this option to disable certificate verification.', verbose_name='Verify Certificate')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("certificate", models.BinaryField()), + ( + "certificate_ca", + models.BinaryField( + blank=True, null=True, verbose_name="Local CA Store" + ), + ), + ( + "verify_cert", + models.BooleanField( + default=True, + help_text="Uncheck this option to disable certificate verification.", + verbose_name="Verify Certificate", + ), + ), ], ), migrations.CreateModel( - name='Partner', + name="Partner", fields=[ - ('name', models.CharField(max_length=100, verbose_name='Partner Name')), - ('as2_name', models.CharField(max_length=100, primary_key=True, serialize=False, verbose_name='AS2 Identifier')), - ('email_address', models.EmailField(blank=True, max_length=254, null=True)), - ('http_auth', models.BooleanField(default=False, verbose_name='Enable Authentication')), - ('http_auth_user', models.CharField(blank=True, max_length=100, null=True)), - ('http_auth_pass', models.CharField(blank=True, max_length=100, null=True)), - ('target_url', models.URLField()), - ('subject', models.CharField(default='EDI Message sent using pyas2', max_length=255)), - ('content_type', models.CharField(choices=[('application/EDI-X12', 'application/EDI-X12'), ('application/EDIFACT', 'application/EDIFACT'), ('application/edi-consent', 'application/edi-consent'), ('application/XML', 'application/XML')], default='application/edi-consent', max_length=100)), - ('compress', models.BooleanField(default=False, verbose_name='Compress Message')), - ('encryption', models.CharField(blank=True, choices=[('tripledes_192_cbc', '3DES'), ('rc2_128_cbc', 'RC2-128'), ('rc4_128_cbc', 'RC4-128'), ('aes_128_cbc', 'AES-128'), ('aes_192_cbc', 'AES-192'), ('aes_256_cbc', 'AES-256')], max_length=20, null=True, verbose_name='Encrypt Message')), - ('signature', models.CharField(blank=True, choices=[('sha1', 'SHA-1'), ('sha224', 'SHA-224'), ('sha256', 'SHA-256'), ('sha384', 'SHA-384'), ('sha512', 'SHA-512')], max_length=20, null=True, verbose_name='Sign Message')), - ('mdn', models.BooleanField(default=False, verbose_name='Request MDN')), - ('mdn_mode', models.CharField(blank=True, choices=[('SYNC', 'Synchronous'), ('ASYNC', 'Asynchronous')], max_length=20, null=True)), - ('mdn_sign', models.CharField(blank=True, choices=[('sha1', 'SHA-1'), ('sha224', 'SHA-224'), ('sha256', 'SHA-256'), ('sha384', 'SHA-384'), ('sha512', 'SHA-512')], max_length=20, null=True, verbose_name='Request Signed MDN')), - ('confirmation_message', models.TextField(blank=True, help_text='Use this field to send a customized message in the MDN Confirmations for this Partner', null=True, verbose_name='Confirmation Message')), - ('keep_filename', models.BooleanField(default=False, help_text='Use Original Filename to to store file on receipt, use this option only if you are sure partner sends unique names', verbose_name='Keep Original Filename')), - ('cmd_send', models.TextField(blank=True, help_text='Command executed after successful message send, replacements are $filename, $sender, $recevier, $messageid and any message header such as $Subject', null=True, verbose_name='Command on Message Send')), - ('cmd_receive', models.TextField(blank=True, help_text='Command executed after successful message receipt, replacements are $filename, $fullfilename, $sender, $recevier, $messageid and any message header such as $Subject', null=True, verbose_name='Command on Message Receipt')), - ('encryption_cert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pyas2.PublicCertificate')), - ('signature_cert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='partner_s', to='pyas2.PublicCertificate')), + ("name", models.CharField(max_length=100, verbose_name="Partner Name")), + ( + "as2_name", + models.CharField( + max_length=100, + primary_key=True, + serialize=False, + verbose_name="AS2 Identifier", + ), + ), + ( + "email_address", + models.EmailField(blank=True, max_length=254, null=True), + ), + ( + "http_auth", + models.BooleanField( + default=False, verbose_name="Enable Authentication" + ), + ), + ( + "http_auth_user", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "http_auth_pass", + models.CharField(blank=True, max_length=100, null=True), + ), + ("target_url", models.URLField()), + ( + "subject", + models.CharField( + default="EDI Message sent using pyas2", max_length=255 + ), + ), + ( + "content_type", + models.CharField( + choices=[ + ("application/EDI-X12", "application/EDI-X12"), + ("application/EDIFACT", "application/EDIFACT"), + ("application/edi-consent", "application/edi-consent"), + ("application/XML", "application/XML"), + ], + default="application/edi-consent", + max_length=100, + ), + ), + ( + "compress", + models.BooleanField(default=False, verbose_name="Compress Message"), + ), + ( + "encryption", + models.CharField( + blank=True, + choices=[ + ("tripledes_192_cbc", "3DES"), + ("rc2_128_cbc", "RC2-128"), + ("rc4_128_cbc", "RC4-128"), + ("aes_128_cbc", "AES-128"), + ("aes_192_cbc", "AES-192"), + ("aes_256_cbc", "AES-256"), + ], + max_length=20, + null=True, + verbose_name="Encrypt Message", + ), + ), + ( + "signature", + models.CharField( + blank=True, + choices=[ + ("sha1", "SHA-1"), + ("sha224", "SHA-224"), + ("sha256", "SHA-256"), + ("sha384", "SHA-384"), + ("sha512", "SHA-512"), + ], + max_length=20, + null=True, + verbose_name="Sign Message", + ), + ), + ("mdn", models.BooleanField(default=False, verbose_name="Request MDN")), + ( + "mdn_mode", + models.CharField( + blank=True, + choices=[("SYNC", "Synchronous"), ("ASYNC", "Asynchronous")], + max_length=20, + null=True, + ), + ), + ( + "mdn_sign", + models.CharField( + blank=True, + choices=[ + ("sha1", "SHA-1"), + ("sha224", "SHA-224"), + ("sha256", "SHA-256"), + ("sha384", "SHA-384"), + ("sha512", "SHA-512"), + ], + max_length=20, + null=True, + verbose_name="Request Signed MDN", + ), + ), + ( + "confirmation_message", + models.TextField( + blank=True, + help_text="Use this field to send a customized message in the MDN Confirmations for this Partner", + null=True, + verbose_name="Confirmation Message", + ), + ), + ( + "keep_filename", + models.BooleanField( + default=False, + help_text="Use Original Filename to to store file on receipt, use this option only if you are sure partner sends unique names", + verbose_name="Keep Original Filename", + ), + ), + ( + "cmd_send", + models.TextField( + blank=True, + help_text="Command executed after successful message send, replacements are $filename, $sender, $recevier, $messageid and any message header such as $Subject", + null=True, + verbose_name="Command on Message Send", + ), + ), + ( + "cmd_receive", + models.TextField( + blank=True, + help_text="Command executed after successful message receipt, replacements are $filename, $fullfilename, $sender, $recevier, $messageid and any message header such as $Subject", + null=True, + verbose_name="Command on Message Receipt", + ), + ), + ( + "encryption_cert", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="pyas2.PublicCertificate", + ), + ), + ( + "signature_cert", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="partner_s", + to="pyas2.PublicCertificate", + ), + ), ], ), migrations.CreateModel( - name='Organization', + name="Organization", fields=[ - ('name', models.CharField(max_length=100, verbose_name='Organization Name')), - ('as2_name', models.CharField(max_length=100, primary_key=True, serialize=False, verbose_name='AS2 Identifier')), - ('email_address', models.EmailField(blank=True, max_length=254, null=True)), - ('confirmation_message', models.TextField(blank=True, help_text='Use this field to send a customized message in the MDN Confirmations for this Organization', null=True, verbose_name='Confirmation Message')), - ('encryption_key', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pyas2.PrivateKey')), - ('signature_key', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='org_s', to='pyas2.PrivateKey')), + ( + "name", + models.CharField(max_length=100, verbose_name="Organization Name"), + ), + ( + "as2_name", + models.CharField( + max_length=100, + primary_key=True, + serialize=False, + verbose_name="AS2 Identifier", + ), + ), + ( + "email_address", + models.EmailField(blank=True, max_length=254, null=True), + ), + ( + "confirmation_message", + models.TextField( + blank=True, + help_text="Use this field to send a customized message in the MDN Confirmations for this Organization", + null=True, + verbose_name="Confirmation Message", + ), + ), + ( + "encryption_key", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="pyas2.PrivateKey", + ), + ), + ( + "signature_key", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="org_s", + to="pyas2.PrivateKey", + ), + ), ], ), migrations.CreateModel( - name='Message', + name="Message", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('message_id', models.CharField(max_length=255)), - ('direction', models.CharField(choices=[('IN', 'Inbound'), ('OUT', 'Outbound')], max_length=5)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(choices=[('S', 'Success'), ('E', 'Error'), ('W', 'Warning'), ('P', 'Pending'), ('R', 'Retry')], max_length=2)), - ('detailed_status', models.TextField(null=True)), - ('headers', models.FileField(blank=True, null=True, upload_to=pyas2.models.get_message_store)), - ('payload', models.FileField(blank=True, null=True, upload_to=pyas2.models.get_message_store)), - ('compressed', models.BooleanField(default=False)), - ('encrypted', models.BooleanField(default=False)), - ('signed', models.BooleanField(default=False)), - ('mdn_mode', models.CharField(choices=[('SYNC', 'Synchronous'), ('ASYNC', 'Asynchronous')], max_length=5, null=True)), - ('mic', models.CharField(max_length=100, null=True)), - ('retries', models.IntegerField(null=True)), - ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='pyas2.Organization')), - ('partner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='pyas2.Partner')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message_id", models.CharField(max_length=255)), + ( + "direction", + models.CharField( + choices=[("IN", "Inbound"), ("OUT", "Outbound")], max_length=5 + ), + ), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField( + choices=[ + ("S", "Success"), + ("E", "Error"), + ("W", "Warning"), + ("P", "Pending"), + ("R", "Retry"), + ], + max_length=2, + ), + ), + ("detailed_status", models.TextField(null=True)), + ( + "headers", + models.FileField( + blank=True, null=True, upload_to=pyas2.models.get_message_store + ), + ), + ( + "payload", + models.FileField( + blank=True, null=True, upload_to=pyas2.models.get_message_store + ), + ), + ("compressed", models.BooleanField(default=False)), + ("encrypted", models.BooleanField(default=False)), + ("signed", models.BooleanField(default=False)), + ( + "mdn_mode", + models.CharField( + choices=[("SYNC", "Synchronous"), ("ASYNC", "Asynchronous")], + max_length=5, + null=True, + ), + ), + ("mic", models.CharField(max_length=100, null=True)), + ("retries", models.IntegerField(null=True)), + ( + "organization", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="pyas2.Organization", + ), + ), + ( + "partner", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="pyas2.Partner", + ), + ), ], - options={ - 'unique_together': {('message_id', 'partner')}, - }, + options={"unique_together": {("message_id", "partner")},}, ), migrations.CreateModel( - name='Mdn', + name="Mdn", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('mdn_id', models.CharField(max_length=255)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('status', models.CharField(choices=[('S', 'Sent'), ('R', 'Received'), ('P', 'Pending')], max_length=2)), - ('signed', models.BooleanField(default=False)), - ('return_url', models.URLField(null=True)), - ('headers', models.FileField(blank=True, null=True, upload_to=pyas2.models.get_mdn_store)), - ('payload', models.FileField(blank=True, null=True, upload_to=pyas2.models.get_mdn_store)), - ('message', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='pyas2.Message')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("mdn_id", models.CharField(max_length=255)), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField( + choices=[("S", "Sent"), ("R", "Received"), ("P", "Pending")], + max_length=2, + ), + ), + ("signed", models.BooleanField(default=False)), + ("return_url", models.URLField(null=True)), + ( + "headers", + models.FileField( + blank=True, null=True, upload_to=pyas2.models.get_mdn_store + ), + ), + ( + "payload", + models.FileField( + blank=True, null=True, upload_to=pyas2.models.get_mdn_store + ), + ), + ( + "message", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="pyas2.Message" + ), + ), ], ), ] diff --git a/pyas2/migrations/0002_auto_20190603_1329.py b/pyas2/migrations/0002_auto_20190603_1329.py index 58e2551..ff591ec 100644 --- a/pyas2/migrations/0002_auto_20190603_1329.py +++ b/pyas2/migrations/0002_auto_20190603_1329.py @@ -6,46 +6,48 @@ class Migration(migrations.Migration): dependencies = [ - ('pyas2', '0001_initial'), + ("pyas2", "0001_initial"), ] operations = [ migrations.AddField( - model_name='partner', - name='https_verify_ssl', - field=models.BooleanField(default=True, - help_text='Uncheck this option to disable SSL ' - 'certificate verification to HTTPS.', - verbose_name='Verify SSL Certificate'), + model_name="partner", + name="https_verify_ssl", + field=models.BooleanField( + default=True, + help_text="Uncheck this option to disable SSL " + "certificate verification to HTTPS.", + verbose_name="Verify SSL Certificate", + ), ), migrations.AddField( - model_name='publiccertificate', - name='valid_from', + model_name="publiccertificate", + name="valid_from", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='publiccertificate', - name='valid_to', + model_name="publiccertificate", + name="valid_to", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='privatekey', - name='valid_from', + model_name="privatekey", + name="valid_from", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='privatekey', - name='valid_to', + model_name="privatekey", + name="valid_to", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='privatekey', - name='serial_number', + model_name="privatekey", + name="serial_number", field=models.CharField(blank=True, max_length=64, null=True), ), migrations.AddField( - model_name='publiccertificate', - name='serial_number', + model_name="publiccertificate", + name="serial_number", field=models.CharField(blank=True, max_length=64, null=True), ), ] diff --git a/pyas2/models.py b/pyas2/models.py index b854cc8..85efef5 100644 --- a/pyas2/models.py +++ b/pyas2/models.py @@ -17,30 +17,30 @@ Mdn as As2Mdn, Message as As2Message, Organization as As2Organization, - Partner as As2Partner + Partner as As2Partner, ) from pyas2lib.utils import extract_certificate_info from pyas2 import settings from pyas2.utils import run_post_send -logger = logging.getLogger('pyas2') +logger = logging.getLogger("pyas2") class PrivateKey(models.Model): name = models.CharField(max_length=255) key = models.BinaryField() - key_pass = models.CharField( max_length=100, verbose_name='Private Key Password') + key_pass = models.CharField(max_length=100, verbose_name="Private Key Password") valid_from = models.DateTimeField(null=True, blank=True) valid_to = models.DateTimeField(null=True, blank=True) serial_number = models.CharField(max_length=64, null=True, blank=True) def save(self, *args, **kwargs): cert_info = extract_certificate_info(self.key) - self.valid_from = cert_info['valid_from'] - self.valid_to = cert_info['valid_to'] - if not cert_info['serial'] is None: - self.serial_number = cert_info['serial'].__str__() + self.valid_from = cert_info["valid_from"] + self.valid_to = cert_info["valid_to"] + if not cert_info["serial"] is None: + self.serial_number = cert_info["serial"].__str__() super().save(*args, **kwargs) def __str__(self): @@ -50,20 +50,24 @@ def __str__(self): class PublicCertificate(models.Model): name = models.CharField(max_length=255) certificate = models.BinaryField() - certificate_ca = models.BinaryField(verbose_name=_('Local CA Store'), null=True, blank=True) + certificate_ca = models.BinaryField( + verbose_name=_("Local CA Store"), null=True, blank=True + ) verify_cert = models.BooleanField( - verbose_name=_('Verify Certificate'), default=True, - help_text=_('Uncheck this option to disable certificate verification.')) + verbose_name=_("Verify Certificate"), + default=True, + help_text=_("Uncheck this option to disable certificate verification."), + ) valid_from = models.DateTimeField(null=True, blank=True) valid_to = models.DateTimeField(null=True, blank=True) serial_number = models.CharField(max_length=64, null=True, blank=True) def save(self, *args, **kwargs): cert_info = extract_certificate_info(self.certificate) - self.valid_from = cert_info['valid_from'] - self.valid_to = cert_info['valid_to'] - if not cert_info['serial'] is None: - self.serial_number = cert_info['serial'].__str__() + self.valid_from = cert_info["valid_from"] + self.valid_to = cert_info["valid_to"] + if not cert_info["serial"] is None: + self.serial_number = cert_info["serial"].__str__() super().save(*args, **kwargs) def __str__(self): @@ -71,41 +75,45 @@ def __str__(self): class Organization(models.Model): - name = models.CharField(verbose_name=_('Organization Name'), max_length=100) + name = models.CharField(verbose_name=_("Organization Name"), max_length=100) as2_name = models.CharField( - verbose_name=_('AS2 Identifier'), max_length=100, primary_key=True) + verbose_name=_("AS2 Identifier"), max_length=100, primary_key=True + ) email_address = models.EmailField(null=True, blank=True) encryption_key = models.ForeignKey( - PrivateKey, null=True, blank=True, on_delete=models.SET_NULL) + PrivateKey, null=True, blank=True, on_delete=models.SET_NULL + ) signature_key = models.ForeignKey( - PrivateKey, related_name='org_s', null=True, blank=True, - on_delete=models.SET_NULL + PrivateKey, + related_name="org_s", + null=True, + blank=True, + on_delete=models.SET_NULL, ) confirmation_message = models.TextField( - verbose_name=_('Confirmation Message'), + verbose_name=_("Confirmation Message"), null=True, blank=True, - help_text=_('Use this field to send a customized message in the ' - 'MDN Confirmations for this Organization') + help_text=_( + "Use this field to send a customized message in the " + "MDN Confirmations for this Organization" + ), ) @property def as2org(self): """ Returns an object of pyas2lib's Organization class""" - params = { - 'as2_name': self.as2_name, - 'mdn_url': settings.MDN_URL - } + params = {"as2_name": self.as2_name, "mdn_url": settings.MDN_URL} if self.signature_key: - params['sign_key'] = bytes(self.signature_key.key) - params['sign_key_pass'] = self.signature_key.key_pass + params["sign_key"] = bytes(self.signature_key.key) + params["sign_key_pass"] = self.signature_key.key_pass if self.encryption_key: - params['decrypt_key'] = bytes(self.encryption_key.key) - params['decrypt_key_pass'] = self.encryption_key.key_pass + params["decrypt_key"] = bytes(self.encryption_key.key) + params["decrypt_key_pass"] = self.encryption_key.key_pass if self.confirmation_message: - params['mdn_confirm_text'] = self.confirmation_message + params["mdn_confirm_text"] = self.confirmation_message return As2Organization(**params) @@ -115,130 +123,163 @@ def __str__(self): class Partner(models.Model): CONTENT_TYPE_CHOICES = ( - ('application/EDI-X12', 'application/EDI-X12'), - ('application/EDIFACT', 'application/EDIFACT'), - ('application/edi-consent', 'application/edi-consent'), - ('application/XML', 'application/XML'), + ("application/EDI-X12", "application/EDI-X12"), + ("application/EDIFACT", "application/EDIFACT"), + ("application/edi-consent", "application/edi-consent"), + ("application/XML", "application/XML"), ) ENCRYPT_ALG_CHOICES = ( - ('tripledes_192_cbc', '3DES'), - ('rc2_128_cbc', 'RC2-128'), - ('rc4_128_cbc', 'RC4-128'), - ('aes_128_cbc', 'AES-128'), - ('aes_192_cbc', 'AES-192'), - ('aes_256_cbc', 'AES-256') + ("tripledes_192_cbc", "3DES"), + ("rc2_128_cbc", "RC2-128"), + ("rc4_128_cbc", "RC4-128"), + ("aes_128_cbc", "AES-128"), + ("aes_192_cbc", "AES-192"), + ("aes_256_cbc", "AES-256"), ) SIGN_ALG_CHOICES = ( - ('sha1', 'SHA-1'), - ('sha224', 'SHA-224'), - ('sha256', 'SHA-256'), - ('sha384', 'SHA-384'), - ('sha512', 'SHA-512') + ("sha1", "SHA-1"), + ("sha224", "SHA-224"), + ("sha256", "SHA-256"), + ("sha384", "SHA-384"), + ("sha512", "SHA-512"), ) MDN_TYPE_CHOICES = ( - ('SYNC', 'Synchronous'), - ('ASYNC', 'Asynchronous'), + ("SYNC", "Synchronous"), + ("ASYNC", "Asynchronous"), ) - name = models.CharField(verbose_name=_('Partner Name'), max_length=100) - as2_name = models.CharField(verbose_name=_('AS2 Identifier'), max_length=100, primary_key=True) + name = models.CharField(verbose_name=_("Partner Name"), max_length=100) + as2_name = models.CharField( + verbose_name=_("AS2 Identifier"), max_length=100, primary_key=True + ) email_address = models.EmailField(null=True, blank=True) - http_auth = models.BooleanField(verbose_name=_('Enable Authentication'), default=False) + http_auth = models.BooleanField( + verbose_name=_("Enable Authentication"), default=False + ) http_auth_user = models.CharField(max_length=100, null=True, blank=True) http_auth_pass = models.CharField(max_length=100, null=True, blank=True) https_verify_ssl = models.BooleanField( - verbose_name=_('Verify SSL Certificate'), default=True, - help_text=_('Uncheck this option to disable SSL certificate verification to HTTPS.')) + verbose_name=_("Verify SSL Certificate"), + default=True, + help_text=_( + "Uncheck this option to disable SSL certificate verification to HTTPS." + ), + ) target_url = models.URLField() - subject = models.CharField(max_length=255, default=_('EDI Message sent using pyas2')) + subject = models.CharField( + max_length=255, default=_("EDI Message sent using pyas2") + ) content_type = models.CharField( - max_length=100, choices=CONTENT_TYPE_CHOICES, default='application/edi-consent') + max_length=100, choices=CONTENT_TYPE_CHOICES, default="application/edi-consent" + ) - compress = models.BooleanField(verbose_name=_('Compress Message'), default=False) + compress = models.BooleanField(verbose_name=_("Compress Message"), default=False) encryption = models.CharField( - max_length=20, verbose_name=_('Encrypt Message'), - choices=ENCRYPT_ALG_CHOICES, null=True, blank=True) + max_length=20, + verbose_name=_("Encrypt Message"), + choices=ENCRYPT_ALG_CHOICES, + null=True, + blank=True, + ) encryption_cert = models.ForeignKey( - PublicCertificate, null=True, blank=True, on_delete=models.SET_NULL) + PublicCertificate, null=True, blank=True, on_delete=models.SET_NULL + ) signature = models.CharField( - max_length=20, verbose_name=_('Sign Message'), - choices=SIGN_ALG_CHOICES, null=True, blank=True) + max_length=20, + verbose_name=_("Sign Message"), + choices=SIGN_ALG_CHOICES, + null=True, + blank=True, + ) signature_cert = models.ForeignKey( - PublicCertificate, related_name='partner_s', null=True, blank=True, - on_delete=models.SET_NULL + PublicCertificate, + related_name="partner_s", + null=True, + blank=True, + on_delete=models.SET_NULL, ) - mdn = models.BooleanField(verbose_name=_('Request MDN'), default=False) - mdn_mode = models.CharField(max_length=20, choices=MDN_TYPE_CHOICES, null=True, blank=True) + mdn = models.BooleanField(verbose_name=_("Request MDN"), default=False) + mdn_mode = models.CharField( + max_length=20, choices=MDN_TYPE_CHOICES, null=True, blank=True + ) mdn_sign = models.CharField( - max_length=20, verbose_name=_('Request Signed MDN'), - choices=SIGN_ALG_CHOICES, null=True, blank=True) + max_length=20, + verbose_name=_("Request Signed MDN"), + choices=SIGN_ALG_CHOICES, + null=True, + blank=True, + ) confirmation_message = models.TextField( - verbose_name=_('Confirmation Message'), + verbose_name=_("Confirmation Message"), null=True, blank=True, help_text=_( - 'Use this field to send a customized message in the MDN ' - 'Confirmations for this Partner') + "Use this field to send a customized message in the MDN " + "Confirmations for this Partner" + ), ) keep_filename = models.BooleanField( - verbose_name=_('Keep Original Filename'), + verbose_name=_("Keep Original Filename"), default=False, help_text=_( - 'Use Original Filename to to store file on receipt, use this option ' - 'only if you are sure partner sends unique names') + "Use Original Filename to to store file on receipt, use this option " + "only if you are sure partner sends unique names" + ), ) cmd_send = models.TextField( - verbose_name=_('Command on Message Send'), + verbose_name=_("Command on Message Send"), null=True, blank=True, help_text=_( - 'Command executed after successful message send, replacements are ' - '$filename, $sender, $recevier, $messageid and any message header ' - 'such as $Subject') + "Command executed after successful message send, replacements are " + "$filename, $sender, $recevier, $messageid and any message header " + "such as $Subject" + ), ) cmd_receive = models.TextField( - verbose_name=_('Command on Message Receipt'), + verbose_name=_("Command on Message Receipt"), null=True, blank=True, help_text=_( - 'Command executed after successful message receipt, replacements ' - 'are $filename, $fullfilename, $sender, $recevier, $messageid and ' - 'any message header such as $Subject') + "Command executed after successful message receipt, replacements " + "are $filename, $fullfilename, $sender, $recevier, $messageid and " + "any message header such as $Subject" + ), ) @property def as2partner(self): """ Returns an object of pyas2lib's Partner class""" params = { - 'as2_name': self.as2_name, - 'compress': self.compress, - 'sign': True if self.signature else False, - 'digest_alg': self.signature, - 'encrypt': True if self.encryption else False, - 'enc_alg': self.encryption, - 'mdn_mode': self.mdn_mode, - 'mdn_digest_alg': self.mdn_sign + "as2_name": self.as2_name, + "compress": self.compress, + "sign": True if self.signature else False, + "digest_alg": self.signature, + "encrypt": True if self.encryption else False, + "enc_alg": self.encryption, + "mdn_mode": self.mdn_mode, + "mdn_digest_alg": self.mdn_sign, } if self.signature_cert: - params['verify_cert'] = bytes(self.signature_cert.certificate) + params["verify_cert"] = bytes(self.signature_cert.certificate) if self.signature_cert.certificate_ca: - params['verify_cert_ca'] = bytes(self.signature_cert.certificate_ca) - params['validate_certs'] = self.signature_cert.verify_cert + params["verify_cert_ca"] = bytes(self.signature_cert.certificate_ca) + params["validate_certs"] = self.signature_cert.verify_cert if self.encryption_cert: - params['encrypt_cert'] = bytes(self.encryption_cert.certificate) + params["encrypt_cert"] = bytes(self.encryption_cert.certificate) if self.encryption_cert.certificate_ca: - params['encrypt_cert_ca'] = bytes(self.encryption_cert.certificate_ca) - params['validate_certs'] = self.encryption_cert.verify_cert + params["encrypt_cert_ca"] = bytes(self.encryption_cert.certificate_ca) + params["validate_certs"] = self.encryption_cert.verify_cert if self.confirmation_message: - params['mdn_confirm_text'] = self.confirmation_message + params["mdn_confirm_text"] = self.confirmation_message return As2Partner(**params) @@ -247,12 +288,18 @@ def __str__(self): class MessageManager(models.Manager): - - def create_from_as2message(self, as2message, payload, direction, status, filename=None, - detailed_status=None): + def create_from_as2message( + self, + as2message, + payload, + direction, + status, + filename=None, + detailed_status=None, + ): """Create the Message from the pyas2lib's Message object""" - if direction == 'IN': + if direction == "IN": organization = as2message.receiver.as2_name if as2message.receiver else None partner = as2message.sender.as2_name if as2message.sender else None else: @@ -269,56 +316,65 @@ def create_from_as2message(self, as2message, payload, direction, status, filenam compressed=as2message.compressed, encrypted=as2message.encrypted, signed=as2message.signed, - detailed_status=detailed_status - ) + detailed_status=detailed_status, + ), ) # Save the headers and payload to store if not filename: - filename = f'{uuid4()}.msg' - message.headers.save(name=f'{filename}.header', content=ContentFile(as2message.headers_str)) + filename = f"{uuid4()}.msg" + message.headers.save( + name=f"{filename}.header", content=ContentFile(as2message.headers_str) + ) message.payload.save(name=filename, content=ContentFile(payload)) # Save the payload to the inbox folder full_filename = None - if direction == 'IN' and status == 'S': + if direction == "IN" and status == "S": if settings.DATA_DIR: dirname = os.path.join( - settings.DATA_DIR, 'messages', organization, 'inbox', partner) + settings.DATA_DIR, "messages", organization, "inbox", partner + ) else: - dirname = os.path.join('messages', organization, 'inbox', partner) + dirname = os.path.join("messages", organization, "inbox", partner) if not message.partner.keep_filename or not filename: - filename = f'{message.message_id}.msg' - full_filename = default_storage.generate_filename(posixpath.join(dirname, filename)) + filename = f"{message.message_id}.msg" + full_filename = default_storage.generate_filename( + posixpath.join(dirname, filename) + ) default_storage.save(name=full_filename, content=ContentFile(payload)) return message, full_filename def get_message_store(instance, filename): - current_date = timezone.now().strftime('%Y%m%d') - if instance.direction == 'OUT': - target_dir = os.path.join('messages', '__store', 'payload', 'sent', current_date) + current_date = timezone.now().strftime("%Y%m%d") + if instance.direction == "OUT": + target_dir = os.path.join( + "messages", "__store", "payload", "sent", current_date + ) else: - target_dir = os.path.join('messages', '__store', 'payload', 'received', current_date) - return '{0}/{1}'.format(target_dir, filename) + target_dir = os.path.join( + "messages", "__store", "payload", "received", current_date + ) + return "{0}/{1}".format(target_dir, filename) class Message(models.Model): DIRECTION_CHOICES = ( - ('IN', _('Inbound')), - ('OUT', _('Outbound')), + ("IN", _("Inbound")), + ("OUT", _("Outbound")), ) STATUS_CHOICES = ( - ('S', _('Success')), - ('E', _('Error')), - ('W', _('Warning')), - ('P', _('Pending')), - ('R', _('Retry')), + ("S", _("Success")), + ("E", _("Error")), + ("W", _("Warning")), + ("P", _("Pending")), + ("R", _("Retry")), ) MODE_CHOICES = ( - ('SYNC', _('Synchronous')), - ('ASYNC', _('Asynchronous')), + ("SYNC", _("Synchronous")), + ("ASYNC", _("Asynchronous")), ) message_id = models.CharField(max_length=255) @@ -346,15 +402,19 @@ class Message(models.Model): objects = MessageManager() class Meta: - unique_together = ('message_id', 'partner') + unique_together = ("message_id", "partner") @property def as2message(self): """ Returns an object of pyas2lib's Message class""" - if self.direction == 'IN': - as2m = As2Message(sender=self.partner.as2partner, receiver=self.organization.as2org) + if self.direction == "IN": + as2m = As2Message( + sender=self.partner.as2partner, receiver=self.organization.as2org + ) else: - as2m = As2Message(sender=self.organization.as2org, receiver=self.partner.as2partner) + as2m = As2Message( + sender=self.organization.as2org, receiver=self.partner.as2partner + ) as2m.message_id = self.message_id as2m.mic = self.mic @@ -364,19 +424,21 @@ def as2message(self): @property def status_icon(self): """ Return the icon for message status """ - if self.status == 'S': - return 'admin/img/icon-yes.svg' - elif self.status == 'E': - return 'admin/img/icon-no.svg' - elif self.status in ['W', 'P', 'R']: - return 'admin/img/icon-alert.svg' + if self.status == "S": + return "admin/img/icon-yes.svg" + elif self.status == "E": + return "admin/img/icon-no.svg" + elif self.status in ["W", "P", "R"]: + return "admin/img/icon-alert.svg" else: - return 'admin/img/icon-unknown.svg' + return "admin/img/icon-unknown.svg" def send_message(self, header, payload): """ Send the message to the partner""" - logger.info(f'Sending message {self.message_id} from organization "{self.organization}" ' - f'to partner "{self.partner}".') + logger.info( + f'Sending message {self.message_id} from organization "{self.organization}" ' + f'to partner "{self.partner}".' + ) # Set up the http auth if specified in the partner profile auth = None @@ -386,50 +448,66 @@ def send_message(self, header, payload): # Send the message to the partner try: response = requests.post( - self.partner.target_url, auth=auth, headers=header, data=payload, - verify=self.partner.https_verify_ssl) + self.partner.target_url, + auth=auth, + headers=header, + data=payload, + verify=self.partner.https_verify_ssl, + ) response.raise_for_status() except requests.exceptions.RequestException as e: - self.status = 'R' - self.detailed_status = f'Failed to send message, error:\n{traceback.format_exc()}' + self.status = "R" + self.detailed_status = ( + f"Failed to send message, error:\n{traceback.format_exc()}" + ) self.save() return # Process the MDN based on the partner profile settings if self.partner.mdn: - if self.partner.mdn_mode == 'ASYNC': - self.status = 'P' + if self.partner.mdn_mode == "ASYNC": + self.status = "P" else: # Process the synchronous MDN received as response # Get the response headers, convert key to lower case # for normalization mdn_headers = dict( - (k.lower().replace('_', '-'), response.headers[k]) for k in response.headers) + (k.lower().replace("_", "-"), response.headers[k]) + for k in response.headers + ) # create the mdn content with message-id and content-type # header and response content - mdn_content = f'message-id: {mdn_headers.get("message-id", self.message_id)}\n' + mdn_content = ( + f'message-id: {mdn_headers.get("message-id", self.message_id)}\n' + ) mdn_content += f'content-type: {mdn_headers["content-type"]}\n\n' - mdn_content = mdn_content.encode('utf-8') + response.content + mdn_content = mdn_content.encode("utf-8") + response.content # Parse the as2 mdn received - logger.debug(f'Received MDN response for message {self.message_id} ' - f'with content: {mdn_content}') + logger.debug( + f"Received MDN response for message {self.message_id} " + f"with content: {mdn_content}" + ) as2mdn = As2Mdn() - status, detailed_status = as2mdn.parse(mdn_content, lambda x, y: self.as2message) + status, detailed_status = as2mdn.parse( + mdn_content, lambda x, y: self.as2message + ) # Update the message status and return the response - if status == 'processed': - self.status = 'S' + if status == "processed": + self.status = "S" run_post_send(self) else: - self.status = 'E' - self.detailed_status = f'Partner failed to process message: {detailed_status}' - Mdn.objects.create_from_as2mdn(as2mdn=as2mdn, message=self, status='R') + self.status = "E" + self.detailed_status = ( + f"Partner failed to process message: {detailed_status}" + ) + Mdn.objects.create_from_as2mdn(as2mdn=as2mdn, message=self, status="R") else: # No MDN requested mark message as success and run command - self.status = 'S' + self.status = "S" run_post_send(self) self.save() @@ -439,7 +517,6 @@ def __str__(self): class MdnManager(models.Manager): - def create_from_as2mdn(self, as2mdn, message, status, return_url=None): """Create the MDN from the pyas2lib's MDN object""" signed = True if as2mdn.digest_alg else False @@ -449,30 +526,34 @@ def create_from_as2mdn(self, as2mdn, message, status, return_url=None): mdn_id=as2mdn.message_id, status=status, signed=signed, - return_url=return_url - ) + return_url=return_url, + ), + ) + filename = f"{uuid4()}.mdn" + mdn.headers.save( + name=f"{filename}.header", content=ContentFile(as2mdn.headers_str) ) - filename = f'{uuid4()}.mdn' - mdn.headers.save(name=f'{filename}.header', content=ContentFile(as2mdn.headers_str)) mdn.payload.save(filename, content=ContentFile(as2mdn.content)) return mdn def get_mdn_store(instance, filename): - current_date = timezone.now().strftime('%Y%m%d') - if instance.status == 'S': - target_dir = os.path.join('messages', '__store', 'mdn', 'sent', current_date) + current_date = timezone.now().strftime("%Y%m%d") + if instance.status == "S": + target_dir = os.path.join("messages", "__store", "mdn", "sent", current_date) else: - target_dir = os.path.join('messages', '__store', 'mdn', 'received', current_date) + target_dir = os.path.join( + "messages", "__store", "mdn", "received", current_date + ) - return '{0}/{1}'.format(target_dir, filename) + return "{0}/{1}".format(target_dir, filename) class Mdn(models.Model): STATUS_CHOICES = ( - ('S', _('Sent')), - ('R', _('Received')), - ('P', _('Pending')), + ("S", _("Sent")), + ("R", _("Received")), + ("P", _("Pending")), ) mdn_id = models.CharField(max_length=255) @@ -500,11 +581,12 @@ def send_async_mdn(self): # Send the mdn to the partner try: response = requests.post( - self.return_url, headers=dict(headers.items()), data=self.payload.read()) + self.return_url, headers=dict(headers.items()), data=self.payload.read() + ) response.raise_for_status() except requests.exceptions.RequestException: return # Update the status of the MDN - self.status = 'S' + self.status = "S" self.save() diff --git a/pyas2/settings.py b/pyas2/settings.py index 252555b..1977b56 100644 --- a/pyas2/settings.py +++ b/pyas2/settings.py @@ -2,22 +2,21 @@ from django.conf import settings -APP_SETTINGS = getattr(settings, 'PYAS2', {}) +APP_SETTINGS = getattr(settings, "PYAS2", {}) # Get the root directory for saving messages DATA_DIR = None -if APP_SETTINGS.get('DATA_DIR') and os.path.isdir(APP_SETTINGS['DATA_DIR']): - DATA_DIR = APP_SETTINGS['DATA_DIR'] +if APP_SETTINGS.get("DATA_DIR") and os.path.isdir(APP_SETTINGS["DATA_DIR"]): + DATA_DIR = APP_SETTINGS["DATA_DIR"] # Max number of times to retry failed sends -MAX_RETRIES = APP_SETTINGS.get('MAX_RETRIES', 5) +MAX_RETRIES = APP_SETTINGS.get("MAX_RETRIES", 5) # URL for receiving asynchronous MDN from partners -MDN_URL = APP_SETTINGS.get('MDN_URL', 'http://localhost:8080/pyas2/as2receive') +MDN_URL = APP_SETTINGS.get("MDN_URL", "http://localhost:8080/pyas2/as2receive") # Max time to wait for asynchronous MDN in minutes -ASYNC_MDN_WAIT = APP_SETTINGS.get('ASYNC_MDN_WAIT', 30) +ASYNC_MDN_WAIT = APP_SETTINGS.get("ASYNC_MDN_WAIT", 30) # Max number of days worth of messages to be saved in archive -MAX_ARCH_DAYS = APP_SETTINGS.get('MAX_ARCH_DAYS', 30) - +MAX_ARCH_DAYS = APP_SETTINGS.get("MAX_ARCH_DAYS", 30) diff --git a/pyas2/templatetags/pyas2.py b/pyas2/templatetags/pyas2.py index fe6a4e4..9e48a52 100644 --- a/pyas2/templatetags/pyas2.py +++ b/pyas2/templatetags/pyas2.py @@ -7,5 +7,5 @@ @register.filter def readfilefield(field): """ Template filter for rendering data from a file field """ - with field.open('r') as f: + with field.open("r") as f: return f.read() diff --git a/pyas2/tests/test_advanced.py b/pyas2/tests/test_advanced.py index d76ffdc..ca61165 100644 --- a/pyas2/tests/test_advanced.py +++ b/pyas2/tests/test_advanced.py @@ -16,75 +16,70 @@ from pyas2.models import PublicCertificate from pyas2.tests.test_basic import SendMessageMock -TEST_DIR = os.path.join((os.path.dirname( - os.path.abspath(__file__))), 'fixtures') +TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), "fixtures") class AdvancedTestCases(TestCase): """Test cases dealing with handling of failures and other features""" + @classmethod def setUpTestData(cls): # Every test needs a client. cls.client = Client() # Load the client and server certificates - with open(os.path.join(TEST_DIR, 'server_private.pem'), 'rb') as fp: - cls.server_key = PrivateKey.objects.create( - key=fp.read(), key_pass='test') + with open(os.path.join(TEST_DIR, "server_private.pem"), "rb") as fp: + cls.server_key = PrivateKey.objects.create(key=fp.read(), key_pass="test") - with open(os.path.join(TEST_DIR, 'server_public.pem'), 'rb') as fp: - cls.server_crt = PublicCertificate.objects.create( - certificate=fp.read()) + with open(os.path.join(TEST_DIR, "server_public.pem"), "rb") as fp: + cls.server_crt = PublicCertificate.objects.create(certificate=fp.read()) - with open(os.path.join(TEST_DIR, 'client_private.pem'), 'rb') as fp: - cls.client_key = PrivateKey.objects.create( - key=fp.read(), key_pass='test') + with open(os.path.join(TEST_DIR, "client_private.pem"), "rb") as fp: + cls.client_key = PrivateKey.objects.create(key=fp.read(), key_pass="test") - with open(os.path.join(TEST_DIR, 'client_public.pem'), 'rb') as fp: - cls.client_crt = PublicCertificate.objects.create( - certificate=fp.read() - ) + with open(os.path.join(TEST_DIR, "client_public.pem"), "rb") as fp: + cls.client_crt = PublicCertificate.objects.create(certificate=fp.read()) def setUp(self): # Setup the server organization and partner Organization.objects.create( - name='AS2 Server', - as2_name='as2server', + name="AS2 Server", + as2_name="as2server", encryption_key=self.server_key, - signature_key=self.server_key + signature_key=self.server_key, ) self.partner = Partner.objects.create( - name='AS2 Client', - as2_name='as2client', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Client", + as2_name="as2client", + target_url="http://localhost:8080/pyas2/as2receive", compress=False, mdn=False, signature_cert=self.client_crt, - encryption_cert=self.client_crt + encryption_cert=self.client_crt, ) # Setup the client organization and partner self.organization = Organization.objects.create( - name='AS2 Client', - as2_name='as2client', + name="AS2 Client", + as2_name="as2client", encryption_key=self.client_key, - signature_key=self.client_key + signature_key=self.client_key, ) # Initialise the payload i.e. the file to be transmitted - with open(os.path.join(TEST_DIR, 'testmessage.edi'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "testmessage.edi"), "rb") as fp: self.payload = fp.read() @classmethod def tearDownClass(cls): # remove all files in the inbox folders - inbox = os.path.join('messages', 'as2server', 'inbox', 'as2client') + inbox = os.path.join("messages", "as2server", "inbox", "as2client") try: files = os.listdir(inbox) except OSError: - files = [] - for the_file in files: + files = [] + for the_file in files: file_path = os.path.join(inbox, the_file) if os.path.isfile(file_path): os.unlink(file_path) @@ -99,56 +94,59 @@ def test_post_send_command(self): """ Test that the command after successful send gets executed.""" partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', - cmd_send='touch %s/$messageid.sent' % TEST_DIR + mdn_mode="SYNC", + mdn_sign="sha1", + cmd_send="touch %s/$messageid.sent" % TEST_DIR, ) in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check that the command got executed - touch_file = os.path.join(TEST_DIR, '%s.sent' % in_message.message_id) + touch_file = os.path.join(TEST_DIR, "%s.sent" % in_message.message_id) self.assertTrue(os.path.exists(touch_file)) os.remove(touch_file) - @mock.patch('requests.post') + @mock.patch("requests.post") def test_post_send_command_async(self, mock_request): """ Test that the command after successful send gets executed with asynchronous MDN.""" partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='ASYNC', - mdn_sign='sha1', - cmd_send='touch %s/$messageid.sent' % TEST_DIR + mdn_mode="ASYNC", + mdn_sign="sha1", + cmd_send="touch %s/$messageid.sent" % TEST_DIR, ) in_message = self.build_and_send(partner) # Send the async mdn to the sender out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') + message_id=in_message.message_id, direction="IN" + ) mock_request.side_effect = SendMessageMock(self.client) out_message.mdn.send_async_mdn() # Check that the command got executed in_message.refresh_from_db() - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check that the command got executed - touch_file = os.path.join(TEST_DIR, '%s.sent' % in_message.message_id) + touch_file = os.path.join(TEST_DIR, "%s.sent" % in_message.message_id) self.assertTrue(os.path.exists(touch_file)) os.remove(touch_file) @@ -156,27 +154,29 @@ def test_post_receive_command(self): """ Test that the command after successful receive gets executed.""" # add the post receive command and save it - self.partner.cmd_receive = 'touch %s/$filename.received' % TEST_DIR + self.partner.cmd_receive = "touch %s/$filename.received" % TEST_DIR self.partner.save() # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check that the command got executed touch_file = os.path.join( - TEST_DIR, '%s.msg.received' % in_message.message_id.replace("@", "")) + TEST_DIR, "%s.msg.received" % in_message.message_id.replace("@", "") + ) self.assertTrue(os.path.exists(touch_file)) os.remove(touch_file) @@ -185,255 +185,266 @@ def test_use_received_filename(self): the file.""" # add the post receive command and save it - self.partner.cmd_receive = 'touch %s/$filename.received' % TEST_DIR + self.partner.cmd_receive = "touch %s/$filename.received" % TEST_DIR self.partner.keep_filename = True self.partner.save() # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check that the command got executed - touch_file = os.path.join(TEST_DIR, 'testmessage.edi.received') + touch_file = os.path.join(TEST_DIR, "testmessage.edi.received") self.assertTrue(os.path.exists(touch_file)) os.remove(touch_file) - @mock.patch('requests.post') + @mock.patch("requests.post") def test_duplicate_error(self, mock_request): partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) # Send the message once as2message = As2Message( - sender=self.organization.as2org, - receiver=partner.as2partner) + sender=self.organization.as2org, receiver=partner.as2partner + ) as2message.build( self.payload, - filename='testmessage.edi', + filename="testmessage.edi", subject=partner.subject, - content_type=partner.content_type + content_type=partner.content_type, ) in_message, _ = Message.objects.create_from_as2message( - as2message=as2message, - payload=self.payload, - direction='OUT', - status='P' + 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') + 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') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") # send it again to cause duplicate error in_message.send_message(as2message.headers, as2message.content) # Make sure out message was created - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") out_message = Message.objects.get( - message_id=in_message.message_id + '_duplicate', direction='IN') - self.assertEqual(out_message.status, 'E') + message_id=in_message.message_id + "_duplicate", direction="IN" + ) + self.assertEqual(out_message.status, "E") def test_org_missing_error(self): # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server2', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + name="AS2 Server", + as2_name="as2server2", + target_url="http://localhost:8080/pyas2/as2receive", + signature="sha1", signature_cert=self.server_crt, - encryption='tripledes_192_cbc', + encryption="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') - self.assertTrue( - 'Unknown AS2 organization' in out_message.detailed_status) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") + self.assertTrue("Unknown AS2 organization" in out_message.detailed_status) def test_partner_missing_error(self): - self.organization.as2_name = 'as2partner2' + self.organization.as2_name = "as2partner2" self.organization.save() # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') - self.assertTrue( - 'Unknown AS2 partner' in out_message.detailed_status) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") + self.assertTrue("Unknown AS2 partner" in out_message.detailed_status) def test_insufficient_security_error(self): - self.partner.encryption = 'tripledes_192_cbc' - self.partner.signature = 'sha1' + self.partner.encryption = "tripledes_192_cbc" + self.partner.signature = "sha1" self.partner.save() # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", signature_cert=self.server_crt, - encryption='tripledes_192_cbc', + encryption="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') - self.assertTrue( - 'signed message not found' in out_message.detailed_status) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") + self.assertTrue("signed message not found" in out_message.detailed_status) # Create the client partner and send the command - partner.encryption = '' + partner.encryption = "" partner.save() in_message = self.build_and_send(partner) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') - self.assertTrue( - 'encrypted message not found' in out_message.detailed_status) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") + self.assertTrue("encrypted message not found" in out_message.detailed_status) def test_decompression_error(self): # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", compress=True, signature_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner, smudge=True) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') - self.assertTrue( - 'Decompression failed' in out_message.detailed_status) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") + self.assertTrue("Decompression failed" in out_message.detailed_status) def test_encryption_error(self): # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - encryption='tripledes_192_cbc', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + encryption="tripledes_192_cbc", encryption_cert=self.client_crt, signature_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner, smudge=False) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') - self.assertTrue( - 'Failed to decrypt' in out_message.detailed_status) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") + self.assertTrue("Failed to decrypt" in out_message.detailed_status) def test_signature_error(self): # Create the client partner and send the command partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + signature="sha1", signature_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner, smudge=True) - self.assertEqual(in_message.status, 'E') + self.assertEqual(in_message.status, "E") # Check the status of the received message out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'E') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "E") self.assertTrue( - 'Failed to verify message signature' in out_message.detailed_status) + "Failed to verify message signature" in out_message.detailed_status + ) def test_sendmessage_command(self): """ Test the command for an sending as2 message """ - test_message = os.path.join(TEST_DIR, 'testmessage.edi') + test_message = os.path.join(TEST_DIR, "testmessage.edi") # Try to run with invalid org and client with self.assertRaises(management.CommandError): management.call_command( - 'sendas2message', 'AS2 Server', 'AS2 Client', test_message) + "sendas2message", "AS2 Server", "AS2 Client", test_message + ) management.call_command( - 'sendas2message', 'as2server', 'as2client', test_message) + "sendas2message", "as2server", "as2client", test_message + ) def test_sendbulk_command(self): """ Test the command for sending all files in the outbox folder """ # Create a file for testing - outbox_dir = os.path.join('messages', 'as2client', 'outbox', 'as2server') + outbox_dir = os.path.join("messages", "as2client", "outbox", "as2server") try: os.makedirs(outbox_dir) except FileExistsError: pass - test_file = Path(os.path.join(outbox_dir, 'testmessage.edi')) + test_file = Path(os.path.join(outbox_dir, "testmessage.edi")) test_file.touch() - management.call_command('sendas2bulk') + management.call_command("sendas2bulk") self.assertFalse(test_file.exists()) def test_manageserver_command(self): @@ -441,72 +452,66 @@ def test_manageserver_command(self): settings.MAX_ARCH_DAYS = -1 # Create a message as2message = As2Message( - sender=self.organization.as2org, - receiver=self.partner.as2partner) + sender=self.organization.as2org, receiver=self.partner.as2partner + ) as2message.build( self.payload, - filename='testmessage.edi', + filename="testmessage.edi", subject=self.partner.subject, - content_type=self.partner.content_type + content_type=self.partner.content_type, ) out_message, _ = Message.objects.create_from_as2message( - as2message=as2message, - payload=self.payload, - direction='OUT', - status='P' + as2message=as2message, payload=self.payload, direction="OUT", status="P" ) out_message.send_message(as2message.headers, as2message.content) # Test the retry command out_message.refresh_from_db() - self.assertEqual(out_message.status, 'R') - management.call_command('manageas2server', retry=True) + self.assertEqual(out_message.status, "R") + management.call_command("manageas2server", retry=True) out_message.refresh_from_db() self.assertEqual(out_message.retries, 1) # Test max retry setting settings.MAX_RETRIES = 1 - management.call_command('manageas2server', retry=True) + management.call_command("manageas2server", retry=True) out_message.refresh_from_db() self.assertEqual(out_message.retries, 2) - self.assertEqual(out_message.status, 'E') + self.assertEqual(out_message.status, "E") # Test the async mdn command for outbound messages - out_message.status = 'P' + out_message.status = "P" out_message.save() settings.ASYNC_MDN_WAIT = 0 - management.call_command('manageas2server', async_mdns=True) + management.call_command("manageas2server", async_mdns=True) out_message.refresh_from_db() - self.assertEqual(out_message.status, 'E') + self.assertEqual(out_message.status, "E") # Test the clean command - management.call_command('manageas2server', clean=True) + management.call_command("manageas2server", clean=True) self.assertEqual( - Message.objects.filter( - message_id=out_message.message_id).count(), 0) + Message.objects.filter(message_id=out_message.message_id).count(), 0 + ) - @mock.patch('requests.post') + @mock.patch("requests.post") def build_and_send(self, partner, mock_request, smudge=False): # Build and send the message to server as2message = As2Message( - sender=self.organization.as2org, - receiver=partner.as2partner) + sender=self.organization.as2org, receiver=partner.as2partner + ) as2message.build( self.payload, - filename='testmessage.edi', + filename="testmessage.edi", subject=partner.subject, - content_type=partner.content_type + content_type=partner.content_type, ) out_message, _ = Message.objects.create_from_as2message( - as2message=as2message, - payload=self.payload, - direction='OUT', - status='P' + as2message=as2message, payload=self.payload, direction="OUT", status="P" ) mock_request.side_effect = SendMessageMock(self.client) out_message.send_message( as2message.headers, - b'xxxx' + as2message.content if smudge else as2message.content + b"xxxx" + as2message.content if smudge else as2message.content, ) return out_message diff --git a/pyas2/tests/test_basic.py b/pyas2/tests/test_basic.py index 10ea022..c2d5a34 100644 --- a/pyas2/tests/test_basic.py +++ b/pyas2/tests/test_basic.py @@ -5,17 +5,24 @@ from django.test import TestCase, Client from requests import Response -from pyas2.models import PrivateKey, PublicCertificate, Organization, Partner, \ - Message, Mdn +from pyas2.models import ( + PrivateKey, + PublicCertificate, + Organization, + Partner, + Message, + Mdn, +) from pyas2lib.as2 import Message as As2Message -TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), 'fixtures') +TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), "fixtures") class BasicServerClientTestCase(TestCase): """Test cases for the AS2 server and client. We will be testing each permutation as defined in RFC 4130 Section 2.4.2 """ + @classmethod def setUpTestData(cls): # Every test needs a client. @@ -23,56 +30,50 @@ def setUpTestData(cls): cls.header_parser = HeaderParser() # Load the client and server certificates - with open(os.path.join(TEST_DIR, 'server_private.pem'), 'rb') as fp: - cls.server_key = PrivateKey.objects.create( - key=fp.read(), key_pass='test') + with open(os.path.join(TEST_DIR, "server_private.pem"), "rb") as fp: + cls.server_key = PrivateKey.objects.create(key=fp.read(), key_pass="test") - with open(os.path.join(TEST_DIR, 'server_public.pem'), 'rb') as fp: - cls.server_crt = PublicCertificate.objects.create( - certificate=fp.read()) + with open(os.path.join(TEST_DIR, "server_public.pem"), "rb") as fp: + cls.server_crt = PublicCertificate.objects.create(certificate=fp.read()) - with open(os.path.join(TEST_DIR, 'client_private.pem'), 'rb') as fp: - cls.client_key = PrivateKey.objects.create( - key=fp.read(), key_pass='test') + with open(os.path.join(TEST_DIR, "client_private.pem"), "rb") as fp: + cls.client_key = PrivateKey.objects.create(key=fp.read(), key_pass="test") - with open(os.path.join(TEST_DIR, 'client_public.pem'), 'rb') as fp: - cls.client_crt = PublicCertificate.objects.create( - certificate=fp.read() - ) + with open(os.path.join(TEST_DIR, "client_public.pem"), "rb") as fp: + cls.client_crt = PublicCertificate.objects.create(certificate=fp.read()) # Setup the server organization and partner Organization.objects.create( - name='AS2 Server', - as2_name='as2server', + name="AS2 Server", + as2_name="as2server", encryption_key=cls.server_key, - signature_key=cls.server_key + signature_key=cls.server_key, ) Partner.objects.create( - name='AS2 Client', - as2_name='as2client', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Client", + as2_name="as2client", + target_url="http://localhost:8080/pyas2/as2receive", compress=False, mdn=False, signature_cert=cls.client_crt, - encryption_cert=cls.client_crt + encryption_cert=cls.client_crt, ) # Setup the client organization and partner cls.organization = Organization.objects.create( - name='AS2 Client', - as2_name='as2client', + name="AS2 Client", + as2_name="as2client", encryption_key=cls.client_key, - signature_key=cls.client_key + signature_key=cls.client_key, ) # Initialise the payload i.e. the file to be transmitted - with open(os.path.join(TEST_DIR, 'testmessage.edi'), 'rb') as fp: + with open(os.path.join(TEST_DIR, "testmessage.edi"), "rb") as fp: cls.payload = fp.read() def tearDown(self): # remove all files in the inbox folders - inbox = os.path.join( - 'messages', 'as2server', 'inbox', 'as2client') + inbox = os.path.join("messages", "as2server", "inbox", "as2client") for the_file in os.listdir(inbox): file_path = os.path.join(inbox, the_file) if os.path.isfile(file_path): @@ -88,7 +89,7 @@ def tearDown(self): def testEndpoint(self): """ Test if the as2 reveive endpoint is active """ - response = self.client.get('/pyas2/as2receive') + response = self.client.get("/pyas2/as2receive") self.assertEqual(response.status_code, 200) def testNoEncryptMessageNoMdn(self): @@ -97,22 +98,23 @@ def testNoEncryptMessageNoMdn(self): # Create the partner with appropriate settings for this case partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - mdn=False + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + mdn=False, ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') - self.assertFalse(hasattr(in_message, 'mdn')) + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") + self.assertFalse(hasattr(in_message, "mdn")) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testNoEncryptMessageMdn(self): @@ -121,25 +123,26 @@ def testNoEncryptMessageMdn(self): # Create the partner with appropriate settings for this case partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", mdn=True, - mdn_mode='SYNC' + mdn_mode="SYNC", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') - self.assertEqual(in_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) self.assertFalse(in_message.mdn.signed) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testNoEncryptMessageSignMdn(self): @@ -148,27 +151,28 @@ def testNoEncryptMessageSignMdn(self): # Create the partner with appropriate settings for this case partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', - signature_cert=self.server_crt + mdn_mode="SYNC", + mdn_sign="sha1", + signature_cert=self.server_crt, ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') - self.assertEqual(in_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) self.assertTrue(in_message.mdn.signed) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testEncryptMessageNoMdn(self): @@ -177,24 +181,25 @@ def testEncryptMessageNoMdn(self): # Create the partner with appropriate settings for this case partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - encryption='tripledes_192_cbc', - encryption_cert=self.server_crt + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + encryption="tripledes_192_cbc", + encryption_cert=self.server_crt, ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testEncryptMessageMdn(self): @@ -202,27 +207,28 @@ def testEncryptMessageMdn(self): unsigned receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - encryption='tripledes_192_cbc', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + encryption="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC' + mdn_mode="SYNC", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testEncryptMessageSignMdn(self): @@ -230,30 +236,31 @@ def testEncryptMessageSignMdn(self): an signed receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - encryption='tripledes_192_cbc', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + encryption="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1', - signature_cert=self.server_crt + mdn_mode="SYNC", + mdn_sign="sha1", + signature_cert=self.server_crt, ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) self.assertTrue(in_message.mdn.signed) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testSignMessageNoMdn(self): @@ -261,24 +268,25 @@ def testSignMessageNoMdn(self): a receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', - signature_cert=self.server_crt + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + signature="sha1", + signature_cert=self.server_crt, ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testSignMessageMdn(self): @@ -286,27 +294,28 @@ def testSignMessageMdn(self): unsigned receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + signature="sha1", signature_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', + mdn_mode="SYNC", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testSignMessageSignMdn(self): @@ -314,29 +323,30 @@ def testSignMessageSignMdn(self): signed receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", + signature="sha1", signature_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1' + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) self.assertTrue(in_message.mdn.signed) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testEncryptSignMessageNoMdn(self): @@ -344,27 +354,28 @@ def testEncryptSignMessageNoMdn(self): does NOT request a receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testEncryptSignMessageMdn(self): @@ -372,30 +383,31 @@ def testEncryptSignMessageMdn(self): requests an unsigned receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', + mdn_mode="SYNC", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testEncryptSignMessageSignMdn(self): @@ -403,32 +415,33 @@ def testEncryptSignMessageSignMdn(self): requests a signed receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1' + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) self.assertTrue(in_message.mdn.signed) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) def testCompressEncryptSignMessageSignMdn(self): @@ -436,65 +449,68 @@ def testCompressEncryptSignMessageSignMdn(self): data and requests an signed receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', + name="AS2 Server", + as2_name="as2server", + target_url="http://localhost:8080/pyas2/as2receive", compress=True, - signature='sha1', + signature="sha1", signature_cert=self.server_crt, - encryption='tripledes_192_cbc', + encryption="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='SYNC', - mdn_sign='sha1' + mdn_mode="SYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.compressed) self.assertTrue(out_message.signed) self.assertTrue(out_message.encrypted) - self.assertEqual(in_message.status, 'S') + self.assertEqual(in_message.status, "S") self.assertIsNotNone(in_message.mdn) self.assertTrue(in_message.mdn.signed) # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) - @mock.patch('requests.post') + @mock.patch("requests.post") def testEncryptSignMessageAsyncSignMdn(self, mock_request): """ Test Permutation 14: Sender sends encrypted and signed data and requests an Asynchronous signed receipt. """ partner = Partner.objects.create( - name='AS2 Server', as2_name='as2server', - target_url='http://localhost:8080/pyas2/as2receive', - signature='sha1', + 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="tripledes_192_cbc", encryption_cert=self.server_crt, mdn=True, - mdn_mode='ASYNC', - mdn_sign='sha1' + mdn_mode="ASYNC", + mdn_sign="sha1", ) in_message = self.build_and_send(partner) # Check if message was processed successfully out_message = Message.objects.get( - message_id=in_message.message_id, direction='IN') - self.assertEqual(out_message.status, 'S') + message_id=in_message.message_id, direction="IN" + ) + self.assertEqual(out_message.status, "S") self.assertTrue(out_message.signed) self.assertTrue(out_message.encrypted) - self.assertEqual(out_message.mdn.status, 'P') + self.assertEqual(out_message.mdn.status, "P") self.assertIsNotNone(out_message.mdn.return_url) # Check mdn not sent - self.assertFalse(hasattr(in_message, 'mdn')) + self.assertFalse(hasattr(in_message, "mdn")) # Send the async mdn to the sender mock_request.side_effect = SendMessageMock(self.client) @@ -503,34 +519,30 @@ def testEncryptSignMessageAsyncSignMdn(self, mock_request): # Make sure the mdn has been created mdn = Mdn.objects.filter(message=in_message).first() self.assertIsNotNone(mdn) - self.assertEqual(mdn.message.status, 'S') + self.assertEqual(mdn.message.status, "S") self.assertTrue(mdn.signed) out_message.mdn.refresh_from_db() - self.assertEqual(out_message.mdn.status, 'S') + self.assertEqual(out_message.mdn.status, "S") # Check if input and output files are the same self.assertTrue( - self.compareFiles(in_message.payload.name, - out_message.payload.name) + self.compareFiles(in_message.payload.name, out_message.payload.name) ) - @mock.patch('requests.post') + @mock.patch("requests.post") def build_and_send(self, partner, mock_request): # Build and send the message to server as2message = As2Message( - sender=self.organization.as2org, - receiver=partner.as2partner) + sender=self.organization.as2org, receiver=partner.as2partner + ) as2message.build( self.payload, - filename='testmessage.edi', + filename="testmessage.edi", subject=partner.subject, - content_type=partner.content_type + content_type=partner.content_type, ) in_message, _ = Message.objects.create_from_as2message( - as2message=as2message, - payload=self.payload, - direction='OUT', - status='P' + as2message=as2message, payload=self.payload, direction="OUT", status="P" ) mock_request.side_effect = SendMessageMock(self.client) @@ -544,29 +556,32 @@ def compareFiles(filename1, filename2): with open(filename2, "rt") as b: # Note that "all" and "zip" are lazy # (will stop at the first line that's not identical) - return all(lineA == lineB for lineA, lineB in - zip(a.readlines(), b.readlines())) + return all( + lineA == lineB for lineA, lineB in zip(a.readlines(), b.readlines()) + ) class SendMessageMock(object): - def __init__(self, test_client): self.test_client = test_client def __call__(self, *args, **kwargs): # Get the content type - content_type = kwargs['headers'].pop('Content-Type') + content_type = kwargs["headers"].pop("Content-Type") # build the http headers http_headers = {} - for key, value in kwargs['headers'].items(): - key = 'HTTP_%s' % key.replace('-', '_').upper() + for key, value in kwargs["headers"].items(): + key = "HTTP_%s" % key.replace("-", "_").upper() http_headers[key] = value # send the test request and check response response = self.test_client.post( - '/pyas2/as2receive', data=kwargs['data'], - content_type=content_type, **http_headers) + "/pyas2/as2receive", + data=kwargs["data"], + content_type=content_type, + **http_headers + ) assert response.status_code == 200 # Create a request.Response from django.HttpResponse diff --git a/pyas2/urls.py b/pyas2/urls.py index 530fa40..e96580e 100644 --- a/pyas2/urls.py +++ b/pyas2/urls.py @@ -5,10 +5,13 @@ urlpatterns = [ - url(r'^as2receive/', views.ReceiveAs2Message.as_view(), name="as2-receive"), + url(r"^as2receive/", views.ReceiveAs2Message.as_view(), name="as2-receive"), # Add the url again without slash for backwards compatibility - url(r'^as2receive', views.ReceiveAs2Message.as_view(), name="as2-receive"), - url(r'^as2send/', views.SendAs2Message.as_view(), name="as2-send"), - url(r'^download/(?P.+)/(?P.+)/', - login_required(views.DownloadFile.as_view()), name='download-file'), + url(r"^as2receive", views.ReceiveAs2Message.as_view(), name="as2-receive"), + url(r"^as2send/", views.SendAs2Message.as_view(), name="as2-send"), + url( + r"^download/(?P.+)/(?P.+)/", + login_required(views.DownloadFile.as_view()), + name="download-file", + ), ] diff --git a/pyas2/utils.py b/pyas2/utils.py index bd8a9b3..5b9af28 100644 --- a/pyas2/utils.py +++ b/pyas2/utils.py @@ -4,7 +4,7 @@ import time from string import Template -logger = logging.getLogger('pyas2') +logger = logging.getLogger("pyas2") def store_file(target_dir, filename, content, archive=False): @@ -12,7 +12,7 @@ def store_file(target_dir, filename, content, archive=False): # Add date sub directory to target folder when archiving if archive: - target_dir = os.path.join(target_dir, time.strftime('%Y%m%d')) + target_dir = os.path.join(target_dir, time.strftime("%Y%m%d")) # Create the target folder if not exists if not os.path.exists(target_dir): @@ -20,12 +20,15 @@ def store_file(target_dir, filename, content, archive=False): # if file already exists then change filename if os.path.isfile(os.path.join(target_dir, filename)): - filename = os.path.splitext(filename)[0] + time.strftime('_%H%M%S') + \ - os.path.splitext(filename)[1] + filename = ( + os.path.splitext(filename)[0] + + time.strftime("_%H%M%S") + + os.path.splitext(filename)[1] + ) # write file to folder and return the path full_filename = os.path.join(target_dir, filename) - with open(full_filename, 'wb') as tf: + with open(full_filename, "wb") as tf: tf.write(content) return full_filename @@ -36,14 +39,14 @@ def run_post_send(message): command = message.partner.cmd_send if command: - logger.debug('Execute post successful send command %s' % command) + logger.debug("Execute post successful send command %s" % command) # Create command template and replace variables in the command command = Template(command) variables = { - 'filename': os.path.basename(message.payload.name), - 'sender': message.organization.as2_name, - 'receiver': message.partner.as2_name, - 'messageid': message.message_id + "filename": os.path.basename(message.payload.name), + "sender": message.organization.as2_name, + "receiver": message.partner.as2_name, + "messageid": message.message_id, } variables.update(message.as2message.headers) @@ -57,16 +60,16 @@ def run_post_receive(message, full_filename): command = message.partner.cmd_receive if command: - logger.debug('Execute post successful receive command %s' % command) + logger.debug("Execute post successful receive command %s" % command) # Create command template and replace variables in the command command = Template(command) variables = { - 'filename': os.path.basename(full_filename), - 'fullfilename': full_filename, - 'sender': message.organization.as2_name, - 'receiver': message.partner.as2_name, - 'messageid': message.message_id + "filename": os.path.basename(full_filename), + "fullfilename": full_filename, + "sender": message.organization.as2_name, + "receiver": message.partner.as2_name, + "messageid": message.message_id, } variables.update(message.as2message.headers) diff --git a/pyas2/views.py b/pyas2/views.py index bc9a1db..72febc1 100644 --- a/pyas2/views.py +++ b/pyas2/views.py @@ -26,10 +26,10 @@ from pyas2.utils import run_post_send from pyas2.forms import SendAs2MessageForm -logger = logging.getLogger('pyas2') +logger = logging.getLogger("pyas2") -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class ReceiveAs2Message(View): """ Class receives AS2 requests from partners. @@ -41,7 +41,8 @@ def find_message(message_id, partner_id): """ Find the message using the message_id and return its pyas2 version""" message = Message.objects.filter( - message_id=message_id, partner_id=partner_id.strip()).first() + message_id=message_id, partner_id=partner_id.strip() + ).first() if message: return message.as2message @@ -49,7 +50,8 @@ def find_message(message_id, partner_id): def check_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()).exists() + message_id=message_id, partner_id=partner_id.strip() + ).exists() @staticmethod def find_organization(org_id): @@ -70,50 +72,61 @@ def find_partner(partner_id): def post(self, request, *args, **kwargs): # extract the headers from the http request - as2headers = '' + as2headers = "" for key in request.META: - if key.startswith('HTTP') or key.startswith('CONTENT'): - as2headers += f'{key.replace("HTTP_", "").replace("_", "-").lower()}: ' \ - f'{request.META[key]}\n' + if key.startswith("HTTP") or key.startswith("CONTENT"): + as2headers += ( + f'{key.replace("HTTP_", "").replace("_", "-").lower()}: ' + f"{request.META[key]}\n" + ) # build the body along with the headers - request_body = as2headers.encode() + b'\r\n' + request.body + request_body = as2headers.encode() + b"\r\n" + request.body logger.debug( f'Received an HTTP POST from {request.META["REMOTE_ADDR"]} ' - f'with payload :\n{request_body}' + f"with payload :\n{request_body}" ) # First try to see if this is an MDN - logger.debug('Check to see if payload is an Asynchronous MDN.') + logger.debug("Check to see if payload is an Asynchronous MDN.") as2mdn = As2Mdn() # Parse the mdn and get the message status status, detailed_status = as2mdn.parse(request_body, self.find_message) - if not detailed_status == 'mdn-not-found': - message = Message.objects.get(message_id=as2mdn.orig_message_id, direction='OUT') + if not detailed_status == "mdn-not-found": + message = Message.objects.get( + message_id=as2mdn.orig_message_id, direction="OUT" + ) logger.info( - f'Asynchronous MDN received for AS2 message {as2mdn.message_id} to organization ' - f'{message.organization.as2_name} from partner {message.partner.as2_name}') + f"Asynchronous MDN received for AS2 message {as2mdn.message_id} to organization " + f"{message.organization.as2_name} from partner {message.partner.as2_name}" + ) # Update the message status and return the response - if status == 'processed': - message.status = 'S' + if status == "processed": + message.status = "S" run_post_send(message) else: - message.status = 'E' - message.detailed_status = f'Partner failed to process message: {detailed_status}' + message.status = "E" + message.detailed_status = ( + f"Partner failed to process message: {detailed_status}" + ) # Save the message and create the mdn message.save() - Mdn.objects.create_from_as2mdn(as2mdn=as2mdn, message=message, status='R') + Mdn.objects.create_from_as2mdn(as2mdn=as2mdn, message=message, status="R") - return HttpResponse(_('AS2 ASYNC MDN has been received')) + return HttpResponse(_("AS2 ASYNC MDN has been received")) else: - logger.debug('Payload is not an MDN parse it as an AS2 Message') + logger.debug("Payload is not an MDN parse it as an AS2 Message") as2message = As2Message() status, exception, as2mdn = as2message.parse( - request_body, self.find_organization, self.find_partner, self.check_message_exists) + request_body, + self.find_organization, + self.find_partner, + self.check_message_exists, + ) logger.info( f'Received an AS2 message with id {as2message.headers.get("message-id")} for ' @@ -123,96 +136,110 @@ def post(self, request, *args, **kwargs): # In case of duplicates update message id if isinstance(exception[0], DuplicateDocument): - as2message.message_id += '_duplicate' + as2message.message_id += "_duplicate" # Create the Message and MDN objects message, full_fn = Message.objects.create_from_as2message( as2message=as2message, filename=as2message.payload.get_filename(), payload=as2message.content, - direction='IN', - status='S' if status == 'processed' else 'E', - detailed_status=exception[1] + direction="IN", + status="S" if status == "processed" else "E", + detailed_status=exception[1], ) # run post receive command on success - if status == 'processed': + if status == "processed": run_post_receive(message, full_fn) # Return the mdn in case of sync else return text message - if as2mdn and as2mdn.mdn_mode == 'SYNC': + if as2mdn and as2mdn.mdn_mode == "SYNC": message.mdn = Mdn.objects.create_from_as2mdn( - as2mdn=as2mdn, message=message, status='S') + as2mdn=as2mdn, message=message, status="S" + ) response = HttpResponse(as2mdn.content) for key, value in as2mdn.headers.items(): response[key] = value return response - elif as2mdn and as2mdn.mdn_mode == 'ASYNC': + elif as2mdn and as2mdn.mdn_mode == "ASYNC": Mdn.objects.create_from_as2mdn( - as2mdn=as2mdn, message=message, status='P', return_url=as2mdn.mdn_url) - return HttpResponse(_('AS2 message has been received')) + as2mdn=as2mdn, + message=message, + status="P", + return_url=as2mdn.mdn_url, + ) + return HttpResponse(_("AS2 message has been received")) def get(self, request, *args, **kwargs): """""" return HttpResponse( - _('To submit an AS2 message, you must POST the message to this URL')) + _("To submit an AS2 message, you must POST the message to this URL") + ) def options(self, request, *args, **kwargs): response = HttpResponse() - response['allow'] = ','.join(['POST', 'GET']) + response["allow"] = ",".join(["POST", "GET"]) return response class SendAs2Message(FormView): - template_name = 'pyas2/send_as2_message.html' + template_name = "pyas2/send_as2_message.html" form_class = SendAs2MessageForm success_url = reverse_lazy( - f'admin:{Message._meta.app_label}_{Message._meta.model_name}_changelist') + f"admin:{Message._meta.app_label}_{Message._meta.model_name}_changelist" + ) def get_context_data(self, **kwargs): context = super(SendAs2Message, self).get_context_data(**kwargs) - context.update({ - 'opts': Partner._meta, - 'change': True, - 'is_popup': False, - 'save_as': False, - 'has_delete_permission': False, - 'has_add_permission': False, - 'has_change_permission': False - }) + context.update( + { + "opts": Partner._meta, + "change": True, + "is_popup": False, + "save_as": False, + "has_delete_permission": False, + "has_add_permission": False, + "has_change_permission": False, + } + ) return context def form_valid(self, form): # Send the file to the partner - payload = form.cleaned_data['file'].read() + payload = form.cleaned_data["file"].read() as2message = As2Message( - sender=form.cleaned_data['organization'].as2org, - receiver=form.cleaned_data['partner'].as2partner + sender=form.cleaned_data["organization"].as2org, + receiver=form.cleaned_data["partner"].as2partner, + ) + logger.debug( + f'Building message from {form.cleaned_data["file"].name} to send to partner ' + f"{as2message.receiver.as2_name} from org {as2message.sender.as2_name}." ) - logger.debug(f'Building message from {form.cleaned_data["file"].name} to send to partner ' - f'{as2message.receiver.as2_name} from org {as2message.sender.as2_name}.') as2message.build( payload, - filename=form.cleaned_data['file'].name, - subject=form.cleaned_data['partner'].subject, - content_type=form.cleaned_data['partner'].content_type + filename=form.cleaned_data["file"].name, + subject=form.cleaned_data["partner"].subject, + content_type=form.cleaned_data["partner"].content_type, ) message, _ = Message.objects.create_from_as2message( as2message=as2message, payload=payload, - filename=form.cleaned_data['file'].name, - direction='OUT', - status='P' + filename=form.cleaned_data["file"].name, + direction="OUT", + status="P", ) message.send_message(as2message.headers, as2message.content) - if message.status in ['S', 'P']: + if message.status in ["S", "P"]: messages.success( - self.request, 'Message has been successfully send to Partner.') + self.request, "Message has been successfully send to Partner." + ) else: messages.error( - self.request, 'Message transmission failed, check Messages tab for details.') + self.request, + "Message transmission failed, check Messages tab for details.", + ) return super(SendAs2Message, self).form_valid(form) @@ -220,34 +247,36 @@ class DownloadFile(View): """ A generic view for downloading files such as payload, certificates...""" def get(self, request, obj_type, obj_id, *args, **kwargs): - filename = '' - file_content = '' + filename = "" + file_content = "" # Get the file content based - if obj_type == 'message_payload': + if obj_type == "message_payload": obj = get_object_or_404(Message, pk=obj_id) filename = os.path.basename(obj.payload.name) file_content = obj.payload.read() - elif obj_type == 'mdn_payload': + elif obj_type == "mdn_payload": obj = get_object_or_404(Mdn, pk=obj_id) filename = os.path.basename(obj.payload.name) file_content = obj.payload.read() - elif obj_type == 'public_cert': + elif obj_type == "public_cert": obj = get_object_or_404(PublicCertificate, pk=obj_id) filename = obj.name file_content = obj.certificate - elif obj_type == 'private_key': + elif obj_type == "private_key": obj = get_object_or_404(PrivateKey, pk=obj_id) filename = obj.name file_content = obj.key # Return the file contents as attachment if filename and file_content: - response = HttpResponse(content_type='application/x-pem-file') - disposition_type = 'attachment' - response['Content-Disposition'] = disposition_type + '; filename=' + filename + response = HttpResponse(content_type="application/x-pem-file") + disposition_type = "attachment" + response["Content-Disposition"] = ( + disposition_type + "; filename=" + filename + ) response.write(bytes(file_content)) return response else: diff --git a/requirements/test.txt b/requirements/test.txt index 2f7c935..7fcfc82 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -3,5 +3,6 @@ pytest-cov==2.8.1 pytest-django==3.9.0 pytest-mock==3.0.0 pylama==7.7.1 +pylint==2.4.4 pytest-black==0.3.8 django-environ==0.4.5 From 09678166f269b92b3d5be8928627c8b0d9b31037 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 12 Apr 2020 09:15:21 +0530 Subject: [PATCH 6/9] fix linter issues --- pyas2/management/commands/sendas2bulk.py | 1 - pyas2/migrations/0001_initial.py | 17 ++++++++++----- pyas2/models.py | 2 +- pyas2/templatetags/pyas2.py | 1 - pyas2/utils.py | 27 ------------------------ pyas2/views.py | 2 +- 6 files changed, 14 insertions(+), 36 deletions(-) diff --git a/pyas2/management/commands/sendas2bulk.py b/pyas2/management/commands/sendas2bulk.py index 69945fa..5d99f32 100644 --- a/pyas2/management/commands/sendas2bulk.py +++ b/pyas2/management/commands/sendas2bulk.py @@ -1,4 +1,3 @@ -import glob import os from django.core.management import call_command from django.core.management.base import BaseCommand diff --git a/pyas2/migrations/0001_initial.py b/pyas2/migrations/0001_initial.py index bf733a0..9eacd23 100644 --- a/pyas2/migrations/0001_initial.py +++ b/pyas2/migrations/0001_initial.py @@ -182,7 +182,8 @@ class Migration(migrations.Migration): "confirmation_message", models.TextField( blank=True, - help_text="Use this field to send a customized message in the MDN Confirmations for this Partner", + help_text="Use this field to send a customized message in the MDN " + "Confirmations for this Partner", null=True, verbose_name="Confirmation Message", ), @@ -191,7 +192,8 @@ class Migration(migrations.Migration): "keep_filename", models.BooleanField( default=False, - help_text="Use Original Filename to to store file on receipt, use this option only if you are sure partner sends unique names", + help_text="Use Original Filename to to store file on receipt, use this " + "option only if you are sure partner sends unique names", verbose_name="Keep Original Filename", ), ), @@ -199,7 +201,9 @@ class Migration(migrations.Migration): "cmd_send", models.TextField( blank=True, - help_text="Command executed after successful message send, replacements are $filename, $sender, $recevier, $messageid and any message header such as $Subject", + help_text="Command executed after successful message send, replacements " + "are $filename, $sender, $recevier, $messageid and any message " + "header such as $Subject", null=True, verbose_name="Command on Message Send", ), @@ -208,7 +212,9 @@ class Migration(migrations.Migration): "cmd_receive", models.TextField( blank=True, - help_text="Command executed after successful message receipt, replacements are $filename, $fullfilename, $sender, $recevier, $messageid and any message header such as $Subject", + help_text="Command executed after successful message receipt, replacements" + " are $filename, $fullfilename, $sender, $recevier, $messageid " + "and any message header such as $Subject", null=True, verbose_name="Command on Message Receipt", ), @@ -258,7 +264,8 @@ class Migration(migrations.Migration): "confirmation_message", models.TextField( blank=True, - help_text="Use this field to send a customized message in the MDN Confirmations for this Organization", + help_text="Use this field to send a customized message in the MDN " + "Confirmations for this Organization", null=True, verbose_name="Confirmation Message", ), diff --git a/pyas2/models.py b/pyas2/models.py index 85efef5..2bea893 100644 --- a/pyas2/models.py +++ b/pyas2/models.py @@ -455,7 +455,7 @@ def send_message(self, header, payload): verify=self.partner.https_verify_ssl, ) response.raise_for_status() - except requests.exceptions.RequestException as e: + except requests.exceptions.RequestException: self.status = "R" self.detailed_status = ( f"Failed to send message, error:\n{traceback.format_exc()}" diff --git a/pyas2/templatetags/pyas2.py b/pyas2/templatetags/pyas2.py index 9e48a52..515e45e 100644 --- a/pyas2/templatetags/pyas2.py +++ b/pyas2/templatetags/pyas2.py @@ -1,5 +1,4 @@ from django import template -from django.utils.html import format_html register = template.Library() diff --git a/pyas2/utils.py b/pyas2/utils.py index 5b9af28..f749966 100644 --- a/pyas2/utils.py +++ b/pyas2/utils.py @@ -1,38 +1,11 @@ # -*- coding: utf-8 -*- import logging import os -import time from string import Template logger = logging.getLogger("pyas2") -def store_file(target_dir, filename, content, archive=False): - """ Function to store content to a file in target dir""" - - # Add date sub directory to target folder when archiving - if archive: - target_dir = os.path.join(target_dir, time.strftime("%Y%m%d")) - - # Create the target folder if not exists - if not os.path.exists(target_dir): - os.makedirs(target_dir) - - # if file already exists then change filename - if os.path.isfile(os.path.join(target_dir, filename)): - filename = ( - os.path.splitext(filename)[0] - + time.strftime("_%H%M%S") - + os.path.splitext(filename)[1] - ) - - # write file to folder and return the path - full_filename = os.path.join(target_dir, filename) - with open(full_filename, "wb") as tf: - tf.write(content) - return full_filename - - def run_post_send(message): """ Execute command after successful send, can be used to notify successful sends """ diff --git a/pyas2/views.py b/pyas2/views.py index 72febc1..cf08805 100644 --- a/pyas2/views.py +++ b/pyas2/views.py @@ -14,7 +14,7 @@ from django.views.generic import FormView from pyas2lib import Message as As2Message from pyas2lib import Mdn as As2Mdn -from pyas2lib.exceptions import * +from pyas2lib.exceptions import DuplicateDocument from pyas2.models import Mdn from pyas2.models import Message From 8d406f2e93ff337f8050a6ce93a78cba7840f4f6 Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 12 Apr 2020 18:49:00 +0530 Subject: [PATCH 7/9] improve test coverage --- pyas2/tests/__init__.py | 3 + pyas2/tests/conftest.py | 26 ++++++ pyas2/tests/test_advanced.py | 99 ++++------------------ pyas2/tests/test_basic.py | 9 +- pyas2/tests/test_commands.py | 156 +++++++++++++++++++++++++++++++++++ pyas2/tests/test_views.py | 105 ++++++++++++++++++++++- pyas2/urls.py | 2 +- 7 files changed, 314 insertions(+), 86 deletions(-) create mode 100644 pyas2/tests/conftest.py create mode 100644 pyas2/tests/test_commands.py diff --git a/pyas2/tests/__init__.py b/pyas2/tests/__init__.py index e69de29..18e23dd 100644 --- a/pyas2/tests/__init__.py +++ b/pyas2/tests/__init__.py @@ -0,0 +1,3 @@ +import os + +TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), "fixtures") diff --git a/pyas2/tests/conftest.py b/pyas2/tests/conftest.py new file mode 100644 index 0000000..78c4d5c --- /dev/null +++ b/pyas2/tests/conftest.py @@ -0,0 +1,26 @@ +"""Define the test fixtures and other configurations for the test cases.""" +import pytest +from pyas2.models import Organization, Partner + + +@pytest.fixture +def organization(): + """Create a organization object for use in the test cases.""" + return Organization.objects.create( + name="AS2 Server", + as2_name="as2server", + confirmation_message="Custom confirmation message.", + ) + + +@pytest.fixture +def partner(): + """Create a partner object for use in the test cases.""" + return Partner.objects.create( + name="AS2 Client", + as2_name="as2client", + target_url="http://localhost:8080/pyas2/as2receive", + confirmation_message="Custom confirmation message.", + compress=False, + mdn=False, + ) diff --git a/pyas2/tests/test_advanced.py b/pyas2/tests/test_advanced.py index ca61165..bb1044e 100644 --- a/pyas2/tests/test_advanced.py +++ b/pyas2/tests/test_advanced.py @@ -1,9 +1,8 @@ +import importlib import os -from pathlib import Path from unittest import mock -from django.core import management -from django.test import Client +from django.test import Client, override_settings from django.test import TestCase from pyas2lib import Message as As2Message @@ -15,8 +14,7 @@ from pyas2.models import PrivateKey from pyas2.models import PublicCertificate from pyas2.tests.test_basic import SendMessageMock - -TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), "fixtures") +from pyas2.tests import TEST_DIR class AdvancedTestCases(TestCase): @@ -98,6 +96,9 @@ def test_post_send_command(self): as2_name="as2server", target_url="http://localhost:8080/pyas2/as2receive", signature="sha1", + http_auth=True, + http_auth_user="admin", + http_auth_pass="password", signature_cert=self.server_crt, encryption="tripledes_192_cbc", encryption_cert=self.server_crt, @@ -152,7 +153,7 @@ def test_post_send_command_async(self, mock_request): def test_post_receive_command(self): """ Test that the command after successful receive gets executed.""" - + # settings.DATA_DIR = TEST_DIR # add the post receive command and save it self.partner.cmd_receive = "touch %s/$filename.received" % TEST_DIR self.partner.save() @@ -179,10 +180,11 @@ def test_post_receive_command(self): ) self.assertTrue(os.path.exists(touch_file)) os.remove(touch_file) + # shutil.rmtree(os.path.join(TEST_DIR, "messages")) + # settings.DATA_DIR = None def test_use_received_filename(self): - """ Test using the filename of the payload received while saving - the file.""" + """ Test using the filename of the payload received while saving the file.""" # add the post receive command and save it self.partner.cmd_receive = "touch %s/$filename.received" % TEST_DIR @@ -420,79 +422,6 @@ def test_signature_error(self): "Failed to verify message signature" in out_message.detailed_status ) - def test_sendmessage_command(self): - """ Test the command for an sending as2 message """ - test_message = os.path.join(TEST_DIR, "testmessage.edi") - - # Try to run with invalid org and client - with self.assertRaises(management.CommandError): - management.call_command( - "sendas2message", "AS2 Server", "AS2 Client", test_message - ) - management.call_command( - "sendas2message", "as2server", "as2client", test_message - ) - - def test_sendbulk_command(self): - """ Test the command for sending all files in the outbox folder """ - # Create a file for testing - outbox_dir = os.path.join("messages", "as2client", "outbox", "as2server") - try: - os.makedirs(outbox_dir) - except FileExistsError: - pass - test_file = Path(os.path.join(outbox_dir, "testmessage.edi")) - test_file.touch() - - management.call_command("sendas2bulk") - self.assertFalse(test_file.exists()) - - def test_manageserver_command(self): - """ Test the command for managing the as2 server """ - settings.MAX_ARCH_DAYS = -1 - # Create a message - as2message = As2Message( - sender=self.organization.as2org, receiver=self.partner.as2partner - ) - as2message.build( - self.payload, - filename="testmessage.edi", - subject=self.partner.subject, - content_type=self.partner.content_type, - ) - out_message, _ = Message.objects.create_from_as2message( - as2message=as2message, payload=self.payload, direction="OUT", status="P" - ) - out_message.send_message(as2message.headers, as2message.content) - - # Test the retry command - out_message.refresh_from_db() - self.assertEqual(out_message.status, "R") - management.call_command("manageas2server", retry=True) - out_message.refresh_from_db() - self.assertEqual(out_message.retries, 1) - - # Test max retry setting - settings.MAX_RETRIES = 1 - management.call_command("manageas2server", retry=True) - out_message.refresh_from_db() - self.assertEqual(out_message.retries, 2) - self.assertEqual(out_message.status, "E") - - # Test the async mdn command for outbound messages - out_message.status = "P" - out_message.save() - settings.ASYNC_MDN_WAIT = 0 - management.call_command("manageas2server", async_mdns=True) - out_message.refresh_from_db() - self.assertEqual(out_message.status, "E") - - # Test the clean command - management.call_command("manageas2server", clean=True) - self.assertEqual( - Message.objects.filter(message_id=out_message.message_id).count(), 0 - ) - @mock.patch("requests.post") def build_and_send(self, partner, mock_request, smudge=False): # Build and send the message to server @@ -515,3 +444,11 @@ def build_and_send(self, partner, mock_request, smudge=False): ) return out_message + + +@override_settings(PYAS2={"DATA_DIR": TEST_DIR}) +def test_setting_data_directory(): + """Test that the data directory gets set correctly.""" + assert settings.DATA_DIR is None + importlib.reload(settings) + assert settings.DATA_DIR is TEST_DIR diff --git a/pyas2/tests/test_basic.py b/pyas2/tests/test_basic.py index c2d5a34..172417e 100644 --- a/pyas2/tests/test_basic.py +++ b/pyas2/tests/test_basic.py @@ -4,6 +4,7 @@ from django.test import TestCase, Client from requests import Response +from requests.exceptions import RequestException from pyas2.models import ( PrivateKey, @@ -13,9 +14,9 @@ Message, Mdn, ) -from pyas2lib.as2 import Message as As2Message +from pyas2.tests import TEST_DIR -TEST_DIR = os.path.join((os.path.dirname(os.path.abspath(__file__))), "fixtures") +from pyas2lib.as2 import Message as As2Message class BasicServerClientTestCase(TestCase): @@ -529,6 +530,10 @@ def testEncryptSignMessageAsyncSignMdn(self, mock_request): self.compareFiles(in_message.payload.name, out_message.payload.name) ) + # Check async mdn failure + mock_request.side_effect = RequestException() + out_message.mdn.send_async_mdn() + @mock.patch("requests.post") def build_and_send(self, partner, mock_request): # Build and send the message to server diff --git a/pyas2/tests/test_commands.py b/pyas2/tests/test_commands.py new file mode 100644 index 0000000..d77ccfc --- /dev/null +++ b/pyas2/tests/test_commands.py @@ -0,0 +1,156 @@ +"""Test the management commands of the pyas2 app.""" +import os +import shutil +from pathlib import Path + +import pytest +from django.conf import settings +from django.core import management +from django.core.files.base import ContentFile + +from pyas2 import settings as app_settings +from pyas2.models import As2Message, Message, Mdn +from pyas2.tests import TEST_DIR +from pyas2.management.commands.sendas2bulk import Command as SendBulkCommand + + +@pytest.mark.django_db +def test_sendbulk_command(mocker, partner, organization): + """ Test the command for sending all files in the outbox folder """ + mocked_call_command = mocker.patch( + "pyas2.management.commands.sendas2bulk.call_command" + ) + + # Call the command + command = SendBulkCommand() + command.handle() + + # Create a file for testing and try again + outbox_dir = os.path.join( + "messages", partner.as2_name, "outbox", organization.as2_name + ) + test_file = Path(os.path.join(outbox_dir, "testmessage.edi")) + test_file.touch() + command.handle() + mocked_call_command.assert_called_with( + "sendas2message", + organization.as2_name, + partner.as2_name, + str(test_file), + delete=True, + ) + + # Try with the data directory + app_settings.DATA_DIR = settings.BASE_DIR + command.handle() + + # Delete the folder + app_settings.DATA_DIR = None + shutil.rmtree(outbox_dir) + + +@pytest.mark.django_db +def test_sendmessage_command(mocker, organization, partner): + """ Test the command for sending an as2 message """ + test_message = os.path.join(TEST_DIR, "testmessage.edi") + + # Try to run with invalid org and client + with pytest.raises(management.CommandError): + management.call_command( + "sendas2message", "AS2 Server", "AS2 Client", test_message + ) + with pytest.raises(management.CommandError): + management.call_command( + "sendas2message", organization.as2_name, "AS2 Client", test_message + ) + + with pytest.raises(management.CommandError): + management.call_command( + "sendas2message", organization.as2_name, partner.as2_name, "testmessage.edi" + ) + + # Try again with a valid org + management.call_command( + "sendas2message", organization.as2_name, partner.as2_name, test_message + ) + + # Try again with delete function + mocked_delete = mocker.patch( + "pyas2.management.commands.sendas2message.default_storage.delete" + ) + management.call_command( + "sendas2message", + organization.as2_name, + partner.as2_name, + test_message, + delete=True, + ) + assert mocked_delete.call_count == 1 + + +@pytest.mark.django_db +def test_manageserver_command(mocker, organization, partner): + """Test the command for managing the as2 server.""" + app_settings.MAX_ARCH_DAYS = -1 + + # Create a message + with open(os.path.join(TEST_DIR, "testmessage.edi"), "rb") as fp: + payload = fp.read() + as2message = As2Message(sender=organization.as2org, receiver=partner.as2partner) + as2message.build( + payload, + filename="testmessage.edi", + subject=partner.subject, + content_type=partner.content_type, + ) + out_message, _ = Message.objects.create_from_as2message( + as2message=as2message, payload=payload, direction="OUT", status="P" + ) + out_message.send_message(as2message.headers, as2message.content) + + # Test the retry command + out_message.refresh_from_db() + assert out_message.status == "R" + management.call_command("manageas2server", retry=True) + out_message.refresh_from_db() + assert out_message.retries == 1 + + # Test max retry setting + app_settings.MAX_RETRIES = 1 + management.call_command("manageas2server", retry=True) + out_message.refresh_from_db() + assert out_message.retries == 2 + assert out_message.status == "E" + + # Test the async mdn command for outbound messages + app_settings.ASYNC_MDN_WAIT = 0 + out_message.status = "P" + out_message.save() + management.call_command("manageas2server", async_mdns=True) + out_message.refresh_from_db() + assert out_message.status == "E" + + # Test the async mdn command for outbound mdns + mdn = Mdn.objects.create(mdn_id="some-mdn-id", message=out_message, status="P") + mdn.headers.save("some-mdn-id.headers", ContentFile("")) + mdn.payload.save("some-mdn-id.mdn", ContentFile("MDN Content")) + management.call_command("manageas2server", async_mdns=True) + mdn.refresh_from_db() + assert mdn.status == "P" + mocked_post = mocker.patch("requests.post") + management.call_command("manageas2server", async_mdns=True) + mdn.refresh_from_db() + assert mocked_post.call_count == 1 + assert mdn.status == "S" + + # Test the clean command + management.call_command("manageas2server", clean=True) + assert Message.objects.filter(message_id=out_message.message_id).count() == 0 + assert Mdn.objects.filter(mdn_id=out_message.mdn.mdn_id).count() == 0 + + # Test the clean command without MDN set + out_message, _ = Message.objects.create_from_as2message( + as2message=as2message, payload=payload, direction="OUT", status="P" + ) + management.call_command("manageas2server", clean=True) + assert Message.objects.filter(message_id=out_message.message_id).count() == 0 diff --git a/pyas2/tests/test_views.py b/pyas2/tests/test_views.py index 22cf62a..e843578 100644 --- a/pyas2/tests/test_views.py +++ b/pyas2/tests/test_views.py @@ -1,18 +1,37 @@ +import os + from django.contrib.auth.models import User +from django.core.files.base import ContentFile from django.test import TestCase, Client from django.urls import reverse -from pyas2.models import PublicCertificate +from pyas2.models import PublicCertificate, PrivateKey, Message, Mdn +from pyas2.tests import TEST_DIR class TestDownloadFileView(TestCase): def setUp(self): self.user = User.objects.create(username="dummy") - with open("pyas2/tests/fixtures/client_public.pem", "rb") as fp: + with open(os.path.join(TEST_DIR, "client_public.pem"), "rb") as fp: self.cert = PublicCertificate.objects.create( name="test-cert", certificate=fp.read() ) + with open(os.path.join(TEST_DIR, "client_private.pem"), "rb") as fp: + self.private_key = PrivateKey.objects.create( + name="test-key", key=fp.read(), key_pass="test" + ) + + with open(os.path.join(TEST_DIR, "testmessage.edi"), "rb") as fp: + self.message = Message.objects.create( + message_id="some-message-id", direction="IN", status="S", + ) + self.message.payload.save("testmessage.edi", fp) + self.mdn = Mdn.objects.create( + mdn_id="some-mdn-id", message=self.message, status="S" + ) + self.mdn.payload.save("some-mdn-id.mdn", ContentFile("MDN Content")) + def test_view_is_protected(self): client = Client() response = client.get( @@ -21,6 +40,16 @@ def test_view_is_protected(self): self.assertEqual(response.status_code, 302) self.assertTrue(response.url.startswith("/accounts/login/")) + def test_unknown_object_type(self): + """Test that we get 404 when an uknown object type is sent.""" + client = Client() + client.force_login(self.user) + + response = client.get( + reverse("download-file", kwargs={"obj_type": "some_type", "obj_id": "1"}) + ) + self.assertEqual(response.status_code, 404) + def test_download_public_certificate(self): client = Client() client.force_login(self.user) @@ -31,5 +60,77 @@ def test_download_public_certificate(self): kwargs={"obj_type": "public_cert", "obj_id": self.cert.id}, ) ) + self.assertEqual(str(self.cert), "test-cert") self.assertEqual(response.status_code, 200) self.assertEqual(bytes(self.cert.certificate), response.content) + + def test_download_private_key(self): + client = Client() + client.force_login(self.user) + + response = client.get( + reverse( + "download-file", + kwargs={"obj_type": "private_key", "obj_id": self.private_key.id}, + ) + ) + self.assertEqual(str(self.private_key), "test-key") + self.assertEqual(response.status_code, 200) + self.assertEqual(bytes(self.private_key.key), response.content) + + def test_download_message_payload(self): + client = Client() + client.force_login(self.user) + + response = client.get( + reverse( + "download-file", + kwargs={"obj_type": "message_payload", "obj_id": self.message.id}, + ) + ) + self.assertEqual(str(self.message), self.message.message_id) + self.assertEqual(response.status_code, 200) + self.assertEqual(bytes(self.message.payload.read()), response.content) + + def test_download_mdn_payload(self): + client = Client() + client.force_login(self.user) + + response = client.get( + reverse( + "download-file", + kwargs={"obj_type": "mdn_payload", "obj_id": self.mdn.id}, + ) + ) + self.assertEqual(str(self.mdn), self.mdn.mdn_id) + self.assertEqual(response.status_code, 200) + self.assertEqual(bytes(self.mdn.payload.read()), response.content) + + +def test_as2_receive_view_options(client): + """Test the options method of the AS2 Receive endpoint.""" + response = client.options("/pyas2/as2receive") + assert response.status_code == 200 + assert response.content == b"" + + +def test_send_as2_message_view(mocker, client, admin_client, organization, partner): + """Test the view for sending the AS2 message from the admin.""" + mocked_send_message = mocker.patch("pyas2.models.Message.send_message") + response = client.get(reverse("as2-send")) + assert response.status_code == 302 + + # Try with the admin client + response = admin_client.get(reverse("as2-send")) + assert response.status_code == 200 + + # Try posting to the form + with open(os.path.join(TEST_DIR, "testmessage.edi"), "rb") as fp: + post_data = { + "organization": organization.as2_name, + "partner": partner.as2_name, + "file": fp, + } + response = admin_client.post(reverse("as2-send"), data=post_data) + assert response.status_code == 302 + assert mocked_send_message.call_count == 1 diff --git a/pyas2/urls.py b/pyas2/urls.py index e96580e..73f1726 100644 --- a/pyas2/urls.py +++ b/pyas2/urls.py @@ -8,7 +8,7 @@ url(r"^as2receive/", views.ReceiveAs2Message.as_view(), name="as2-receive"), # Add the url again without slash for backwards compatibility url(r"^as2receive", views.ReceiveAs2Message.as_view(), name="as2-receive"), - url(r"^as2send/", views.SendAs2Message.as_view(), name="as2-send"), + url(r"^as2send/", login_required(views.SendAs2Message.as_view()), name="as2-send"), url( r"^download/(?P.+)/(?P.+)/", login_required(views.DownloadFile.as_view()), From 104941fa2e9f13c419385929f9043c276168a60a Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 12 Apr 2020 19:31:18 +0530 Subject: [PATCH 8/9] bump version of pyas2lib to 1.3.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b42a54a..15b387b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ README = f.read() install_requires = [ - 'pyas2lib==1.3.0', + 'pyas2lib==1.3.1', 'django>=2.1.9', 'requests' ] From b3b7ecce82b6b675bf202bf49a7dcf3a5144552b Mon Sep 17 00:00:00 2001 From: abhishekram Date: Sun, 12 Apr 2020 19:51:52 +0530 Subject: [PATCH 9/9] handle case where a 200 is returned when sending a message and no mdn is present --- pyas2/models.py | 11 +++++++---- setup.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyas2/models.py b/pyas2/models.py index 2bea893..1513a16 100644 --- a/pyas2/models.py +++ b/pyas2/models.py @@ -491,20 +491,23 @@ def send_message(self, header, payload): f"with content: {mdn_content}" ) as2mdn = As2Mdn() - status, detailed_status = as2mdn.parse( + mdn_status, mdn_detailed_status = as2mdn.parse( mdn_content, lambda x, y: self.as2message ) # Update the message status and return the response - if status == "processed": + if mdn_status == "processed": self.status = "S" run_post_send(self) else: self.status = "E" self.detailed_status = ( - f"Partner failed to process message: {detailed_status}" + f"Partner failed to process message: {mdn_detailed_status}" + ) + if mdn_detailed_status != "mdn-not-found": + Mdn.objects.create_from_as2mdn( + as2mdn=as2mdn, message=self, status="R" ) - Mdn.objects.create_from_as2mdn(as2mdn=as2mdn, message=self, status="R") else: # No MDN requested mark message as success and run command self.status = "S" diff --git a/setup.py b/setup.py index 15b387b..0a66d97 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', "Topic :: Security :: Cryptography", "Topic :: Communications", ],