Add checking of unexpected calls and unexpected provided body to
RequestMockBuilder.

Reviewed in http://codereview.appspot.com/4996043/
diff --git a/apiclient/errors.py b/apiclient/errors.py
index beac5f4..c017912 100644
--- a/apiclient/errors.py
+++ b/apiclient/errors.py
@@ -70,10 +70,30 @@
   """Link type unknown or unexpected."""
   pass
 
+
 class UnacceptableMimeTypeError(Error):
   """That is an unacceptable mimetype for this operation."""
   pass
 
+
 class MediaUploadSizeError(Error):
   """Media is larger than the method can accept."""
   pass
+
+
+class UnexpectedMethodError(Error):
+  """Exception raised by RequestMockBuilder on unexpected calls."""
+
+  def __init__(self, methodId=None):
+    """Constructor for an UnexpectedMethodError."""
+    super(UnexpectedMethodError, self).__init__(
+        'Received unexpected call %s' % methodId)
+
+
+class UnexpectedBodyError(Error):
+  """Exception raised by RequestMockBuilder on unexpected bodies."""
+
+  def __init__(self, expected, provided):
+    """Constructor for an UnexpectedMethodError."""
+    super(UnexpectedBodyError, self).__init__(
+        'Expected: [%s] - Provided: [%s]' % (expected, provided))
diff --git a/apiclient/http.py b/apiclient/http.py
index 5f4be00..081a4ed 100644
--- a/apiclient/http.py
+++ b/apiclient/http.py
@@ -30,6 +30,8 @@
 
 from model import JsonModel
 from errors import HttpError
+from errors import UnexpectedBodyError
+from errors import UnexpectedMethodError
 from anyjson import simplejson
 
 
@@ -124,9 +126,11 @@
   """A simple mock of HttpRequest
 
     Pass in a dictionary to the constructor that maps request methodIds to
-    tuples of (httplib2.Response, content) that should be returned when that
-    method is called. None may also be passed in for the httplib2.Response, in
-    which case a 200 OK response will be generated.
+    tuples of (httplib2.Response, content, opt_expected_body) that should be
+    returned when that method is called. None may also be passed in for the
+    httplib2.Response, in which case a 200 OK response will be generated.
+    If an opt_expected_body (str or dict) is provided, it will be compared to
+    the body and UnexpectedBodyError will be raised on inequality.
 
     Example:
       response = '{"data": {"id": "tag:google.c...'
@@ -138,13 +142,14 @@
       apiclient.discovery.build("buzz", "v1", requestBuilder=requestBuilder)
 
     Methods that you do not supply a response for will return a
-    200 OK with an empty string as the response content. The methodId
-    is taken from the rpcName in the discovery document.
+    200 OK with an empty string as the response content or raise an excpetion if
+    check_unexpected is set to True. The methodId is taken from the rpcName
+    in the discovery document.
 
     For more details see the project wiki.
   """
 
-  def __init__(self, responses):
+  def __init__(self, responses, check_unexpected=False):
     """Constructor for RequestMockBuilder
 
     The constructed object should be a callable object
@@ -154,8 +159,11 @@
                 of (httplib2.Response, content). The methodId
                 comes from the 'rpcName' field in the discovery
                 document.
+    check_unexpected - A boolean setting whether or not UnexpectedMethodError
+                       should be raised on unsupplied method.
     """
     self.responses = responses
+    self.check_unexpected = check_unexpected
 
   def __call__(self, http, postproc, uri, method='GET', body=None,
                headers=None, methodId=None):
@@ -165,8 +173,23 @@
     parameters and the expected response.
     """
     if methodId in self.responses:
-      resp, content = self.responses[methodId]
+      response = self.responses[methodId]
+      resp, content = response[:2]
+      if len(response) > 2:
+        # Test the body against the supplied expected_body.
+        expected_body = response[2]
+        if bool(expected_body) != bool(body):
+          # Not expecting a body and provided one
+          # or expecting a body and not provided one.
+          raise UnexpectedBodyError(expected_body, body)
+        if isinstance(expected_body, str):
+          expected_body = simplejson.loads(expected_body)
+        body = simplejson.loads(body)
+        if body != expected_body:
+          raise UnexpectedBodyError(expected_body, body)
       return HttpRequestMock(resp, content, postproc)
+    elif self.check_unexpected:
+      raise UnexpectedMethodError(methodId)
     else:
       model = JsonModel(False)
       return HttpRequestMock(None, '{}', model.response)
diff --git a/tests/test_mocks.py b/tests/test_mocks.py
index f2da9d9..bfe55cf 100644
--- a/tests/test_mocks.py
+++ b/tests/test_mocks.py
@@ -26,6 +26,8 @@
 import unittest
 
 from apiclient.errors import HttpError
+from apiclient.errors import UnexpectedBodyError
+from apiclient.errors import UnexpectedMethodError
 from apiclient.discovery import build
 from apiclient.http import RequestMockBuilder
 from apiclient.http import HttpMock
@@ -56,6 +58,76 @@
     activity = buzz.activities().get(postId='tag:blah', userId='@me').execute()
     self.assertEqual({"foo": "bar"}, activity)
 
+  def test_unexpected_call(self):
+    requestBuilder = RequestMockBuilder({}, check_unexpected=True)
+
+    buzz = build('buzz', 'v1', http=self.http, requestBuilder=requestBuilder)
+
+    try:
+      buzz.activities().get(postId='tag:blah', userId='@me').execute()
+      self.fail('UnexpectedMethodError should have been raised')
+    except UnexpectedMethodError:
+      pass
+
+  def test_simple_unexpected_body(self):
+    requestBuilder = RequestMockBuilder({
+        'chili.activities.insert': (None, '{"data": {"foo": "bar"}}', None)
+        })
+    buzz = build('buzz', 'v1', http=self.http, requestBuilder=requestBuilder)
+
+    try:
+      buzz.activities().insert(userId='@me', body='{}').execute()
+      self.fail('UnexpectedBodyError should have been raised')
+    except UnexpectedBodyError:
+      pass
+
+  def test_simple_expected_body(self):
+    requestBuilder = RequestMockBuilder({
+        'chili.activities.insert': (None, '{"data": {"foo": "bar"}}', '{}')
+        })
+    buzz = build('buzz', 'v1', http=self.http, requestBuilder=requestBuilder)
+
+    try:
+      buzz.activities().insert(userId='@me', body='').execute()
+      self.fail('UnexpectedBodyError should have been raised')
+    except UnexpectedBodyError:
+      pass
+
+  def test_simple_wrong_body(self):
+    requestBuilder = RequestMockBuilder({
+        'chili.activities.insert': (None, '{"data": {"foo": "bar"}}',
+                                    '{"data": {"foo": "bar"}}')
+        })
+    buzz = build('buzz', 'v1', http=self.http, requestBuilder=requestBuilder)
+
+    try:
+      buzz.activities().insert(
+          userId='@me', body='{"data": {"foo": "blah"}}').execute()
+      self.fail('UnexpectedBodyError should have been raised')
+    except UnexpectedBodyError:
+      pass
+
+  def test_simple_matching_str_body(self):
+    requestBuilder = RequestMockBuilder({
+        'chili.activities.insert': (None, '{"data": {"foo": "bar"}}',
+                                    '{"data": {"foo": "bar"}}')
+        })
+    buzz = build('buzz', 'v1', http=self.http, requestBuilder=requestBuilder)
+
+    activity = buzz.activities().insert(
+        userId='@me', body={'data': {'foo': 'bar'}}).execute()
+    self.assertEqual({'foo': 'bar'}, activity)
+
+  def test_simple_matching_dict_body(self):
+    requestBuilder = RequestMockBuilder({
+        'chili.activities.insert': (None, '{"data": {"foo": "bar"}}',
+                                    {'data': {'foo': 'bar'}})
+        })
+    buzz = build('buzz', 'v1', http=self.http, requestBuilder=requestBuilder)
+
+    activity = buzz.activities().insert(
+        userId='@me', body={'data': {'foo': 'bar'}}).execute()
+    self.assertEqual({'foo': 'bar'}, activity)
 
   def test_errors(self):
     errorResponse = httplib2.Response({'status': 500, 'reason': 'Server Error'})
@@ -73,7 +145,5 @@
       self.assertEqual('Server Error', e.resp.reason)
 
 
-
-
 if __name__ == '__main__':
   unittest.main()