Repository: pact-foundation/pact-python

Issue:
`given`/provider state should not be necessary
Only `Pact.given()` inserts a new element into the `_interactions` array, meaning that if you don't call it for the *first* test, you'll get an exception when calling any other interaction-related methods, and if you don't call it for *subsequent* tests, you'll overwrite the previous interaction (possibly partially, leading to nonsense).

According to [the JSON schema](https://bitbucket.org/atlassian/pact-json-schema/src/8d0b9d2f38fe517dfbce0e5d0b8755eaefaacb5b/schemas/v2/schema.json?at=master&fileviewer=file-view-default#schema.json-16:30), `providerState` is an optional field, which makes sense, since not every interaction *will* require state to be set up.

As is, `pact-python` requires a bogus `given()` call for stateless interactions (and therefore requires the provider to handle that bogus state-setting).

(Less seriously, but still I think inelegantly, this also means that `given` has to be called *first*, even though the mock-English encouraged by the method names and chaining would suggest that there's no difference between "given …, upon receiving …, will respond with …" and "will respond with …, upon receiving …, given …"

Comments:

Patch:
diff --git a/pact/pact.py b/pact/pact.py
index e855274..45326de 100644
--- a/pact/pact.py
+++ b/pact/pact.py
@@ -38,6 +38,8 @@ class Pact(object):
 
     HEADERS = {'X-Pact-Mock-Service': 'true'}
 
+    MANDATORY_FIELDS = {'response', 'description', 'request'}
+
     def __init__(self, consumer, provider, host_name='localhost', port=1234,
                  log_dir=None, ssl=False, sslcert=None, sslkey=None,
                  cors=False, pact_dir=None, version='2.0.0',
@@ -114,7 +116,8 @@ class Pact(object):
         :type provider_state: basestring
         :rtype: Pact
         """
-        self._interactions.insert(0, {'provider_state': provider_state})
+        self._insert_interaction_if_complete()
+        self._interactions[0]['provider_state'] = provider_state
         return self
 
     def setup(self):
@@ -190,6 +193,7 @@ class Pact(object):
         :type scenario: basestring
         :rtype: Pact
         """
+        self._insert_interaction_if_complete()
         self._interactions[0]['description'] = scenario
         return self
 
@@ -231,6 +235,7 @@ class Pact(object):
         :type query: dict, basestring, or None
         :rtype: Pact
         """
+        self._insert_interaction_if_complete()
         self._interactions[0]['request'] = Request(
             method, path, body=body, headers=headers, query=query).json()
         return self
@@ -248,11 +253,27 @@ class Pact(object):
         :type body: Matcher, dict, list, basestring, or None
         :rtype: Pact
         """
+        self._insert_interaction_if_complete()
         self._interactions[0]['response'] = Response(status,
                                                      headers=headers,
                                                      body=body).json()
         return self
 
+    def _insert_interaction_if_complete(self):
+        """
+        Insert a new interaction if current interaction is complete.
+
+        An interaction is complete if it has all the mandatory fields.
+        If there are no interactions, a new interaction will be added.
+
+        :rtype: None
+        """
+        if not self._interactions:
+            self._interactions.append({})
+        elif all(field in self._interactions[0]
+                 for field in self.MANDATORY_FIELDS):
+            self._interactions.insert(0, {})
+
     def _wait_for_server_start(self):
         """
         Wait for the mock service to be ready for requests.
diff --git a/setup.py b/setup.py
index 4d7c021..e5806be 100644
--- a/setup.py
+++ b/setup.py
@@ -109,7 +109,7 @@ def read(filename):
 
 
 dependencies = [
-    'click>=2.0.0,<=6.7',
+    'click>=7.0.0',
     'psutil>=2.0.0',
     'requests>=2.5.0',
     'six>=1.9.0',


Test patch:
diff --git a/pact/test/test_pact.py b/pact/test/test_pact.py
index 2cf5539..64c061f 100644
--- a/pact/test/test_pact.py
+++ b/pact/test/test_pact.py
@@ -76,6 +76,27 @@ class PactTestCase(TestCase):
         self.assertEqual(target._interactions[0]['response'],
                          {'status': 200, 'body': 'success'})
 
+    def test_definition_without_given(self):
+        target = Pact(self.consumer, self.provider)
+        (target
+            .upon_receiving('a specific request to the server')
+            .with_request('GET', '/path')
+            .will_respond_with(200, body='success'))
+
+        self.assertEqual(len(target._interactions), 1)
+
+        self.assertIsNone(
+            target._interactions[0].get('provider_state'))
+
+        self.assertEqual(
+            target._interactions[0]['description'],
+            'a specific request to the server')
+
+        self.assertEqual(target._interactions[0]['request'],
+                         {'path': '/path', 'method': 'GET'})
+        self.assertEqual(target._interactions[0]['response'],
+                         {'status': 200, 'body': 'success'})
+
     def test_definition_all_options(self):
         target = Pact(self.consumer, self.provider)
         (target
@@ -146,6 +167,40 @@ class PactTestCase(TestCase):
         self.assertEqual(target._interactions[0]['response'],
                          {'status': 200, 'body': 'success'})
 
+    def test_definition_multiple_interactions_without_given(self):
+        target = Pact(self.consumer, self.provider)
+        (target
+         .upon_receiving('a specific request to the server')
+         .with_request('GET', '/foo')
+         .will_respond_with(200, body='success')
+         .upon_receiving('a different request to the server')
+         .with_request('GET', '/bar')
+         .will_respond_with(200, body='success'))
+
+        self.assertEqual(len(target._interactions), 2)
+
+        self.assertIsNone(
+            target._interactions[1].get('provider_state'))
+        self.assertIsNone(
+            target._interactions[0].get('provider_state'))
+
+        self.assertEqual(
+            target._interactions[1]['description'],
+            'a specific request to the server')
+        self.assertEqual(
+            target._interactions[0]['description'],
+            'a different request to the server')
+
+        self.assertEqual(target._interactions[1]['request'],
+                         {'path': '/foo', 'method': 'GET'})
+        self.assertEqual(target._interactions[0]['request'],
+                         {'path': '/bar', 'method': 'GET'})
+
+        self.assertEqual(target._interactions[1]['response'],
+                         {'status': 200, 'body': 'success'})
+        self.assertEqual(target._interactions[0]['response'],
+                         {'status': 200, 'body': 'success'})
+
 
 class PactSetupTestCase(PactTestCase):
     def setUp(self):
diff --git a/pact/test/test_verify.py b/pact/test/test_verify.py
index f0b77a7..cdd1aad 100644
--- a/pact/test/test_verify.py
+++ b/pact/test/test_verify.py
@@ -69,7 +69,7 @@ class mainTestCase(TestCase):
     def test_provider_base_url_is_required(self):
         result = self.runner.invoke(verify.main, [])
         self.assertEqual(result.exit_code, 2)
-        self.assertIn(b'--provider-base-url', result.output_bytes)
+        self.assertIn(b'--provider-base-url', result.stdout_bytes)
         self.assertFalse(self.mock_Popen.called)
 
     def test_pact_urls_are_required(self):
@@ -77,14 +77,14 @@ class mainTestCase(TestCase):
             verify.main, ['--provider-base-url=http://localhost'])
 
         self.assertEqual(result.exit_code, 1)
-        self.assertIn(b'at least one', result.output_bytes)
+        self.assertIn(b'at least one', result.stdout_bytes)
         self.assertFalse(self.mock_Popen.called)
 
     def test_local_pact_urls_must_exist(self):
         self.mock_isfile.return_value = False
         result = self.runner.invoke(verify.main, self.default_opts)
         self.assertEqual(result.exit_code, 1)
-        self.assertIn(b'./pacts/consumer-provider.json', result.output_bytes)
+        self.assertIn(b'./pacts/consumer-provider.json', result.stdout_bytes)
         self.assertFalse(self.mock_Popen.called)
 
     def test_failed_verification(self):
@@ -150,7 +150,7 @@ class mainTestCase(TestCase):
         self.assertEqual(result.exit_code, 0)
         self.assertIn(
             b'Multiple --pact-urls arguments are deprecated.',
-            result.output_bytes)
+            result.stdout_bytes)
         self.mock_Popen.return_value.wait.assert_called_once_with()
         self.assertEqual(self.mock_Popen.call_count, 1)
         self.assertProcess(
@@ -166,7 +166,7 @@ class mainTestCase(TestCase):
         ])
         self.assertEqual(result.exit_code, 1)
         self.assertIn(
-            b'Provider application version is required', result.output_bytes)
+            b'Provider application version is required', result.stdout_bytes)
         self.assertFalse(self.mock_Popen.return_value.communicate.called)