[Asterisk-code-review] Add tests to verify pidf+xml & xpidf+xml NOTIFY bodies. (testsuite[master])

Matt Jordan asteriskteam at digium.com
Wed Aug 19 08:45:06 CDT 2015


Matt Jordan has submitted this change and it was merged.

Change subject: Add tests to verify pidf+xml & xpidf+xml NOTIFY bodies.
......................................................................


Add tests to verify pidf+xml & xpidf+xml NOTIFY bodies.

This adds tests to verify pidf+xml & xpidf+xml NOTIFY bodies in PJSIP. The
tests set different custom states and verifies that the NOTIFY bodies match
what is expected. The tests SHARE a test module to listen for SIP packets and
to verify bodies.

The pcap.py library has been modified so XPIDF bodies are identified.

AFS-129

Change-Id: Ib852d1d42ea391fe4e7d1abf0eb7ea6eabb62cba
---
M lib/python/asterisk/pcap.py
M tests/channels/pjsip/subscriptions/presence/tests.yaml
A tests/channels/pjsip/subscriptions/presence/verify_bodies/presence.py
A tests/channels/pjsip/subscriptions/presence/verify_bodies/tests.yaml
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/extensions.conf
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/pjsip.conf
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/sipp/subscribe.xml
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/test-config.yaml
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/extensions.conf
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/pjsip.conf
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/sipp/subscribe.xml
A tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/test-config.yaml
12 files changed, 1,288 insertions(+), 0 deletions(-)

Approvals:
  Mark Michelson: Looks good to me, but someone else must approve
  Matt Jordan: Looks good to me, approved; Verified



diff --git a/lib/python/asterisk/pcap.py b/lib/python/asterisk/pcap.py
index 2db2b9c..e0065b3 100644
--- a/lib/python/asterisk/pcap.py
+++ b/lib/python/asterisk/pcap.py
@@ -225,6 +225,15 @@
         self.content_id = content_id
 
 
+class XPIDFPacket(Packet):
+    '''A XPIDF presence body. Owned by SIPPacket or a MultipartPacket.'''
+
+    def __init__(self, ascii_packet, raw_packet, content_id):
+        Packet.__init__(self, packet_type="XPIDF", raw_packet=raw_packet)
+        self.xml = ascii_packet.strip()
+        self.content_id = content_id
+
+
 class MWIPacket(Packet):
     '''An MWI body. Owned by SIPPacket or a MultipartPacket.'''
 
@@ -309,6 +318,8 @@
             return RLMIPacket(ascii_packet, raw_packet)
         elif (body_type == 'application/pidf+xml'):
             return PIDFPacket(ascii_packet, raw_packet, content_id)
+        elif (body_type == 'application/xpidf+xml'):
+            return XPIDFPacket(ascii_packet, raw_packet, content_id)
         elif (body_type == 'application/simple-message-summary'):
             return MWIPacket(ascii_packet, raw_packet, content_id)
         else:
diff --git a/tests/channels/pjsip/subscriptions/presence/tests.yaml b/tests/channels/pjsip/subscriptions/presence/tests.yaml
index 9123536..96684a6 100644
--- a/tests/channels/pjsip/subscriptions/presence/tests.yaml
+++ b/tests/channels/pjsip/subscriptions/presence/tests.yaml
@@ -10,3 +10,4 @@
     - test: 'presencestate_repeat'
     - test: 'presencestate_repeat_okay'
     - test: 'dialog_info_xml'
+    - dir: 'verify_bodies'
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/presence.py b/tests/channels/pjsip/subscriptions/presence/verify_bodies/presence.py
new file mode 100644
index 0000000..143ea42
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/presence.py
@@ -0,0 +1,421 @@
+#!/usr/bin/env python
+"""Pluggable module for tests that verify NOTIFY bodies.
+
+Copyright (C) 2015, Digium, Inc.
+John Bigelow <jbigelow at digium.com>
+
+This program is free software, distributed under the terms of
+the GNU General Public License Version 2.
+"""
+
+import sys
+import logging
+import xml.etree.ElementTree as ET
+import re
+
+sys.path.append('lib/python')
+
+from pcap import VOIPListener
+from twisted.internet import reactor
+
+LOGGER = logging.getLogger(__name__)
+
+
+class BodyCheck(VOIPListener):
+    """SIP notify listener and expected results generator.
+
+    A test module that observes incoming SIP notifies and generates the
+    expected results for the body of each.
+    """
+    def __init__(self, module_config, test_object):
+        """Constructor
+
+        Arguments:
+        module_config Dictionary containing test configuration
+        test_object The test object for the running test.
+        """
+        self.set_pcap_defaults(module_config)
+        VOIPListener.__init__(self, module_config, test_object)
+
+        self.test_object = test_object
+        self.token = test_object.create_fail_token("Haven't handled all "
+                                                   "expected NOTIFY packets.")
+        self.expected_config = module_config['expected_body']
+        self.expected_notifies = int(module_config['expected_notifies'])
+        self.body_type = module_config['expected_body_type']
+        self.notify_count = 0
+
+        if self.body_type.upper() not in ('PIDF', 'XPIDF'):
+            msg = "Body type of '{0}' not supported."
+            raise Exception(msg.format(self.body_type))
+
+        if self.expected_config.get('namespaces') is not None:
+            if self.expected_config['namespaces'].get('default') is None:
+                msg = "Namespaces configuration does not include a 'default'."
+                raise Exception(msg)
+
+        # Add calback for SIP packets
+        self.add_callback('SIP', self.packet_handler)
+
+    def gen_expected_data(self):
+        """Generate expected data results.
+
+        Generates a single dictionary containing the expected results for a
+        body.
+
+        Returns:
+        Dictionary of expected results.
+        """
+        expected_data = {}
+        # Use full tags if we have namespaces.
+        if self.expected_config.get('namespaces') is not None:
+            full_tags = self.gen_full_tags()
+        else:
+            full_tags = self.expected_config['tags']
+
+        # Get expected attributes corresponding to the notify body received.
+        attribs = self.expected_config['attributes'][self.notify_count - 1]
+
+        text = self.expected_config.get('text')
+        # Get expected text corresponding to the notify body received.
+        if text is not None:
+            text = text[self.notify_count - 1]
+
+        # Build dict of the expected results
+        for full_tag in full_tags:
+            expected_data[full_tag] = {}
+            for tag in attribs.keys():
+                if tag not in full_tag:
+                    continue
+                expected_data[full_tag]['attribs'] = attribs[tag]
+            try:
+                for tag in text.keys():
+                    if tag not in full_tag:
+                        continue
+                    expected_data[full_tag]['text'] = text[tag]
+            except AttributeError:
+                pass
+
+        return expected_data
+
+    def gen_full_tags(self):
+        """Generate fully qualified element tags.
+
+        This generates fully qualified element tags by prefixing the tag name
+        with it's corresponding namespace that is enclosed in curly braces.
+        This is so our expected tags will properly match ElementTree tags.
+
+        The format for an Element tag is: {<namespace>}<tag name>
+
+        Returns:
+        List of full tag names.
+        """
+        full_tags = []
+        namespaces = self.expected_config['namespaces']
+
+        for tag in self.expected_config['tags']:
+            try:
+                prefix, tag = tag.split(':')
+                namespace = '{' + namespaces[prefix] + '}'
+            except ValueError:
+                namespace = '{' + namespaces['default'] + '}'
+            except KeyError as keyerr:
+                msg = "Key {0} not found in namespace configuration for tag."
+                raise Exception(msg.format(keyerr))
+
+            full_tags.append("{0}{1}".format(namespace, tag))
+
+        return full_tags
+
+    def set_pcap_defaults(self, module_config):
+        """Set default PcapListener config that isn't explicitly overridden.
+
+        Arguments:
+        module_config Dict of module configuration
+        """
+        pcap_defaults = {'device': 'lo', 'snaplen': 2000,
+                         'bpf-filter': 'udp port 5061', 'debug-packets': False,
+                         'buffer-size': 4194304, 'register-observer': True}
+        for name, value in pcap_defaults.items():
+            module_config[name] = module_config.get(name, value)
+
+    def packet_handler(self, packet):
+        """Handle incoming SIP packets and verify contents.
+
+        Check to see if a packet is a NOTIFY packet with the expected body
+        type. If so then verify the body in the packet against the expected
+        results.
+
+        Arguments:
+        packet Incoming SIP Packet
+        """
+
+        LOGGER.debug('Received SIP packet')
+
+        if 'NOTIFY' not in packet.request_line:
+            LOGGER.debug('Ignoring packet, not a NOTIFY.')
+            return
+
+        if packet.body.packet_type != self.body_type.upper():
+            msg = "Ignoring packet, NOTIFY does not contain a '{0}' body type."
+            LOGGER.warn(msg.format(self.body_type.upper()))
+            return
+
+        self.notify_count += 1
+
+        # Generate dict of expected results for this notify body and validate
+        # the body using it.
+        expected = self.gen_expected_data()
+        validator = Validator(self.test_object, packet, expected)
+        if not validator.verify_body():
+            LOGGER.error('Body validation failed.')
+            return
+
+        info_msg = "Body #{0} validated successfully."
+        LOGGER.info(info_msg.format(self.notify_count))
+
+        if self.notify_count == self.expected_notifies:
+            self.test_object.remove_fail_token(self.token)
+            self.test_object.set_passed(True)
+            self.test_object.stop_reactor()
+
+
+class Validator(object):
+    """Validate a PIDF/XPIDF body against a set of expected data."""
+    def __init__(self, test_object, packet, expected_data):
+        """Constructor
+
+        Arguments:
+        test_object The test object for the running test.
+        packet A packet containing a SIP NOTIFY with a pidf or xpidf body.
+        """
+        super(Validator, self).__init__()
+        self.test_object = test_object
+        self.packet = packet
+        self.body_types = ('PIDF', 'XPIDF')
+        self.expected_data = expected_data
+
+    def verify_body(self):
+        """Verify a PIDF/XPIDF body.
+
+        This uses XML ElementTree to parse the PIDF/XPIDF body. It verifies
+        that the XML is not malformed and verifies the elements match what is
+        expected. This will fail the test and stop the reactor if the body type
+        is not recognized or if the body could not be parsed.
+
+        Returns:
+        True if body type is supported, body is successfully parsed, and body
+        matches what is expected. False otherwise.
+        """
+        if self.packet.body.packet_type not in self.body_types:
+            msg = "Unrecognized body type of '{0}'"
+            self.fail_test(msg.format(self.packet.body.packet_type))
+            return False
+
+        # Attempt to parse the body
+        try:
+            root = ET.fromstring(self.packet.body.xml)
+        except Exception as ex:
+            self.fail_test("Exception when parsing body XML: %s" % ex)
+            return False
+
+        # Verify top-level elements and their children
+        for element in root.findall('.'):
+            if not self.verify_element(element):
+                return False
+
+        return True
+
+    def verify_element(self, element):
+        """Verify the element matches what is expected.
+
+        This verifies the tag, attributes, text, and extra text of an element.
+        If child elements are found this will call back into itself to verify
+        them.
+
+        Arguments:
+        element Element object.
+
+        Returns:
+        True if the element matches what is expected. False otherwise.
+        """
+        # Verify tag, attributes, text, and extra text of the element.
+        if not self.verify_tag(element):
+            return False
+        if not self.verify_attributes(element):
+            return False
+        if not self.verify_text(element):
+            return False
+        if not self.verify_extra_text(element):
+            return False
+
+        # Find child elements
+        children = element.findall('*')
+        if not children:
+            return True
+
+        # Verify child elements.
+        for child in children:
+            if not self.verify_element(child):
+                return False
+
+        return True
+
+    def verify_tag(self, element):
+        """Verify element tag is expected.
+
+        This will fail the test and stop the reactor if the element tag is not
+        expected.
+
+        Arguments:
+        element Element object.
+
+        Returns:
+        True if element tag is in expected tags. False otherwise.
+        """
+        LOGGER.debug("Checking tag: '{0}'".format(element.tag))
+        if element.tag in self.expected_data.keys():
+            return True
+
+        self.fail_test("Unexpected tag: '{0}'.".format(element.tag))
+
+        return False
+
+    def verify_attributes(self, element):
+        """Verify element attributes.
+
+        Ensure the element contains only the attributes that are expected and
+        the attribute values match what are expected. This will fail the test
+        and stop the reactor if conditions are not met.
+
+        Arguments:
+        element Element object.
+
+        Returns:
+        True if attributes not expected and none found, expected attribute
+        values match found attribute values. Otherwise False.
+        """
+        expected = self.expected_data[element.tag].get('attribs')
+        LOGGER.debug("Checking attributes.")
+
+        # If attributes are not expected and none are in the element then
+        # there's nothing more to do.
+        if not element.keys() and expected is None:
+            msg = "Attributes not expected and none found."
+            LOGGER.debug(msg.format())
+            return True
+
+        # Check if we expect attributes but element doesn't have any.
+        if not element.keys() and expected is not None:
+            msg = "Expected attributes not found: {0}"
+            self.fail_test(msg.format(', '.join(expected.keys())))
+            return False
+
+        # Check if we don't expect attributes but element has some.
+        if element.keys() and expected is None:
+            msg = "Unexpected attributes found: {0}"
+            self.fail_test(msg.format(', '.join(element.keys())))
+            return False
+
+        # Ensure all expected attributes exist in the element.
+        not_found = [ex for ex in expected.keys() if ex not in element.keys()]
+        if not_found:
+            msg = "Expected attributes not found in element: {0}"
+            self.fail_test(msg.format(', '.join(not_found)))
+            return False
+
+        for xml_attrib in element.keys():
+            LOGGER.debug("Checking attribute: '{0}'".format(xml_attrib))
+            # Check if we don't expect attributes this particular attribute for
+            # this element.
+            if expected.get(xml_attrib) is None:
+                msg = "Unexpected attribute found: '{0}'"
+                self.fail_test(msg.format(xml_attrib))
+                return False
+
+            if not re.match(expected[xml_attrib], element.get(xml_attrib)):
+                msg = "Attribute '{0}' value '{1}' does not match '{2}'"
+                self.fail_test(msg.format(xml_attrib, element.get(xml_attrib),
+                                          expected[xml_attrib]))
+                return False
+
+        return True
+
+    def verify_text(self, element):
+        """Verify element text.
+
+        Ensure the element text matches the expected text. This will fail the
+        test and stop the reactor if conditions are not met.
+
+        Arguments:
+        element Element object.
+
+        Returns:
+        True if element text matches expected text. Otherwise False.
+        """
+        expected = self.expected_data[element.tag].get('text', '')
+        element_text = element.text
+
+        # Set to empty string if None so we can strip it and try to match it.
+        if element_text is None:
+            element_text = ''
+        element_text = element_text.strip()
+
+        LOGGER.debug("Checking text: '{0}'".format(element_text))
+        # Check if we don't expect any text or we don't expect this particular
+        # text for this element.
+        if element_text and not expected:
+            msg = "Unexpected text found: '{0}'"
+            self.fail_test(msg.format(element_text))
+            return False
+
+        # Check if we expect text but element doesn't have any.
+        if not element_text and expected:
+            msg = "Expected text not found: '{0}'"
+            self.fail_test(msg.format(expected))
+            return False
+
+        if not re.match(expected, element_text):
+            msg = "Element text '{0}' does not match '{1}'"
+            self.fail_test(msg.format(element_text, expected))
+            return False
+
+        return True
+
+    def verify_extra_text(self, element):
+        """Verify extra text is not present in element.
+
+        Ensure there is no extra text in the element. This will fail the test
+        and stop the reactor if extra text is found.
+
+        Arguments:
+        element Element object.
+
+        Returns:
+        True if extra text was not found or only whitespace was found.
+        Otherwise False.
+        """
+        LOGGER.debug("Checking for extra text.")
+        if element.tail is None:
+            return True
+
+        # Ignore any whitespace
+        extra_text = str(element.tail)
+        extra_text = extra_text.strip()
+        if not extra_text:
+            return True
+
+        msg = "Unexpected extra text found on element '%s': '%s'"
+        self.fail_test(msg.format(element.tag, extra_text))
+
+        return False
+
+    def fail_test(self, message):
+        """Mark the test as failed and stop the reactor
+
+        Arguments:
+        message Reason for the test failure
+        """
+        LOGGER.error(message)
+        self.test_object.set_passed(False)
+        self.test_object.stop_reactor()
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/tests.yaml b/tests/channels/pjsip/subscriptions/presence/verify_bodies/tests.yaml
new file mode 100644
index 0000000..2345db0
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/tests.yaml
@@ -0,0 +1,4 @@
+# Enter tests here in the order they should be considered for execution:
+tests:
+    - test: 'verify_pidf'
+    - test: 'verify_xpidf'
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/extensions.conf b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/extensions.conf
new file mode 100644
index 0000000..b8dcca3
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/extensions.conf
@@ -0,0 +1,2 @@
+[default]
+exten => bob,hint,Custom:bob
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/pjsip.conf b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/pjsip.conf
new file mode 100644
index 0000000..2e71a90
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/configs/ast1/pjsip.conf
@@ -0,0 +1,11 @@
+[global]
+type=global
+debug=no
+
+[local-transport]
+type=transport
+bind = 127.0.0.1
+
+[alice]
+type=endpoint
+context=default
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/sipp/subscribe.xml b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/sipp/subscribe.xml
new file mode 100644
index 0000000..8c335b8
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/sipp/subscribe.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "sipp.dtd">
+
+<scenario name="Subscribe">
+  <send retrans="500">
+    <![CDATA[
+
+      SUBSCRIBE sip:bob@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: "alice" <sip:alice@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: <sip:bob@[remote_ip]:[remote_port]>
+      Call-ID: [call_id]
+      CSeq: 1 SUBSCRIBE
+      Contact: "alice" <sip:alice@[local_ip]:[local_port]>
+      Expires: 600
+      Max-Forwards: 70
+      Event: presence
+      Supported: replaces, 100rel, timer, norefersub
+      Accept: application/pidf+xml
+      Allow-Events: presence, message-summary, refer
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv response="200" rtd="true" />
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+</scenario>
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/test-config.yaml b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/test-config.yaml
new file mode 100644
index 0000000..17bba83
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_pidf/test-config.yaml
@@ -0,0 +1,266 @@
+testinfo:
+    summary: 'Ensure NOTIFY bodies for pidf+xml subscriptions are correct.'
+    description: |
+        'A SIPp instance subscribes to "bob" using pidf+xml. Upon the
+        first TestEvent indicating that there is an active subscription, the
+        state of "Custom:bob" is changed. Each AMI event indicating that a
+        state change occurred triggers another state change until all states
+        have been set. The SIPp scenario expects to receive a NOTIFY message
+        for each state and simply responds to them.
+
+        A test module is used to verify each NOTIFY body that it finds while
+        listening for SIP packets. The module parses the body and verifies the
+        tags, attributes, and text for each element using the specified
+        configuration. If the body is not able to be parsed(IE. malformed XML)
+        or any component of the body does not match what is expected, the test
+        fails.'
+
+properties:
+    minversion: '12.5.0'
+    dependencies:
+        - buildoption: 'TEST_FRAMEWORK'
+        - sipp :
+            version : 'v3.0'
+        - python: 'twisted'
+        - python: 'starpy'
+        - python: 'yappcap'
+        - asterisk: 'res_pjsip'
+        - asterisk: 'res_pjsip_exten_state'
+        - asterisk: 'res_pjsip_pidf_body_generator'
+    tags:
+        - pjsip
+
+test-modules:
+    add-relative-to-search-path: ['..']
+    test-object:
+        config-section: sipp-config
+        typename: 'sipp.SIPpTestCase'
+    modules:
+        -
+            config-section: test-config
+            typename: 'presence.BodyCheck'
+        -
+            config-section: ami-config
+            typename: 'pluggable_modules.EventActionModule'
+
+sipp-config:
+    reactor-timeout: 30
+    fail-on-any: True
+    test-iterations:
+        -
+            scenarios:
+                - { 'key-args': {'scenario': 'subscribe.xml', '-p': '5061'},
+                    'ordered-args': ['-timeout_error'] }
+
+test-config:
+    # Expected number of NOTIFY messages with the expected body type.
+    expected_notifies: '7'
+    # Expected body type.
+    expected_body_type: 'pidf'
+    # Expected body.
+    expected_body:
+        # Expected namespaces for PIDF bodies.
+        namespaces:
+            default: 'urn:ietf:params:xml:ns:pidf'
+            pp: 'urn:ietf:params:xml:ns:pidf:person'
+            ep: 'urn:ietf:params:xml:ns:pidf:rpid:rpid-person'
+        # All expected element tag names for PIDF bodies.
+        tags:
+            ['presence', 'note', 'tuple', 'status', 'basic', 'contact',
+             'pp:person', 'ep:activities']
+        # Regex patterns for element attributes for each body in order. Some
+        # attribute values of elements differ for each body depending on the
+        # body type and state changes. Therefore we list them in the order that
+        # matches the state changes. For this test the attribute values should
+        # be the same for all bodies.
+        attributes:
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+            -
+                presence:
+                    entity: '^sip:bob at 127.0.0.1(:5060)?$'
+                tuple:
+                    id: '^bob$'
+                contact:
+                    priority: '^1$'
+        # Regex patterns for element text for each body in order. Some text
+        # of elements differ for each body depending on the body type and state
+        # changes. Therefore we list them in the order that matches the state
+        # changes.
+        text:
+            -
+                note: 'Ready'
+                basic: 'open'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+            -
+                note: 'Ringing'
+                basic: 'closed'
+                activities: 'ep:busy'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+            -
+                note: 'On the phone'
+                basic: 'closed'
+                activities: 'ep:busy'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+            -
+                note: 'On hold'
+                basic: 'closed'
+                activities: 'ep:busy'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+            -
+                note: 'On the phone'
+                basic: 'closed'
+                activities: 'ep:busy'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+            -
+                note: 'Unavailable'
+                basic: 'closed'
+                activities: 'ep:away'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+            -
+                note: 'Ready'
+                basic: 'open'
+                contact: '^"alice" <sip:alice at 127.0.0.1(:5060)?>$'
+
+ami-config:
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'TestEvent'
+                    State: 'SUBSCRIPTION_STATE_SET'
+                    StateText: 'ACTIVE'
+                    Endpoint: 'alice'
+            count: '>1'
+            trigger-on-count: True
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'RINGING'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Ringing'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'INUSE'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'InUse'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'ONHOLD'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Hold'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'BUSY'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Busy'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'UNAVAILABLE'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Unavailable'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'NOT_INUSE'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Idle'
+            count: '1'
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/extensions.conf b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/extensions.conf
new file mode 100644
index 0000000..b8dcca3
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/extensions.conf
@@ -0,0 +1,2 @@
+[default]
+exten => bob,hint,Custom:bob
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/pjsip.conf b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/pjsip.conf
new file mode 100644
index 0000000..2e71a90
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/configs/ast1/pjsip.conf
@@ -0,0 +1,11 @@
+[global]
+type=global
+debug=no
+
+[local-transport]
+type=transport
+bind = 127.0.0.1
+
+[alice]
+type=endpoint
+context=default
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/sipp/subscribe.xml b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/sipp/subscribe.xml
new file mode 100644
index 0000000..d359595
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/sipp/subscribe.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "sipp.dtd">
+
+<scenario name="Subscribe">
+  <send retrans="500">
+    <![CDATA[
+
+      SUBSCRIBE sip:bob@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: "alice" <sip:alice@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: <sip:bob@[remote_ip]:[remote_port]>
+      Call-ID: [call_id]
+      CSeq: 1 SUBSCRIBE
+      Contact: "alice" <sip:alice@[local_ip]:[local_port]>
+      Expires: 600
+      Max-Forwards: 70
+      Event: presence
+      Supported: replaces, 100rel, timer, norefersub
+      Accept: application/xpidf+xml
+      Allow-Events: presence, message-summary, refer
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv response="200" rtd="true" />
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv request="NOTIFY" crlf="true"/>
+
+  <send>
+    <![CDATA[
+
+      SIP/2.0 200 OK
+      [last_Via:]
+      [last_From:]
+      [last_To:]
+      [last_Call-ID:]
+      [last_CSeq:]
+      Contact: <sip:[local_ip]:[local_port];transport=[transport]>
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+</scenario>
diff --git a/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/test-config.yaml b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/test-config.yaml
new file mode 100644
index 0000000..b6bfc53
--- /dev/null
+++ b/tests/channels/pjsip/subscriptions/presence/verify_bodies/verify_xpidf/test-config.yaml
@@ -0,0 +1,265 @@
+testinfo:
+    summary: 'Ensure NOTIFY bodies for xpidf+xml subscriptions are correct.'
+    description: |
+        'A SIPp instance subscribes to "bob" using xpidf+xml. Upon the
+        first TestEvent indicating that there is an active subscription, the
+        state of "Custom:bob" is changed. Each AMI event indicating that a
+        state change occurred triggers another state change until all states
+        have been set. The SIPp scenario expects to receive a NOTIFY message
+        for each state and simply responds to them.
+
+        A test module is used to verify each NOTIFY body that it finds while
+        listening for SIP packets. The module parses the body and verifies the
+        tags, attributes, and text for each element using the specified
+        configuration. If the body is not able to be parsed(IE. malformed XML)
+        or any component of the body does not match what is expected, the test
+        fails.'
+
+properties:
+    minversion: '12.5.0'
+    dependencies:
+        - buildoption: 'TEST_FRAMEWORK'
+        - sipp :
+            version : 'v3.0'
+        - python: 'twisted'
+        - python: 'starpy'
+        - python: 'yappcap'
+        - asterisk: 'res_pjsip'
+        - asterisk: 'res_pjsip_exten_state'
+        - asterisk: 'res_pjsip_xpidf_body_generator'
+    tags:
+        - pjsip
+
+test-modules:
+    add-relative-to-search-path: ['..']
+    test-object:
+        config-section: sipp-config
+        typename: 'sipp.SIPpTestCase'
+    modules:
+        -
+            config-section: test-config
+            typename: 'presence.BodyCheck'
+        -
+            config-section: ami-config
+            typename: 'pluggable_modules.EventActionModule'
+
+sipp-config:
+    reactor-timeout: 30
+    fail-on-any: True
+    test-iterations:
+        -
+            scenarios:
+                - { 'key-args': {'scenario': 'subscribe.xml', '-p': '5061'},
+                    'ordered-args': ['-timeout_error'] }
+
+test-config:
+    # Expected number of NOTIFY messages with the expected body type.
+    expected_notifies: '7'
+    # Expected body type.
+    expected_body_type: 'xpidf'
+    # Expected body.
+    expected_body:
+        # All expected element tag names for XPIDF bodies.
+        tags:
+            ['presence', 'presentity', 'atom', 'address', 'status',
+             'msnsubstatus']
+        # Regex patterns for element attributes for each body in order. Some
+        # attribute values of elements differ for each body depending on the
+        # body type and state changes. Therefore we list them in the order that
+        # matches the state changes. For this test some attribute values will
+        # differ between all bodies.
+        attributes:
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^open$'
+                msnsubstatus:
+                    substatus: '^online$'
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^inuse$'
+                msnsubstatus:
+                    substatus: '^onthephone$'
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^inuse$'
+                msnsubstatus:
+                    substatus: '^onthephone$'
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^closed$'
+                msnsubstatus:
+                    substatus: '^offline$'
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^closed$'
+                msnsubstatus:
+                    substatus: '^offline$'
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^closed$'
+                msnsubstatus:
+                    substatus: '^offline$'
+            -
+                presentity:
+                    uri: '^sip:bob at 127.0.0.1(:5060)?;method=SUBSCRIBE$'
+                atom:
+                    atomid: '^[a-z0-9-]+$'
+                    id: '^bob$'
+                address:
+                    uri: '^"alice" <sip:alice at 127.0.0.1(:5060)?>;user=ip$'
+                    priority: '^0\.80000$'
+                status:
+                    status: '^open$'
+                msnsubstatus:
+                    substatus: '^online$'
+
+ami-config:
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'TestEvent'
+                    State: 'SUBSCRIPTION_STATE_SET'
+                    StateText: 'ACTIVE'
+                    Endpoint: 'alice'
+            count: '>1'
+            trigger-on-count: True
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'RINGING'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Ringing'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'INUSE'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'InUse'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'ONHOLD'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Hold'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'BUSY'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Busy'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'UNAVAILABLE'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Unavailable'
+            count: '1'
+        ami-actions:
+            action:
+               action: 'SetVar'
+               variable: 'DEVICE_STATE(Custom:bob)'
+               value: 'NOT_INUSE'
+    -
+        ami-events:
+            id: '0'
+            conditions:
+                match:
+                    Event: 'ExtensionStatus'
+                    Exten: 'bob'
+                    Hint: 'Custom:bob'
+                    StatusText: 'Idle'
+            count: '1'

-- 
To view, visit https://gerrit.asterisk.org/1023
To unsubscribe, visit https://gerrit.asterisk.org/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ib852d1d42ea391fe4e7d1abf0eb7ea6eabb62cba
Gerrit-PatchSet: 4
Gerrit-Project: testsuite
Gerrit-Branch: master
Gerrit-Owner: John Bigelow <jbigelow at digium.com>
Gerrit-Reviewer: John Bigelow <jbigelow at digium.com>
Gerrit-Reviewer: Mark Michelson <mmichelson at digium.com>
Gerrit-Reviewer: Matt Jordan <mjordan at digium.com>



More information about the asterisk-code-review mailing list