diff --git a/.github/workflows/pythontest.yml b/.github/workflows/pythontest.yml new file mode 100644 index 0000000..8b15b45 --- /dev/null +++ b/.github/workflows/pythontest.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python Test + +on: + push: + branches: + - '**' + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + - name: Lint + run: | + pip install pylint + pylint -rn --errors-only ./smpp + - name: Test + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: | + pip install coveralls pytest-cov + pytest --cov=smpp tests/ + coveralls diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2b6e60e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: python -python: - - "2.6" - - "2.7" -# command to install dependencies -install: "pip install -r requirements.txt --use-mirrors" -# command to run tests -script: py.test diff --git a/LICENSE b/LICENSE index 455571f..87a31b2 100644 --- a/LICENSE +++ b/LICENSE @@ -10,7 +10,4 @@ Copyright 2009-2010 Mozes, Inc. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the License for the specific language governing permissions and - limitations under the License. - -With the exception of namedtuple.py which is licensed under the Python -Software Foundation license: http://www.opensource.org/licenses/PythonSoftFoundation \ No newline at end of file + limitations under the License. \ No newline at end of file diff --git a/README.markdown b/README.md similarity index 89% rename from README.markdown rename to README.md index eddd365..ad0fa82 100644 --- a/README.markdown +++ b/README.md @@ -1,22 +1,26 @@ -smpp.pdu is a Python library for parsing Protocol Data Units (PDUs) in SMPP protocol +# smpp.pdu +smpp.pdu is a Python library for parsing Protocol Data Units (PDUs) in SMPP protocol http://www.nowsms.com/discus/messages/1/24856.html +[![Tests](https://github.com/DomAmato/smpp.pdu/workflows/Python%20Test/badge.svg)](https://github.com/DomAmato/smpp.pdu/actions) +[![Coverage Status](https://coveralls.io/repos/github/DomAmato/smpp.pdu/badge.svg?branch=master)](https://coveralls.io/github/DomAmato/smpp.pdu?branch=master) + Examples ======== Decoding (parsing) PDUs -------------------------- import binascii - import StringIO + from io import BytesIO from smpp.pdu.pdu_encoding import PDUEncoder hex = '0000004d00000005000000009f88f12441575342440001013136353035353531323334000101313737333535353430373000000000000000000300117468657265206973206e6f2073706f6f6e' binary = binascii.a2b_hex(hex) - file = StringIO.StringIO(binary) + file = BytesIO(binary) pdu = PDUEncoder().decode(file) - print "PDU: %s" % pdu + print("PDU: %s" % pdu) # Prints the following: # @@ -63,11 +67,11 @@ Creating and encoding PDUs data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), short_message='HELLO', ) - print "PDU: %s" % pdu + print("PDU: %s" % pdu) binary = PDUEncoder().encode(pdu) hexStr = binascii.b2a_hex(binary) - print "HEX: %s" % hexStr + print("HEX: %s" % hexStr) # Prints the following: # diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e3caefb..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -enum diff --git a/setup.py b/setup.py index 2dce1cd..7139e5f 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ import os -from setuptools import setup, find_packages -from pkg_resources import resource_string from setuptools import setup, find_packages + setup( name = "smpp.pdu", version = "0.3", @@ -10,18 +9,12 @@ author_email = "roger.hoover@gmail.com", description = "Library for parsing Protocol Data Units (PDUs) in SMPP protocol", license = 'Apache License 2.0', - packages = find_packages(), - long_description=resource_string(__name__, 'README.markdown'), + packages = find_packages(exclude=["tests"]), keywords = "smpp pdu", url = "https://github.com/mozes/smpp.pdu", py_modules=["smpp.pdu"], include_package_data = True, - package_data={'smpp.pdu': ['README.markdown']}, - zip_safe = False, - install_requires = [ - 'enum', - ], - test_suite = 'smpp.pdu.tests', + zip_safe = False, classifiers=[ "Development Status :: 5 - Production/Stable", "Topic :: System :: Networking", @@ -29,6 +22,11 @@ "License :: OSI Approved :: Apache Software License", "Intended Audience :: Developers", "Programming Language :: Python", + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', "Topic :: Software Development :: Libraries :: Python Modules", ], ) diff --git a/smpp/__init__.py b/smpp/__init__.py index 96f38be..750fafb 100644 --- a/smpp/__init__.py +++ b/smpp/__init__.py @@ -13,4 +13,4 @@ See the License for the specific language governing permissions and limitations under the License. """ -__import__('pkg_resources').declare_namespace(__name__) \ No newline at end of file +__path__ = __import__('pkgutil').extend_path(__path__, __name__) \ No newline at end of file diff --git a/smpp/pdu/constants.py b/smpp/pdu/constants.py index 7972102..8b24167 100644 --- a/smpp/pdu/constants.py +++ b/smpp/pdu/constants.py @@ -13,199 +13,300 @@ See the License for the specific language governing permissions and limitations under the License. """ + +""" +Updated code parts are marked with "Jasmin update" comment +""" command_status_value_map = { - 0x00000000L : { - 'name' : 'ESME_ROK', - 'description' : 'No error', + 0x00000000: { + 'name': 'ESME_ROK', + 'description': 'No error', }, - 0x00000001L : { - 'name' : 'ESME_RINVMSGLEN', + 0x00000001: { + 'name': 'ESME_RINVMSGLEN', 'description': 'Message Length is invalid', }, - 0x00000002L : { - 'name' : 'ESME_RINVCMDLEN', + 0x00000002: { + 'name': 'ESME_RINVCMDLEN', 'description': 'Command Length is invalid', }, - 0x00000003L : { - 'name' : 'ESME_RINVCMDID', + 0x00000003: { + 'name': 'ESME_RINVCMDID', 'description': 'Invalid Command ID', }, - 0x00000004L : { - 'name' : 'ESME_RINVBNDSTS', + 0x00000004: { + 'name': 'ESME_RINVBNDSTS', 'description': 'Invalid BIND Status for given command', }, - 0x00000005L : { - 'name' : 'ESME_RALYBND', + 0x00000005: { + 'name': 'ESME_RALYBND', 'description': 'ESME Already in Bound State', }, - 0x00000006L : { - 'name' : 'ESME_RINVPRTFLG', + 0x00000006: { + 'name': 'ESME_RINVPRTFLG', 'description': 'Invalid Priority Flag', }, - 0x00000007L : { - 'name' : 'ESME_RINVREGDLVFLG', + 0x00000007: { + 'name': 'ESME_RINVREGDLVFLG', 'description': 'Invalid Registered Delivery Flag', }, - 0x00000008L : { - 'name' : 'ESME_RSYSERR', + 0x00000008: { + 'name': 'ESME_RSYSERR', 'description': 'System Error', }, - 0x0000000AL : { - 'name' : 'ESME_RINVSRCADR', + 0x0000000A: { + 'name': 'ESME_RINVSRCADR', 'description': 'Invalid Source Address', }, - 0x0000000BL : { - 'name' : 'ESME_RINVDSTADR', + 0x0000000B: { + 'name': 'ESME_RINVDSTADR', 'description': 'Invalid Dest Addr', }, - 0x0000000CL : { - 'name' : 'ESME_RINVMSGID', + 0x0000000C: { + 'name': 'ESME_RINVMSGID', 'description': 'Message ID is invalid', }, - 0x0000000DL : { - 'name' : 'ESME_RBINDFAIL', + 0x0000000D: { + 'name': 'ESME_RBINDFAIL', 'description': 'Bind Failed', }, - 0x0000000EL : { - 'name' : 'ESME_RINVPASWD', + 0x0000000E: { + 'name': 'ESME_RINVPASWD', 'description': 'Invalid Password', }, - 0x0000000FL : { - 'name' : 'ESME_RINVSYSID', + 0x0000000F: { + 'name': 'ESME_RINVSYSID', 'description': 'Invalid System ID', }, - 0x00000011L : { - 'name' : 'ESME_RCANCELFAIL', + 0x00000011: { + 'name': 'ESME_RCANCELFAIL', 'description': 'Cancel SM Failed', }, - 0x00000013L : { - 'name' : 'ESME_RREPLACEFAIL', + 0x00000013: { + 'name': 'ESME_RREPLACEFAIL', 'description': 'Replace SM Failed', }, - 0x00000014L : { - 'name' : 'ESME_RMSGQFUL', + 0x00000014: { + 'name': 'ESME_RMSGQFUL', 'description': 'Message Queue Full', }, - 0x00000015L : { - 'name' : 'ESME_RINVSERTYP', + 0x00000015: { + 'name': 'ESME_RINVSERTYP', 'description': 'Invalid Service Type', }, - 0x00000033L : { - 'name' : 'ESME_RINVNUMDESTS', + 0x00000033: { + 'name': 'ESME_RINVNUMDESTS', 'description': 'Invalid number of destinations', }, - 0x00000034L : { - 'name' : 'ESME_RINVDLNAME', + 0x00000034: { + 'name': 'ESME_RINVDLNAME', 'description': 'Invalid Distribution List Name', }, - 0x00000040L : { - 'name' : 'ESME_RINVDESTFLAG', + 0x00000040: { + 'name': 'ESME_RINVDESTFLAG', 'description': 'Destination flag is invalid (submit_multi)', }, - 0x00000042L : { - 'name' : 'ESME_RINVSUBREP', + 0x00000042: { + 'name': 'ESME_RINVSUBREP', 'description': 'Invalid submit with replace request (i.e. submit_sm with replace_if_present_flag set)', }, - 0x00000043L : { - 'name' : 'ESME_RINVESMCLASS', + 0x00000043: { + 'name': 'ESME_RINVESMCLASS', 'description': 'Invalid esm_class field data', }, - 0x00000044L : { - 'name' : 'ESME_RCNTSUBDL', + 0x00000044: { + 'name': 'ESME_RCNTSUBDL', 'description': 'Cannot Submit to Distribution List', }, - 0x00000045L : { - 'name' : 'ESME_RSUBMITFAIL', + 0x00000045: { + 'name': 'ESME_RSUBMITFAIL', 'description': 'submit_sm or submit_multi failed', }, - 0x00000048L : { - 'name' : 'ESME_RINVSRCTON', + 0x00000048: { + 'name': 'ESME_RINVSRCTON', 'description': 'Invalid Source address TON', }, - 0x00000049L : { - 'name' : 'ESME_RINVSRCNPI', + 0x00000049: { + 'name': 'ESME_RINVSRCNPI', 'description': 'Invalid Source address NPI', }, - 0x00000050L : { - 'name' : 'ESME_RINVDSTTON', + 0x00000050: { + 'name': 'ESME_RINVDSTTON', 'description': 'Invalid Destination address TON', }, - 0x00000051L : { - 'name' : 'ESME_RINVDSTNPI', + 0x00000051: { + 'name': 'ESME_RINVDSTNPI', 'description': 'Invalid Destination address NPI', }, - 0x00000053L : { - 'name' : 'ESME_RINVSYSTYP', + 0x00000053: { + 'name': 'ESME_RINVSYSTYP', 'description': 'Invalid system_type field', }, - 0x00000054L : { - 'name' : 'ESME_RINVREPFLAG', + 0x00000054: { + 'name': 'ESME_RINVREPFLAG', 'description': 'Invalid replace_if_present flag', }, - 0x00000055L : { - 'name' : 'ESME_RINVNUMMSGS', + 0x00000055: { + 'name': 'ESME_RINVNUMMSGS', 'description': 'Invalid number of messages', }, - 0x00000058L : { - 'name' : 'ESME_RTHROTTLED', + 0x00000058: { + 'name': 'ESME_RTHROTTLED', 'description': 'Throttling error (ESME has exceeded allowed message limits', }, - 0x00000061L : { - 'name' : 'ESME_RINVSCHED', + 0x00000061: { + 'name': 'ESME_RINVSCHED', 'description': 'Invalid Scheduled Delivery Time', }, - 0x00000062L : { - 'name' : 'ESME_RINVEXPIRY', + 0x00000062: { + 'name': 'ESME_RINVEXPIRY', 'description': 'Invalid message validity period (Expiry time)', }, - 0x00000063L : { - 'name' : 'ESME_RINVDFTMSGID', + 0x00000063: { + 'name': 'ESME_RINVDFTMSGID', 'description': 'Predefined Message Invalid or Not Found', }, - 0x00000064L : { - 'name' : 'ESME_RX_T_APPN', + 0x00000064: { + 'name': 'ESME_RX_T_APPN', 'description': 'ESME Receiver Temporary App Error Code', }, - 0x00000065L : { - 'name' : 'ESME_RX_P_APPN', + 0x00000065: { + 'name': 'ESME_RX_P_APPN', 'description': 'ESME Receiver Permanent App Error Code', }, - 0x00000066L : { - 'name' : 'ESME_RX_R_APPN', + 0x00000066: { + 'name': 'ESME_RX_R_APPN', 'description': 'ESME Receiver Reject Message Error Code', }, - 0x00000067L : { - 'name' : 'ESME_RQUERYFAIL', + 0x00000067: { + 'name': 'ESME_RQUERYFAIL', 'description': 'query_sm request failed', }, - 0x000000C0L : { - 'name' : 'ESME_RINVOPTPARSTREAM', + 0x000000C0: { + 'name': 'ESME_RINVOPTPARSTREAM', 'description': 'Error in the optional part of the PDU Body', }, - 0x000000C1L : { - 'name' : 'ESME_ROPTPARNOTALLWD', + 0x000000C1: { + 'name': 'ESME_ROPTPARNOTALLWD', 'description': 'Optional Parameter not allowed', }, - 0x000000C2L : { - 'name' : 'ESME_RINVPARLEN', + 0x000000C2: { + 'name': 'ESME_RINVPARLEN', 'description': 'Invalid Parameter Length', }, - 0x000000C3L : { - 'name' : 'ESME_RMISSINGOPTPARAM', + 0x000000C3: { + 'name': 'ESME_RMISSINGOPTPARAM', 'description': 'Expected Optional Parameter missing', }, - 0x000000C4L : { - 'name' : 'ESME_RINVOPTPARAMVAL', + 0x000000C4: { + 'name': 'ESME_RINVOPTPARAMVAL', 'description': 'Invalid Optional Parameter Value', }, - 0x000000FEL : { - 'name' : 'ESME_RDELIVERYFAILURE', + 0x000000FE: { + 'name': 'ESME_RDELIVERYFAILURE', 'description': 'Delivery Failure (used for data_sm_resp)', }, - 0x000000FFL : { - 'name' : 'ESME_RUNKNOWNERR', + 0x000000FF: { + 'name': 'ESME_RUNKNOWNERR', 'description': 'Unknown Error', }, + # Jasmin update: + 0x00000100: { + 'name': 'ESME_RSERTYPUNAUTH', + 'description': 'ESME Not authorised to use specified service_type', + }, + 0x00000101: { + 'name': 'ESME_RPROHIBITED', + 'description': 'ESME Prohibited from using specified operation', + }, + 0x00000102: { + 'name': 'ESME_RSERTYPUNAVAIL', + 'description': 'Specified service_type is unavailable', + }, + 0x00000103: { + 'name': 'ESME_RSERTYPDENIED', + 'description': 'Specified service_type is denied', + }, + 0x00000104: { + 'name': 'ESME_RINVDCS', + 'description': 'Invalid Data Coding Scheme', + }, + 0x00000105: { + 'name': 'ESME_RINVSRCADDRSUBUNIT', + 'description': 'Source Address Sub unit is Invalid', + }, + 0x00000106: { + 'name': 'ESME_RINVDSTADDRSUBUNIT', + 'description': 'Destination Address Sub unit is Invalid', + }, + 0x00000107: { + 'name': 'ESME_RINVBCASTFREQINT', + 'description': 'Broadcast Frequency Interval is invalid', + }, + 0x00000108: { + 'name': 'ESME_RINVBCASTALIAS_NAME', + 'description': 'Broadcast Alias Name is invalid', + }, + 0x00000109: { + 'name': 'ESME_RINVBCASTAREAFMT', + 'description': 'Broadcast Area Format is invalid', + }, + 0x0000010a: { + 'name': 'ESME_RINVNUMBCAST_AREAS', + 'description': 'Number of Broadcast Areas is invalid', + }, + 0x0000010b: { + 'name': 'ESME_RINVBCASTCNTTYPE', + 'description': 'Broadcast Content Type is invalid', + }, + 0x0000010c: { + 'name': 'ESME_RINVBCASTMSGCLASS', + 'description': 'Broadcast Message Class is invalid', + }, + 0x0000010d: { + 'name': 'ESME_RBCASTFAIL', + 'description': 'broadcast_sm operation failed', + }, + 0x0000010e: { + 'name': 'ESME_RBCASTQUERYFAIL', + 'description': 'query_broadcast_sm operation failed', + }, + 0x0000010f: { + 'name': 'ESME_RBCASTCANCELFAIL', + 'description': 'cancel_broadcast_sm operation failed', + }, + 0x00000110: { + 'name': 'ESME_RINVBCAST_REP', + 'description': 'Number of Repeated Broadcasts is invalid', + }, + 0x00000111: { + 'name': 'ESME_RINVBCASTSRVGRP', + 'description': 'Broadcast Service Group is invalid', + }, + 0x00000112: { + 'name': 'ESME_RINVBCASTCHANIND', + 'description': 'Broadcast Channel Indicator is invalid', + }, + # Jasmin update: + -1: { + 'name': 'RESERVEDSTATUS_SMPP_EXTENSION', + 'description': 'Reserved for SMPP extension', + }, + # Jasmin update: + -2: { + 'name': 'RESERVEDSTATUS_VENDOR_SPECIFIC', + 'description': 'Reserved for SMSC vendor specific errors', + }, + # Jasmin update: + -3: { + 'name': 'RESERVEDSTATUS', + 'description': 'Reserved', + }, + # Jasmin update: + -4: { + 'name': 'RESERVEDSTATUS_UNKNOWN_STATUS', + 'description': 'Unknown status', + }, } command_status_name_map = dict([(val['name'], key) for (key, val) in command_status_value_map.items()]) @@ -287,6 +388,8 @@ 'alert_on_message_delivery': 0x130C, 'its_reply_type': 0x1380, 'its_session_info': 0x1383, + # Jasmin update: bypass vendor specific tags + 'vendor_specific_bypass': -1, } tag_value_map = dict([(val, key) for (key, val) in tag_name_map.items()]) @@ -320,13 +423,15 @@ 'SMSC_DELIVERY_RECEIPT_REQUESTED': 0x01, 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE': 0x02, } -registered_delivery_receipt_value_map = dict([(val, key) for (key, val) in registered_delivery_receipt_name_map.items()]) +registered_delivery_receipt_value_map = dict( + [(val, key) for (key, val) in registered_delivery_receipt_name_map.items()]) registered_delivery_sme_originated_acks_name_map = { 'SME_DELIVERY_ACK_REQUESTED': 0x04, 'SME_MANUAL_ACK_REQUESTED': 0x08, } -registered_delivery_sme_originated_acks_value_map = dict([(val, key) for (key, val) in registered_delivery_sme_originated_acks_name_map.items()]) +registered_delivery_sme_originated_acks_value_map = dict( + [(val, key) for (key, val) in registered_delivery_sme_originated_acks_name_map.items()]) addr_subunit_name_map = { 'UNKNOWN': 0x00, @@ -408,7 +513,8 @@ 'DEFAULT_ALPHABET': 0x00, 'DATA_8BIT': 0x04, } -data_coding_gsm_message_coding_value_map = dict([(val, key) for (key, val) in data_coding_gsm_message_coding_name_map.items()]) +data_coding_gsm_message_coding_value_map = dict( + [(val, key) for (key, val) in data_coding_gsm_message_coding_name_map.items()]) data_coding_gsm_message_class_name_map = { 'NO_MESSAGE_CLASS': 0x00, @@ -416,7 +522,8 @@ 'CLASS_2': 0x02, 'CLASS_3': 0x03, } -data_coding_gsm_message_class_value_map = dict([(val, key) for (key, val) in data_coding_gsm_message_class_name_map.items()]) +data_coding_gsm_message_class_value_map = dict( + [(val, key) for (key, val) in data_coding_gsm_message_class_name_map.items()]) dest_flag_name_map = { 'SME_ADDRESS': 0x01, @@ -440,12 +547,15 @@ 'TBCD': 0x00, 'ASCII': 0x01, } -callback_num_digit_mode_indicator_value_map = dict([(val, key) for (key, val) in callback_num_digit_mode_indicator_name_map.items()]) +callback_num_digit_mode_indicator_value_map = dict( + [(val, key) for (key, val) in callback_num_digit_mode_indicator_name_map.items()]) subaddress_type_tag_name_map = { 'NSAP_EVEN': 0x80, 'NSAP_ODD': 0x88, 'USER_SPECIFIED': 0xa0, + # Jasmin update: (#325) + 'RESERVED': 0x00, } subaddress_type_tag_value_map = dict([(val, key) for (key, val) in subaddress_type_tag_name_map.items()]) @@ -456,6 +566,15 @@ } ms_availability_status_value_map = dict([(val, key) for (key, val) in ms_availability_status_name_map.items()]) +# Jasmin update: +network_error_code_name_map = { + 'ANSI-136': 0x01, + 'IS-95': 0x02, + 'GSM': 0x03, + 'RESERVED': 0x04, +} +network_error_code_value_map = dict([(val, key) for (key, val) in network_error_code_name_map.items()]) + network_type_name_map = { 'UNKNOWN': 0x00, 'GSM': 0x01, diff --git a/smpp/pdu/encoding.py b/smpp/pdu/encoding.py index 7bdad87..73ca225 100644 --- a/smpp/pdu/encoding.py +++ b/smpp/pdu/encoding.py @@ -16,7 +16,7 @@ from smpp.pdu import pdu_types from smpp.pdu.error import PDUCorruptError -class IEncoder(object): +class IEncoder: def encode(self, value): """Takes an object representing the type and returns a byte string""" @@ -25,7 +25,7 @@ def encode(self, value): def decode(self, file): """Takes file stream in and returns an object representing the type""" raise NotImplementedError() - + def read(self, file, size): bytesRead = file.read(size) length = len(bytesRead) diff --git a/smpp/pdu/error.py b/smpp/pdu/error.py index 67b53be..75bebe7 100644 --- a/smpp/pdu/error.py +++ b/smpp/pdu/error.py @@ -30,7 +30,7 @@ class SMPPClientConnectionCorruptedError(SMPPClientError): class SMPPClientSessionStateError(SMPPClientError): """Raised when illegal operations are attempted for the client's session state """ - + class SMPPTransactionError(SMPPError): """Raised for transaction errors """ @@ -38,9 +38,9 @@ def __init__(self, response, request=None): self.response = response self.request = request SMPPError.__init__(self, self.getErrorStr()) - + def getErrorStr(self): - errCodeName = str(self.response.status) + errCodeName = self.response.status.name errCodeVal = constants.command_status_name_map[errCodeName] errCodeDesc = constants.command_status_value_map[errCodeVal] return '%s (%s)' % (errCodeName, errCodeDesc) @@ -65,7 +65,9 @@ def __init__(self, errStr, commandStatus): SMPPError.__init__(self, "%s: %s" % (self.getStatusDescription(), errStr)) def getStatusDescription(self): - intVal = constants.command_status_name_map[str(self.status)] + # _name_ gets the str name value of an enum + # https://docs.python.org/3/library/enum.html#supported-sunder-names + intVal = constants.command_status_name_map[self.status.name] return constants.command_status_value_map[intVal]['description'] class SessionStateError(SMPPProtocolError): diff --git a/smpp/pdu/gsm_encoding.py b/smpp/pdu/gsm_encoding.py index 9a96863..6673869 100644 --- a/smpp/pdu/gsm_encoding.py +++ b/smpp/pdu/gsm_encoding.py @@ -25,33 +25,38 @@ class UDHInformationElementIdentifierUnknownError(UDHParseError): pass class Int8Encoder(IEncoder): - + def encode(self, value): return struct.pack('!B', value) def decode(self, file): byte = self.read(file, 1) - return struct.unpack('!B', byte)[0] + if isinstance(byte, bytes): + return struct.unpack('!B', byte)[0] + return struct.unpack('!B', bytes([byte]))[0] class Int16Encoder(IEncoder): - + def encode(self, value): return struct.pack('!H', value) def decode(self, file): - bytes = self.read(file, 2) - return struct.unpack('!H', bytes)[0] + dec_bytes = self.read(file, 2) + if isinstance(dec_bytes, bytes): + return struct.unpack('!H', dec_bytes)[0] + return struct.unpack('!H', bytes([dec_bytes]))[0] class InformationElementIdentifierEncoder(IEncoder): int8Encoder = Int8Encoder() nameMap = gsm_constants.information_element_identifier_name_map valueMap = gsm_constants.information_element_identifier_value_map - + def encode(self, value): - name = str(value) - if name not in self.nameMap: - raise ValueError("Unknown InformationElementIdentifier name %s" % name) - return self.int8Encoder.encode(self.nameMap[name]) + # _name_ gets the str name value of an enum + # https://docs.python.org/3/library/enum.html#supported-sunder-names + if value.name not in self.nameMap: + raise ValueError("Unknown InformationElementIdentifier name %s" % value.name) + return self.int8Encoder.encode(self.nameMap[value.name]) def decode(self, file): intVal = self.int8Encoder.decode(file) @@ -59,24 +64,24 @@ def decode(self, file): errStr = "Unknown InformationElementIdentifier value %s" % intVal raise UDHInformationElementIdentifierUnknownError(errStr) name = self.valueMap[intVal] - return getattr(gsm_types.InformationElementIdentifier, name) + return getattr(gsm_types.InformationElementIdentifier, name) class IEConcatenatedSMEncoder(IEncoder): int8Encoder = Int8Encoder() int16Encoder = Int16Encoder() - + def __init__(self, is16bitRefNum): self.is16bitRefNum = is16bitRefNum - + def encode(self, cms): - bytes = '' + enc_bytes = b'' if self.is16bitRefNum: - bytes += self.int16Encoder.encode(cms.referenceNum) + enc_bytes += self.int16Encoder.encode(cms.referenceNum) else: - bytes += self.int8Encoder.encode(cms.referenceNum) - bytes += self.int8Encoder.encode(cms.maximumNum) - bytes += self.int8Encoder.encode(cms.sequenceNum) - return bytes + enc_bytes += self.int8Encoder.encode(cms.referenceNum) + enc_bytes += self.int8Encoder.encode(cms.maximumNum) + enc_bytes += self.int8Encoder.encode(cms.sequenceNum) + return enc_bytes def decode(self, file): refNum = None @@ -95,7 +100,7 @@ class InformationElementEncoder(IEncoder): gsm_types.InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM: IEConcatenatedSMEncoder(False), gsm_types.InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM: IEConcatenatedSMEncoder(True), } - + def encode(self, iElement): dataBytes = None if iElement.identifier in self.dataEncoders: @@ -103,46 +108,46 @@ def encode(self, iElement): else: dataBytes = iElement.data length = len(dataBytes) - - bytes = '' - bytes += self.iEIEncoder.encode(iElement.identifier) - bytes += self.int8Encoder.encode(length) - bytes += dataBytes - return bytes + + enc_bytes = b'' + enc_bytes += self.iEIEncoder.encode(iElement.identifier) + enc_bytes += self.int8Encoder.encode(length) + enc_bytes += dataBytes + return enc_bytes def decode(self, file): fStart = file.tell() - + identifier = None try: identifier = self.iEIEncoder.decode(file) except UDHInformationElementIdentifierUnknownError: #Continue parsing after this so that these can be ignored pass - + length = self.int8Encoder.decode(file) data = None if identifier in self.dataEncoders: data = self.dataEncoders[identifier].decode(file) elif length > 0: data = self.read(file, length) - + parsed = file.tell() - fStart if parsed != length + 2: raise UDHParseError("Invalid length: expected %d, parsed %d" % (length + 2, parsed)) - + if identifier is None: return None - + return gsm_types.InformationElement(identifier, data) - + class UserDataHeaderEncoder(IEncoder): iEEncoder = InformationElementEncoder() int8Encoder = Int8Encoder() - + def encode(self, udh): nonRepeatable = {} - iEBytes = '' + iEBytes = b'' for iElement in udh: if not self.isIdentifierRepeatable(iElement.identifier): if iElement.identifier in nonRepeatable: @@ -151,7 +156,7 @@ def encode(self, udh): if identifier in nonRepeatable: raise ValueError("%s and %s are mutually exclusive elements" % (str(iElement.identifier), str(identifier))) nonRepeatable[iElement.identifier] = None - iEBytes += self.iEEncoder.encode(iElement) + iEBytes += self.iEEncoder.encode(iElement) headerLen = len(iEBytes) return self.int8Encoder.encode(headerLen) + iEBytes @@ -174,11 +179,11 @@ def decode(self, file): if identifier in nonRepeatable: del nonRepeatable[identifier] bytesRead = file.tell() - iStart - return repeatable + nonRepeatable.values() - + return repeatable + list(nonRepeatable.values()) + def isIdentifierRepeatable(self, identifier): - return gsm_constants.information_element_identifier_full_value_map[gsm_constants.information_element_identifier_name_map[str(identifier)]]['repeatable'] - + return gsm_constants.information_element_identifier_full_value_map[gsm_constants.information_element_identifier_name_map[identifier.name]]['repeatable'] + def getIdentifierExclusionList(self, identifier): - nameList = gsm_constants.information_element_identifier_full_value_map[gsm_constants.information_element_identifier_name_map[str(identifier)]]['excludes'] + nameList = gsm_constants.information_element_identifier_full_value_map[gsm_constants.information_element_identifier_name_map[identifier.name]]['excludes'] return [getattr(gsm_types.InformationElementIdentifier, name) for name in nameList] diff --git a/smpp/pdu/gsm_types.py b/smpp/pdu/gsm_types.py index dc51334..7df1db8 100644 --- a/smpp/pdu/gsm_types.py +++ b/smpp/pdu/gsm_types.py @@ -14,10 +14,10 @@ limitations under the License. """ from enum import Enum -from smpp.pdu.namedtuple import namedtuple +from collections import namedtuple from smpp.pdu import gsm_constants -InformationElementIdentifier = Enum(*gsm_constants.information_element_identifier_name_map.keys()) +InformationElementIdentifier = Enum('InformationElementIdentifier', list(gsm_constants.information_element_identifier_name_map.keys())) InformationElement = namedtuple('InformationElement', 'identifier, data') diff --git a/smpp/pdu/namedtuple.py b/smpp/pdu/namedtuple.py deleted file mode 100644 index 35e4e4f..0000000 --- a/smpp/pdu/namedtuple.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -This file comes from http://code.activestate.com/recipes/500261-named-tuples/ -Licensed under Python Software Foundation license: http://www.opensource.org/licenses/PythonSoftFoundation - -""" - -from operator import itemgetter as _itemgetter -from keyword import iskeyword as _iskeyword -import sys as _sys - -#pylint: disable-msg=E0102 -def namedtuple(typename, field_names, verbose=False, rename=False): - """Returns a new subclass of tuple with named fields. - - >>> Point = namedtuple('Point', 'x y') - >>> Point.__doc__ # docstring for the new class - 'Point(x, y)' - >>> p = Point(11, y=22) # instantiate with positional args or keywords - >>> p[0] + p[1] # indexable like a plain tuple - 33 - >>> x, y = p # unpack like a regular tuple - >>> x, y - (11, 22) - >>> p.x + p.y # fields also accessable by name - 33 - >>> d = p._asdict() # convert to a dictionary - >>> d['x'] - 11 - >>> Point(**d) # convert from a dictionary - Point(x=11, y=22) - >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields - Point(x=100, y=22) - - """ - - # Parse and validate the field names. Validation serves two purposes, - # generating informative error messages and preventing template injection attacks. - if isinstance(field_names, basestring): - field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas - field_names = tuple(map(str, field_names)) - if rename: - names = list(field_names) - seen = set() - for i, name in enumerate(names): - if (not min(c.isalnum() or c=='_' for c in name) or _iskeyword(name) - or not name or name[0].isdigit() or name.startswith('_') - or name in seen): - names[i] = '_%d' % i - seen.add(name) - field_names = tuple(names) - for name in (typename,) + field_names: - if not min(c.isalnum() or c=='_' for c in name): - raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name) - if _iskeyword(name): - raise ValueError('Type names and field names cannot be a keyword: %r' % name) - if name[0].isdigit(): - raise ValueError('Type names and field names cannot start with a number: %r' % name) - seen_names = set() - for name in field_names: - if name.startswith('_') and not rename: - raise ValueError('Field names cannot start with an underscore: %r' % name) - if name in seen_names: - raise ValueError('Encountered duplicate field name: %r' % name) - seen_names.add(name) - - # Create and fill-in the class template - numfields = len(field_names) - argtxt = repr(field_names).replace("'", "")[1:-1] # tuple repr without parens or quotes - reprtxt = ', '.join('%s=%%r' % name for name in field_names) - template = '''class %(typename)s(tuple): - '%(typename)s(%(argtxt)s)' \n - __slots__ = () \n - _fields = %(field_names)r \n - def __new__(_cls, %(argtxt)s): - return _tuple.__new__(_cls, (%(argtxt)s)) \n - @classmethod - def _make(cls, iterable, new=tuple.__new__, len=len): - 'Make a new %(typename)s object from a sequence or iterable' - result = new(cls, iterable) - if len(result) != %(numfields)d: - raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result)) - return result \n - def __repr__(self): - return '%(typename)s(%(reprtxt)s)' %% self \n - def _asdict(self): - 'Return a new dict which maps field names to their values' - return dict(zip(self._fields, self)) \n - def _replace(_self, **kwds): - 'Return a new %(typename)s object replacing specified fields with new values' - result = _self._make(map(kwds.pop, %(field_names)r, _self)) - if kwds: - raise ValueError('Got unexpected field names: %%r' %% kwds.keys()) - return result \n - def __getnewargs__(self): - return tuple(self) \n\n''' % locals() - for i, name in enumerate(field_names): - template += ' %s = _property(_itemgetter(%d))\n' % (name, i) - if verbose: - print template - - # Execute the template string in a temporary namespace - namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename, - _property=property, _tuple=tuple) - try: - exec template in namespace - except SyntaxError, e: - raise SyntaxError(e.message + ':\n' + template) - result = namespace[typename] - - # For pickling to work, the __module__ variable needs to be set to the frame - # where the named tuple is created. Bypass this step in enviroments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython). - try: - result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass - - return result - - - - - - -if __name__ == '__main__': - # verify that instances can be pickled - from cPickle import loads, dumps - Point = namedtuple('Point', 'x, y', True) - p = Point(x=10, y=20) - assert p == loads(dumps(p, -1)) - - # test and demonstrate ability to override methods - class Point(namedtuple('Point', 'x y')): - @property - def hypot(self): - return (self.x ** 2 + self.y ** 2) ** 0.5 - def __str__(self): - return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot) - - for p in Point(3,4), Point(14,5), Point(9./7,6): - print p - - class Point(namedtuple('Point', 'x y')): - 'Point class with optimized _make() and _replace() without error-checking' - _make = classmethod(tuple.__new__) - def _replace(self, _map=map, **kwds): - return self._make(_map(kwds.get, ('x', 'y'), self)) - - print Point(11, 22)._replace(x=100) - - import doctest - TestResults = namedtuple('TestResults', 'failed attempted') - print TestResults(*doctest.testmod()) - diff --git a/smpp/pdu/operations.py b/smpp/pdu/operations.py index 3b84b91..8fed7f8 100644 --- a/smpp/pdu/operations.py +++ b/smpp/pdu/operations.py @@ -20,7 +20,7 @@ class BindTransmitterResp(PDUResponse): commandId = CommandId.bind_transmitter_resp mandatoryParams = ['system_id'] optionalParams = ['sc_interface_version'] - + class BindTransmitter(PDURequest): requireAck = BindTransmitterResp commandId = CommandId.bind_transmitter @@ -39,7 +39,7 @@ class BindReceiverResp(PDUResponse): commandId = CommandId.bind_receiver_resp mandatoryParams = ['system_id'] optionalParams = ['sc_interface_version'] - + class BindReceiver(PDURequest): requireAck = BindReceiverResp commandId = CommandId.bind_receiver @@ -58,7 +58,7 @@ class BindTransceiverResp(PDUResponse): commandId = CommandId.bind_transceiver_resp mandatoryParams = ['system_id'] optionalParams = ['sc_interface_version'] - + class BindTransceiver(PDURequest): requireAck = BindTransceiverResp commandId = CommandId.bind_transceiver @@ -81,7 +81,7 @@ class Outbind(PDU): class UnbindResp(PDUResponse): commandId = CommandId.unbind_resp - + class Unbind(PDURequest): requireAck = UnbindResp commandId = CommandId.unbind @@ -93,7 +93,7 @@ class SubmitSMResp(PDUResponse): noBodyOnError = True commandId = CommandId.submit_sm_resp mandatoryParams = ['message_id'] - + class SubmitSM(PDUDataRequest): requireAck = SubmitSMResp commandId = CommandId.submit_sm @@ -118,6 +118,10 @@ class SubmitSM(PDUDataRequest): 'short_message', ] optionalParams = [ + # Avoid raising exceptions when having vendor specific tags, just + # bypass them + 'vendor_specific_bypass', + 'user_message_reference', 'source_port', 'source_addr_subunit', @@ -233,6 +237,15 @@ class DeliverSM(PDUDataRequest): 'short_message', ] optionalParams = [ + # Jasmin update: + # *_network_type are not optional parameters in standard SMPP + # it is added for compatibility with some providers (c.f. #120) + 'source_network_type', + 'dest_network_type', + # Avoid raising exceptions when having vendor specific tags, just + # bypass them + 'vendor_specific_bypass', + 'user_message_reference', 'source_port', 'destination_port', @@ -373,10 +386,10 @@ class ReplaceSM(PDUDataRequest): 'sm_length', 'short_message', ] - + class EnquireLinkResp(PDUResponse): commandId = CommandId.enquire_link_resp - + class EnquireLink(PDURequest): requireAck = EnquireLinkResp commandId = CommandId.enquire_link @@ -394,7 +407,7 @@ class AlertNotification(PDU): optionalParams = [ 'ms_availability_status', ] - + PDUS = {} def _register(): @@ -408,4 +421,4 @@ def _register(): _register() def getPDUClass(commandId): - return PDUS[commandId] \ No newline at end of file + return PDUS[commandId] diff --git a/smpp/pdu/pdu_encoding.py b/smpp/pdu/pdu_encoding.py index 06b609d..ee7ab58 100644 --- a/smpp/pdu/pdu_encoding.py +++ b/smpp/pdu/pdu_encoding.py @@ -13,50 +13,62 @@ See the License for the specific language governing permissions and limitations under the License. """ -import struct, string, binascii -from smpp.pdu import smpp_time + +""" +Updated code parts are marked with "Jasmin update" comment +""" +import binascii +import struct +from enum import Enum + from smpp.pdu import constants, pdu_types, operations +from smpp.pdu import smpp_time from smpp.pdu.error import PDUParseError, PDUCorruptError +from smpp.pdu.pdu_types import CommandId -class IEncoder(object): - def encode(self, value): +# Jasmin update: + +class IEncoder: + def encode(self, value, name=''): """Takes an object representing the type and returns a byte string""" raise NotImplementedError() def decode(self, file): """Takes file stream in and returns an object representing the type""" raise NotImplementedError() - + def read(self, file, size): bytesRead = file.read(size) length = len(bytesRead) if length == 0: raise PDUCorruptError("Unexpected EOF", pdu_types.CommandStatus.ESME_RINVMSGLEN) if length != size: - raise PDUCorruptError("Length mismatch. Expecting %d bytes. Read %d" % (size, length), pdu_types.CommandStatus.ESME_RINVMSGLEN) + raise PDUCorruptError("Length mismatch. Expecting %d bytes. Read %d" % (size, length), + pdu_types.CommandStatus.ESME_RINVMSGLEN) return bytesRead + class EmptyEncoder(IEncoder): - - def encode(self, value): - return '' + def encode(self, value, name=''): + return b'' def decode(self, file): return None + class PDUNullableFieldEncoder(IEncoder): nullHex = None nullable = True decodeNull = False requireNull = False - + def __init__(self, **kwargs): self.nullable = kwargs.get('nullable', self.nullable) self.decodeNull = kwargs.get('decodeNull', self.decodeNull) self.requireNull = kwargs.get('requireNull', self.requireNull) self._validateParams() - + def _validateParams(self): if self.decodeNull: if not self.nullable: @@ -65,27 +77,27 @@ def _validateParams(self): if not self.decodeNull: raise ValueError("decodeNull must be set if requireNull is set") - def encode(self, value): + def encode(self, value, name=''): if value is None: if not self.nullable: - raise ValueError("Field is not nullable") + raise ValueError("Field %s is not nullable" % name) if self.nullHex is None: - raise NotImplementedError("No value for null") + raise NotImplementedError("No fallback value for null field %s" % name) return binascii.a2b_hex(self.nullHex) if self.requireNull: - raise ValueError("Field must be null") + raise ValueError("Field %s must be null" % name) return self._encode(value) def decode(self, file): - bytes = self._read(file) + dec_bytes = self._read(file) if self.decodeNull: if self.nullHex is None: raise NotImplementedError("No value for null") - if self.nullHex == binascii.b2a_hex(bytes): + if self.nullHex == binascii.b2a_hex(dec_bytes): return None if self.requireNull: raise PDUParseError("Field must be null", pdu_types.CommandStatus.ESME_RUNKNOWNERR) - return self._decode(bytes) + return self._decode(dec_bytes) def _encode(self, value): """Takes an object representing the type and returns a byte string""" @@ -95,10 +107,11 @@ def _read(self, file): """Takes file stream in and returns raw bytes""" raise NotImplementedError() - def _decode(self, bytes): + def _decode(self, dec_bytes): """Takes bytes in and returns an object representing the type""" raise NotImplementedError() - + + class IntegerBaseEncoder(PDUNullableFieldEncoder): size = None sizeFmtMap = { @@ -106,20 +119,12 @@ class IntegerBaseEncoder(PDUNullableFieldEncoder): 2: '!H', 4: '!L', } - - #pylint: disable-msg=E0213 - def assertFmtSizes(sizeFmtMap): - for (size, fmt) in sizeFmtMap.items(): - assert struct.calcsize(fmt) == size - - #Verify platform sizes match protocol - assertFmtSizes(sizeFmtMap) def __init__(self, **kwargs): PDUNullableFieldEncoder.__init__(self, **kwargs) - - self.nullHex = '00' * self.size - + + self.nullHex = b'00' * self.size + self.max = 2 ** (8 * self.size) - 1 self.min = 0 if 'max' in kwargs: @@ -137,24 +142,30 @@ def _encode(self, value): if value > self.max: raise ValueError("Value %d exceeds max %d" % (value, self.max)) if value < self.min: - raise ValueError("Value %d is less than min %d" % (value, self.min)) + raise ValueError("Value %d is less than min %d" % (value, self.min)) return struct.pack(self.sizeFmtMap[self.size], value) - + def _read(self, file): return self.read(file, self.size) - - def _decode(self, bytes): - return struct.unpack(self.sizeFmtMap[self.size], bytes)[0] + + def _decode(self, dec_bytes): + if isinstance(dec_bytes, bytes): + return struct.unpack(self.sizeFmtMap[self.size], dec_bytes)[0] + return struct.unpack(self.sizeFmtMap[self.size], bytes([dec_bytes]))[0] + class Int4Encoder(IntegerBaseEncoder): size = 4 - + + class Int1Encoder(IntegerBaseEncoder): size = 1 - + + class Int2Encoder(IntegerBaseEncoder): size = 2 + class OctetStringEncoder(PDUNullableFieldEncoder): nullable = False @@ -171,21 +182,29 @@ def _encode(self, value): length = len(value) if self.getSize() is not None: if length != self.getSize(): - raise ValueError("Value size %d does not match expected %d" % (length, self.getSize())) + raise ValueError("Value (%s) size %d does not match expected %d" % (value, length, self.getSize())) + + if isinstance(value, int): + return bytes([value]) + elif isinstance(value, str): + return value.encode() return value def _read(self, file): if self.getSize() is None: raise AssertionError("Missing size to decode") if self.getSize() == 0: - return '' + return b'' return self.read(file, self.getSize()) - - def _decode(self, bytes): - return bytes + + def _decode(self, dec_bytes): + if isinstance(dec_bytes, str): + return dec_bytes.encode() + return dec_bytes + class COctetStringEncoder(PDUNullableFieldEncoder): - nullHex = '00' + nullHex = b'00' decodeErrorClass = PDUParseError decodeErrorStatus = pdu_types.CommandStatus.ESME_RUNKNOWNERR @@ -198,35 +217,39 @@ def __init__(self, maxSize=None, **kwargs): self.decodeErrorStatus = kwargs.get('decodeErrorStatus', self.decodeErrorStatus) def _encode(self, value): - asciiVal = value.encode('ascii') + if isinstance(value, str): + asciiVal = value.encode('ascii') + else: + asciiVal = value length = len(asciiVal) if self.maxSize is not None: if length + 1 > self.maxSize: raise ValueError("COctetString is longer than allowed maximum size (%d): %s" % (self.maxSize, asciiVal)) - encoded = struct.pack("%ds" % length, asciiVal) + '\0' + encoded = struct.pack("%ds" % length, asciiVal) + b'\0' assert len(encoded) == length + 1 return encoded def _read(self, file): - result = '' + result = b'' while True: c = self.read(file, 1) result += c - if c == '\0': + if c == b'\0': break return result - - def _decode(self, bytes): + + def _decode(self, dec_bytes): if self.maxSize is not None: - if len(bytes) > self.maxSize: + if len(dec_bytes) > self.maxSize: errStr = "COctetString is longer than allowed maximum size (%d)" % (self.maxSize) - raise self.decodeErrorClass(errStr, self.decodeErrorStatus) - return bytes[:-1] + raise self.decodeErrorClass(errStr, self.decodeErrorStatus) + return dec_bytes[:-1] + class IntegerWrapperEncoder(PDUNullableFieldEncoder): fieldName = None - nameMap = None - valueMap = None + nameMap = {} + valueMap = {} encoder = None pduType = None decodeErrorClass = PDUParseError @@ -240,23 +263,56 @@ def __init__(self, **kwargs): self.decodeErrorStatus = kwargs.get('decodeErrorStatus', self.decodeErrorStatus) def _encode(self, value): - name = str(value) - if name not in self.nameMap: - raise ValueError("Unknown %s name %s" % (self.fieldName, name)) - intVal = self.nameMap[name] + if isinstance(value, Enum): + if value.name not in self.nameMap: + raise ValueError("Unknown %s name %s" % (self.fieldName, value.name)) + intVal = self.nameMap[value.name] + elif isinstance(value, bytes): + value = value.decode() + if value not in self.nameMap: + raise ValueError("Unknown %s name %s" % (self.fieldName, value)) + intVal = self.nameMap[value] + else: + if value not in self.nameMap: + raise ValueError("Unknown %s name %s" % (self.fieldName, value)) + intVal = self.nameMap[value] return self.encoder.encode(intVal) - + def _read(self, file): return self.encoder._read(file) - - def _decode(self, bytes): - intVal = self.encoder._decode(bytes) - if intVal not in self.valueMap: + + def _decode(self, dec_bytes): + intVal = self.encoder._decode(dec_bytes) + + # Jasmin update: bypass vendor specific tags + # Vendor specific tag is not supported by Jasmin but must + # not raise an error + if self.fieldName == 'tag' and intVal == 0: + # Tag in range: "Reserved" + return self.pduType.vendor_specific_bypass + elif self.fieldName == 'tag' and intVal >= 0x0100 and intVal <= 0x01FF: + # Tag in range: "Reserved" + return self.pduType.vendor_specific_bypass + elif self.fieldName == 'tag' and intVal >= 0x0600 and intVal <= 0x10FF: + # Tag in range: "Reserved for SMPP Protocol Extension" + return self.pduType.vendor_specific_bypass + elif self.fieldName == 'tag' and intVal >= 0x1100 and intVal <= 0x11FF: + # Tag in range: "Reserved" + return self.pduType.vendor_specific_bypass + elif self.fieldName == 'tag' and intVal >= 0x1400 and intVal <= 0x3FFF: + # Tag in range: "Reserved for SMSC Vendor specific optional parameters" + return self.pduType.vendor_specific_bypass + elif self.fieldName == 'tag' and intVal >= 0x4000 and intVal <= 0xFFFF: + # Tag in range: "Reserved" + return self.pduType.vendor_specific_bypass + elif intVal not in self.valueMap: errStr = "Unknown %s value %s" % (self.fieldName, hex(intVal)) raise self.decodeErrorClass(errStr, self.decodeErrorStatus) + name = self.valueMap[intVal] return getattr(self.pduType, name) + class CommandIdEncoder(IntegerWrapperEncoder): fieldName = 'command_id' nameMap = constants.command_id_name_map @@ -265,24 +321,46 @@ class CommandIdEncoder(IntegerWrapperEncoder): pduType = pdu_types.CommandId decodeErrorClass = PDUCorruptError decodeErrorStatus = pdu_types.CommandStatus.ESME_RINVCMDID - + + class CommandStatusEncoder(Int4Encoder): nullable = False def _encode(self, value): - name = str(value) - if name not in constants.command_status_name_map: - raise ValueError("Unknown command_status name %s" % name) - intval = constants.command_status_name_map[name] + # _name_ gets the str name value of an enum + # https://docs.python.org/3/library/enum.html#supported-sunder-names + if value.name not in constants.command_status_name_map: + raise ValueError("Unknown command_status name %s" % value.name) + intval = constants.command_status_name_map[value.name] return Int4Encoder().encode(intval) - def _decode(self, bytes): - intval = Int4Encoder()._decode(bytes) + def _decode(self, dec_bytes): + intval = Int4Encoder()._decode(dec_bytes) if intval not in constants.command_status_value_map: - raise PDUParseError("Unknown command_status %s" % intval, pdu_types.CommandStatus.ESME_RUNKNOWNERR) - name = constants.command_status_value_map[intval]['name'] + # Jasmin update: + # as of Table 5-2: SMPP Error Codes + # (256 .. 1023) 0x00000100 .. 0x000003FF = Reserved for SMPP extension + # (1024 .. 1279) 0x00000400 .. 0x000004FF = Reserved for SMSC vendor specific errors + # (1280 ...) 0x00000500 ... = Reserved + # + # In order to avoid raising a PDUParseError on one of these reserved error codes, + # jasmin will return a general status indicating a reserved field + if 256 <= intval: + if 256 <= intval <= 1023: + name = constants.command_status_value_map[-1]['name'] + elif 1024 <= intval <= 1279: + name = constants.command_status_value_map[-2]['name'] + elif 1280 <= intval: + name = constants.command_status_value_map[-3]['name'] + else: + # RESERVEDSTATUS_UNKNOWN_STATUS + name = constants.command_status_value_map[-4]['name'] + else: + name = constants.command_status_value_map[intval]['name'] + return getattr(pdu_types.CommandStatus, name) + class TagEncoder(IntegerWrapperEncoder): fieldName = 'tag' nameMap = constants.tag_name_map @@ -290,17 +368,18 @@ class TagEncoder(IntegerWrapperEncoder): encoder = Int2Encoder() pduType = pdu_types.Tag decodeErrorStatus = pdu_types.CommandStatus.ESME_RINVOPTPARSTREAM - + + class EsmClassEncoder(Int1Encoder): modeMask = 0x03 typeMask = 0x3c gsmFeaturesMask = 0xc0 def _encode(self, esmClass): - modeName = str(esmClass.mode) - typeName = str(esmClass.type) - gsmFeatureNames = [str(f) for f in esmClass.gsmFeatures] - + modeName = esmClass.mode.name + typeName = esmClass.type.name + gsmFeatureNames = [f.name for f in esmClass.gsmFeatures] + if modeName not in constants.esm_class_mode_name_map: raise ValueError("Unknown esm_class mode name %s" % modeName) if typeName not in constants.esm_class_type_name_map: @@ -308,103 +387,116 @@ def _encode(self, esmClass): for featureName in gsmFeatureNames: if featureName not in constants.esm_class_gsm_features_name_map: raise ValueError("Unknown esm_class GSM feature name %s" % featureName) - + modeVal = constants.esm_class_mode_name_map[modeName] typeVal = constants.esm_class_type_name_map[typeName] gsmFeatureVals = [constants.esm_class_gsm_features_name_map[fName] for fName in gsmFeatureNames] - + intVal = modeVal | typeVal for fVal in gsmFeatureVals: intVal |= fVal - + return Int1Encoder().encode(intVal) - - def _decode(self, bytes): - intVal = Int1Encoder()._decode(bytes) + + def _decode(self, dec_bytes): + intVal = Int1Encoder()._decode(dec_bytes) modeVal = intVal & self.modeMask typeVal = intVal & self.typeMask gsmFeaturesVal = intVal & self.gsmFeaturesMask - + if modeVal not in constants.esm_class_mode_value_map: raise PDUParseError("Unknown esm_class mode %s" % modeVal, pdu_types.CommandStatus.ESME_RINVESMCLASS) if typeVal not in constants.esm_class_type_value_map: raise PDUParseError("Unknown esm_class type %s" % typeVal, pdu_types.CommandStatus.ESME_RINVESMCLASS) - + modeName = constants.esm_class_mode_value_map[modeVal] typeName = constants.esm_class_type_value_map[typeVal] - gsmFeatureNames = [constants.esm_class_gsm_features_value_map[fVal] for fVal in constants.esm_class_gsm_features_value_map.keys() if fVal & gsmFeaturesVal] - + gsmFeatureNames = [constants.esm_class_gsm_features_value_map[fVal] for fVal in + list(constants.esm_class_gsm_features_value_map.keys()) if fVal & gsmFeaturesVal] + mode = getattr(pdu_types.EsmClassMode, modeName) type = getattr(pdu_types.EsmClassType, typeName) gsmFeatures = [getattr(pdu_types.EsmClassGsmFeatures, fName) for fName in gsmFeatureNames] - + return pdu_types.EsmClass(mode, type, gsmFeatures) + class RegisteredDeliveryEncoder(Int1Encoder): receiptMask = 0x03 smeOriginatedAcksMask = 0x0c intermediateNotificationMask = 0x10 def _encode(self, registeredDelivery): - receiptName = str(registeredDelivery.receipt) - smeOriginatedAckNames = [str(a) for a in registeredDelivery.smeOriginatedAcks] - + receiptName = registeredDelivery.receipt.name + smeOriginatedAckNames = [a.name for a in registeredDelivery.smeOriginatedAcks] + if receiptName not in constants.registered_delivery_receipt_name_map: raise ValueError("Unknown registered_delivery receipt name %s" % receiptName) for ackName in smeOriginatedAckNames: if ackName not in constants.registered_delivery_sme_originated_acks_name_map: raise ValueError("Unknown registered_delivery SME orginated ack name %s" % ackName) - + receiptVal = constants.registered_delivery_receipt_name_map[receiptName] - smeOriginatedAckVals = [constants.registered_delivery_sme_originated_acks_name_map[ackName] for ackName in smeOriginatedAckNames] + smeOriginatedAckVals = [constants.registered_delivery_sme_originated_acks_name_map[ackName] for ackName in + smeOriginatedAckNames] intermediateNotificationVal = 0 if registeredDelivery.intermediateNotification: intermediateNotificationVal = self.intermediateNotificationMask - + intVal = receiptVal | intermediateNotificationVal for aVal in smeOriginatedAckVals: intVal |= aVal - + return Int1Encoder().encode(intVal) - - def _decode(self, bytes): - intVal = Int1Encoder()._decode(bytes) + + def _decode(self, dec_bytes): + intVal = Int1Encoder()._decode(dec_bytes) receiptVal = intVal & self.receiptMask smeOriginatedAcksVal = intVal & self.smeOriginatedAcksMask intermediateNotificationVal = intVal & self.intermediateNotificationMask - + if receiptVal not in constants.registered_delivery_receipt_value_map: - raise PDUParseError("Unknown registered_delivery receipt %s" % receiptVal, pdu_types.CommandStatus.ESME_RINVREGDLVFLG) - + raise PDUParseError("Unknown registered_delivery receipt %s" % receiptVal, + pdu_types.CommandStatus.ESME_RINVREGDLVFLG) + receiptName = constants.registered_delivery_receipt_value_map[receiptVal] - smeOriginatedAckNames = [constants.registered_delivery_sme_originated_acks_value_map[aVal] for aVal in constants.registered_delivery_sme_originated_acks_value_map.keys() if aVal & smeOriginatedAcksVal] - + smeOriginatedAckNames = [constants.registered_delivery_sme_originated_acks_value_map[aVal] for aVal in + list(constants.registered_delivery_sme_originated_acks_value_map.keys()) if + aVal & smeOriginatedAcksVal] + receipt = getattr(pdu_types.RegisteredDeliveryReceipt, receiptName) - smeOriginatedAcks = [getattr(pdu_types.RegisteredDeliverySmeOriginatedAcks, aName) for aName in smeOriginatedAckNames] + smeOriginatedAcks = [getattr(pdu_types.RegisteredDeliverySmeOriginatedAcks, aName) for aName in + smeOriginatedAckNames] intermediateNotification = False if intermediateNotificationVal: intermediateNotification = True - + return pdu_types.RegisteredDelivery(receipt, smeOriginatedAcks, intermediateNotification) + class DataCodingEncoder(Int1Encoder): schemeMask = 0xf0 schemeDataMask = 0x0f gsmMsgCodingMask = 0x04 gsmMsgClassMask = 0x03 - + def _encode(self, dataCoding): return Int1Encoder().encode(self._encodeAsInt(dataCoding)) - + def _encodeAsInt(self, dataCoding): - if dataCoding.scheme == pdu_types.DataCodingScheme.RAW: + # Jasmin update: + # Comparing dataCoding.scheme to pdu_types.DataCodingScheme.RAW would result + # to False even if the values are the same, this is because Enum object have + # no right __eq__ to compare values + # Fix: compare Enum indexes (.index) + if dataCoding.scheme.value == pdu_types.DataCodingScheme.RAW.value: return dataCoding.schemeData - if dataCoding.scheme == pdu_types.DataCodingScheme.DEFAULT: + if dataCoding.scheme.value == pdu_types.DataCodingScheme.DEFAULT.value: return self._encodeDefaultSchemeAsInt(dataCoding) return self._encodeSchemeAsInt(dataCoding) - + def _encodeDefaultSchemeAsInt(self, dataCoding): - defaultName = str(dataCoding.schemeData) + defaultName = dataCoding.schemeData.name if defaultName not in constants.data_coding_default_name_map: raise ValueError("Unknown data_coding default name %s" % defaultName) return constants.data_coding_default_name_map[defaultName] @@ -413,80 +505,92 @@ def _encodeSchemeAsInt(self, dataCoding): schemeVal = self._encodeSchemeNameAsInt(dataCoding) schemeDataVal = self._encodeSchemeDataAsInt(dataCoding) return schemeVal | schemeDataVal - + def _encodeSchemeNameAsInt(self, dataCoding): - schemeName = str(dataCoding.scheme) + schemeName = dataCoding.scheme.name if schemeName not in constants.data_coding_scheme_name_map: raise ValueError("Unknown data_coding scheme name %s" % schemeName) return constants.data_coding_scheme_name_map[schemeName] - + def _encodeSchemeDataAsInt(self, dataCoding): - if dataCoding.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS: + # Jasmin update: + # Related to #182 + # When pdu is unpickled (from smpps or http api), the comparison below will always + # be False since memory addresses of both objects are different. + # Using name will get the comparison on the 'GSM_MESSAGE_CLASS' string value + #pylint: disable=no-member + if dataCoding.scheme.name == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS.name: return self._encodeGsmMsgSchemeDataAsInt(dataCoding) + # Jasmin update: + # As reported in https://github.com/mozes/smpp.pdu/issues/12 + # raise ValueError("Unknown data coding scheme %s" % dataCoding.scheme) + # ~~~~~~~~~~~ raise ValueError("Unknown data coding scheme %s" % dataCoding.scheme) - + def _encodeGsmMsgSchemeDataAsInt(self, dataCoding): - msgCodingName = str(dataCoding.schemeData.msgCoding) - msgClassName = str(dataCoding.schemeData.msgClass) + msgCodingName = dataCoding.schemeData.msgCoding.name + msgClassName = dataCoding.schemeData.msgClass.name if msgCodingName not in constants.data_coding_gsm_message_coding_name_map: raise ValueError("Unknown data_coding gsm msg coding name %s" % msgCodingName) if msgClassName not in constants.data_coding_gsm_message_class_name_map: raise ValueError("Unknown data_coding gsm msg class name %s" % msgClassName) - + msgCodingVal = constants.data_coding_gsm_message_coding_name_map[msgCodingName] msgClassVal = constants.data_coding_gsm_message_class_name_map[msgClassName] return msgCodingVal | msgClassVal - def _decode(self, bytes): - intVal = Int1Encoder()._decode(bytes) + def _decode(self, dec_bytes): + intVal = Int1Encoder()._decode(dec_bytes) scheme = self._decodeScheme(intVal) schemeData = self._decodeSchemeData(scheme, intVal) return pdu_types.DataCoding(scheme, schemeData) - + def _decodeScheme(self, intVal): schemeVal = intVal & self.schemeMask if schemeVal in constants.data_coding_scheme_value_map: schemeName = constants.data_coding_scheme_value_map[schemeVal] return getattr(pdu_types.DataCodingScheme, schemeName) - + if intVal in constants.data_coding_default_value_map: return pdu_types.DataCodingScheme.DEFAULT - + return pdu_types.DataCodingScheme.RAW - + def _decodeSchemeData(self, scheme, intVal): if scheme == pdu_types.DataCodingScheme.RAW: return intVal if scheme == pdu_types.DataCodingScheme.DEFAULT: return self._decodeDefaultSchemeData(intVal) + #pylint: disable=no-member if scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS: schemeDataVal = intVal & self.schemeDataMask return self._decodeGsmMsgSchemeData(schemeDataVal) raise ValueError("Unexpected data coding scheme %s" % scheme) - + def _decodeDefaultSchemeData(self, intVal): if intVal not in constants.data_coding_default_value_map: raise ValueError("Unknown data_coding default value %s" % intVal) defaultName = constants.data_coding_default_value_map[intVal] return getattr(pdu_types.DataCodingDefault, defaultName) - + def _decodeGsmMsgSchemeData(self, schemeDataVal): msgCodingVal = schemeDataVal & self.gsmMsgCodingMask msgClassVal = schemeDataVal & self.gsmMsgClassMask - + if msgCodingVal not in constants.data_coding_gsm_message_coding_value_map: raise ValueError("Unknown data_coding gsm msg coding value %s" % msgCodingVal) if msgClassVal not in constants.data_coding_gsm_message_class_value_map: raise ValueError("Unknown data_coding gsm msg class value %s" % msgClassVal) - + msgCodingName = constants.data_coding_gsm_message_coding_value_map[msgCodingVal] msgClassName = constants.data_coding_gsm_message_class_value_map[msgClassVal] - + msgCoding = getattr(pdu_types.DataCodingGsmMsgCoding, msgCodingName) msgClass = getattr(pdu_types.DataCodingGsmMsgClass, msgClassName) return pdu_types.DataCodingGsmMsg(msgCoding, msgClass) - + + class AddrTonEncoder(IntegerWrapperEncoder): fieldName = 'addr_ton' nameMap = constants.addr_ton_name_map @@ -494,6 +598,7 @@ class AddrTonEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.AddrTon + class AddrNpiEncoder(IntegerWrapperEncoder): fieldName = 'addr_npi' nameMap = constants.addr_npi_name_map @@ -501,6 +606,7 @@ class AddrNpiEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.AddrNpi + class PriorityFlagEncoder(IntegerWrapperEncoder): fieldName = 'priority_flag' nameMap = constants.priority_flag_name_map @@ -509,6 +615,7 @@ class PriorityFlagEncoder(IntegerWrapperEncoder): pduType = pdu_types.PriorityFlag decodeErrorStatus = pdu_types.CommandStatus.ESME_RINVPRTFLG + class ReplaceIfPresentFlagEncoder(IntegerWrapperEncoder): fieldName = 'replace_if_present_flag' nameMap = constants.replace_if_present_flap_name_map @@ -516,6 +623,7 @@ class ReplaceIfPresentFlagEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.ReplaceIfPresentFlag + class DestFlagEncoder(IntegerWrapperEncoder): nullable = False fieldName = 'dest_flag' @@ -524,6 +632,7 @@ class DestFlagEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.DestFlag + class MessageStateEncoder(IntegerWrapperEncoder): nullable = False fieldName = 'message_state' @@ -532,6 +641,7 @@ class MessageStateEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.MessageState + class CallbackNumDigitModeIndicatorEncoder(IntegerWrapperEncoder): nullable = False fieldName = 'callback_num_digit_mode_indicator' @@ -541,29 +651,32 @@ class CallbackNumDigitModeIndicatorEncoder(IntegerWrapperEncoder): pduType = pdu_types.CallbackNumDigitModeIndicator decodeErrorStatus = pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL + class CallbackNumEncoder(OctetStringEncoder): digitModeIndicatorEncoder = CallbackNumDigitModeIndicatorEncoder() tonEncoder = AddrTonEncoder() npiEncoder = AddrNpiEncoder() def _encode(self, callbackNum): - encoded = '' + encoded = b'' encoded += self.digitModeIndicatorEncoder._encode(callbackNum.digitModeIndicator) encoded += self.tonEncoder._encode(callbackNum.ton) encoded += self.npiEncoder._encode(callbackNum.npi) encoded += callbackNum.digits return encoded - - def _decode(self, bytes): - if len(bytes) < 3: - raise PDUParseError("Invalid callback_num size %s" % len(bytes), pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL) - - digitModeIndicator = self.digitModeIndicatorEncoder._decode(bytes[0]) - ton = self.tonEncoder._decode(bytes[1]) - npi = self.npiEncoder._decode(bytes[2]) - digits = bytes[3:] + + def _decode(self, dec_bytes): + if len(dec_bytes) < 3: + raise PDUParseError("Invalid callback_num size %s" % len(dec_bytes), + pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL) + + digitModeIndicator = self.digitModeIndicatorEncoder._decode(dec_bytes[0]) + ton = self.tonEncoder._decode(dec_bytes[1]) + npi = self.npiEncoder._decode(dec_bytes[2]) + digits = dec_bytes[3:] return pdu_types.CallbackNum(digitModeIndicator, ton, npi, digits) + class SubaddressTypeTagEncoder(IntegerWrapperEncoder): nullable = False fieldName = 'subaddress_type_tag' @@ -573,24 +686,30 @@ class SubaddressTypeTagEncoder(IntegerWrapperEncoder): pduType = pdu_types.SubaddressTypeTag decodeErrorStatus = pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL + class SubaddressEncoder(OctetStringEncoder): typeTagEncoder = SubaddressTypeTagEncoder() def _encode(self, subaddress): - encoded = '' + encoded = b'' encoded += self.typeTagEncoder._encode(subaddress.typeTag) valSize = self.getSize() - 1 if self.getSize() is not None else None encoded += OctetStringEncoder(valSize)._encode(subaddress.value) return encoded - - def _decode(self, bytes): - if len(bytes) < 2: - raise PDUParseError("Invalid subaddress size %s" % len(bytes), pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL) - typeTag = self.typeTagEncoder._decode(bytes[0]) - value = OctetStringEncoder(self.getSize() - 1)._decode(bytes[1:]) + def _decode(self, dec_bytes): + if len(dec_bytes) < 2: + raise PDUParseError("Invalid subaddress size %s" % len(dec_bytes), pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL) + + try: + typeTag = self.typeTagEncoder._decode(dec_bytes[0]) + except PDUParseError as e: + typeTag = 'RESERVED' + + value = OctetStringEncoder(self.getSize() - 1)._decode(dec_bytes[1:]) return pdu_types.Subaddress(typeTag, value) + class AddrSubunitEncoder(IntegerWrapperEncoder): fieldName = 'addr_subunit' nameMap = constants.addr_subunit_name_map @@ -598,6 +717,7 @@ class AddrSubunitEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.AddrSubunit + class NetworkTypeEncoder(IntegerWrapperEncoder): fieldName = 'network_type' nameMap = constants.network_type_name_map @@ -605,6 +725,7 @@ class NetworkTypeEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.NetworkType + class BearerTypeEncoder(IntegerWrapperEncoder): fieldName = 'bearer_type' nameMap = constants.bearer_type_name_map @@ -612,6 +733,7 @@ class BearerTypeEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.BearerType + class PayloadTypeEncoder(IntegerWrapperEncoder): fieldName = 'payload_type' nameMap = constants.payload_type_name_map @@ -619,6 +741,7 @@ class PayloadTypeEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.PayloadType + class PrivacyIndicatorEncoder(IntegerWrapperEncoder): fieldName = 'privacy_indicator' nameMap = constants.privacy_indicator_name_map @@ -626,6 +749,7 @@ class PrivacyIndicatorEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.PrivacyIndicator + class LanguageIndicatorEncoder(IntegerWrapperEncoder): fieldName = 'language_indicator' nameMap = constants.language_indicator_name_map @@ -633,13 +757,15 @@ class LanguageIndicatorEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.LanguageIndicator + class DisplayTimeEncoder(IntegerWrapperEncoder): fieldName = 'display_time' nameMap = constants.display_time_name_map valueMap = constants.display_time_value_map encoder = Int1Encoder() pduType = pdu_types.DisplayTime - + + class MsAvailabilityStatusEncoder(IntegerWrapperEncoder): fieldName = 'ms_availability_status' nameMap = constants.ms_availability_status_name_map @@ -647,6 +773,43 @@ class MsAvailabilityStatusEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.MsAvailabilityStatus +class NetworkErrorCodeNetworkTypeEncoder(IntegerWrapperEncoder): + nullable = False + fieldName = 'network_error_code_network_type' + nameMap = constants.network_error_code_name_map + valueMap = constants.network_error_code_value_map + encoder = Int1Encoder() + pduType = pdu_types.NetworkErrorCodeNetworkType + decodeErrorStatus = pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL + +class NetworkErrorCodeEncoder(OctetStringEncoder): + networkTypeEncoder = NetworkErrorCodeNetworkTypeEncoder() + + def _encode(self, networkError): + encoded = b'' + encoded += self.networkTypeEncoder._encode(networkError.networkType) + encoded += networkError.value + return encoded + + def _decode(self, dec_bytes): + if len(dec_bytes) < 2: + raise PDUParseError("Invalid network error size %s" % len(dec_bytes), pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL) + + try: + typeInt = dec_bytes[0] + if typeInt not in constants.network_error_code_value_map: + # byte is being decoded as the ascii value most likely, try transforming the ascii value to an int + typeInt = int(chr(typeInt)) + networkType = self.networkTypeEncoder._decode(typeInt) + except PDUParseError as e: + networkType = 'RESERVED' + except ValueError: + # probably could not parse the ascii value to an int default to reserved + networkType = 'RESERVED' + + value = dec_bytes[1:] + return pdu_types.NetworkErrorCode(networkType, value) + class DeliveryFailureReasonEncoder(IntegerWrapperEncoder): fieldName = 'delivery_failure_reason' nameMap = constants.delivery_failure_reason_name_map @@ -654,6 +817,7 @@ class DeliveryFailureReasonEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.DeliveryFailureReason + class MoreMessagesToSendEncoder(IntegerWrapperEncoder): fieldName = 'more_messages_to_send' nameMap = constants.more_messages_to_send_name_map @@ -661,8 +825,9 @@ class MoreMessagesToSendEncoder(IntegerWrapperEncoder): encoder = Int1Encoder() pduType = pdu_types.MoreMessagesToSend + class TimeEncoder(PDUNullableFieldEncoder): - nullHex = '00' + nullHex = b'00' decodeNull = True encoder = COctetStringEncoder(17) decodeErrorClass = PDUParseError @@ -676,34 +841,40 @@ def __init__(self, **kwargs): def _encode(self, time): str = smpp_time.unparse(time) - return self.encoder._encode(str) - + return self.encoder._encode(str) + def _read(self, file): return self.encoder._read(file) - - def _decode(self, bytes): - timeStr = self.encoder._decode(bytes) + + def _decode(self, dec_bytes): + timeStr = self.encoder._decode(dec_bytes) try: return smpp_time.parse(timeStr) - except Exception, e: + except Exception as e: errStr = str(e) - raise self.decodeErrorClass(errStr, self.decodeErrorStatus) + raise self.decodeErrorClass(errStr, self.decodeErrorStatus) + class ShortMessageEncoder(IEncoder): smLengthEncoder = Int1Encoder(max=254) - - def encode(self, shortMessage): + + def encode(self, shortMessage, name=''): if shortMessage is None: - shortMessage = '' + shortMessage = b'' smLength = len(shortMessage) + return self.smLengthEncoder.encode(smLength) + OctetStringEncoder(smLength).encode(shortMessage) def decode(self, file): smLength = self.smLengthEncoder.decode(file) return OctetStringEncoder(smLength).decode(file) -class OptionEncoder(IEncoder): +class MessagePayloadEncoder(OctetStringEncoder): + pass + + +class OptionEncoder(IEncoder): def __init__(self): from smpp.pdu.pdu_types import Tag as T self.length = None @@ -734,42 +905,46 @@ def __init__(self): T.sar_segment_seqnum: Int1Encoder(), T.sc_interface_version: Int1Encoder(), T.display_time: DisplayTimeEncoder(), - #T.ms_validity: MsValidityEncoder(), - #T.dpf_result: DpfResultEncoder(), - #T.set_dpf: SetDpfEncoder(), + # T.ms_validity: MsValidityEncoder(), + # T.dpf_result: DpfResultEncoder(), + # T.set_dpf: SetDpfEncoder(), T.ms_availability_status: MsAvailabilityStatusEncoder(), - #T.network_error_code: NetworkErrorCodeEncoder(), - T.message_payload: OctetStringEncoder(self.getLength), + # Jasmin update: + T.network_error_code: NetworkErrorCodeEncoder(self.getLength), + T.message_payload: MessagePayloadEncoder(self.getLength), T.delivery_failure_reason: DeliveryFailureReasonEncoder(), T.more_messages_to_send: MoreMessagesToSendEncoder(), T.message_state: MessageStateEncoder(), T.callback_num: CallbackNumEncoder(self.getLength), - #T.callback_num_pres_ind: CallbackNumPresIndEncoder(), + # T.callback_num_pres_ind: CallbackNumPresIndEncoder(), # T.callback_num_atag: CallbackNumAtag(), T.number_of_messages: Int1Encoder(max=99), T.sms_signal: OctetStringEncoder(self.getLength), T.alert_on_message_delivery: EmptyEncoder(), - #T.its_reply_type: ItsReplyTypeEncoder(), + # T.its_reply_type: ItsReplyTypeEncoder(), # T.its_session_info: ItsSessionInfoEncoder(), # T.ussd_service_op: UssdServiceOpEncoder(), + # Jasmin update: bypass vendor specific tags + T.vendor_specific_bypass: OctetStringEncoder(self.getLength), } def getLength(self): return self.length - def encode(self, option): + def encode(self, option, name=''): if option.tag not in self.options: raise ValueError("Unknown option %s" % str(option)) encoder = self.options[option.tag] encodedValue = encoder.encode(option.value) length = len(encodedValue) - return string.join([ + return b''.join([ TagEncoder().encode(option.tag), Int2Encoder().encode(length), encodedValue, - ], '') - + ]) + def decode(self, file): + # Jasmin update: bypass vendor specific tags tag = TagEncoder().decode(file) self.length = Int2Encoder().decode(file) if tag not in self.options: @@ -779,16 +954,20 @@ def decode(self, file): value = None try: value = encoder.decode(file) - except PDUParseError, e: + except PDUParseError as e: e.status = pdu_types.CommandStatus.ESME_RINVOPTPARAMVAL raise e - + iAfterDecode = file.tell() parseLen = iAfterDecode - iBeforeDecode if parseLen != self.length: - raise PDUParseError("Invalid option length: labeled [%d] but parsed [%d]" % (self.length, parseLen), pdu_types.CommandStatus.ESME_RINVPARLEN) + raise PDUParseError("Invalid option length: labeled [%d] but parsed [%d]" % (self.length, parseLen), + pdu_types.CommandStatus.ESME_RINVPARLEN) + # Reset the length otherwise it carries over to other encoding/decoding operations + self.length = None return pdu_types.Option(tag, value) - + + class PDUEncoder(IEncoder): HEADER_LEN = 16 @@ -796,7 +975,7 @@ class PDUEncoder(IEncoder): 'command_length': Int4Encoder(), 'command_id': CommandIdEncoder(), 'command_status': CommandStatusEncoder(), - #the spec says max=0x7FFFFFFF but vendors don't respect this + # the spec says max=0x7FFFFFFF but vendors don't respect this 'sequence_number': Int4Encoder(min=0x00000001), } HeaderParams = [ @@ -808,18 +987,22 @@ class PDUEncoder(IEncoder): DefaultRequiredParamEncoders = { 'system_id': COctetStringEncoder(16, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSYSID), - 'password': COctetStringEncoder(9, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVPASWD), + 'password': COctetStringEncoder(16, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVPASWD), 'system_type': COctetStringEncoder(13), 'interface_version': Int1Encoder(), 'addr_ton': AddrTonEncoder(), 'addr_npi': AddrNpiEncoder(), 'address_range': COctetStringEncoder(41), 'service_type': COctetStringEncoder(6, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSERTYP), - 'source_addr_ton': AddrTonEncoder(fieldName='source_addr_ton', decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSRCTON), - 'source_addr_npi': AddrNpiEncoder(fieldName='source_addr_npi', decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSRCNPI), + 'source_addr_ton': AddrTonEncoder(fieldName='source_addr_ton', + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSRCTON), + 'source_addr_npi': AddrNpiEncoder(fieldName='source_addr_npi', + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSRCNPI), 'source_addr': COctetStringEncoder(21, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSRCADR), - 'dest_addr_ton': AddrTonEncoder(fieldName='dest_addr_ton', decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDSTTON), - 'dest_addr_npi': AddrNpiEncoder(fieldName='dest_addr_npi', decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDSTNPI), + 'dest_addr_ton': AddrTonEncoder(fieldName='dest_addr_ton', + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDSTTON), + 'dest_addr_npi': AddrNpiEncoder(fieldName='dest_addr_npi', + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDSTNPI), 'destination_addr': COctetStringEncoder(21, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDSTADR), 'esm_class': EsmClassEncoder(), 'esme_addr_ton': AddrTonEncoder(fieldName='esme_addr_ton'), @@ -832,7 +1015,9 @@ class PDUEncoder(IEncoder): 'registered_delivery': RegisteredDeliveryEncoder(), 'replace_if_present_flag': ReplaceIfPresentFlagEncoder(), 'data_coding': DataCodingEncoder(), - 'sm_default_msg_id': Int1Encoder(min=1, max=254, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDFTMSGID), + # Jasmin update: + # Minimum for sm_default_msg_id can be 0 (reserved value) + 'sm_default_msg_id': Int1Encoder(min=0, max=254, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDFTMSGID), 'short_message': ShortMessageEncoder(), 'message_id': COctetStringEncoder(65, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVMSGID), # 'number_of_dests': Int1Encoder(max=254), @@ -840,9 +1025,9 @@ class PDUEncoder(IEncoder): # 'dl_name': COctetStringEncoder(21), 'message_state': MessageStateEncoder(), 'final_date': TimeEncoder(), - 'error_code':Int1Encoder(decodeNull=True), + 'error_code': Int1Encoder(decodeNull=True), } - + CustomRequiredParamEncoders = { pdu_types.CommandId.alert_notification: { 'source_addr': COctetStringEncoder(65, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSRCADR), @@ -852,11 +1037,14 @@ class PDUEncoder(IEncoder): 'destination_addr': COctetStringEncoder(65, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVDSTADR), }, pdu_types.CommandId.deliver_sm: { - 'schedule_delivery_time': TimeEncoder(requireNull=True, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSCHED), - 'validity_period': TimeEncoder(requireNull=True, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVEXPIRY), + 'schedule_delivery_time': TimeEncoder(requireNull=True, + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVSCHED), + 'validity_period': TimeEncoder(requireNull=False, + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVEXPIRY), }, pdu_types.CommandId.deliver_sm_resp: { - 'message_id': COctetStringEncoder(decodeNull=True, requireNull=True, decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVMSGID), + 'message_id': COctetStringEncoder(decodeNull=True, requireNull=True, + decodeErrorStatus=pdu_types.CommandStatus.ESME_RINVMSGID), } } @@ -865,44 +1053,58 @@ def __init__(self): def getRequiredParamEncoders(self, pdu): if pdu.id in self.CustomRequiredParamEncoders: - return dict(self.DefaultRequiredParamEncoders.items() + self.CustomRequiredParamEncoders[pdu.id].items()) + return {**self.DefaultRequiredParamEncoders, **self.CustomRequiredParamEncoders[pdu.id]} return self.DefaultRequiredParamEncoders - def encode(self, pdu): + def encode(self, pdu, name=''): body = self.encodeBody(pdu) return self.encodeHeader(pdu, body) + body - + def decode(self, file): iBeforeDecode = file.tell() headerParams = self.decodeHeader(file) pduKlass = operations.getPDUClass(headerParams['command_id']) pdu = pduKlass(headerParams['sequence_number'], headerParams['command_status']) self.decodeBody(file, pdu, headerParams['command_length'] - self.HEADER_LEN) - + iAfterDecode = file.tell() parsedLen = iAfterDecode - iBeforeDecode - if parsedLen != headerParams['command_length']: - raise PDUCorruptError("Invalid command length: expected %d, parsed %d" % (headerParams['command_length'], parsedLen), pdu_types.CommandStatus.ESME_RINVCMDLEN) - + # Jasmin update: + # Related to #124, don't error if parsedLen is greater than command_length, + # there can be some padding in PDUs, this is a fix to be confirmed for stability + if headerParams['command_length'] > parsedLen: + padBytes = file.read(headerParams['command_length'] - parsedLen) + if len(padBytes) != headerParams['command_length'] - parsedLen: + raise PDUCorruptError("Invalid command length: expected %d, parsed %d, padding bytes not found" % ( + headerParams['command_length'], parsedLen), pdu_types.CommandStatus.ESME_RINVCMDLEN) + elif parsedLen < headerParams['command_length']: + raise PDUCorruptError( + "Invalid command length: expected %d, parsed %d" % (headerParams['command_length'], parsedLen), + pdu_types.CommandStatus.ESME_RINVCMDLEN) + return pdu - + def decodeHeader(self, file): headerParams = self.decodeRequiredParams(self.HeaderParams, self.HeaderEncoders, file) if headerParams['command_length'] < self.HEADER_LEN: - raise PDUCorruptError("Invalid command_length %d" % headerParams['command_length'], pdu_types.CommandStatus.ESME_RINVCMDLEN) + raise PDUCorruptError("Invalid command_length %d" % headerParams['command_length'], + pdu_types.CommandStatus.ESME_RINVCMDLEN) return headerParams - + def decodeBody(self, file, pdu, bodyLength): mandatoryParams = {} optionalParams = {} - - #Some PDU responses have no defined body when the status is not 0 + + # Some PDU responses have no defined body when the status is not 0 + # c.f. 4.1.2. "BIND_TRANSMITTER_RESP" # c.f. 4.1.4. "BIND_RECEIVER_RESP" # c.f. 4.4.2. SMPP PDU Definition "SUBMIT_SM_RESP" - if pdu.status != pdu_types.CommandStatus.ESME_ROK: - if pdu.noBodyOnError: + if pdu.commandId in ( + CommandId.bind_receiver_resp, CommandId.bind_transmitter_resp, CommandId.bind_transceiver_resp, + CommandId.submit_sm_resp): + if pdu.status != pdu_types.CommandStatus.ESME_ROK and pdu.noBodyOnError: return - + iBeforeMParams = file.tell() if len(pdu.mandatoryParams) > 0: mandatoryParams = self.decodeRequiredParams(pdu.mandatoryParams, self.getRequiredParamEncoders(pdu), file) @@ -910,26 +1112,32 @@ def decodeBody(self, file, pdu, bodyLength): mParamsLen = iAfterMParams - iBeforeMParams if len(pdu.optionalParams) > 0: optionalParams = self.decodeOptionalParams(pdu.optionalParams, file, bodyLength - mParamsLen) - pdu.params = dict(mandatoryParams.items() + optionalParams.items()) - + + pdu.params.update(mandatoryParams) + pdu.params.update(optionalParams) + def encodeBody(self, pdu): - body = '' - - #Some PDU responses have no defined body when the status is not 0 + body = b'' + + # Some PDU responses have no defined body when the status is not 0 + # c.f. 4.1.2. "BIND_TRANSMITTER_RESP" # c.f. 4.1.4. "BIND_RECEIVER_RESP" # c.f. 4.4.2. SMPP PDU Definition "SUBMIT_SM_RESP" - if pdu.status != pdu_types.CommandStatus.ESME_ROK: - if pdu.noBodyOnError: + if pdu.commandId in ( + CommandId.bind_receiver_resp, CommandId.bind_transmitter_resp, CommandId.bind_transceiver_resp, + CommandId.submit_sm_resp): + if pdu.status != pdu_types.CommandStatus.ESME_ROK and pdu.noBodyOnError: return body - + for paramName in pdu.mandatoryParams: if paramName not in pdu.params: raise ValueError("Missing required parameter: %s" % paramName) - + body += self.encodeRequiredParams(pdu.mandatoryParams, self.getRequiredParamEncoders(pdu), pdu.params) body += self.encodeOptionalParams(pdu.optionalParams, pdu.params) + body += self.encodeRawParams(pdu.custom_tlvs) return body - + def encodeHeader(self, pdu, body): cmdLength = len(body) + self.HEADER_LEN headerParams = { @@ -943,31 +1151,71 @@ def encodeHeader(self, pdu, body): return header def encodeOptionalParams(self, optionalParams, params): - result = '' + # Jasmin update: + # Do not encode vendor_specific_bypass parameter: + if 'vendor_specific_bypass' in params: + del params['vendor_specific_bypass'] + + result = b'' for paramName in optionalParams: if paramName in params: tag = getattr(pdu_types.Tag, paramName) value = params[paramName] result += self.optionEncoder.encode(pdu_types.Option(tag, value)) return result - + + def encodeRawParams(self, tlvs): + # Jasmin update: + # Do not encode vendor_specific_bypass parameter: + result = b'' + for tlv in tlvs: + if len(tlv) != 4: + continue + tag, length, value_type, value = tlv + + if value_type == 'Int1': + encoded_value = Int1Encoder().encode(value) + elif value_type == 'Int2': + encoded_value = Int2Encoder().encode(value) + elif value_type == 'Int4': + encoded_value = Int4Encoder().encode(value) + elif value_type == 'OctetString': + encoded_value = OctetStringEncoder().encode(value) + elif value_type == 'COctetString': + encoded_value = COctetStringEncoder().encode(value) + else: + continue # Unknown tlv + + if length is None: + length = len(encoded_value) + elif len(encoded_value) < length: + # Needs some padding + encoded_value += (length - len(encoded_value)) * '\0' + + result += Int2Encoder().encode(tag) + Int2Encoder().encode(length) + encoded_value + return result + def decodeOptionalParams(self, paramList, file, optionsLength): optionalParams = {} iBefore = file.tell() while file.tell() - iBefore < optionsLength: option = self.optionEncoder.decode(file) - optionName = str(option.tag) - if optionName not in paramList: + optionName = option.tag.name + + # Jasmin update: + # Silently drop vendor_specific_bypass optional param + if optionName == 'vendor_specific_bypass': + continue + elif optionName not in paramList: raise PDUParseError("Invalid option %s" % optionName, pdu_types.CommandStatus.ESME_ROPTPARNOTALLWD) optionalParams[optionName] = option.value return optionalParams - + def encodeRequiredParams(self, paramList, encoderMap, params): - return string.join([encoderMap[paramName].encode(params[paramName]) for paramName in paramList], '') - + return b''.join([encoderMap[paramName].encode(params[paramName], name=paramName) for paramName in paramList]) + def decodeRequiredParams(self, paramList, encoderMap, file): params = {} for paramName in paramList: params[paramName] = encoderMap[paramName].decode(file) return params - diff --git a/smpp/pdu/pdu_types.py b/smpp/pdu/pdu_types.py index ccfe8e0..72819b0 100644 --- a/smpp/pdu/pdu_types.py +++ b/smpp/pdu/pdu_types.py @@ -13,144 +13,166 @@ See the License for the specific language governing permissions and limitations under the License. """ +""" +Updated code parts are marked with "Jasmin update" comment +""" from enum import Enum -from smpp.pdu.namedtuple import namedtuple +from collections import namedtuple from smpp.pdu import constants -CommandId = Enum(*constants.command_id_name_map.keys()) +CommandId = Enum('CommandId', list(constants.command_id_name_map.keys())) -CommandStatus = Enum(*constants.command_status_name_map.keys()) +CommandStatus = Enum('CommandStatus', list(constants.command_status_name_map.keys())) -Tag = Enum(*constants.tag_name_map.keys()) +Tag = Enum('Tag', list(constants.tag_name_map.keys())) -Option = namedtuple('Option', 'tag, value') +Option = namedtuple('Option', ['tag', 'value']) -EsmClassMode = Enum(*constants.esm_class_mode_name_map.keys()) -EsmClassType = Enum(*constants.esm_class_type_name_map.keys()) -EsmClassGsmFeatures = Enum(*constants.esm_class_gsm_features_name_map.keys()) +EsmClassMode = Enum('EsmClassMode', list(constants.esm_class_mode_name_map.keys())) +EsmClassType = Enum('EsmClassType', list(constants.esm_class_type_name_map.keys())) +EsmClassGsmFeatures = Enum('EsmClassGsmFeatures', list(constants.esm_class_gsm_features_name_map.keys())) -EsmClassBase = namedtuple('EsmClass', 'mode, type, gsmFeatures') +EsmClassBase = namedtuple('EsmClass', ['mode', 'type', 'gsmFeatures']) class EsmClass(EsmClassBase): - + def __new__(cls, mode, type, gsmFeatures=[]): return EsmClassBase.__new__(cls, mode, type, set(gsmFeatures)) - + def __repr__(self): return 'EsmClass[mode: %s, type: %s, gsmFeatures: %s]' % (self.mode, self.type, self.gsmFeatures) -RegisteredDeliveryReceipt = Enum(*constants.registered_delivery_receipt_name_map.keys()) -RegisteredDeliverySmeOriginatedAcks = Enum(*constants.registered_delivery_sme_originated_acks_name_map.keys()) +RegisteredDeliveryReceipt = Enum('RegisteredDeliveryReceipt', list(constants.registered_delivery_receipt_name_map.keys())) +RegisteredDeliverySmeOriginatedAcks = Enum('RegisteredDeliverySmeOriginatedAcks', list(constants.registered_delivery_sme_originated_acks_name_map.keys())) -RegisteredDeliveryBase = namedtuple('RegisteredDelivery', 'receipt, smeOriginatedAcks, intermediateNotification') +RegisteredDeliveryBase = namedtuple('RegisteredDelivery', ['receipt', 'smeOriginatedAcks', 'intermediateNotification']) class RegisteredDelivery(RegisteredDeliveryBase): - + def __new__(cls, receipt, smeOriginatedAcks=[], intermediateNotification=False): return RegisteredDeliveryBase.__new__(cls, receipt, set(smeOriginatedAcks), intermediateNotification) - + def __repr__(self): return 'RegisteredDelivery[receipt: %s, smeOriginatedAcks: %s, intermediateNotification: %s]' % (self.receipt, self.smeOriginatedAcks, self.intermediateNotification) -AddrTon = Enum(*constants.addr_ton_name_map.keys()) -AddrNpi = Enum(*constants.addr_npi_name_map.keys()) -PriorityFlag = Enum(*constants.priority_flag_name_map.keys()) -ReplaceIfPresentFlag = Enum(*constants.replace_if_present_flap_name_map.keys()) +AddrTon = Enum('AddrTon', list(constants.addr_ton_name_map.keys())) +AddrNpi = Enum('AddrNpi', list(constants.addr_npi_name_map.keys())) +PriorityFlag = Enum('PriorityFlag', list(constants.priority_flag_name_map.keys())) +ReplaceIfPresentFlag = Enum('ReplaceIfPresentFlag', list(constants.replace_if_present_flap_name_map.keys())) -DataCodingScheme = Enum('RAW', 'DEFAULT', *constants.data_coding_scheme_name_map.keys()) -DataCodingDefault = Enum(*constants.data_coding_default_name_map.keys()) -DataCodingGsmMsgCoding = Enum(*constants.data_coding_gsm_message_coding_name_map.keys()) -DataCodingGsmMsgClass = Enum(*constants.data_coding_gsm_message_class_name_map.keys()) +DataCodingScheme = Enum('DataCodingScheme', 'RAW, DEFAULT,' + ','.join(list(constants.data_coding_scheme_name_map.keys()))) +DataCodingDefault = Enum('DataCodingDefault', list(constants.data_coding_default_name_map.keys())) +DataCodingGsmMsgCoding = Enum('DataCodingGsmMsgCoding', list(constants.data_coding_gsm_message_coding_name_map.keys())) +DataCodingGsmMsgClass = Enum('DataCodingGsmMsgClass', list(constants.data_coding_gsm_message_class_name_map.keys())) -DataCodingGsmMsgBase = namedtuple('DataCodingGsmMsg', 'msgCoding, msgClass') +DataCodingGsmMsgBase = namedtuple('DataCodingGsmMsg', ['msgCoding', 'msgClass']) class DataCodingGsmMsg(DataCodingGsmMsgBase): - + def __new__(cls, msgCoding, msgClass): return DataCodingGsmMsgBase.__new__(cls, msgCoding, msgClass) - + def __repr__(self): return 'DataCodingGsmMsg[msgCoding: %s, msgClass: %s]' % (self.msgCoding, self.msgClass) -class DataCoding(object): - +class DataCoding: + def __init__(self, scheme=DataCodingScheme.DEFAULT, schemeData=DataCodingDefault.SMSC_DEFAULT_ALPHABET): self.scheme = scheme self.schemeData = schemeData def __repr__(self): return 'DataCoding[scheme: %s, schemeData: %s]' % (self.scheme, self.schemeData) - + def __eq__(self, other): if self.scheme != other.scheme: return False if self.schemeData != other.schemeData: return False return True - + def __ne__(self, other): return not self.__eq__(other) -DestFlag = Enum(*constants.dest_flag_name_map.keys()) -MessageState = Enum(*constants.message_state_name_map.keys()) -CallbackNumDigitModeIndicator = Enum(*constants.callback_num_digit_mode_indicator_name_map.keys()) -SubaddressTypeTag = Enum(*constants.subaddress_type_tag_name_map.keys()) +DestFlag = Enum('DestFlag', list(constants.dest_flag_name_map.keys())) +MessageState = Enum('MessageState', list(constants.message_state_name_map.keys())) +CallbackNumDigitModeIndicator = Enum('CallbackNumDigitModeIndicator', list(constants.callback_num_digit_mode_indicator_name_map.keys())) +SubaddressTypeTag = Enum('SubaddressTypeTag', list(constants.subaddress_type_tag_name_map.keys())) -CallbackNumBase = namedtuple('CallbackNum', 'digitModeIndicator, ton, npi, digits') +CallbackNumBase = namedtuple('CallbackNum', ['digitModeIndicator', 'ton', 'npi', 'digits']) class CallbackNum(CallbackNumBase): - + def __new__(cls, digitModeIndicator, ton=AddrTon.UNKNOWN, npi=AddrNpi.UNKNOWN, digits=None): return CallbackNumBase.__new__(cls, digitModeIndicator, ton, npi, digits) - + def __repr__(self): return 'CallbackNum[digitModeIndicator: %s, ton: %s, npi: %s, digits: %s]' % (self.digitModeIndicator, self.ton, self.npi, self.digits) -SubaddressBase = namedtuple('Subaddress', 'typeTag, value') +SubaddressBase = namedtuple('Subaddress', ['typeTag', 'value']) class Subaddress(SubaddressBase): - + def __new__(cls, typeTag, value): return SubaddressBase.__new__(cls, typeTag, value) - + def __repr__(self): return 'Subaddress[typeTag: %s, value: %s]' % (self.typeTag, self.value) -AddrSubunit = Enum(*constants.addr_subunit_name_map.keys()) -NetworkType = Enum(*constants.network_type_name_map.keys()) -BearerType = Enum(*constants.bearer_type_name_map.keys()) -PayloadType = Enum(*constants.payload_type_name_map.keys()) -PrivacyIndicator = Enum(*constants.privacy_indicator_name_map.keys()) -LanguageIndicator = Enum(*constants.language_indicator_name_map.keys()) -DisplayTime = Enum(*constants.display_time_name_map.keys()) -MsAvailabilityStatus = Enum(*constants.ms_availability_status_name_map.keys()) -DeliveryFailureReason = Enum(*constants.delivery_failure_reason_name_map.keys()) -MoreMessagesToSend = Enum(*constants.more_messages_to_send_name_map.keys()) - -class PDU(object): +AddrSubunit = Enum('AddrSubunit', list(constants.addr_subunit_name_map.keys())) +NetworkType = Enum('NetworkType', list(constants.network_type_name_map.keys())) +BearerType = Enum('BearerType', list(constants.bearer_type_name_map.keys())) +PayloadType = Enum('PayloadType', list(constants.payload_type_name_map.keys())) +PrivacyIndicator = Enum('PrivacyIndicator', list(constants.privacy_indicator_name_map.keys())) +LanguageIndicator = Enum('LanguageIndicator', list(constants.language_indicator_name_map.keys())) +DisplayTime = Enum('DisplayTime', list(constants.display_time_name_map.keys())) +MsAvailabilityStatus = Enum('MsAvailabilityStatus', list(constants.ms_availability_status_name_map.keys())) + + +NetworkErrorCodeNetworkType = Enum('NetworkErrorCodeNetworkType', list(constants.network_error_code_name_map.keys())) + +NetworkErrorCodeBase = namedtuple('NetworkErrorCode', ['networkType', 'value']) +class NetworkErrorCode(NetworkErrorCodeBase): + def __new__(cls, networkType, value): + return NetworkErrorCodeBase.__new__(cls, networkType, value) + + def __repr__(self): + return 'NetworkErrorCode[networkType: %s, value: %s]' % (self.networkType, self.value) + +DeliveryFailureReason = Enum('DeliveryFailureReason', list(constants.delivery_failure_reason_name_map.keys())) +MoreMessagesToSend = Enum('MoreMessagesToSend', list(constants.more_messages_to_send_name_map.keys())) + +class PDU: commandId = None mandatoryParams = [] optionalParams = [] - + def __init__(self, seqNum=None, status=CommandStatus.ESME_ROK, **kwargs): self.id = self.commandId self.seqNum = seqNum self.status = status - self.params = kwargs + # TLV format + # (tag:int, length:int|None, type:str, value:str/int) + self.custom_tlvs = kwargs.pop('custom_tlvs', []) + # format every string arg as a byte string + self.params = dict([(key, val.encode()) if isinstance(val, str) else (key, val) for (key, val) in kwargs.items()]) + for mParam in self.mandatoryParams: if mParam not in self.params: self.params[mParam] = None - + def __repr__(self): + # Jasmin update: + # Displaying values with %r converter since %s doesnt work with unicode r = "PDU [command: %s, sequence_number: %s, command_status: %s" % (self.id, self.seqNum, self.status) for mParam in self.mandatoryParams: if mParam in self.params: - r += "\n%s: %s" % (mParam, self.params[mParam]) - for oParam in self.params.keys(): + r += "\n%s: %r" % (mParam, self.params[mParam]) + for oParam in list(self.params): if oParam not in self.mandatoryParams: - r += "\n%s: %s" % (oParam, self.params[oParam]) + r += "\n%s: %r" % (oParam, self.params[oParam]) r += '\n]' return r - + def __eq__(self, pdu): if self.id != pdu.id: return False @@ -158,13 +180,15 @@ def __eq__(self, pdu): return False if self.status != pdu.status: return False + print(self.params) + print(pdu.params) if self.params != pdu.params: return False return True - + def __ne__(self, other): return not self.__eq__(other) - + class PDURequest(PDU): requireAck = None @@ -177,11 +201,11 @@ def __init__(self, seqNum=None, status=CommandStatus.ESME_ROK, **kwargs): c.f. 4.4.2. SMPP PDU Definition "SUBMIT_SM_RESP" """ PDU.__init__(self, seqNum, status, **kwargs) - + if self.noBodyOnError: if status != CommandStatus.ESME_ROK: self.params = {} - + class PDUDataRequest(PDURequest): pass diff --git a/smpp/pdu/sm_encoding.py b/smpp/pdu/sm_encoding.py index 4f3c0ca..b606562 100644 --- a/smpp/pdu/sm_encoding.py +++ b/smpp/pdu/sm_encoding.py @@ -13,74 +13,77 @@ See the License for the specific language governing permissions and limitations under the License. """ -import struct, StringIO +from io import BytesIO +import struct from smpp.pdu.operations import DeliverSM, DataSM -from smpp.pdu.pdu_types import * -from smpp.pdu.namedtuple import namedtuple +from smpp.pdu.pdu_types import DataCodingDefault, DataCodingScheme, EsmClassGsmFeatures +from collections import namedtuple from smpp.pdu.gsm_types import InformationElementIdentifier from smpp.pdu.gsm_encoding import UserDataHeaderEncoder ShortMessageString = namedtuple('ShortMessageString', 'bytes, unicode, udh') -class SMStringEncoder(object): +class SMStringEncoder: userDataHeaderEncoder = UserDataHeaderEncoder() - + def decodeSM(self, pdu): data_coding = pdu.params['data_coding'] #TODO - when to look for message_payload instead of short_message?? (smBytes, udhBytes, smStrBytes) = self.splitSM(pdu) udh = self.decodeUDH(udhBytes) - + if data_coding.scheme == DataCodingScheme.DEFAULT: unicodeStr = None if data_coding.schemeData == DataCodingDefault.SMSC_DEFAULT_ALPHABET: - unicodeStr = unicode(smStrBytes, 'ascii') + unicodeStr = str(smStrBytes, 'ascii') elif data_coding.schemeData == DataCodingDefault.IA5_ASCII: - unicodeStr = unicode(smStrBytes, 'ascii') + unicodeStr = str(smStrBytes, 'ascii') elif data_coding.schemeData == DataCodingDefault.UCS2: - unicodeStr = unicode(smStrBytes, 'UTF-16BE') + unicodeStr = str(smStrBytes, 'UTF-16BE') elif data_coding.schemeData == DataCodingDefault.LATIN_1: - unicodeStr = unicode(smStrBytes, 'latin_1') + unicodeStr = str(smStrBytes, 'latin_1') if unicodeStr is not None: return ShortMessageString(smBytes, unicodeStr, udh) - + raise NotImplementedError("I don't know what to do!!! Data coding %s" % str(data_coding)) def containsUDH(self, pdu): if EsmClassGsmFeatures.UDHI_INDICATOR_SET in pdu.params['esm_class'].gsmFeatures: return True return False - + def isConcatenatedSM(self, pdu): return self.getConcatenatedSMInfoElement(pdu) != None - + def getConcatenatedSMInfoElement(self, pdu): (smBytes, udhBytes, smStrBytes) = self.splitSM(pdu) udh = self.decodeUDH(udhBytes) if udh is None: return None return self.findConcatenatedSMInfoElement(udh) - + def findConcatenatedSMInfoElement(self, udh): iElems = [iElem for iElem in udh if iElem.identifier in (InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM)] assert len(iElems) <= 1 if len(iElems) == 1: return iElems[0] return None - + def decodeUDH(self, udhBytes): if udhBytes is not None: - return self.userDataHeaderEncoder.decode(StringIO.StringIO(udhBytes)) + return self.userDataHeaderEncoder.decode(BytesIO(udhBytes)) return None - + def splitSM(self, pdu): short_message = pdu.params['short_message'] if self.containsUDH(pdu): if len(short_message) == 0: raise ValueError("Empty short message") - headerLen = struct.unpack('!B', short_message[0])[0] + if isinstance(short_message[0], bytes): + headerLen = struct.unpack('!B', short_message[0])[0] + else: + headerLen = struct.unpack('!B', bytes([short_message[0]]))[0] if headerLen + 1 > len(short_message): raise ValueError("Invalid header len (%d). Longer than short_message len (%d) + 1" % (headerLen, len(short_message))) return (short_message, short_message[:headerLen+1], short_message[headerLen+1:]) return (short_message, None, short_message) - \ No newline at end of file diff --git a/smpp/pdu/smpp_time.py b/smpp/pdu/smpp_time.py index d0dc86c..ed6fe1e 100644 --- a/smpp/pdu/smpp_time.py +++ b/smpp/pdu/smpp_time.py @@ -15,13 +15,14 @@ """ from datetime import datetime, tzinfo, timedelta -from smpp.pdu.namedtuple import namedtuple +from collections import namedtuple class FixedOffset(tzinfo): """Fixed offset in minutes east from UTC.""" - def __init__(self, offsetMin, name): - self.__offset = timedelta(minutes = offsetMin) + # Jasmin update, #267 + def __init__(self, offsetMin=0, name=None): + self.__offset = timedelta(minutes=offsetMin) self.__name = name def utcoffset(self, dt): @@ -54,95 +55,103 @@ def parse_nn(nn_str): if nn < 0 or nn > 48: raise ValueError("time difference must be 0-48") return nn - + def unparse_nn(nn): if nn < 0 or nn > 48: raise ValueError("time difference must be 0-48") return '%02d' % nn -def parse_absolute_time(str): - (YYMMDDhhmmss, t, nn, p) = (str[:12], str[12:13], str[13:15], str[15]) +def parse_absolute_time(t_str): + if isinstance(t_str, bytes): + t_str = t_str.decode() + (YYMMDDhhmmss, t, nn, p) = (t_str[:12], t_str[12:13], t_str[13:15], t_str[15]) + + if isinstance(p, int): + p = chr(p) + if p not in ['+', '-']: raise ValueError("Invalid offset indicator %s" % p) - + tenthsOfSeconds = parse_t(t) quarterHrOffset = parse_nn(nn) - + microseconds = tenthsOfSeconds * 100 * 1000 - + tzinfo = None if quarterHrOffset > 0: minOffset = quarterHrOffset * 15 if p == '-': minOffset *= -1 - tzinfo = FixedOffset(minOffset, None) - + tzinfo = FixedOffset(minOffset, None) + timeVal = parse_YYMMDDhhmmss(YYMMDDhhmmss) return timeVal.replace(microsecond=microseconds,tzinfo=tzinfo) - -def parse_relative_time(dtstr): + +def parse_relative_time(dt_str): # example 600 seconds is: '000000001000000R' try: - year = int(dtstr[:2]) - month = int(dtstr[2:4]) - day = int(dtstr[4:6]) - hour = int(dtstr[6:8]) - minute = int(dtstr[8:10]) - second = int(dtstr[10:12]) - dsecond = int(dtstr[12:13]) + year = int(dt_str[:2]) + month = int(dt_str[2:4]) + day = int(dt_str[4:6]) + hour = int(dt_str[6:8]) + minute = int(dt_str[8:10]) + second = int(dt_str[10:12]) + dsecond = int(dt_str[12:13]) # According to spec dsecond should be set to 0 if dsecond != 0: raise ValueError("SMPP v3.4 spec violation: tenths of second value is %s instead of 0"% dsecond) - except IndexError, e: - raise ValueError("Error %s : Unable to parse relative Validity Period %s" % e,dtstr) + except IndexError as e: + raise ValueError("Error %s : Unable to parse relative Validity Period %s" % e,dt_str) return SMPPRelativeTime(year,month,day,hour,minute,second) - - + def parse_YYMMDDhhmmss(YYMMDDhhmmss): return datetime.strptime(YYMMDDhhmmss, YYMMDDHHMMSS_FORMAT) - + def unparse_YYMMDDhhmmss(dt): return dt.strftime(YYMMDDHHMMSS_FORMAT) - + def unparse_absolute_time(dt): if not isinstance(dt, datetime): raise ValueError("input must be a datetime but got %s" % type(dt)) YYMMDDhhmmss = unparse_YYMMDDhhmmss(dt) tenthsOfSeconds = dt.microsecond/(100*1000) quarterHrOffset = 0 - p = '+' + p = b'+' if dt.tzinfo is not None: utcOffset = dt.tzinfo.utcoffset(datetime.now()) utcOffsetSecs = utcOffset.days * 60 * 60 * 24 + utcOffset.seconds quarterHrOffset = utcOffsetSecs / (15*60) if quarterHrOffset < 0: - p = '-' + p = b'-' quarterHrOffset *= -1 - return YYMMDDhhmmss + unparse_t(tenthsOfSeconds) + unparse_nn(quarterHrOffset) + p + return (YYMMDDhhmmss + unparse_t(tenthsOfSeconds) + unparse_nn(quarterHrOffset)).encode() + p def unparse_relative_time(rel): if not isinstance(rel, SMPPRelativeTime): raise ValueError("input must be a SMPPRelativeTime") relstr = "%s%s%s%s%s%s000R" % (str("%.2d" % rel.years), str("%.2d" % rel.months), str("%.2d" % rel.days), str("%.2d" % rel.hours), str("%.2d" % rel.minutes), str("%.2d" % rel.seconds)) - return relstr + return relstr.encode() -def parse(str): +def parse(t_str): """Takes an SMPP time string in. Returns datetime.datetime for absolute time format Returns SMPPRelativeTime for relative time format (note: datetime.timedelta cannot - because the SMPP relative time interval depends on the SMSC current date/time) + because the SMPP relative time interval depends on the SMSC current date/time) """ - if len(str) != 16: - raise ValueError("Invalid time length %d" % len(str)) - if (str[-1]) == 'R': - return parse_relative_time(str) - return parse_absolute_time(str) - + if isinstance(t_str, bytes): + t_str = t_str.decode() + print(t_str[-1]) + if len(t_str) != 16: + raise ValueError("Invalid time length %d" % len(t_str)) + if (t_str[-1]) == 'R': + return parse_relative_time(t_str) + return parse_absolute_time(t_str) + def unparse(dt_or_rel): """Takes in either a datetime or an SMPPRelativeTime Returns an SMPP time string @@ -150,6 +159,3 @@ def unparse(dt_or_rel): if isinstance(dt_or_rel, SMPPRelativeTime): return unparse_relative_time(dt_or_rel) return unparse_absolute_time(dt_or_rel) - - - diff --git a/smpp/pdu/tests/__init__.py b/smpp/pdu/tests/__init__.py deleted file mode 100644 index 5d41fcf..0000000 --- a/smpp/pdu/tests/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Copyright 2009-2010 Mozes, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" \ No newline at end of file diff --git a/smpp/pdu/tests/test_pdu_encoding.py b/smpp/pdu/tests/test_pdu_encoding.py deleted file mode 100644 index 3c4dd4a..0000000 --- a/smpp/pdu/tests/test_pdu_encoding.py +++ /dev/null @@ -1,586 +0,0 @@ -""" -Copyright 2009-2010 Mozes, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" -import unittest -import StringIO -import binascii -from datetime import datetime -from smpp.pdu.smpp_time import SMPPRelativeTime -from smpp.pdu.pdu_encoding import * -from smpp.pdu.pdu_types import * -from smpp.pdu.operations import * - -class EncoderTest(unittest.TestCase): - - def do_conversion_test(self, encoder, value, hexdumpValue): - encoded = encoder.encode(value) - hexEncoded = binascii.b2a_hex(encoded) - if hexdumpValue != hexEncoded: - print "\nHex Value:\n%s" % hexdumpValue - print "Hex Encoded:\n%s" % hexEncoded - chars1 = list(hexdumpValue) - chars2 = list(hexEncoded) - for i in range(0, len(hexEncoded)): - if chars1[i] != chars2[i]: - print "Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i]) - - self.assertEquals(hexdumpValue, hexEncoded) - file = StringIO.StringIO(encoded) - decoded = encoder.decode(file) - self.assertEquals(value, decoded) - - def do_encode_test(self, encoder, value, hexdumpValue): - encoded = encoder.encode(value) - hexEncoded = binascii.b2a_hex(encoded) - if hexdumpValue != hexEncoded: - print "\nHex Value:\n%s" % hexdumpValue - print "Hex Encoded:\n%s" % hexEncoded - chars1 = list(hexdumpValue) - chars2 = list(hexEncoded) - for i in range(0, len(hexEncoded)): - if chars1[i] != chars2[i]: - print "Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i]) - - self.assertEquals(hexdumpValue, hexEncoded) - - def do_decode_test(self, encoder, value, hexdumpValue): - decoded = self.decode(encoder.decode, hexdumpValue) - self.assertEquals(value, decoded) - - def do_null_encode_test(self, encoder, nullDecodeVal, hexdumpValue): - encoded = encoder.encode(None) - self.assertEquals(hexdumpValue, binascii.b2a_hex(encoded)) - file = StringIO.StringIO(encoded) - decoded = encoder.decode(file) - self.assertEquals(nullDecodeVal, decoded) - - def decode(self, decodeFunc, hexdumpValue): - return decodeFunc(StringIO.StringIO(binascii.a2b_hex(hexdumpValue))) - - def do_decode_parse_error_test(self, decodeFunc, status, hexdumpValue): - try: - decoded = self.decode(decodeFunc, hexdumpValue) - self.assertTrue(False, 'Decode did not throw exception. Result was: %s' % str(decoded)) - except PDUParseError, e: - self.assertEquals(status, e.status) - - def do_decode_corrupt_data_error_test(self, decodeFunc, status, hexdumpValue): - try: - decoded = self.decode(decodeFunc, hexdumpValue) - self.assertTrue(False, 'Decode did not throw exception. Result was: %s' % str(decoded)) - except PDUCorruptError, e: - self.assertEquals(status, e.status) - -class EmptyEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(EmptyEncoder(), None, '') - -class IntegerEncoderTest(EncoderTest): - - def test_int4(self): - self.do_conversion_test(Int4Encoder(), 0x800001FF, '800001ff') - - def test_int1(self): - encoder = Int1Encoder() - self.do_conversion_test(encoder, 255, 'ff') - self.assertRaises(ValueError, encoder.encode, 256) - self.do_null_encode_test(encoder, 0, '00') - - def test_int1_max(self): - self.assertRaises(ValueError, Int1Encoder, max=256) - encoder = Int1Encoder(max=254) - self.do_conversion_test(encoder, 254, 'fe') - self.assertRaises(ValueError, encoder.encode, 255) - - def test_int1_min(self): - self.assertRaises(ValueError, Int1Encoder, min=-1) - encoder = Int1Encoder(min=1) - self.do_conversion_test(encoder, 1, '01') - self.do_conversion_test(encoder, None, '00') - self.assertRaises(ValueError, encoder.encode, 0) - - def test_int2(self): - self.do_conversion_test(Int2Encoder(), 0x41AC, '41ac') - -class COctetStringEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(COctetStringEncoder(), 'hello', '68656c6c6f00') - self.do_conversion_test(COctetStringEncoder(6), 'hello', '68656c6c6f00') - self.do_conversion_test(COctetStringEncoder(1), '', '00') - self.do_null_encode_test(COctetStringEncoder(), '', '00') - self.assertRaises(ValueError, COctetStringEncoder, 0) - - def test_maxLength_exceeded(self): - encoder = COctetStringEncoder(5, decodeErrorStatus=CommandStatus.ESME_RINVSRCADR) - self.assertRaises(ValueError, encoder.encode, 'hello') - self.do_decode_parse_error_test(encoder.decode, CommandStatus.ESME_RINVSRCADR, '68656c6c6f00') - - def test_ascii_required(self): - encoder = COctetStringEncoder() - self.assertRaises(ValueError, encoder.encode, u'\x9b\xa2\x7c') - - def test_requireNull(self): - encoder = COctetStringEncoder(decodeNull=True, requireNull=True) - self.do_conversion_test(encoder, None, '00') - self.assertRaises(ValueError, encoder.encode, 'test') - self.do_decode_parse_error_test(encoder.decode, CommandStatus.ESME_RUNKNOWNERR, '68656c6c6f00') - -class OctetStringEncoderTest(EncoderTest): - - def test_conversion(self): - hex = '68656c6c6f' - self.do_conversion_test(OctetStringEncoder(len(hex)/2), binascii.a2b_hex(hex), hex) - self.do_conversion_test(OctetStringEncoder(0), '', '') - - def test_maxLength_exceeded(self): - encoder = OctetStringEncoder(1) - self.assertRaises(ValueError, encoder.encode, binascii.a2b_hex('ffaa')) - -class CommandIdEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(CommandIdEncoder(), CommandId.enquire_link_resp, '80000015') - - def test_decode_invalid_command_id(self): - self.do_decode_corrupt_data_error_test(CommandIdEncoder().decode, CommandStatus.ESME_RINVCMDID, 'f0000009') - -class CommandStatusEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(CommandStatusEncoder(), CommandStatus.ESME_RUNKNOWNERR, '000000ff') - -class TagEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(TagEncoder(), Tag.language_indicator, '020d') - -class EsmClassEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(EsmClassEncoder(), EsmClass(EsmClassMode.DATAGRAM, EsmClassType.INTERMEDIATE_DELIVERY_NOTIFICATION, [EsmClassGsmFeatures.SET_REPLY_PATH]), 'a1') - self.do_null_encode_test(EsmClassEncoder(), EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT, []), '00') - - def test_decode_invalid_type(self): - self.do_decode_parse_error_test(EsmClassEncoder().decode, CommandStatus.ESME_RINVESMCLASS, '30') - -class RegisteredDeliveryEncoderTest(EncoderTest): - - def test_conversion(self): - value = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_DELIVERY_ACK_REQUESTED, RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED], True) - self.do_conversion_test(RegisteredDeliveryEncoder(), value, '1d') - self.do_null_encode_test(RegisteredDeliveryEncoder(), RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED, [], False), '00') - - def test_decode_invalid_receipt(self): - self.do_decode_parse_error_test(RegisteredDeliveryEncoder().decode, CommandStatus.ESME_RINVREGDLVFLG, '03') - -class AddrTonEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(AddrTonEncoder(fieldName='source_addr_ton'), AddrTon.ALPHANUMERIC, '05') - -class PriorityFlagEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(PriorityFlagEncoder(), PriorityFlag.LEVEL_2, '02') - - def test_decode_invalid(self): - self.do_decode_parse_error_test(PriorityFlagEncoder().decode, CommandStatus.ESME_RINVPRTFLG, '0f') - -class AddrNpiEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(AddrNpiEncoder(fieldName='source_addr_npi'), AddrNpi.LAND_MOBILE, '06') - -class AddrSubunitEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(AddrSubunitEncoder(), AddrSubunit.MOBILE_EQUIPMENT, '02') - -class NetworkTypeEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(NetworkTypeEncoder(), NetworkType.GSM, '01') - -class BearerTypeEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(BearerTypeEncoder(), BearerType.USSD, '04') - -class PayloadTypeEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(PayloadTypeEncoder(), PayloadType.WCMP, '01') - -class PrivacyIndicatorEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(PrivacyIndicatorEncoder(), PrivacyIndicator.CONFIDENTIAL, '02') - -class LanguageIndicatorEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(LanguageIndicatorEncoder(), LanguageIndicator.SPANISH, '03') - -class DisplayTimeEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(DisplayTimeEncoder(), DisplayTime.INVOKE, '02') - -class MsAvailabilityStatusEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(MsAvailabilityStatusEncoder(), MsAvailabilityStatus.DENIED, '01') - -class ReplaceIfPresentFlagEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(ReplaceIfPresentFlagEncoder(), ReplaceIfPresentFlag.REPLACE, '01') - self.do_null_encode_test(ReplaceIfPresentFlagEncoder(), ReplaceIfPresentFlag.DO_NOT_REPLACE, '00') - -class DataCodingEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(DataCodingEncoder(), DataCoding(schemeData=DataCodingDefault.LATIN_1), '03') - self.do_null_encode_test(DataCodingEncoder(), DataCoding(schemeData=DataCodingDefault.SMSC_DEFAULT_ALPHABET), '00') - self.do_conversion_test(DataCodingEncoder(), DataCoding(DataCodingScheme.RAW, 48), '30') - self.do_conversion_test(DataCodingEncoder(), DataCoding(DataCodingScheme.RAW, 11), '0b') - self.do_conversion_test(DataCodingEncoder(), DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_1)), 'f1') - -class DestFlagEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(DestFlagEncoder(), DestFlag.DISTRIBUTION_LIST_NAME, '02') - self.assertRaises(ValueError, DestFlagEncoder().encode, None) - self.do_decode_parse_error_test(DestFlagEncoder().decode, CommandStatus.ESME_RUNKNOWNERR, '00') - -class MessageStateEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(MessageStateEncoder(), MessageState.REJECTED, '08') - self.assertRaises(ValueError, MessageStateEncoder().encode, None) - self.do_decode_parse_error_test(MessageStateEncoder().decode, CommandStatus.ESME_RUNKNOWNERR, '00') - -class CallbackNumDigitModeIndicatorEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(CallbackNumDigitModeIndicatorEncoder(), CallbackNumDigitModeIndicator.ASCII, '01') - self.assertRaises(ValueError, CallbackNumDigitModeIndicatorEncoder().encode, None) - -class CallbackNumEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(CallbackNumEncoder(13), CallbackNum(CallbackNumDigitModeIndicator.ASCII, digits='8033237457'), '01000038303333323337343537') - - def test_decode_invalid_type(self): - self.do_decode_parse_error_test(CallbackNumEncoder(13).decode, CommandStatus.ESME_RINVOPTPARAMVAL, '02000038303333323337343537') - - def test_decode_invalid_size(self): - self.do_decode_parse_error_test(CallbackNumEncoder(2).decode, CommandStatus.ESME_RINVOPTPARAMVAL, '0100') - -class SubaddressTypeTagEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(SubaddressTypeTagEncoder(), SubaddressTypeTag.USER_SPECIFIED, 'a0') - self.assertRaises(ValueError, SubaddressTypeTagEncoder().encode, None) - -class SubaddressEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(SubaddressEncoder(4), Subaddress(SubaddressTypeTag.USER_SPECIFIED, value='742'), 'a0373432') - - def test_decode_invalid_type(self): - self.do_decode_parse_error_test(SubaddressEncoder(4).decode, CommandStatus.ESME_RINVOPTPARAMVAL, 'a1373432') - - def test_decode_invalid_size(self): - self.do_decode_parse_error_test(SubaddressEncoder(1).decode, CommandStatus.ESME_RINVOPTPARAMVAL, 'a0373432') - -class TimeEncoderEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(TimeEncoder(), datetime(2007, 9, 27, 23, 34, 29, 800000), binascii.b2a_hex('070927233429800+' + '\0')) - self.do_conversion_test(TimeEncoder(), None, '00') - - def test_requireNull(self): - encoder = TimeEncoder(requireNull=True) - self.do_conversion_test(encoder, None, '00') - self.assertRaises(ValueError, encoder.encode, datetime.now()) - self.do_decode_parse_error_test(encoder.decode, CommandStatus.ESME_RUNKNOWNERR, binascii.b2a_hex('070927233429800+' + '\0')) - - def test_decode_invalid(self): - self.do_decode_parse_error_test(TimeEncoder(decodeErrorStatus=CommandStatus.ESME_RINVSRCADR).decode, CommandStatus.ESME_RINVSRCADR, binascii.b2a_hex('070927233429800' + '\0')) - -class ShortMessageEncoderTest(EncoderTest): - - def test_conversion(self): - self.do_conversion_test(ShortMessageEncoder(), 'hello', '0568656c6c6f') - self.do_null_encode_test(ShortMessageEncoder(), '', '00') - -class OptionEncoderTest(EncoderTest): - - def test_dest_addr_subunit(self): - self.do_conversion_test(OptionEncoder(), Option(Tag.dest_addr_subunit, AddrSubunit.MOBILE_EQUIPMENT), '0005000102') - - def test_decode_invalid_dest_addr_subunit(self): - self.do_decode_parse_error_test(OptionEncoder().decode, CommandStatus.ESME_RINVOPTPARAMVAL, '00050001ff') - - def test_message_payload(self): - hexVal = 'ffaa01ce' - self.do_conversion_test(OptionEncoder(), Option(Tag.message_payload, binascii.a2b_hex(hexVal)), '04240004' + hexVal) - - def test_alert_on_message_delivery(self): - self.do_conversion_test(OptionEncoder(), Option(Tag.alert_on_message_delivery, None), '130c0000') - -class PDUEncoderTest(EncoderTest): - - def do_bind_conversion_test(self, pduBindKlass, reqCommandIdHex, respCommandIdHex): - reqPdu = pduBindKlass(2, CommandStatus.ESME_ROK, - system_id='test', - password='secret', - system_type='OTA', - interface_version=0x34, - addr_ton=AddrTon.NATIONAL, - addr_npi=AddrNpi.LAND_MOBILE, - address_range='127.0.0.*', - ) - self.do_conversion_test(PDUEncoder(), reqPdu, '0000002d%s00000000000000027465737400736563726574004f5441003402063132372e302e302e2a00' % reqCommandIdHex) - respPdu = reqPdu.requireAck(1, CommandStatus.ESME_ROK, system_id='TSI7588', sc_interface_version=0x34) - self.do_conversion_test(PDUEncoder(), respPdu, '0000001d%s000000000000000154534937353838000210000134' % respCommandIdHex) - - - - def test_BindTransmitter_conversion(self): - self.do_bind_conversion_test(BindTransmitter, '00000002', '80000002') - - def test_BindReceiver_conversion(self): - self.do_bind_conversion_test(BindReceiver, '00000001', '80000001') - - def test_BindTransceiver_conversion(self): - self.do_bind_conversion_test(BindTransceiver, '00000009', '80000009') - - def test_Unbind_conversion(self): - pdu = Unbind(4) - self.do_conversion_test(PDUEncoder(), pdu, '00000010000000060000000000000004') - - def test_UnbindResp_conversion(self): - pdu = UnbindResp(5, CommandStatus.ESME_ROK) - self.do_conversion_test(PDUEncoder(), pdu, '00000010800000060000000000000005') - - def test_GenericNack_conversion(self): - pdu = GenericNack(None, CommandStatus.ESME_RSYSERR) - self.do_conversion_test(PDUEncoder(), pdu, '00000010800000000000000800000000') - - def test_DeliverSM_syniverse_MO_conversion(self): - pdu = DeliverSM(2676551972, - service_type = 'AWSBD', - source_addr_ton=AddrTon.INTERNATIONAL, - source_addr_npi=AddrNpi.ISDN, - source_addr='16505551234', - dest_addr_ton=AddrTon.INTERNATIONAL, - dest_addr_npi=AddrNpi.ISDN, - destination_addr='17735554070', - esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), - protocol_id=0, - priority_flag=PriorityFlag.LEVEL_0, - registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), - replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, - data_coding=DataCoding(schemeData=DataCodingDefault.LATIN_1), - short_message='there is no spoon', - ) - self.do_conversion_test(PDUEncoder(), pdu, '0000004d00000005000000009f88f12441575342440001013136353035353531323334000101313737333535353430373000000000000000000300117468657265206973206e6f2073706f6f6e') - - def test_DeliverSM_handset_ack_conversion(self): - pdu = DeliverSM(10, - service_type = 'CMT', - source_addr_ton=AddrTon.INTERNATIONAL, - source_addr_npi=AddrNpi.UNKNOWN, - source_addr='6515555678', - dest_addr_ton=AddrTon.INTERNATIONAL, - dest_addr_npi=AddrNpi.UNKNOWN, - destination_addr='123', - esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.SMSC_DELIVERY_RECEIPT), - protocol_id=0, - priority_flag=PriorityFlag.LEVEL_0, - registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), - replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, - data_coding=DataCoding(schemeData=DataCodingDefault.SMSC_DEFAULT_ALPHABET), - short_message='id:1891273321 sub:001 dlvrd:001 submit date:1305050826 done date:1305050826 stat:DELIVRD err:000 Text:DLVRD TO MOBILE\x00', - message_state=MessageState.DELIVERED, - receipted_message_id='70BA8A69', - ) - self.do_conversion_test(PDUEncoder(), pdu, '000000b900000005000000000000000a434d5400010036353135353535363738000100313233000400000000000000007669643a31383931323733333231207375623a30303120646c7672643a303031207375626d697420646174653a3133303530353038323620646f6e6520646174653a3133303530353038323620737461743a44454c49565244206572723a30303020546578743a444c56524420544f204d4f42494c45000427000102001e0009373042413841363900') - - def test_DeliverSM_sybase_MO_conversion(self): - pdu = DeliverSM(1, - service_type = 'CMT', - source_addr_ton=AddrTon.INTERNATIONAL, - source_addr_npi=AddrNpi.UNKNOWN, - source_addr='3411149500001', - dest_addr_ton=AddrTon.INTERNATIONAL, - dest_addr_npi=AddrNpi.UNKNOWN, - destination_addr='12345455', - esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), - protocol_id=0, - priority_flag=PriorityFlag.LEVEL_0, - registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), - replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, - data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), - short_message='HELLO\x00', - ) - self.do_conversion_test(PDUEncoder(), pdu, '0000003f000000050000000000000001434d540001003334313131343935303030303100010031323334353435350000000000000000f2000648454c4c4f00') - - def test_DeliverSM_with_subaddress(self): - pdu = DeliverSM(1, - service_type = 'BM8', - source_addr_ton=AddrTon.INTERNATIONAL, - source_addr_npi=AddrNpi.ISDN, - source_addr='46123456789', - dest_addr_ton=AddrTon.INTERNATIONAL, - dest_addr_npi=AddrNpi.ISDN, - destination_addr='14046653410', - esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), - protocol_id=0, - priority_flag=PriorityFlag.LEVEL_0, - registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), - replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, - data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), - short_message="Hello I'm a bigg fan of you", - source_subaddress=Subaddress(SubaddressTypeTag.USER_SPECIFIED, '742'), - dest_subaddress=Subaddress(SubaddressTypeTag.USER_SPECIFIED, '4131'), - ) - self.do_conversion_test(PDUEncoder(), pdu, '00000066000000050000000000000001424d38000101343631323334353637383900010131343034363635333431300000000000000000f2001b48656c6c6f2049276d206120626967672066616e206f6620796f7502020004a037343202030005a034313331') - - def test_EnquireLink_conversion(self): - pdu = EnquireLink(6, CommandStatus.ESME_ROK) - self.do_conversion_test(PDUEncoder(), pdu, '00000010000000150000000000000006') - - def test_EnquireLinkResp_conversion(self): - pdu = EnquireLinkResp(7) - self.do_conversion_test(PDUEncoder(), pdu, '00000010800000150000000000000007') - - def test_AlertNotification_conversion(self): - pdu = AlertNotification( - source_addr_ton=AddrTon.NATIONAL, - source_addr_npi=AddrNpi.ISDN, - source_addr='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - esme_addr_ton=AddrTon.INTERNATIONAL, - esme_addr_npi=AddrNpi.LAND_MOBILE, - esme_addr='YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY', - ms_availability_status=MsAvailabilityStatus.DENIED, - ) - self.do_conversion_test(PDUEncoder(), pdu, '0000008900000102000000000000000002015858585858585858585858585858585858585858585858585858585858585858585858585858585858585858585858585858585858580001065959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959000422000101') - - def test_QuerySMResp_conversion(self): - pdu = QuerySMResp( - message_id = 'Smsc2003', - final_date = None, - message_state = MessageState.UNKNOWN, - error_code = None, - ) - self.do_conversion_test(PDUEncoder(), pdu, '0000001c800000030000000000000000536d73633230303300000700') - - def test_SubmitSM_conversion(self): - pdu = SubmitSM(9284, - service_type='', - source_addr_ton=AddrTon.ALPHANUMERIC, - source_addr_npi=AddrNpi.UNKNOWN, - source_addr='mobileway', - dest_addr_ton=AddrTon.INTERNATIONAL, - dest_addr_npi=AddrNpi.ISDN, - destination_addr='1208230', - esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), - protocol_id=0, - priority_flag=PriorityFlag.LEVEL_0, - registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), - replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, - data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), - short_message='HELLO', - ) - self.do_conversion_test(PDUEncoder(), pdu, '000000360000000400000000000024440005006d6f62696c65776179000101313230383233300000000000000100f2000548454c4c4f') - - def test_SubmitSM_ringtone_conversion(self): - pdu = SubmitSM(455569, - service_type='', - source_addr_ton=AddrTon.ALPHANUMERIC, - source_addr_npi=AddrNpi.UNKNOWN, - source_addr='mobileway', - dest_addr_ton=AddrTon.INTERNATIONAL, - dest_addr_npi=AddrNpi.ISDN, - destination_addr='3369809342', - esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT, [EsmClassGsmFeatures.UDHI_INDICATOR_SET]), - protocol_id=0, - priority_flag=PriorityFlag.LEVEL_0, - registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), - replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, - data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DATA_8BIT, DataCodingGsmMsgClass.CLASS_1)), - short_message=binascii.a2b_hex('06050415811581024a3a5db5a5cdcda5bdb8040084d8c51381481381481381481381481381381481581681781881881061881061b81081181081881061881061681081781081881061881061b81081181081881061881061681081781081b81881321081b81881221081b818811210824dc1446000') - ) - self.do_conversion_test(PDUEncoder(), pdu, '000000a900000004000000000006f3910005006d6f62696c65776179000101333336393830393334320040000000000100f5007506050415811581024a3a5db5a5cdcda5bdb8040084d8c51381481381481381481381481381381481581681781881881061881061b81081181081881061881061681081781081881061881061b81081181081881061881061681081781081b81881321081b81881221081b818811210824dc1446000') - - def test_decode_command_length_too_short(self): - self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVCMDLEN, '0000000f000000060000000000000000') - - def test_decode_command_length_too_long(self): - self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVCMDLEN, '00000011000000060000000000000000ff') - - def test_decodeHeader_command_length_too_short(self): - self.do_decode_corrupt_data_error_test(PDUEncoder().decodeHeader, CommandStatus.ESME_RINVCMDLEN, '0000000f000000060000000000000000') - - def test_decode_bad_message_length_msg_too_short(self): - self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVMSGLEN, '000000fd80000009000000000000000154534937353838000210000134') - - def test_decode_bad_message_length_msg_too_long(self): - self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVCMDLEN, '0000001c80000009000000000000000154534937353838000210000134') - - def test_decode_bad_message_ends_in_middle_of_option(self): - self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVMSGLEN, '0000001b8000000900000000000000015453493735383800021000') - - def test_SubmitSMResp_error_has_no_body(self): - pdu = SubmitSMResp(1234, status=CommandStatus.ESME_RMSGQFUL) - self.assertTrue(len(SubmitSMResp.mandatoryParams) > 0) - self.assertEquals(0, len(pdu.params)) - self.do_conversion_test(PDUEncoder(), pdu, '000000108000000400000014000004d2') - - def test_BindReceiverResp_error_has_no_body(self): - pdu = BindReceiverResp(3456, status=CommandStatus.ESME_RINVPASWD) - self.assertTrue(len(BindReceiverResp.mandatoryParams) > 0) - self.assertEquals(0, len(pdu.params)) - self.do_conversion_test(PDUEncoder(), pdu, '00000010800000010000000e00000d80') - - def test_BindTransmitterResp_error_has_no_body(self): - pdu = BindTransmitterResp(3456, status=CommandStatus.ESME_RINVPASWD) - self.assertTrue(len(BindTransmitterResp.mandatoryParams) > 0) - self.assertEquals(0, len(pdu.params)) - self.do_conversion_test(PDUEncoder(), pdu, '00000010800000020000000e00000d80') - - def test_BindTransceiverResp_error_has_no_body(self): - pdu = BindTransceiverResp(3456, status=CommandStatus.ESME_RINVPASWD) - self.assertEquals(0, len(pdu.params)) - self.do_conversion_test(PDUEncoder(), pdu, '00000010800000090000000e00000d80') - - def test_BindTransceiverResp_error_has_no_body_status_set_later(self): - hex = '00000010800000090000000e00000d80' - pdu = BindTransceiverResp(3456, system_id="XYZ") - pdu.status = CommandStatus.ESME_RINVPASWD - #Even though the system_id param was set, it will not be encoded - self.do_encode_test(PDUEncoder(), pdu, hex) - #It will decode with no params set - pduExpected = BindTransceiverResp(3456, status=CommandStatus.ESME_RINVPASWD) - self.assertEquals(0, len(pduExpected.params)) - self.do_decode_test(PDUEncoder(), pduExpected, hex) - -if __name__ == '__main__': - unittest.main() diff --git a/smpp/pdu/tests/test_sm_encoding.py b/smpp/pdu/tests/test_sm_encoding.py deleted file mode 100644 index 148cfbe..0000000 --- a/smpp/pdu/tests/test_sm_encoding.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Copyright 2009-2010 Mozes, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" -import unittest, binascii, StringIO -from smpp.pdu.sm_encoding import SMStringEncoder -from smpp.pdu.pdu_types import * -from smpp.pdu.gsm_types import * -from smpp.pdu.pdu_encoding import PDUEncoder - -class SMDecoderTest(unittest.TestCase): - - def getPDU(self, hexStr): - return PDUEncoder().decode(StringIO.StringIO(binascii.a2b_hex(hexStr))) - - def test_decode_UCS2(self): - pduHex = '000000480000000500000000dfd03a56415753424400010131353535313233343536370001013137373338323834303730000000000000000008000c00f10075014400ed00fc0073' - pdu = self.getPDU(pduHex) - smStr = SMStringEncoder().decodeSM(pdu) - self.assertEquals('\x00\xf1\x00u\x01D\x00\xed\x00\xfc\x00s', smStr.bytes) - self.assertEquals(u'\xf1u\u0144\xed\xfcs', smStr.unicode) - self.assertEquals(None, smStr.udh) - - def test_decode_default_alphabet(self): - #'T- Mobile flip phone \xa7 \xa8 N random special charcters' - pduHex = '0000006f00000005000000005d3fe724544d4f4249000101313535353132333435363700010131373733383238343037300000000000000000000033542d204d6f62696c6520666c69702070686f6e6520a720a8204e2072616e646f6d207370656369616c20636861726374657273' - pdu = self.getPDU(pduHex) - self.assertRaises(UnicodeDecodeError, SMStringEncoder().decodeSM, pdu) - - def test_decode_latin1(self): - pduHex = '0000004200000005000000002a603d56415753424400010131353535313233343536370001013137373338323834303730000000000000000003000645737061f161' - pdu = self.getPDU(pduHex) - smStr = SMStringEncoder().decodeSM(pdu) - self.assertEquals('Espa\xf1a', smStr.bytes) - self.assertEquals(u'Espa\xf1a', smStr.unicode) - self.assertEquals(None, smStr.udh) - - def test_decode_ascii(self): - pduHex = '00000054000000050000000008c72a4154454c4550000101313535353535353535353500010131343034363635333431300000ff010000000001000e49732074686973206a757374696e0201000100020d000101' - pdu = self.getPDU(pduHex) - smStr = SMStringEncoder().decodeSM(pdu) - self.assertEquals('Is this justin', smStr.bytes) - self.assertEquals('Is this justin', smStr.unicode) - self.assertEquals(None, smStr.udh) - - def test_decode_octet_unspecified_common(self): - pduHex = '000000a900000005000000003cf78935415753424400010131353535313233343536370001013134303436363533343130004000000000000004006d06050423f40000424547494e3a56434152440d0a56455253494f4e3a322e310d0a4e3b434841525345543d5554462d383a4269656265723b4a757374696e0d0a54454c3b564f4943453b434841525345543d5554462d383a343034363635333431300d0a454e443a5643415244' - pdu = self.getPDU(pduHex) - self.assertRaises(NotImplementedError, SMStringEncoder().decodeSM, pdu) - - def test_decode_default_alphabet_with_udh(self): - pduHex = '000000da0000000500000000da4b62474652414e4300010131353535313233343536370001013134303436363533343130004000000000000000009e0500032403016869206a757374696e20686f772061726520796f753f204d79206e616d6520697320706570652069276d206672656e636820616e6420692077616e74656420746f2074656c6c20796f7520686f77206d7563682069206c6f766520796f752c20796f75206b6e6f7720796f75207361766564206d79206c69666520616e642069207265616c6c79207468616e6b20796f7520666f72207468' - pdu = self.getPDU(pduHex) - smStr = SMStringEncoder().decodeSM(pdu) - self.assertEquals("\x05\x00\x03$\x03\x01hi justin how are you? My name is pepe i'm french and i wanted to tell you how much i love you, you know you saved my life and i really thank you for th", smStr.bytes) - self.assertEquals("hi justin how are you? My name is pepe i'm french and i wanted to tell you how much i love you, you know you saved my life and i really thank you for th", smStr.unicode) - self.assertEquals([InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01))], smStr.udh) - - def test_isConcatenatedSM_true(self): - pduHex = '000000da0000000500000000da4b62474652414e4300010131353535313233343536370001013134303436363533343130004000000000000000009e0500032403016869206a757374696e20686f772061726520796f753f204d79206e616d6520697320706570652069276d206672656e636820616e6420692077616e74656420746f2074656c6c20796f7520686f77206d7563682069206c6f766520796f752c20796f75206b6e6f7720796f75207361766564206d79206c69666520616e642069207265616c6c79207468616e6b20796f7520666f72207468' - pdu = self.getPDU(pduHex) - self.assertTrue(SMStringEncoder().isConcatenatedSM(pdu)) - iElem = SMStringEncoder().getConcatenatedSMInfoElement(pdu) - self.assertEquals(InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01)), iElem) - - def test_isConcatenatedSM_false(self): - pduHex = '000000490000000500000000b9b7e456544d4f424900010131353535313233343536370001013134303436363533343130000000000000000000000d49206c7576206a757374696e21' - pdu = self.getPDU(pduHex) - self.assertFalse(SMStringEncoder().isConcatenatedSM(pdu)) - iElem = SMStringEncoder().getConcatenatedSMInfoElement(pdu) - self.assertEquals(None, iElem) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/smpp/pdu/tests/test_smpp_time.py b/smpp/pdu/tests/test_smpp_time.py deleted file mode 100644 index e0db738..0000000 --- a/smpp/pdu/tests/test_smpp_time.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Copyright 2009-2010 Mozes, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" -import unittest -from smpp.pdu import smpp_time -from datetime import datetime, timedelta - -class SMPPTimeTest(unittest.TestCase): - - def test_parse_t(self): - self.assertEquals(0, smpp_time.parse_t('0')) - self.assertEquals('0', smpp_time.unparse_t(0)) - self.assertEquals(9, smpp_time.parse_t('9')) - self.assertEquals('9', smpp_time.unparse_t(9)) - self.assertRaises(ValueError, smpp_time.parse_t, 'a') - self.assertRaises(ValueError, smpp_time.parse_t, '03') - - def test_parse_nn(self): - self.assertEquals(0, smpp_time.parse_nn('00')) - self.assertEquals('00', smpp_time.unparse_nn(0)) - self.assertEquals(48, smpp_time.parse_nn('48')) - self.assertEquals('48', smpp_time.unparse_nn(48)) - self.assertRaises(ValueError, smpp_time.parse_nn, '49') - self.assertRaises(ValueError, smpp_time.parse_nn, '0') - - def test_parse_relative(self): - str = '020610233429000R' - rel = smpp_time.parse(str) - self.assertEquals(smpp_time.SMPPRelativeTime, rel.__class__) - self.assertEquals(2, rel.years) - self.assertEquals(6, rel.months) - self.assertEquals(10, rel.days) - self.assertEquals(23, rel.hours) - self.assertEquals(34, rel.minutes) - self.assertEquals(29, rel.seconds) - self.assertEquals(str, smpp_time.unparse(rel)) - - def test_parse_relative_mins_only(self): - str = '000000001000000R' - rel = smpp_time.parse(str) - self.assertEquals(smpp_time.SMPPRelativeTime, rel.__class__) - self.assertEquals(0, rel.years) - self.assertEquals(0, rel.months) - self.assertEquals(0, rel.days) - self.assertEquals(0, rel.hours) - self.assertEquals(10, rel.minutes) - self.assertEquals(0, rel.seconds) - self.assertEquals(str, smpp_time.unparse(rel)) - - def test_parse_absolute_no_offset(self): - str = '070927233429800+' - dt = smpp_time.parse(str) - self.assertEquals(2007, dt.year) - self.assertEquals(9, dt.month) - self.assertEquals(27, dt.day) - self.assertEquals(23, dt.hour) - self.assertEquals(34, dt.minute) - self.assertEquals(29, dt.second) - self.assertEquals(800000, dt.microsecond) - self.assertEquals(None, dt.tzinfo) - self.assertEquals(str, smpp_time.unparse(dt)) - - def test_parse_absolute_positive_offset(self): - str = '070927233429848+' - dt = smpp_time.parse(str) - self.assertEquals(2007, dt.year) - self.assertEquals(9, dt.month) - self.assertEquals(27, dt.day) - self.assertEquals(23, dt.hour) - self.assertEquals(34, dt.minute) - self.assertEquals(29, dt.second) - self.assertEquals(800000, dt.microsecond) - self.assertEquals(timedelta(hours=12), dt.tzinfo.utcoffset(None)) - self.assertEquals(str, smpp_time.unparse(dt)) - - def test_parse_absolute_negative_offset(self): - str = '070927233429848-' - dt = smpp_time.parse(str) - self.assertEquals(2007, dt.year) - self.assertEquals(9, dt.month) - self.assertEquals(27, dt.day) - self.assertEquals(23, dt.hour) - self.assertEquals(34, dt.minute) - self.assertEquals(29, dt.second) - self.assertEquals(800000, dt.microsecond) - self.assertEquals(timedelta(hours=-12), dt.tzinfo.utcoffset(None)) - self.assertEquals(str, smpp_time.unparse(dt)) - - - -if __name__ == '__main__': - unittest.main() diff --git a/smpp/pdu/tests/test_gsm_encoding.py b/tests/test_gsm_encoding.py similarity index 81% rename from smpp/pdu/tests/test_gsm_encoding.py rename to tests/test_gsm_encoding.py index 58c6888..fd196dd 100644 --- a/smpp/pdu/tests/test_gsm_encoding.py +++ b/tests/test_gsm_encoding.py @@ -13,54 +13,59 @@ See the License for the specific language governing permissions and limitations under the License. """ +from io import BytesIO import unittest -import StringIO import binascii +import sys + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") from smpp.pdu.gsm_encoding import * from smpp.pdu.gsm_types import * class EncoderTest(unittest.TestCase): - + def do_conversion_test(self, encoder, value, hexdumpValue): encoded = encoder.encode(value) hexEncoded = binascii.b2a_hex(encoded) if hexdumpValue != hexEncoded: - print "\nHex Value:\n%s" % hexdumpValue - print "Hex Encoded:\n%s" % hexEncoded + print("\nHex Value:\n%s" % hexdumpValue) + print("Hex Encoded:\n%s" % hexEncoded) chars1 = list(hexdumpValue) chars2 = list(hexEncoded) for i in range(0, len(hexEncoded)): if chars1[i] != chars2[i]: - print "Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i]) - - self.assertEquals(hexdumpValue, hexEncoded) - file = StringIO.StringIO(encoded) + print("Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i])) + + self.assertEqual(hexdumpValue.encode(), hexEncoded) + file = BytesIO(encoded) decoded = encoder.decode(file) - self.assertEquals(value, decoded) - + self.assertEqual(value, decoded) + def do_null_encode_test(self, encoder, nullDecodeVal, hexdumpValue): encoded = encoder.encode(None) - self.assertEquals(hexdumpValue, binascii.b2a_hex(encoded)) - file = StringIO.StringIO(encoded) + self.assertEqual(hexdumpValue.encode(), binascii.b2a_hex(encoded)) + file = BytesIO(encoded) decoded = encoder.decode(file) - self.assertEquals(nullDecodeVal, decoded) - + self.assertEqual(nullDecodeVal, decoded) + def decode(self, decodeFunc, hexdumpValue): - bytes = binascii.a2b_hex(hexdumpValue) - # print "hex: %s, num bytes %s" % (hexdumpValue, len(bytes)) - file = StringIO.StringIO(bytes) + hexbytes = binascii.a2b_hex(hexdumpValue) + # print("hex: %s, num bytes %s" % (hexdumpValue, len(bytes))) + file = BytesIO(hexbytes) error = None decoded = None try: decoded = decodeFunc(file) - except Exception, e: + except Exception as e: error = e - # print "file index: %s" % file.tell() - self.assertEquals(len(bytes), file.tell()) + # print("file index: %s" % file.tell()) + self.assertEqual(len(hexbytes), file.tell()) if error: raise error return decoded - + def do_decode_udh_parse_error_test(self, decodeFunc, hexdumpValue): try: decoded = self.decode(decodeFunc, hexdumpValue) @@ -69,10 +74,10 @@ def do_decode_udh_parse_error_test(self, decodeFunc, hexdumpValue): pass class InformationElementIdentifierEncoderTest(EncoderTest): - + def test_conversion(self): self.do_conversion_test(InformationElementIdentifierEncoder(), InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM, '08') - + class IEConcatenatedSMEncoderTest(EncoderTest): def test_conversion(self): @@ -85,32 +90,32 @@ def test_conversion(self): self.do_conversion_test(InformationElementEncoder(), InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0xFA, 0x03, 0x02)), '0003fa0302') self.do_conversion_test(InformationElementEncoder(), InformationElement(InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM, IEConcatenatedSM(0x9CFA, 0x03, 0x02)), '08049cfa0302') self.do_conversion_test(InformationElementEncoder(), InformationElement(InformationElementIdentifier.HYPERLINK_FORMAT_ELEMENT, binascii.a2b_hex('9cfa0302')), '21049cfa0302') - + def test_decode_unknown_identifier(self): decoded = self.decode(InformationElementEncoder().decode, '02049cfa0302') - self.assertEquals(None, decoded) + self.assertEqual(None, decoded) decoded = self.decode(InformationElementEncoder().decode, '0200') - self.assertEquals(None, decoded) - + self.assertEqual(None, decoded) + def test_invalid_length(self): - self.do_decode_udh_parse_error_test(InformationElementEncoder().decode, '0002fa0302') + self.do_decode_udh_parse_error_test(InformationElementEncoder().decode, '0002fa0302') self.do_decode_udh_parse_error_test(InformationElementEncoder().decode, '0004fa0302') - + class UserDataHeaderEncoderTest(EncoderTest): def test_conversion(self): udh = [InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01))] self.do_conversion_test(UserDataHeaderEncoder(), udh, '050003240301') - + def test_decode_repeated_non_repeatable_element(self): udh = self.decode(UserDataHeaderEncoder().decode, '0c0804abcd030208049cfa0302') udhExpected = [InformationElement(InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM, IEConcatenatedSM(0x9CFA, 0x03, 0x02))] - self.assertEquals(udhExpected, udh) - + self.assertEqual(udhExpected, udh) + def test_decode_with_unknown_elements(self): udh = self.decode(UserDataHeaderEncoder().decode, '0f0203ffffff0201ff00032403010200') udhExpected = [InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01))] - self.assertEquals(udhExpected, udh) + self.assertEqual(udhExpected, udh) def test_encode_repeated_non_repeatable_element(self): ie1 = InformationElement(InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM, IEConcatenatedSM(0x9CFA, 0x03, 0x02)) @@ -121,14 +126,10 @@ def test_encode_repeated_non_repeatable_element(self): def test_decode_mutually_exclusive_elements(self): udh = self.decode(UserDataHeaderEncoder().decode, '0b000324030108049cfa0302') udhExpected = [InformationElement(InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM, IEConcatenatedSM(0x9CFA, 0x03, 0x02))] - self.assertEquals(udhExpected, udh) + self.assertEqual(udhExpected, udh) def test_encode_mutually_exclusive_elements(self): ie1 = InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01)) ie2 = InformationElement(InformationElementIdentifier.CONCATENATED_SM_16BIT_REF_NUM, IEConcatenatedSM(0xABCD, 0x04, 0x01)) udh = [ie1, ie2] - self.assertRaises(ValueError, UserDataHeaderEncoder().encode, udh) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + self.assertRaises(ValueError, UserDataHeaderEncoder().encode, udh) diff --git a/tests/test_pdu_encoding.py b/tests/test_pdu_encoding.py new file mode 100644 index 0000000..1aa4eb8 --- /dev/null +++ b/tests/test_pdu_encoding.py @@ -0,0 +1,748 @@ +""" +Copyright 2009-2010 Mozes, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from io import BytesIO +import unittest +import binascii +from datetime import datetime +import sys + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") +from smpp.pdu.smpp_time import SMPPRelativeTime +from smpp.pdu.pdu_encoding import * +from smpp.pdu.pdu_types import * +from smpp.pdu.operations import * + +class EncoderTest(unittest.TestCase): + + def do_decode_encode_test(self, encoder, value, hexdumpValue): + binary = BytesIO(binascii.a2b_hex(hexdumpValue)) + decoded = encoder.decode(binary) + self.assertEqual(value, decoded) + + encoded = encoder.encode(value) + hexEncoded = binascii.b2a_hex(encoded) + if hexdumpValue != hexEncoded: + print("\nHex Value:\n%s" % hexdumpValue) + print("Hex Encoded:\n%s" % hexEncoded) + chars1 = list(hexdumpValue) + chars2 = list(hexEncoded) + for i in range(0, len(hexEncoded)): + if chars1[i] != chars2[i]: + print("Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i])) + + self.assertEqual(hexdumpValue.encode(), hexEncoded) + + + def do_encode_decode_test(self, encoder, value, hexdumpValue): + encoded = encoder.encode(value) + hexEncoded = binascii.b2a_hex(encoded) + if hexdumpValue.encode() != hexEncoded: + print("\nHex Value:\n%s" % hexdumpValue) + print("Hex Encoded:\n%s" % hexEncoded) + chars1 = list(hexdumpValue) + chars2 = list(hexEncoded) + for i in range(0, len(hexEncoded)): + if chars1[i] != chars2[i]: + print("Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i])) + + self.assertEqual(hexdumpValue.encode(), hexEncoded) + file = BytesIO(encoded) + decoded = encoder.decode(file) + self.assertEqual(value, decoded) + + def do_encode_test(self, encoder, value, hexdumpValue): + encoded = encoder.encode(value) + hexEncoded = binascii.b2a_hex(encoded) + if hexdumpValue.encode() != hexEncoded: + print("\nHex Value:\n%s" % hexdumpValue) + print("Hex Encoded:\n%s" % hexEncoded) + chars1 = list(hexdumpValue) + chars2 = list(hexEncoded) + for i in range(0, len(hexEncoded)): + if chars1[i] != chars2[i]: + print("Letter %d diff [%s] [%s]" % (i, chars1[i], chars2[i])) + + self.assertEqual(hexdumpValue.encode(), hexEncoded) + + def do_decode_test(self, encoder, value, hexdumpValue): + decoded = self.decode(encoder.decode, hexdumpValue) + self.assertEqual(value, decoded) + + def do_null_encode_test(self, encoder, nullDecodeVal, hexdumpValue): + encoded = encoder.encode(None) + self.assertEqual(hexdumpValue.encode(), binascii.b2a_hex(encoded)) + file = BytesIO(encoded) + decoded = encoder.decode(file) + self.assertEqual(nullDecodeVal, decoded) + + def decode(self, decodeFunc, hexdumpValue): + return decodeFunc(BytesIO(binascii.a2b_hex(hexdumpValue))) + + def do_decode_parse_error_test(self, decodeFunc, status, hexdumpValue): + try: + decoded = self.decode(decodeFunc, hexdumpValue) + self.assertTrue(False, 'Decode did not throw exception. Result was: %s' % str(decoded)) + except PDUParseError as e: + self.assertEqual(status, e.status) + + def do_decode_corrupt_data_error_test(self, decodeFunc, status, hexdumpValue): + try: + decoded = self.decode(decodeFunc, hexdumpValue) + self.assertTrue(False, 'Decode did not throw exception. Result was: %s' % str(decoded)) + except PDUCorruptError as e: + self.assertEqual(status, e.status) + +class EmptyEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(EmptyEncoder(), None, '') + +class IntegerEncoderTest(EncoderTest): + + def test_int4(self): + self.do_encode_decode_test(Int4Encoder(), 0x800001FF, '800001ff') + + def test_int1(self): + encoder = Int1Encoder() + self.do_encode_decode_test(encoder, 255, 'ff') + self.assertRaises(ValueError, encoder.encode, 256) + self.do_null_encode_test(encoder, 0, '00') + + def test_int1_max(self): + self.assertRaises(ValueError, Int1Encoder, max=256) + encoder = Int1Encoder(max=254) + self.do_encode_decode_test(encoder, 254, 'fe') + self.assertRaises(ValueError, encoder.encode, 255) + + def test_int1_min(self): + self.assertRaises(ValueError, Int1Encoder, min=-1) + encoder = Int1Encoder(min=1) + self.do_encode_decode_test(encoder, 1, '01') + self.do_encode_decode_test(encoder, None, '00') + self.assertRaises(ValueError, encoder.encode, 0) + + def test_int2(self): + self.do_encode_decode_test(Int2Encoder(), 0x41AC, '41ac') + +class COctetStringEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(COctetStringEncoder(), b'hello', '68656c6c6f00') + self.do_encode_decode_test(COctetStringEncoder(6), b'hello', '68656c6c6f00') + self.do_encode_decode_test(COctetStringEncoder(1), b'', '00') + self.do_null_encode_test(COctetStringEncoder(), b'', '00') + self.assertRaises(ValueError, COctetStringEncoder, 0) + + def test_maxLength_exceeded(self): + encoder = COctetStringEncoder(5, decodeErrorStatus=CommandStatus.ESME_RINVSRCADR) + self.assertRaises(ValueError, encoder.encode, 'hello') + self.do_decode_parse_error_test(encoder.decode, CommandStatus.ESME_RINVSRCADR, '68656c6c6f00') + + def test_ascii_required(self): + encoder = COctetStringEncoder() + self.assertRaises(ValueError, encoder.encode, u'\x9b\xa2\x7c') + + def test_requireNull(self): + encoder = COctetStringEncoder(decodeNull=True, requireNull=True) + self.do_encode_decode_test(encoder, None, '00') + self.assertRaises(ValueError, encoder.encode, 'test') + self.do_decode_parse_error_test(encoder.decode, CommandStatus.ESME_RUNKNOWNERR, '68656c6c6f00') + +class OctetStringEncoderTest(EncoderTest): + + def test_conversion(self): + hexstr = '68656c6c6f' + self.do_encode_decode_test(OctetStringEncoder(int(len(hexstr)/2)), binascii.a2b_hex(hexstr), hexstr) + self.do_encode_decode_test(OctetStringEncoder(0), b'', '') + + def test_maxLength_exceeded(self): + encoder = OctetStringEncoder(1) + self.assertRaises(ValueError, encoder.encode, binascii.a2b_hex('ffaa')) + +class CommandIdEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(CommandIdEncoder(), CommandId.enquire_link_resp, '80000015') + + def test_decode_invalid_command_id(self): + self.do_decode_corrupt_data_error_test(CommandIdEncoder().decode, CommandStatus.ESME_RINVCMDID, 'f0000009') + +class CommandStatusEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(CommandStatusEncoder(), CommandStatus.ESME_RUNKNOWNERR, '000000ff') + +class TagEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(TagEncoder(), Tag.language_indicator, '020d') + +class EsmClassEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(EsmClassEncoder(), EsmClass(EsmClassMode.DATAGRAM, EsmClassType.INTERMEDIATE_DELIVERY_NOTIFICATION, [EsmClassGsmFeatures.SET_REPLY_PATH]), 'a1') + self.do_null_encode_test(EsmClassEncoder(), EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT, []), '00') + + def test_decode_invalid_type(self): + self.do_decode_parse_error_test(EsmClassEncoder().decode, CommandStatus.ESME_RINVESMCLASS, '30') + +class RegisteredDeliveryEncoderTest(EncoderTest): + + def test_conversion(self): + value = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_DELIVERY_ACK_REQUESTED, RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED], True) + self.do_encode_decode_test(RegisteredDeliveryEncoder(), value, '1d') + self.do_null_encode_test(RegisteredDeliveryEncoder(), RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED, [], False), '00') + + def test_decode_invalid_receipt(self): + self.do_decode_parse_error_test(RegisteredDeliveryEncoder().decode, CommandStatus.ESME_RINVREGDLVFLG, '03') + +class AddrTonEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(AddrTonEncoder(fieldName='source_addr_ton'), AddrTon.ALPHANUMERIC, '05') + +class PriorityFlagEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(PriorityFlagEncoder(), PriorityFlag.LEVEL_2, '02') + + def test_decode_invalid(self): + self.do_decode_parse_error_test(PriorityFlagEncoder().decode, CommandStatus.ESME_RINVPRTFLG, '0f') + +class AddrNpiEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(AddrNpiEncoder(fieldName='source_addr_npi'), AddrNpi.LAND_MOBILE, '06') + +class AddrSubunitEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(AddrSubunitEncoder(), AddrSubunit.MOBILE_EQUIPMENT, '02') + +class NetworkTypeEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(NetworkTypeEncoder(), NetworkType.GSM, '01') + +class BearerTypeEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(BearerTypeEncoder(), BearerType.USSD, '04') + +class PayloadTypeEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(PayloadTypeEncoder(), PayloadType.WCMP, '01') + +class PrivacyIndicatorEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(PrivacyIndicatorEncoder(), PrivacyIndicator.CONFIDENTIAL, '02') + +class LanguageIndicatorEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(LanguageIndicatorEncoder(), LanguageIndicator.SPANISH, '03') + +class DisplayTimeEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(DisplayTimeEncoder(), DisplayTime.INVOKE, '02') + +class MsAvailabilityStatusEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(MsAvailabilityStatusEncoder(), MsAvailabilityStatus.DENIED, '01') + +class ReplaceIfPresentFlagEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(ReplaceIfPresentFlagEncoder(), ReplaceIfPresentFlag.REPLACE, '01') + self.do_null_encode_test(ReplaceIfPresentFlagEncoder(), ReplaceIfPresentFlag.DO_NOT_REPLACE, '00') + +class DataCodingEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(DataCodingEncoder(), DataCoding(schemeData=DataCodingDefault.LATIN_1), '03') + self.do_null_encode_test(DataCodingEncoder(), DataCoding(schemeData=DataCodingDefault.SMSC_DEFAULT_ALPHABET), '00') + self.do_encode_decode_test(DataCodingEncoder(), DataCoding(DataCodingScheme.RAW, 48), '30') + self.do_encode_decode_test(DataCodingEncoder(), DataCoding(DataCodingScheme.RAW, 11), '0b') + self.do_encode_decode_test(DataCodingEncoder(), DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_1)), 'f1') + +class DestFlagEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(DestFlagEncoder(), DestFlag.DISTRIBUTION_LIST_NAME, '02') + self.assertRaises(ValueError, DestFlagEncoder().encode, None) + self.do_decode_parse_error_test(DestFlagEncoder().decode, CommandStatus.ESME_RUNKNOWNERR, '00') + +class MessageStateEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(MessageStateEncoder(), MessageState.REJECTED, '08') + self.assertRaises(ValueError, MessageStateEncoder().encode, None) + self.do_decode_parse_error_test(MessageStateEncoder().decode, CommandStatus.ESME_RUNKNOWNERR, '00') + +class CallbackNumDigitModeIndicatorEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(CallbackNumDigitModeIndicatorEncoder(), CallbackNumDigitModeIndicator.ASCII, '01') + self.assertRaises(ValueError, CallbackNumDigitModeIndicatorEncoder().encode, None) + +class CallbackNumEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(CallbackNumEncoder(13), CallbackNum(CallbackNumDigitModeIndicator.ASCII, digits=b'8033237457'), '01000038303333323337343537') + + def test_decode_invalid_type(self): + self.do_decode_parse_error_test(CallbackNumEncoder(13).decode, CommandStatus.ESME_RINVOPTPARAMVAL, '02000038303333323337343537') + + def test_decode_invalid_size(self): + self.do_decode_parse_error_test(CallbackNumEncoder(2).decode, CommandStatus.ESME_RINVOPTPARAMVAL, '0100') + +class SubaddressTypeTagEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(SubaddressTypeTagEncoder(), SubaddressTypeTag.USER_SPECIFIED, 'a0') + self.assertRaises(ValueError, SubaddressTypeTagEncoder().encode, None) + +class SubaddressEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(SubaddressEncoder(4), Subaddress(SubaddressTypeTag.USER_SPECIFIED, value=b'742'), 'a0373432') + + def test_decode_invalid_type(self): + "#325: any invalid type will be marked as RESERVED" + self.do_encode_decode_test(SubaddressEncoder(4), Subaddress(SubaddressTypeTag.RESERVED, value=b'742'), '00373432') + + def test_decode_invalid_size(self): + self.do_decode_parse_error_test(SubaddressEncoder(1).decode, CommandStatus.ESME_RINVOPTPARAMVAL, 'a0373432') + +class TimeEncoderEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(TimeEncoder(), datetime(2007, 9, 27, 23, 34, 29, 800000), binascii.b2a_hex(b'070927233429800+' + b'\0').decode()) + self.do_encode_decode_test(TimeEncoder(), None, '00') + + def test_requireNull(self): + encoder = TimeEncoder(requireNull=True) + self.do_encode_decode_test(encoder, None, '00') + self.assertRaises(ValueError, encoder.encode, datetime.now()) + self.do_decode_parse_error_test(encoder.decode, CommandStatus.ESME_RUNKNOWNERR, binascii.b2a_hex(b'070927233429800+' + b'\0').decode()) + + def test_decode_invalid(self): + self.do_decode_parse_error_test(TimeEncoder(decodeErrorStatus=CommandStatus.ESME_RINVSRCADR).decode, CommandStatus.ESME_RINVSRCADR, binascii.b2a_hex(b'070927233429800' + b'\0').decode()) + +class ShortMessageEncoderTest(EncoderTest): + + def test_conversion(self): + self.do_encode_decode_test(ShortMessageEncoder(), b'hello', '0568656c6c6f') + self.do_null_encode_test(ShortMessageEncoder(), b'', '00') + +class OptionEncoderTest(EncoderTest): + + def test_dest_addr_subunit(self): + self.do_encode_decode_test(OptionEncoder(), Option(Tag.dest_addr_subunit, AddrSubunit.MOBILE_EQUIPMENT), '0005000102') + + def test_decode_invalid_dest_addr_subunit(self): + self.do_decode_parse_error_test(OptionEncoder().decode, CommandStatus.ESME_RINVOPTPARAMVAL, '00050001ff') + + def test_message_payload(self): + hexVal = 'ffaa01ce' + self.do_encode_decode_test(OptionEncoder(), Option(Tag.message_payload, binascii.a2b_hex(hexVal)), '04240004' + hexVal) + + def test_alert_on_message_delivery(self): + self.do_encode_decode_test(OptionEncoder(), Option(Tag.alert_on_message_delivery, None), '130c0000') + +class PDUEncoderTest(EncoderTest): + + def do_bind_conversion_test(self, pduBindKlass, reqCommandIdHex, respCommandIdHex): + reqPdu = pduBindKlass(2, CommandStatus.ESME_ROK, + system_id=b'test', + password=b'secret', + system_type=b'OTA', + interface_version=0x34, + addr_ton=AddrTon.NATIONAL, + addr_npi=AddrNpi.LAND_MOBILE, + address_range=b'127.0.0.*', + ) + self.do_encode_decode_test(PDUEncoder(), reqPdu, '0000002d%s00000000000000027465737400736563726574004f5441003402063132372e302e302e2a00' % reqCommandIdHex) + respPdu = reqPdu.requireAck(1, CommandStatus.ESME_ROK, system_id='TSI7588', sc_interface_version=0x34) + self.do_encode_decode_test(PDUEncoder(), respPdu, '0000001d%s000000000000000154534937353838000210000134' % respCommandIdHex) + + + + def test_BindTransmitter_conversion(self): + self.do_bind_conversion_test(BindTransmitter, '00000002', '80000002') + + def test_BindReceiver_conversion(self): + self.do_bind_conversion_test(BindReceiver, '00000001', '80000001') + + def test_BindTransceiver_conversion(self): + self.do_bind_conversion_test(BindTransceiver, '00000009', '80000009') + + def test_Unbind_conversion(self): + pdu = Unbind(4) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010000000060000000000000004') + + def test_UnbindResp_conversion(self): + pdu = UnbindResp(5, CommandStatus.ESME_ROK) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010800000060000000000000005') + + def test_GenericNack_conversion(self): + pdu = GenericNack(None, CommandStatus.ESME_RSYSERR) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010800000000000000800000000') + + def test_DeliverSM_syniverse_MO_conversion(self): + pdu = DeliverSM(2676551972, + service_type=b'AWSBD', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.ISDN, + source_addr=b'16505551234', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr=b'17735554070', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(schemeData=DataCodingDefault.LATIN_1), + short_message=b'there is no spoon', + sm_default_msg_id=0, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '0000004d00000005000000009f88f12441575342440001013136353035353531323334000101313737333535353430373000000000000000000300117468657265206973206e6f2073706f6f6e') + + def test_DeliverSM_handset_ack_conversion(self): + pdu = DeliverSM(10, + service_type=b'CMT', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.UNKNOWN, + source_addr=b'6515555678', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.UNKNOWN, + destination_addr=b'123', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.SMSC_DELIVERY_RECEIPT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(schemeData=DataCodingDefault.SMSC_DEFAULT_ALPHABET), + short_message=b'id:1891273321 sub:001 dlvrd:001 submit date:1305050826 done date:1305050826 stat:DELIVRD err:000 Text:DLVRD TO MOBILE\x00', + message_state=MessageState.DELIVERED, + receipted_message_id=b'70BA8A69', + sm_default_msg_id=0, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '000000b900000005000000000000000a434d5400010036353135353535363738000100313233000400000000000000007669643a31383931323733333231207375623a30303120646c7672643a303031207375626d697420646174653a3133303530353038323620646f6e6520646174653a3133303530353038323620737461743a44454c49565244206572723a30303020546578743a444c56524420544f204d4f42494c45000427000102001e0009373042413841363900') + + def test_DeliverSM_sybase_MO_conversion(self): + pdu = DeliverSM(1, + service_type=b'CMT', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.UNKNOWN, + source_addr=b'3411149500001', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.UNKNOWN, + destination_addr=b'12345455', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), + short_message=b'HELLO\x00', + sm_default_msg_id = 0, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '0000003f000000050000000000000001434d540001003334313131343935303030303100010031323334353435350000000000000000f2000648454c4c4f00') + + def test_DeliverSM_with_subaddress(self): + pdu = DeliverSM(1, + service_type=b'BM8', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.ISDN, + source_addr=b'46123456789', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr=b'14046653410', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), + short_message="Hello I'm a bigg fan of you", + source_subaddress=Subaddress(SubaddressTypeTag.USER_SPECIFIED, b'742'), + dest_subaddress=Subaddress(SubaddressTypeTag.USER_SPECIFIED, b'4131'), + sm_default_msg_id=0, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000066000000050000000000000001424d38000101343631323334353637383900010131343034363635333431300000000000000000f2001b48656c6c6f2049276d206120626967672066616e206f6620796f7502020004a037343202030005a034313331') + + def test_DeliverSM_0348(self): + pdu = SubmitSM(455569, + service_type=b'', + source_addr_ton=AddrTon.ALPHANUMERIC, + source_addr_npi=AddrNpi.UNKNOWN, + source_addr=b'0348', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr=b'3969809342', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT, [EsmClassGsmFeatures.UDHI_INDICATOR_SET]), + protocol_id=0x7F, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DATA_8BIT, DataCodingGsmMsgClass.CLASS_2)), + sm_default_msg_id=0, + short_message=binascii.a2b_hex('027000002815162115150000001BB5B34A2FAB312CFA8ECDD7779158747AC742C463CDD53B41963E49979D95AC'), + ) + + self.do_encode_decode_test(PDUEncoder(), pdu, '0000005c00000004000000000006f391000500303334380001013339363938303933343200407f0000000100f6002d027000002815162115150000001bb5b34a2fab312cfa8ecdd7779158747ac742c463cdd53b41963e49979d95ac') + + + def test_EnquireLink_conversion(self): + pdu = EnquireLink(6, CommandStatus.ESME_ROK) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010000000150000000000000006') + + def test_EnquireLinkResp_conversion(self): + pdu = EnquireLinkResp(7) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010800000150000000000000007') + + def test_AlertNotification_conversion(self): + pdu = AlertNotification( + source_addr_ton=AddrTon.NATIONAL, + source_addr_npi=AddrNpi.ISDN, + source_addr=b'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + esme_addr_ton=AddrTon.INTERNATIONAL, + esme_addr_npi=AddrNpi.LAND_MOBILE, + esme_addr=b'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY', + ms_availability_status=MsAvailabilityStatus.DENIED, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '0000008900000102000000000000000002015858585858585858585858585858585858585858585858585858585858585858585858585858585858585858585858585858585858580001065959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959595959000422000101') + + def test_QuerySMResp_conversion(self): + pdu = QuerySMResp( + message_id = 'Smsc2003', + final_date = None, + message_state = MessageState.UNKNOWN, + error_code = None, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '0000001c800000030000000000000000536d73633230303300000700') + + def test_SubmitSM_conversion(self): + pdu = SubmitSM(9284, + service_type=b'', + source_addr_ton=AddrTon.ALPHANUMERIC, + source_addr_npi=AddrNpi.UNKNOWN, + source_addr=b'mobileway', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr=b'1208230', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, DataCodingGsmMsgClass.CLASS_2)), + sm_default_msg_id=0, + short_message=b'HELLO', + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '000000360000000400000000000024440005006d6f62696c65776179000101313230383233300000000000000100f2000548454c4c4f') + + def test_SubmitSM_ringtone_conversion(self): + pdu = SubmitSM(455569, + service_type=b'', + source_addr_ton=AddrTon.ALPHANUMERIC, + source_addr_npi=AddrNpi.UNKNOWN, + source_addr=b'mobileway', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr=b'3369809342', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT, [EsmClassGsmFeatures.UDHI_INDICATOR_SET]), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, DataCodingGsmMsg(DataCodingGsmMsgCoding.DATA_8BIT, DataCodingGsmMsgClass.CLASS_1)), + sm_default_msg_id=0, + short_message=binascii.a2b_hex('06050415811581024a3a5db5a5cdcda5bdb8040084d8c51381481381481381481381481381381481581681781881881061881061b81081181081881061881061681081781081881061881061b81081181081881061881061681081781081b81881321081b81881221081b818811210824dc1446000') + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '000000a900000004000000000006f3910005006d6f62696c65776179000101333336393830393334320040000000000100f5007506050415811581024a3a5db5a5cdcda5bdb8040084d8c51381481381481381481381481381381481581681781881881061881061b81081181081881061881061681081781081881061881061b81081181081881061881061681081781081b81881321081b81881221081b818811210824dc1446000') + + def test_DeliverSM_with_network_error_code(self): + """Related to #117""" + + pdu = DeliverSM(1, + service_type='', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.ISDN, + source_addr='4915256794887', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr='04051306999', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + schedule_delivery_time=None, + validity_period=None, + registered_delivery=RegisteredDelivery( + RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, + DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, + DataCodingGsmMsgClass.CLASS_2)), + sm_default_msg_id=0, + short_message='id:bc59b8aa-2fd2-4035-8113-19301e050079 sub:001 dlvrd:001 submit date:150508144058 done date:150508144058 stat:DELIVRD err:000 text:-', + network_error_code=NetworkErrorCode(NetworkErrorCodeNetworkType.GSM, b'\x00\x00'), + message_state=MessageState.DELIVERED, + receipted_message_id='bc59b8aa-2fd2-4035-8113-19301e050079' + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '000000f30000000500000000000000010001013439313532353637393438383700010130343035313330363939390000000000000000f2008569643a62633539623861612d326664322d343033352d383131332d313933303165303530303739207375623a30303120646c7672643a303031207375626d697420646174653a31353035303831343430353820646f6e6520646174653a31353035303831343430353820737461743a44454c49565244206572723a30303020746578743a2d042300030300000427000102001e002562633539623861612d326664322d343033352d383131332d31393330316530353030373900') + + def test_reuse_encoder_for_encode_and_decode(self): + pdu = DeliverSM(1, + service_type='', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.ISDN, + source_addr='4915256794887', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr='04051306999', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + schedule_delivery_time=None, + validity_period=None, + registered_delivery=RegisteredDelivery( + RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(DataCodingScheme.GSM_MESSAGE_CLASS, + DataCodingGsmMsg(DataCodingGsmMsgCoding.DEFAULT_ALPHABET, + DataCodingGsmMsgClass.CLASS_2)), + sm_default_msg_id=0, + short_message='id:bc59b8aa-2fd2-4035-8113-19301e050079 sub:001 dlvrd:001 submit date:150508144058 done date:150508144058 stat:DELIVRD err:000 text:-', + network_error_code=NetworkErrorCode(NetworkErrorCodeNetworkType.GSM, b'\x00\x00'), + message_state=MessageState.DELIVERED, + message_payload='Some string payload', + receipted_message_id='bc59b8aa-2fd2-4035-8113-19301e050079' + ) + self.do_decode_encode_test(PDUEncoder(), pdu, '0000010a0000000500000000000000010001013439313532353637393438383700010130343035313330363939390000000000000000f2008569643a62633539623861612d326664322d343033352d383131332d313933303165303530303739207375623a30303120646c7672643a303031207375626d697420646174653a31353035303831343430353820646f6e6520646174653a31353035303831343430353820737461743a44454c49565244206572723a30303020746578743a2d04240013536f6d6520737472696e67207061796c6f6164042300030300000427000102001e002562633539623861612d326664322d343033352d383131332d31393330316530353030373900') + + def test_DeliverSM_with_vendor_specific_bypass(self): + """#449: fixing the 'Value -1 is less than min 0' error caused by the vendor_specific_bypass parameter""" + pdu = DeliverSM(1141690945, + service_type='', + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.ISDN, + source_addr='27727331834', + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + destination_addr='27600701040', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery( + RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + data_coding=DataCoding(schemeData=DataCodingDefault.SMSC_DEFAULT_ALPHABET), + short_message='Replied tue 16 Aug 10h11', + sm_default_msg_id=0, + vendor_specific_bypass='2782913594\x00', + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '0000004f0000000500000000440cd2410001013237373237333331383334000101323736303037303130343000000000000001000000185265706c6965642074756520313620417567203130683131') + + def test_SubmitSM_with_data_coding_mclass_1(self): + pdu = SubmitSM(2, + source_addr=b'385915222656', + destination_addr=b'385953926992', + short_message=b'jsmtest2 dc f1', + data_coding=DataCoding( + DataCodingScheme.GSM_MESSAGE_CLASS, + DataCodingGsmMsg( + DataCodingGsmMsgCoding.DEFAULT_ALPHABET, + DataCodingGsmMsgClass.CLASS_1 + ) + ), + service_type='', + esm_class=EsmClass(EsmClassMode.DEFAULT, EsmClassType.DEFAULT, []), + protocol_id=0, + priority_flag=PriorityFlag.LEVEL_0, + registered_delivery=RegisteredDelivery( + RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED), + replace_if_present_flag=ReplaceIfPresentFlag.DO_NOT_REPLACE, + sm_default_msg_id=0, + source_addr_ton=AddrTon.INTERNATIONAL, + source_addr_npi=AddrNpi.ISDN, + dest_addr_ton=AddrTon.INTERNATIONAL, + dest_addr_npi=AddrNpi.ISDN, + ) + self.do_encode_decode_test(PDUEncoder(), pdu, '000000470000000400000000000000020001013338353931353232323635360001013338353935333932363939320000000000000100f1000e6a736d7465737432206463206631') + + + def test_decode_command_length_too_short(self): + self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVCMDLEN, '0000000f000000060000000000000000') + + @unittest.skip("Padding changes in #124 obsolete these tests") + def test_decode_command_length_too_long(self): + self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVCMDLEN, '00000011000000060000000000000000ff') + + def test_decodeHeader_command_length_too_short(self): + self.do_decode_corrupt_data_error_test(PDUEncoder().decodeHeader, CommandStatus.ESME_RINVCMDLEN, '0000000f000000060000000000000000') + + def test_decode_bad_message_length_msg_too_short(self): + self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVMSGLEN, '000000fd80000009000000000000000154534937353838000210000134') + + @unittest.skip("Padding changes in #124 obsolete these tests") + def test_decode_bad_message_length_msg_too_long(self): + self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVCMDLEN, '0000001c80000009000000000000000154534937353838000210000134') + + def test_decode_bad_message_ends_in_middle_of_option(self): + self.do_decode_corrupt_data_error_test(PDUEncoder().decode, CommandStatus.ESME_RINVMSGLEN, '0000001b8000000900000000000000015453493735383800021000') + + def test_SubmitSMResp_error_has_no_body(self): + pdu = SubmitSMResp(1234, status=CommandStatus.ESME_RMSGQFUL) + self.assertTrue(len(SubmitSMResp.mandatoryParams) > 0) + self.assertEqual(0, len(pdu.params)) + self.do_encode_decode_test(PDUEncoder(), pdu, '000000108000000400000014000004d2') + + def test_BindReceiverResp_error_has_no_body(self): + pdu = BindReceiverResp(3456, status=CommandStatus.ESME_RINVPASWD) + self.assertTrue(len(BindReceiverResp.mandatoryParams) > 0) + self.assertEqual(0, len(pdu.params)) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010800000010000000e00000d80') + + def test_BindTransmitterResp_error_has_no_body(self): + pdu = BindTransmitterResp(3456, status=CommandStatus.ESME_RINVPASWD) + self.assertTrue(len(BindTransmitterResp.mandatoryParams) > 0) + self.assertEqual(0, len(pdu.params)) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010800000020000000e00000d80') + + def test_BindTransceiverResp_error_has_no_body(self): + pdu = BindTransceiverResp(3456, status=CommandStatus.ESME_RINVPASWD) + self.assertEqual(0, len(pdu.params)) + self.do_encode_decode_test(PDUEncoder(), pdu, '00000010800000090000000e00000d80') + + def test_BindTransceiverResp_error_has_no_body_status_set_later(self): + hex = '00000010800000090000000e00000d80' + pdu = BindTransceiverResp(3456, system_id="XYZ") + pdu.status = CommandStatus.ESME_RINVPASWD + #Even though the system_id param was set, it will not be encoded + self.do_encode_test(PDUEncoder(), pdu, hex) + #It will decode with no params set + pduExpected = BindTransceiverResp(3456, status=CommandStatus.ESME_RINVPASWD) + self.assertEqual(0, len(pduExpected.params)) + self.do_decode_test(PDUEncoder(), pduExpected, hex) diff --git a/smpp/pdu/tests/test_pdu_types.py b/tests/test_pdu_types.py similarity index 90% rename from smpp/pdu/tests/test_pdu_types.py rename to tests/test_pdu_types.py index 17e83be..6773151 100644 --- a/smpp/pdu/tests/test_pdu_types.py +++ b/tests/test_pdu_types.py @@ -14,6 +14,11 @@ limitations under the License. """ import unittest +import sys + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") from smpp.pdu.pdu_types import * class EsmClassTest(unittest.TestCase): @@ -21,35 +26,31 @@ class EsmClassTest(unittest.TestCase): def test_equality_with_array_and_set(self): e1 = EsmClass(EsmClassMode.DATAGRAM, EsmClassType.DEFAULT, set([EsmClassGsmFeatures.SET_REPLY_PATH])) e2 = EsmClass(EsmClassMode.DATAGRAM, EsmClassType.DEFAULT, [EsmClassGsmFeatures.SET_REPLY_PATH]) - self.assertEquals(e1, e2) - + self.assertEqual(e1, e2) + def test_equality_with_different_array_order(self): e1 = EsmClass(EsmClassMode.DATAGRAM, EsmClassType.DEFAULT, [EsmClassGsmFeatures.SET_REPLY_PATH, EsmClassGsmFeatures.UDHI_INDICATOR_SET]) e2 = EsmClass(EsmClassMode.DATAGRAM, EsmClassType.DEFAULT, [EsmClassGsmFeatures.UDHI_INDICATOR_SET, EsmClassGsmFeatures.SET_REPLY_PATH]) - self.assertEquals(e1, e2) - + self.assertEqual(e1, e2) + def test_equality_with_array_duplicates(self): e1 = EsmClass(EsmClassMode.DATAGRAM, EsmClassType.DEFAULT, [EsmClassGsmFeatures.SET_REPLY_PATH, EsmClassGsmFeatures.SET_REPLY_PATH]) e2 = EsmClass(EsmClassMode.DATAGRAM, EsmClassType.DEFAULT, [EsmClassGsmFeatures.SET_REPLY_PATH]) - self.assertEquals(e1, e2) + self.assertEqual(e1, e2) class RegisteredDeliveryTest(unittest.TestCase): def test_equality_with_array_and_set(self): r1 = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, set([RegisteredDeliverySmeOriginatedAcks.SME_DELIVERY_ACK_REQUESTED])) r2 = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_DELIVERY_ACK_REQUESTED]) - self.assertEquals(r1, r2) - + self.assertEqual(r1, r2) + def test_equality_with_different_array_order(self): r1 = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED, RegisteredDeliverySmeOriginatedAcks.SME_DELIVERY_ACK_REQUESTED]) r2 = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_DELIVERY_ACK_REQUESTED, RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED]) - self.assertEquals(r1, r2) - + self.assertEqual(r1, r2) + def test_equality_with_array_duplicates(self): r1 = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED, RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED]) r2 = RegisteredDelivery(RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED, [RegisteredDeliverySmeOriginatedAcks.SME_MANUAL_ACK_REQUESTED]) - self.assertEquals(r1, r2) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + self.assertEqual(r1, r2) \ No newline at end of file diff --git a/tests/test_sm_encoding.py b/tests/test_sm_encoding.py new file mode 100644 index 0000000..b109131 --- /dev/null +++ b/tests/test_sm_encoding.py @@ -0,0 +1,89 @@ +""" +Copyright 2009-2010 Mozes, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +from io import BytesIO +import unittest +import binascii +import sys + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") +from smpp.pdu.sm_encoding import SMStringEncoder +from smpp.pdu.pdu_types import * +from smpp.pdu.gsm_types import * +from smpp.pdu.pdu_encoding import PDUEncoder + +class SMDecoderTest(unittest.TestCase): + + def getPDU(self, hexStr): + return PDUEncoder().decode(BytesIO(binascii.a2b_hex(hexStr))) + + def test_decode_UCS2(self): + pduHex = b'000000480000000500000000dfd03a56415753424400010131353535313233343536370001013137373338323834303730000000000000000008000c00f10075014400ed00fc0073' + pdu = self.getPDU(pduHex) + smStr = SMStringEncoder().decodeSM(pdu) + self.assertEqual(b'\x00\xf1\x00u\x01D\x00\xed\x00\xfc\x00s', smStr.bytes) + self.assertEqual(u'\xf1u\u0144\xed\xfcs', smStr.unicode) + self.assertEqual(None, smStr.udh) + + def test_decode_default_alphabet(self): + #'T- Mobile flip phone \xa7 \xa8 N random special charcters' + pduHex = b'0000006f00000005000000005d3fe724544d4f4249000101313535353132333435363700010131373733383238343037300000000000000000000033542d204d6f62696c6520666c69702070686f6e6520a720a8204e2072616e646f6d207370656369616c20636861726374657273' + pdu = self.getPDU(pduHex) + self.assertRaises(UnicodeDecodeError, SMStringEncoder().decodeSM, pdu) + + def test_decode_latin1(self): + pduHex = b'0000004200000005000000002a603d56415753424400010131353535313233343536370001013137373338323834303730000000000000000003000645737061f161' + pdu = self.getPDU(pduHex) + smStr = SMStringEncoder().decodeSM(pdu) + self.assertEqual(b'Espa\xf1a', smStr.bytes) + self.assertEqual(u'Espa\xf1a', smStr.unicode) + self.assertEqual(None, smStr.udh) + + def test_decode_ascii(self): + pduHex = b'00000054000000050000000008c72a4154454c4550000101313535353535353535353500010131343034363635333431300000ff010000000001000e49732074686973206a757374696e0201000100020d000101' + pdu = self.getPDU(pduHex) + smStr = SMStringEncoder().decodeSM(pdu) + self.assertEqual(b'Is this justin', smStr.bytes) + self.assertEqual('Is this justin', smStr.unicode) + self.assertEqual(None, smStr.udh) + + def test_decode_octet_unspecified_common(self): + pduHex = b'000000a900000005000000003cf78935415753424400010131353535313233343536370001013134303436363533343130004000000000000004006d06050423f40000424547494e3a56434152440d0a56455253494f4e3a322e310d0a4e3b434841525345543d5554462d383a4269656265723b4a757374696e0d0a54454c3b564f4943453b434841525345543d5554462d383a343034363635333431300d0a454e443a5643415244' + pdu = self.getPDU(pduHex) + self.assertRaises(NotImplementedError, SMStringEncoder().decodeSM, pdu) + + def test_decode_default_alphabet_with_udh(self): + pduHex = b'000000da0000000500000000da4b62474652414e4300010131353535313233343536370001013134303436363533343130004000000000000000009e0500032403016869206a757374696e20686f772061726520796f753f204d79206e616d6520697320706570652069276d206672656e636820616e6420692077616e74656420746f2074656c6c20796f7520686f77206d7563682069206c6f766520796f752c20796f75206b6e6f7720796f75207361766564206d79206c69666520616e642069207265616c6c79207468616e6b20796f7520666f72207468' + pdu = self.getPDU(pduHex) + smStr = SMStringEncoder().decodeSM(pdu) + self.assertEqual(b"\x05\x00\x03$\x03\x01hi justin how are you? My name is pepe i'm french and i wanted to tell you how much i love you, you know you saved my life and i really thank you for th", smStr.bytes) + self.assertEqual("hi justin how are you? My name is pepe i'm french and i wanted to tell you how much i love you, you know you saved my life and i really thank you for th", smStr.unicode) + self.assertEqual([InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01))], smStr.udh) + + def test_isConcatenatedSM_true(self): + pduHex = b'000000da0000000500000000da4b62474652414e4300010131353535313233343536370001013134303436363533343130004000000000000000009e0500032403016869206a757374696e20686f772061726520796f753f204d79206e616d6520697320706570652069276d206672656e636820616e6420692077616e74656420746f2074656c6c20796f7520686f77206d7563682069206c6f766520796f752c20796f75206b6e6f7720796f75207361766564206d79206c69666520616e642069207265616c6c79207468616e6b20796f7520666f72207468' + pdu = self.getPDU(pduHex) + self.assertTrue(SMStringEncoder().isConcatenatedSM(pdu)) + iElem = SMStringEncoder().getConcatenatedSMInfoElement(pdu) + self.assertEqual(InformationElement(InformationElementIdentifier.CONCATENATED_SM_8BIT_REF_NUM, IEConcatenatedSM(0x24, 0x03, 0x01)), iElem) + + def test_isConcatenatedSM_false(self): + pduHex = b'000000490000000500000000b9b7e456544d4f424900010131353535313233343536370001013134303436363533343130000000000000000000000d49206c7576206a757374696e21' + pdu = self.getPDU(pduHex) + self.assertFalse(SMStringEncoder().isConcatenatedSM(pdu)) + iElem = SMStringEncoder().getConcatenatedSMInfoElement(pdu) + self.assertEqual(None, iElem) \ No newline at end of file diff --git a/tests/test_smpp_time.py b/tests/test_smpp_time.py new file mode 100644 index 0000000..8ecf98d --- /dev/null +++ b/tests/test_smpp_time.py @@ -0,0 +1,105 @@ +""" +Copyright 2009-2010 Mozes, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import unittest +import sys + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") +from smpp.pdu import smpp_time +from datetime import datetime, timedelta + +class SMPPTimeTest(unittest.TestCase): + + def test_parse_t(self): + self.assertEqual(0, smpp_time.parse_t('0')) + self.assertEqual('0', smpp_time.unparse_t(0)) + self.assertEqual(9, smpp_time.parse_t('9')) + self.assertEqual('9', smpp_time.unparse_t(9)) + self.assertRaises(ValueError, smpp_time.parse_t, 'a') + self.assertRaises(ValueError, smpp_time.parse_t, '03') + + def test_parse_nn(self): + self.assertEqual(0, smpp_time.parse_nn('00')) + self.assertEqual('00', smpp_time.unparse_nn(0)) + self.assertEqual(48, smpp_time.parse_nn('48')) + self.assertEqual('48', smpp_time.unparse_nn(48)) + self.assertRaises(ValueError, smpp_time.parse_nn, '49') + self.assertRaises(ValueError, smpp_time.parse_nn, '0') + + def test_parse_relative(self): + tstr = b'020610233429000R' + rel = smpp_time.parse(tstr) + self.assertEqual(smpp_time.SMPPRelativeTime, rel.__class__) + self.assertEqual(2, rel.years) + self.assertEqual(6, rel.months) + self.assertEqual(10, rel.days) + self.assertEqual(23, rel.hours) + self.assertEqual(34, rel.minutes) + self.assertEqual(29, rel.seconds) + self.assertEqual(tstr, smpp_time.unparse(rel)) + + def test_parse_relative_mins_only(self): + tstr = b'000000001000000R' + rel = smpp_time.parse(tstr) + self.assertEqual(smpp_time.SMPPRelativeTime, rel.__class__) + self.assertEqual(0, rel.years) + self.assertEqual(0, rel.months) + self.assertEqual(0, rel.days) + self.assertEqual(0, rel.hours) + self.assertEqual(10, rel.minutes) + self.assertEqual(0, rel.seconds) + self.assertEqual(tstr, smpp_time.unparse(rel)) + + def test_parse_absolute_no_offset(self): + tstr = b'070927233429800+' + dt = smpp_time.parse(tstr) + self.assertEqual(2007, dt.year) + self.assertEqual(9, dt.month) + self.assertEqual(27, dt.day) + self.assertEqual(23, dt.hour) + self.assertEqual(34, dt.minute) + self.assertEqual(29, dt.second) + self.assertEqual(800000, dt.microsecond) + self.assertEqual(None, dt.tzinfo) + self.assertEqual(tstr, smpp_time.unparse(dt)) + + def test_parse_absolute_positive_offset(self): + tstr = b'070927233429848+' + dt = smpp_time.parse(tstr) + self.assertEqual(2007, dt.year) + self.assertEqual(9, dt.month) + self.assertEqual(27, dt.day) + self.assertEqual(23, dt.hour) + self.assertEqual(34, dt.minute) + self.assertEqual(29, dt.second) + self.assertEqual(800000, dt.microsecond) + self.assertEqual(timedelta(hours=12), dt.tzinfo.utcoffset(None)) + self.assertEqual(tstr, smpp_time.unparse(dt)) + + def test_parse_absolute_negative_offset(self): + tstr = b'070927233429848-' + dt = smpp_time.parse(tstr) + self.assertEqual(2007, dt.year) + self.assertEqual(9, dt.month) + self.assertEqual(27, dt.day) + self.assertEqual(23, dt.hour) + self.assertEqual(34, dt.minute) + self.assertEqual(29, dt.second) + self.assertEqual(800000, dt.microsecond) + self.assertEqual(timedelta(hours=-12), dt.tzinfo.utcoffset(None)) + self.assertEqual(tstr, smpp_time.unparse(dt)) +