diff --git a/pactman/__init__.py b/pactman/__init__.py index de0082d..d36d21c 100644 --- a/pactman/__init__.py +++ b/pactman/__init__.py @@ -1,6 +1,6 @@ """Python methods for interactive with a Pact Mock Service.""" from .mock.consumer import Consumer -from .mock.matchers import EachLike, Equals, Includes, Like, SomethingLike, Term +from .mock.matchers import EachLike, Equals, Format, Includes, Like, SomethingLike, Term from .mock.pact import Pact from .mock.provider import Provider @@ -8,6 +8,7 @@ "Consumer", "EachLike", "Equals", + "Format", "Includes", "Like", "Pact", diff --git a/pactman/mock/matchers.py b/pactman/mock/matchers.py index f0e780e..0e1d8ee 100644 --- a/pactman/mock/matchers.py +++ b/pactman/mock/matchers.py @@ -1,5 +1,8 @@ """Classes for defining request and response data that is variable.""" +import datetime +from enum import Enum + class Matcher(object): """Base class for defining complex contract expectations.""" @@ -448,3 +451,174 @@ def get_matching_rules_v3(input, path): rules = MatchingRuleV3() rules.generate(input, path) return rules + + +class Format: + """ + Class of regular expressions for common formats. + + Example: + >>> from pact import Consumer, Provider + >>> from pact.matchers import Format + >>> pact = Consumer('consumer').has_pact_with(Provider('provider')) + >>> (pact.given('the current user is logged in as `tester`') + ... .upon_receiving('a request for the user profile') + ... .with_request('get', '/profile') + ... .will_respond_with(200, body={ + ... 'id': Format().identifier, + ... 'lastUpdated': Format().time + ... })) + + Would expect `id` to be any valid int and `lastUpdated` to be a valid time. + When the consumer runs this contract, the value of that will be returned + is the second value passed to Term in the given function, for the time + example it would be datetime.datetime(2000, 2, 1, 12, 30, 0, 0).time() + + """ + + def __init__(self): + """Create a new Formatter.""" + self.identifier = self.integer_or_identifier() + self.integer = self.integer_or_identifier() + self.decimal = self.decimal() + self.ip_address = self.ip_address() + self.hexadecimal = self.hexadecimal() + self.ipv6_address = self.ipv6_address() + self.uuid = self.uuid() + self.timestamp = self.timestamp() + self.date = self.date() + self.time = self.time() + + def integer_or_identifier(self): + """ + Match any integer. + + :return: a Like object with an integer. + :rtype: Like + """ + return Like(1) + + def decimal(self): + """ + Match any decimal. + + :return: a Like object with a decimal. + :rtype: Like + """ + return Like(1.0) + + def ip_address(self): + """ + Match any ip address. + + :return: a Term object with an ip address regex. + :rtype: Term + """ + return Term(self.Regexes.ip_address.value, '127.0.0.1') + + def hexadecimal(self): + """ + Match any hexadecimal. + + :return: a Term object with a hexdecimal regex. + :rtype: Term + """ + return Term(self.Regexes.hexadecimal.value, '3F') + + def ipv6_address(self): + """ + Match any ipv6 address. + + :return: a Term object with an ipv6 address regex. + :rtype: Term + """ + return Term(self.Regexes.ipv6_address.value, '::ffff:192.0.2.128') + + def uuid(self): + """ + Match any uuid. + + :return: a Term object with a uuid regex. + :rtype: Term + """ + return Term( + self.Regexes.uuid.value, 'fc763eba-0905-41c5-a27f-3934ab26786c' + ) + + def timestamp(self): + """ + Match any timestamp. + + :return: a Term object with a timestamp regex. + :rtype: Term + """ + return Term( + self.Regexes.timestamp.value, datetime.datetime( + 2000, 2, 1, 12, 30, 0, 0 + ).isoformat() + ) + + def date(self): + """ + Match any date. + + :return: a Term object with a date regex. + :rtype: Term + """ + return Term( + self.Regexes.date.value, datetime.datetime( + 2000, 2, 1, 12, 30, 0, 0 + ).date().isoformat() + ) + + def time(self): + """ + Match any time. + + :return: a Term object with a time regex. + :rtype: Term + """ + return Term( + self.Regexes.time_regex.value, datetime.datetime( + 2000, 2, 1, 12, 30, 0, 0 + ).time().isoformat() + ) + + class Regexes(Enum): + """Regex Enum for common formats.""" + + ip_address = r'(\d{1,3}\.)+\d{1,3}' + hexadecimal = r'[0-9a-fA-F]+' + ipv6_address = r'(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}\Z)|' \ + r'(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\Z)|(\A([0-9a-f]' \ + r'{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\Z)|(\A([0-9a-f]{1,4}:)' \ + r'{1,4}(:[0-9a-f]{1,4}){1,3}\Z)|(\A([0-9a-f]{1,4}:){1,5}(:[0-' \ + r'9a-f]{1,4}){1,2}\Z)|(\A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4})' \ + r'{1,1}\Z)|(\A(([0-9a-f]{1,4}:){1,7}|:):\Z)|(\A:(:[0-9a-f]{1,4})' \ + r'{1,7}\Z)|(\A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]' \ + r'?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A(([0-9a-f]' \ + r'{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25' \ + r'[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|(\A([0-9a-f]{1,4}:){5}:[' \ + r'0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4' \ + r']\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]' \ + r'{1,4}){1,4}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]' \ + r'\d|[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}' \ + r'){1,3}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0' \ + r'-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,' \ + r'2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]' \ + r'?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:' \ + r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?' \ + r'\d)){3}\Z)|(\A(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]\d|[0' \ + r'-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A:(:[' \ + r'0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]' \ + r'|2[0-4]\d|[0-1]?\d?\d)){3}\Z)' + uuid = r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + timestamp = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3(' \ + r'[12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-' \ + r'9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2' \ + r'[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d' \ + r'([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$' + date = r'^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|' \ + r'0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|' \ + r'[12]\d{2}|3([0-5]\d|6[1-6])))?)' + time_regex = r'^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$' diff --git a/pactman/test/mock_matchers/test_format.py b/pactman/test/mock_matchers/test_format.py new file mode 100644 index 0000000..c0a3a5c --- /dev/null +++ b/pactman/test/mock_matchers/test_format.py @@ -0,0 +1,141 @@ +from datetime import datetime +from unittest import TestCase + +from pactman import Format + + +class FormatTestCase(TestCase): + @classmethod + def setUpClass(cls): + cls.formatter = Format() + + def test_identifier(self): + identifier = self.formatter.identifier.ruby_protocol() + self.assertEqual(identifier, {"json_class": "Pact::SomethingLike", "contents": 1}) + + def test_integer(self): + integer = self.formatter.integer.ruby_protocol() + self.assertEqual(integer, {"json_class": "Pact::SomethingLike", "contents": 1}) + + def test_decimal(self): + decimal = self.formatter.integer.ruby_protocol() + self.assertEqual(decimal, {"json_class": "Pact::SomethingLike", "contents": 1.0}) + + def test_ip_address(self): + ip_address = self.formatter.ip_address.ruby_protocol() + self.assertEqual( + ip_address, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.ip_address.value, + "o": 0, + }, + "generate": "127.0.0.1", + }, + }, + ) + + def test_hexadecimal(self): + hexadecimal = self.formatter.hexadecimal.ruby_protocol() + self.assertEqual( + hexadecimal, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.hexadecimal.value, + "o": 0, + }, + "generate": "3F", + }, + }, + ) + + def test_ipv6_address(self): + ipv6_address = self.formatter.ipv6_address.ruby_protocol() + self.assertEqual( + ipv6_address, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.ipv6_address.value, + "o": 0, + }, + "generate": "::ffff:192.0.2.128", + }, + }, + ) + + def test_uuid(self): + uuid = self.formatter.uuid.ruby_protocol() + self.assertEqual( + uuid, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.uuid.value, + "o": 0, + }, + "generate": "fc763eba-0905-41c5-a27f-3934ab26786c", + }, + }, + ) + + def test_timestamp(self): + timestamp = self.formatter.timestamp.ruby_protocol() + self.assertEqual( + timestamp, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.timestamp.value, + "o": 0, + }, + "generate": datetime(2000, 2, 1, 12, 30, 0, 0).isoformat(), + }, + }, + ) + + def test_date(self): + date = self.formatter.date.ruby_protocol() + self.assertEqual( + date, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.date.value, + "o": 0, + }, + "generate": datetime(2000, 2, 1, 12, 30, 0, 0).date().isoformat(), + }, + }, + ) + + def test_time(self): + time = self.formatter.time.ruby_protocol() + self.assertEqual( + time, + { + "json_class": "Pact::Term", + "data": { + "matcher": { + "json_class": "Regexp", + "s": self.formatter.Regexes.time_regex.value, + "o": 0, + }, + "generate": datetime(2000, 2, 1, 12, 30, 0, 0).time().isoformat(), + }, + }, + )