aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPau Espin Pedrol <pespin@sysmocom.de>2017-05-30 15:33:57 +0200
committerPau Espin Pedrol <pespin@sysmocom.de>2017-06-18 08:48:09 +0000
commit2d16f6fd2c88844c9e9b5257795cdc85877e6235 (patch)
tree4b18677c253b5ae553cfd0c3747534f437dc72d3
parent640c7760e9cad7e26de685838378293805478413 (diff)
Add support for SMPP testing
As defined in [1], the different related actors are implemented in this commit: ESME and SMSC. SMSC: In Osmocom, the SMSC is currently implemented inside the NITB or the MSC. A new Smsc abstract class is created to shared code between the NITB and the MSC, and also makes it easier for later when the SMSC is splitted. ESMEs can be dynamically added to its configuration in a similar way to how the BTSs are added. ESME: A new class Esme is created which can be used by tests to control an ESME to interact with the SMSC. The ESME functionalities are implemented using python-smpplib. Required version of this library is at least 43cc6f819ec76b2c0a9d36d1d439308634716227, which contains support for python 3 and some required features to poll the socket. This commit already contains a few tests which checks different features and tests the API. Extending tested features or scenarios can be later done quite easily. The tests are not enabled by default right now, because there are several of them in a suite and the ip_address resources are not freed after every tests which ends up in the suite failing due to missing reserved resources. All the tests run alone work though. When the issue is fixed they can then be added to the default list of tests to be run. [1] http://opensmpp.org/specs/SMPP_v3_4_Issue1_2.pdf Change-Id: I14ca3cb009d6d646a449ca99b0200da12085c0da
-rw-r--r--selftest/template_test.ok11
-rwxr-xr-xselftest/template_test.py13
-rw-r--r--selftest/template_test/osmo-nitb.cfg.tmpl18
-rw-r--r--src/osmo_gsm_tester/esme.py138
-rw-r--r--src/osmo_gsm_tester/osmo_msc.py5
-rw-r--r--src/osmo_gsm_tester/osmo_nitb.py5
-rw-r--r--src/osmo_gsm_tester/sms.py18
-rw-r--r--src/osmo_gsm_tester/smsc.py50
-rw-r--r--src/osmo_gsm_tester/suite.py10
-rw-r--r--src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl14
-rw-r--r--src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl14
-rw-r--r--src/osmo_gsm_tester/test.py6
-rwxr-xr-xsuites/aoip_smpp/esme_connect_policy_acceptall.py37
-rwxr-xr-xsuites/aoip_smpp/esme_connect_policy_closed.py57
-rwxr-xr-xsuites/aoip_smpp/esme_ms_sms.py56
-rw-r--r--suites/aoip_smpp/suite.conf10
-rwxr-xr-xsuites/smpp/esme_connect_policy_acceptall.py32
-rwxr-xr-xsuites/smpp/esme_connect_policy_closed.py51
-rwxr-xr-xsuites/smpp/esme_ms_sms.py49
-rw-r--r--suites/smpp/suite.conf10
20 files changed, 574 insertions, 30 deletions
diff --git a/selftest/template_test.ok b/selftest/template_test.ok
index 1267dac..688361f 100644
--- a/selftest/template_test.ok
+++ b/selftest/template_test.ok
@@ -136,10 +136,13 @@ network
phys_chan_config val_phys_chan_config_3
smpp
local-tcp-ip val_ip_address 2775
- system-id test
- policy closed
- esme test
- password test
+ system-id test-nitb
+ policy val_smsc_policy
+ esme val_system_id_esme0
+ password val_password_esme0
+ default-route
+ esme val_system_id_esme1
+ no password
default-route
ctrl
bind val_ip_address
diff --git a/selftest/template_test.py b/selftest/template_test.py
index 45347b6..f8c32a5 100755
--- a/selftest/template_test.py
+++ b/selftest/template_test.py
@@ -35,6 +35,11 @@ mock_bts = {
)
}
+mock_esme = {
+ 'system_id': 'val_system_id',
+ 'password': 'val_password'
+}
+
def clone_mod(d, val_ext):
c = dict(d)
for name in c.keys():
@@ -47,6 +52,10 @@ def clone_mod(d, val_ext):
mock_bts0 = clone_mod(mock_bts, '_bts0')
mock_bts1 = clone_mod(mock_bts, '_bts1')
+mock_esme0 = clone_mod(mock_esme, '_esme0')
+mock_esme1 = clone_mod(mock_esme, '_esme1')
+mock_esme1['password'] = ''
+
vals = dict(nitb=dict(
net=dict(
mcc='val_mcc',
@@ -59,6 +68,10 @@ vals = dict(nitb=dict(
),
ip_address=dict(addr='val_ip_address'),
),
+ smsc=dict(
+ policy='val_smsc_policy',
+ esme_list=(mock_esme0, mock_esme1)
+ ),
)
print(template.render('osmo-nitb.cfg', vals))
diff --git a/selftest/template_test/osmo-nitb.cfg.tmpl b/selftest/template_test/osmo-nitb.cfg.tmpl
index 3404b7f..7a76878 100644
--- a/selftest/template_test/osmo-nitb.cfg.tmpl
+++ b/selftest/template_test/osmo-nitb.cfg.tmpl
@@ -47,12 +47,18 @@ network
timer t3119 0
timer t3141 0
smpp
- local-tcp-ip ${smpp_bind_ip} 2775
- system-id test
- policy closed
- esme test
- password test
- default-route
+ local-tcp-ip ${nitb.ip_address.addr} 2775
+ system-id test-nitb
+ policy ${smsc.policy}
+ %for esme in esme_list:
+ esme ${esme.system_id}
+ % if esme.password == '':
+ no password
+ % else:
+ password ${esme.password}
+ % endif
+ default-route
+ %endfor
ctrl
bind ${ctrl_bind_ip}
%for bts in bts_list:
diff --git a/src/osmo_gsm_tester/esme.py b/src/osmo_gsm_tester/esme.py
new file mode 100644
index 0000000..f92863d
--- /dev/null
+++ b/src/osmo_gsm_tester/esme.py
@@ -0,0 +1,138 @@
+# osmo_gsm_tester: SMPP ESME to talk to SMSC
+#
+# Copyright (C) 2017 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Pau Espin Pedrol <pespin@sysmocom.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import smpplib.gsm
+import smpplib.client
+import smpplib.consts
+import smpplib.exceptions
+
+from . import log, util, event_loop, sms
+
+# if you want to know what's happening inside python-smpplib
+#import logging
+#logging.basicConfig(level='DEBUG')
+
+MAX_SYS_ID_LEN = 16
+MAX_PASSWD_LEN = 16
+
+class Esme(log.Origin):
+ client = None
+ smsc = None
+
+ def __init__(self, msisdn):
+ self.msisdn = msisdn
+ # Get last characters of msisdn to stay inside MAX_SYS_ID_LEN. Similar to modulus operator.
+ self.set_system_id('esme-' + self.msisdn[-11:])
+ super().__init__(log.C_TST, self.system_id)
+ self.set_password('esme-pwd')
+ self.connected = False
+ self.bound = False
+ self.listening = False
+
+ def __del__(self):
+ try:
+ self.disconnect()
+ except smpplib.exceptions.ConnectionError:
+ pass
+
+ def set_smsc(self, smsc):
+ self.smsc = smsc
+
+ def set_system_id(self, name):
+ if len(name) > MAX_SYS_ID_LEN:
+ raise log.Error('Esme system_id too long! %d vs %d', len(name), MAX_SYS_ID_LEN)
+ self.system_id = name
+
+ def set_password(self, password):
+ if len(password) > MAX_PASSWD_LEN:
+ raise log.Error('Esme password too long! %d vs %d', len(password), MAX_PASSWD_LEN)
+ self.password = password
+
+ def conf_for_smsc(self):
+ config = { 'system_id': self.system_id, 'password': self.password }
+ return config
+
+ def poll(self):
+ self.client.poll()
+
+ def start_listening(self):
+ self.listening = True
+ event_loop.register_poll_func(self.poll)
+
+ def stop_listening(self):
+ if not self.listening:
+ return
+ self.listening = False
+ # Empty the queue before processing the unbind + disconnect PDUs
+ event_loop.unregister_poll_func(self.poll)
+ self.poll()
+
+ def connect(self):
+ host, port = self.smsc.addr_port
+ if self.client:
+ self.disconnect()
+ self.client = smpplib.client.Client(host, port, timeout=None)
+ self.client.set_message_sent_handler(
+ lambda pdu: self.dbg('message sent:', repr(pdu)) )
+ self.client.set_message_received_handler(
+ lambda pdu: self.dbg('message received:', repr(pdu)) )
+ self.client.connect()
+ self.connected = True
+ self.client.bind_transceiver(system_id=self.system_id, password=self.password)
+ self.bound = True
+ self.log('Connected and bound successfully. Starting to listen')
+ self.start_listening()
+
+ def disconnect(self):
+ self.stop_listening()
+ if self.bound:
+ self.client.unbind()
+ self.bound = False
+ if self.connected:
+ self.client.disconnect()
+ self.connected = False
+
+ def run_method_expect_failure(self, errcode, method, *args):
+ try:
+ method(*args)
+ #it should not succeed, raise an exception:
+ raise log.Error('SMPP Failure: %s should have failed with SMPP error %d (%s) but succeeded.' % (method, errcode, smpplib.consts.DESCRIPTIONS[errcode]))
+ except smpplib.exceptions.PDUError as e:
+ if e.args[1] != errcode:
+ raise e
+
+ def sms_send(self, sms_obj):
+ parts, encoding_flag, msg_type_flag = smpplib.gsm.make_parts(str(sms_obj))
+
+ self.log('Sending SMS "%s" to %s' % (str(sms_obj), sms_obj.dst_msisdn()))
+ for part in parts:
+ pdu = self.client.send_message(
+ source_addr_ton=smpplib.consts.SMPP_TON_INTL,
+ source_addr_npi=smpplib.consts.SMPP_NPI_ISDN,
+ source_addr=sms_obj.src_msisdn(),
+ dest_addr_ton=smpplib.consts.SMPP_TON_INTL,
+ dest_addr_npi=smpplib.consts.SMPP_NPI_ISDN,
+ destination_addr=sms_obj.dst_msisdn(),
+ short_message=part,
+ data_coding=encoding_flag,
+ esm_class=smpplib.consts.SMPP_MSGMODE_FORWARD,
+ registered_delivery=False,
+ )
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/osmo_msc.py b/src/osmo_gsm_tester/osmo_msc.py
index 063b477..2c9b1e3 100644
--- a/src/osmo_gsm_tester/osmo_msc.py
+++ b/src/osmo_gsm_tester/osmo_msc.py
@@ -20,7 +20,7 @@
import os
import pprint
-from . import log, util, config, template, process, osmo_ctrl, pcap_recorder
+from . import log, util, config, template, process, osmo_ctrl, pcap_recorder, smsc
class OsmoMsc(log.Origin):
suite_run = None
@@ -30,6 +30,7 @@ class OsmoMsc(log.Origin):
process = None
hlr = None
config = None
+ smsc = None
def __init__(self, suite_run, hlr, mgcpgw, ip_address):
super().__init__(log.C_RUN, 'osmo-msc_%s' % ip_address.get('addr'))
@@ -37,6 +38,7 @@ class OsmoMsc(log.Origin):
self.ip_address = ip_address
self.hlr = hlr
self.mgcpgw = mgcpgw
+ self.smsc = smsc.Smsc((ip_address.get('addr'), 2775))
def start(self):
self.log('Starting osmo-msc')
@@ -73,6 +75,7 @@ class OsmoMsc(log.Origin):
config.overlay(values, dict(msc=dict(ip_address=self.ip_address)))
config.overlay(values, self.mgcpgw.conf_for_msc())
config.overlay(values, self.hlr.conf_for_msc())
+ config.overlay(values, self.smsc.get_config())
self.config = values
self.dbg('MSC CONFIG:\n' + pprint.pformat(values))
diff --git a/src/osmo_gsm_tester/osmo_nitb.py b/src/osmo_gsm_tester/osmo_nitb.py
index 484358e..3ef5276 100644
--- a/src/osmo_gsm_tester/osmo_nitb.py
+++ b/src/osmo_gsm_tester/osmo_nitb.py
@@ -21,7 +21,7 @@ import os
import re
import pprint
-from . import log, util, config, template, process, osmo_ctrl, pcap_recorder
+from . import log, util, config, template, process, osmo_ctrl, pcap_recorder, smsc
class OsmoNitb(log.Origin):
suite_run = None
@@ -30,12 +30,14 @@ class OsmoNitb(log.Origin):
config_file = None
process = None
bts = None
+ smsc = None
def __init__(self, suite_run, ip_address):
super().__init__(log.C_RUN, 'osmo-nitb_%s' % ip_address.get('addr'))
self.suite_run = suite_run
self.ip_address = ip_address
self.bts = []
+ self.smsc = smsc.Smsc((ip_address.get('addr'), 2775))
def start(self):
self.log('Starting osmo-nitb')
@@ -75,6 +77,7 @@ class OsmoNitb(log.Origin):
for bts in self.bts:
bts_list.append(bts.conf_for_bsc())
config.overlay(values, dict(nitb=dict(net=dict(bts_list=bts_list))))
+ config.overlay(values, self.smsc.get_config())
self.config = values
self.dbg('NITB CONFIG:\n' + pprint.pformat(values))
diff --git a/src/osmo_gsm_tester/sms.py b/src/osmo_gsm_tester/sms.py
index 570ef96..e264b66 100644
--- a/src/osmo_gsm_tester/sms.py
+++ b/src/osmo_gsm_tester/sms.py
@@ -21,14 +21,16 @@ class Sms:
_last_sms_idx = 0
msg = None
- def __init__(self, from_msisdn=None, to_msisdn=None, *tokens):
+ def __init__(self, src_msisdn=None, dst_msisdn=None, *tokens):
Sms._last_sms_idx += 1
+ self._src_msisdn = src_msisdn
+ self._dst_msisdn = dst_msisdn
msgs = ['message nr. %d' % Sms._last_sms_idx]
msgs.extend(tokens)
- if from_msisdn:
- msgs.append('from %s' % from_msisdn)
- if to_msisdn:
- msgs.append('to %s' % to_msisdn)
+ if src_msisdn:
+ msgs.append('from %s' % src_msisdn)
+ if dst_msisdn:
+ msgs.append('to %s' % dst_msisdn)
self.msg = ', '.join(msgs)
def __str__(self):
@@ -42,6 +44,12 @@ class Sms:
return self.msg == other.msg
return self.msg == other
+ def src_msisdn(self):
+ return self._src_msisdn
+
+ def dst_msisdn(self):
+ return self._dst_msisdn
+
def matches(self, msg):
return self.msg == msg
diff --git a/src/osmo_gsm_tester/smsc.py b/src/osmo_gsm_tester/smsc.py
new file mode 100644
index 0000000..4837f37
--- /dev/null
+++ b/src/osmo_gsm_tester/smsc.py
@@ -0,0 +1,50 @@
+# osmo_gsm_tester: smsc interface
+#
+# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
+#
+# Author: Pau Espin Pedrol <pespin@sysmocom.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from . import log, config, util, template, process
+
+class Smsc:
+ esmes = None
+
+ SMSC_POLICY_CLOSED = 'closed'
+ SMSC_POLICY_ACCEPT_ALL = 'accept-all'
+
+ def __init__(self, smpp_addr_port):
+ self.addr_port = smpp_addr_port
+ self.policy = self.SMSC_POLICY_CLOSED
+ self.esmes = []
+
+ def get_config(self):
+ values = { 'smsc': { 'policy': self.policy } }
+ esme_list = []
+ for esme in self.esmes:
+ esme_list.append(esme.conf_for_smsc())
+ config.overlay(values, dict(smsc=dict(esme_list=esme_list)))
+ return values
+
+ def esme_add(self, esme):
+ if esme.system_id == '':
+ raise log.Error('esme system_id cannot be empty')
+ self.esmes.append(esme)
+ esme.set_smsc(self)
+
+ def set_smsc_policy(self, smsc_policy):
+ self.policy = smsc_policy
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index f4b9260..71b8dc7 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -23,7 +23,7 @@ import time
import copy
import traceback
import pprint
-from . import config, log, template, util, resource, schema, ofono_client, event_loop
+from . import config, log, template, util, resource, schema, ofono_client, event_loop, esme, sms
from . import osmo_nitb
from . import osmo_hlr, osmo_mgcpgw, osmo_msc, osmo_bsc, osmo_stp
from . import test
@@ -99,7 +99,7 @@ class Test(log.Origin):
log.large_separator(self.suite_run.trial.name(), self.suite_run.name(), self.name(), sublevel=3)
self.status = Test.UNKNOWN
self.start_timestamp = time.time()
- test.setup(self.suite_run, self, ofono_client, sys.modules[__name__], event_loop)
+ test.setup(self.suite_run, self, ofono_client, sys.modules[__name__], event_loop, sms)
with self.redirect_stdout():
util.run_python_file('%s.%s' % (self.suite_run.definition.name(), self.basename),
self.path)
@@ -363,8 +363,12 @@ class SuiteRun(log.Origin):
l.append(self.modem())
return l
+ def esme(self):
+ esme_obj = esme.Esme(self.msisdn())
+ return esme_obj
+
def msisdn(self):
- msisdn = self.resources_pool.next_msisdn(self.origin)
+ msisdn = self.resources_pool.next_msisdn(self)
self.log('using MSISDN', msisdn)
return msisdn
diff --git a/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl
index 247365e..89982e0 100644
--- a/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-msc.cfg.tmpl
@@ -23,10 +23,16 @@ ctrl
bind ${msc.ip_address.addr}
smpp
local-tcp-ip ${msc.ip_address.addr} 2775
- system-id test
- policy closed
- esme test
- password test
+ system-id test-msc
+ policy ${smsc.policy}
+%for esme in smsc.esme_list:
+ esme ${esme.system_id}
+% if esme.password == '':
+ no password
+% else:
+ password ${esme.password}
+% endif
default-route
+%endfor
hlr
remote-ip ${hlr.ip_address.addr}
diff --git a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
index a47ac02..23cc225 100644
--- a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
@@ -76,10 +76,16 @@ network
%endfor
smpp
local-tcp-ip ${nitb.ip_address.addr} 2775
- system-id test
- policy closed
- esme test
- password test
+ system-id test-nitb
+ policy ${smsc.policy}
+%for esme in smsc.esme_list:
+ esme ${esme.system_id}
+% if esme.password == '':
+ no password
+% else:
+ password ${esme.password}
+% endif
default-route
+%endfor
ctrl
bind ${nitb.ip_address.addr}
diff --git a/src/osmo_gsm_tester/test.py b/src/osmo_gsm_tester/test.py
index 2958501..49911b3 100644
--- a/src/osmo_gsm_tester/test.py
+++ b/src/osmo_gsm_tester/test.py
@@ -33,9 +33,10 @@ sleep = None
poll = None
prompt = None
Timeout = None
+Sms = None
-def setup(suite_run, _test, ofono_client, suite_module, event_module):
- global trial, suite, test, resources, log, dbg, err, wait, wait_no_raise, sleep, poll, prompt, Timeout
+def setup(suite_run, _test, ofono_client, suite_module, event_module, sms_module):
+ global trial, suite, test, resources, log, dbg, err, wait, wait_no_raise, sleep, poll, prompt, Timeout, Sms
trial = suite_run.trial
suite = suite_run
test = _test
@@ -49,5 +50,6 @@ def setup(suite_run, _test, ofono_client, suite_module, event_module):
poll = event_module.poll
prompt = suite_run.prompt
Timeout = suite_module.Timeout
+ Sms = sms_module.Sms
# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/suites/aoip_smpp/esme_connect_policy_acceptall.py b/suites/aoip_smpp/esme_connect_policy_acceptall.py
new file mode 100755
index 0000000..2a954d5
--- /dev/null
+++ b/suites/aoip_smpp/esme_connect_policy_acceptall.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'accept-all' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) which do not appear on
+# the config file
+
+from osmo_gsm_tester.test import *
+
+hlr = suite.hlr()
+bts = suite.bts() # bts not started, only needed for mgcpgw
+mgcpgw = suite.mgcpgw(bts_ip=bts.remote_addr())
+msc = suite.msc(hlr, mgcpgw)
+smsc = msc.smsc
+esme = suite.esme()
+
+# Here we deliberately omit calling smsc.esme_add() to avoid having it included
+# in the smsc config.
+smsc.set_smsc_policy(smsc.SMSC_POLICY_ACCEPT_ALL)
+esme.set_smsc(smsc)
+
+hlr.start()
+msc.start()
+mgcpgw.start()
+
+# Due to accept-all policy, connect() should work even if we didn't previously
+# configure the esme in the smsc, no matter the system_id / password we use.
+log('Test connect with non-empty values in system_id and password')
+esme.set_system_id('foo')
+esme.set_password('bar')
+esme.connect()
+esme.disconnect()
+
+log('Test connect with empty values in system_id and password')
+esme.set_system_id('')
+esme.set_password('')
+esme.connect()
+esme.disconnect()
diff --git a/suites/aoip_smpp/esme_connect_policy_closed.py b/suites/aoip_smpp/esme_connect_policy_closed.py
new file mode 100755
index 0000000..29b25d1
--- /dev/null
+++ b/suites/aoip_smpp/esme_connect_policy_closed.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'closed' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * SMPP interface of SMSC rejects ESMEs with known system id but wrong password.
+# * SMPP interface of SMSC rejects ESEMs with unknown system id
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVPASWD = 0x0000000E
+SMPP_ESME_RINVSYSID = 0x0000000F
+
+hlr = suite.hlr()
+bts = suite.bts()
+mgcpgw = suite.mgcpgw(bts_ip=bts.remote_addr())
+msc = suite.msc(hlr, mgcpgw)
+smsc = msc.smsc
+
+esme = suite.esme()
+esme_no_pwd = suite.esme()
+esme_no_pwd.set_password('')
+
+smsc.set_smsc_policy(smsc.SMSC_POLICY_CLOSED)
+smsc.esme_add(esme)
+smsc.esme_add(esme_no_pwd)
+
+hlr.start()
+msc.start()
+mgcpgw.start()
+
+log('Test with correct credentials (no password)')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials (no password, non empty)')
+esme_no_pwd.set_password('foobar')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials')
+esme.connect()
+esme.disconnect()
+
+log('Test with bad password, checking for failure')
+correct_password = esme.password
+new_password = 'barfoo' if correct_password == 'foobar' else 'foobar'
+esme.set_password(new_password)
+esme.run_method_expect_failure(SMPP_ESME_RINVPASWD, esme.connect)
+esme.set_password(correct_password)
+
+log('Test with bad system_id, checking for failure')
+correct_system_id = esme.system_id
+new_system_id = 'barfoo' if correct_system_id == 'foobar' else 'foobar'
+esme.set_system_id(new_system_id)
+esme.run_method_expect_failure(SMPP_ESME_RINVSYSID, esme.connect)
+esme.set_system_id(correct_system_id)
diff --git a/suites/aoip_smpp/esme_ms_sms.py b/suites/aoip_smpp/esme_ms_sms.py
new file mode 100755
index 0000000..7f9ef18
--- /dev/null
+++ b/suites/aoip_smpp/esme_ms_sms.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * ESME can send an SMS to an already registered MS when SMSC is in 'forward' mode.
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVDSTADR = 0x0000000B
+
+hlr = suite.hlr()
+bts = suite.bts()
+mgcpgw = suite.mgcpgw(bts_ip=bts.remote_addr())
+msc = suite.msc(hlr, mgcpgw)
+bsc = suite.bsc(msc)
+stp = suite.stp()
+bsc.bts_add(bts)
+
+ms = suite.modem()
+esme = suite.esme()
+msc.smsc.esme_add(esme)
+
+hlr.start()
+stp.start()
+msc.start()
+mgcpgw.start()
+bsc.start()
+bts.start()
+
+esme.connect()
+hlr.subscriber_add(ms)
+ms.connect(msc.mcc_mnc())
+
+ms.log_info()
+print('waiting for modem to attach...')
+wait(ms.is_connected, msc.mcc_mnc())
+wait(msc.subscriber_attached, ms)
+
+print('sending first sms...')
+msg = Sms(esme.msisdn, ms.msisdn, 'smpp send message')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+print('sending second sms (unicode chars not in gsm aplhabet)...')
+msg = Sms(esme.msisdn, ms.msisdn, 'chars:[кизаçйж]')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+# FIXME: This test is not failing with error but succeeds, need to check why: (forward vs store policy?)
+# wrong_msisdn = ms.msisdn + esme.msisdn
+# print('sending third sms (with wrong msisdn %s)' % wrong_msisdn)
+# msg = Sms(esme.msisdn, wrong_msisdn, 'smpp message with wrong dest')
+# esme.run_method_expect_failure(SMPP_ESME_RINVDSTADR, esme.sms_send, msg)
+
+esme.disconnect()
diff --git a/suites/aoip_smpp/suite.conf b/suites/aoip_smpp/suite.conf
new file mode 100644
index 0000000..46f8d09
--- /dev/null
+++ b/suites/aoip_smpp/suite.conf
@@ -0,0 +1,10 @@
+resources:
+ ip_address:
+ - times: 5 # msc, bsc, hlr, stp, mgw
+ bts:
+ - times: 1
+ modem:
+ - times: 1
+
+defaults:
+ timeout: 60s
diff --git a/suites/smpp/esme_connect_policy_acceptall.py b/suites/smpp/esme_connect_policy_acceptall.py
new file mode 100755
index 0000000..d22703d
--- /dev/null
+++ b/suites/smpp/esme_connect_policy_acceptall.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'accept-all' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) which do not appear on
+# the config file
+
+from osmo_gsm_tester.test import *
+
+nitb = suite.nitb()
+smsc = nitb.smsc
+esme = suite.esme()
+
+# Here we deliberately omit calling smsc.esme_add() to avoid having it included
+# in the smsc config.
+smsc.set_smsc_policy(smsc.SMSC_POLICY_ACCEPT_ALL)
+esme.set_smsc(smsc)
+
+nitb.start()
+
+# Due to accept-all policy, connect() should work even if we didn't previously
+# configure the esme in the smsc, no matter the system_id / password we use.
+log('Test connect with non-empty values in system_id and password')
+esme.set_system_id('foo')
+esme.set_password('bar')
+esme.connect()
+esme.disconnect()
+
+log('Test connect with empty values in system_id and password')
+esme.set_system_id('')
+esme.set_password('')
+esme.connect()
+esme.disconnect()
diff --git a/suites/smpp/esme_connect_policy_closed.py b/suites/smpp/esme_connect_policy_closed.py
new file mode 100755
index 0000000..7fac276
--- /dev/null
+++ b/suites/smpp/esme_connect_policy_closed.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases while in 'closed' policy:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * SMPP interface of SMSC rejects ESMEs with known system id but wrong password.
+# * SMPP interface of SMSC rejects ESEMs with unknown system id
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVPASWD = 0x0000000E
+SMPP_ESME_RINVSYSID = 0x0000000F
+
+nitb = suite.nitb()
+smsc = nitb.smsc
+esme = suite.esme()
+esme_no_pwd = suite.esme()
+esme_no_pwd.set_password('')
+
+smsc.set_smsc_policy(smsc.SMSC_POLICY_CLOSED)
+smsc.esme_add(esme)
+smsc.esme_add(esme_no_pwd)
+
+nitb.start()
+
+log('Test with correct credentials (no password)')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials (no password, non empty)')
+esme_no_pwd.set_password('foobar')
+esme_no_pwd.connect()
+esme_no_pwd.disconnect()
+
+log('Test with correct credentials')
+esme.connect()
+esme.disconnect()
+
+log('Test with bad password, checking for failure')
+correct_password = esme.password
+new_password = 'barfoo' if correct_password == 'foobar' else 'foobar'
+esme.set_password(new_password)
+esme.run_method_expect_failure(SMPP_ESME_RINVPASWD, esme.connect)
+esme.set_password(correct_password)
+
+log('Test with bad system_id, checking for failure')
+correct_system_id = esme.system_id
+new_system_id = 'barfoo' if correct_system_id == 'foobar' else 'foobar'
+esme.set_system_id(new_system_id)
+esme.run_method_expect_failure(SMPP_ESME_RINVSYSID, esme.connect)
+esme.set_system_id(correct_system_id)
diff --git a/suites/smpp/esme_ms_sms.py b/suites/smpp/esme_ms_sms.py
new file mode 100755
index 0000000..bc9d7d4
--- /dev/null
+++ b/suites/smpp/esme_ms_sms.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+# This test checks following use-cases:
+# * SMPP interface of SMSC accepts SMPP clients (ESMEs) with password previously
+# defined in its configuration file.
+# * ESME can send an SMS to an already registered MS when SMSC is in 'forward' mode.
+
+from osmo_gsm_tester.test import *
+
+SMPP_ESME_RINVDSTADR = 0x0000000B
+
+nitb = suite.nitb()
+bts = suite.bts()
+ms = suite.modem()
+esme = suite.esme()
+
+print('start nitb and bts...')
+nitb.bts_add(bts)
+nitb.smsc.esme_add(esme)
+nitb.start()
+bts.start()
+
+esme.connect()
+nitb.subscriber_add(ms)
+ms.connect(nitb.mcc_mnc())
+
+ms.log_info()
+print('waiting for modem to attach...')
+wait(ms.is_connected, nitb.mcc_mnc())
+wait(nitb.subscriber_attached, ms)
+
+print('sending first sms...')
+msg = Sms(esme.msisdn, ms.msisdn, 'smpp send message')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+print('sending second sms (unicode chars not in gsm aplhabet)...')
+msg = Sms(esme.msisdn, ms.msisdn, 'chars:[кизаçйж]')
+esme.sms_send(msg)
+wait(ms.sms_was_received, msg)
+
+
+# FIXME: This test is not failing with error but succeeds, need to check why: (forward vs store policy?)
+# wrong_msisdn = ms.msisdn + esme.msisdn
+# print('sending third sms (with wrong msisdn %s)' % wrong_msisdn)
+# msg = Sms(esme.msisdn, wrong_msisdn, 'smpp message with wrong dest')
+# esme.run_method_expect_failure(SMPP_ESME_RINVDSTADR, esme.sms_send, msg)
+
+esme.disconnect()
diff --git a/suites/smpp/suite.conf b/suites/smpp/suite.conf
new file mode 100644
index 0000000..eb59abb
--- /dev/null
+++ b/suites/smpp/suite.conf
@@ -0,0 +1,10 @@
+resources:
+ ip_address:
+ - times: 1
+ bts:
+ - times: 1
+ modem:
+ - times: 1
+
+defaults:
+ timeout: 60s