github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/rest_api/tests/unit/components.py (about)

     1  # Copyright 2017 Intel Corporation
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  # ------------------------------------------------------------------------------
    15  
    16  # pylint: disable=attribute-defined-outside-init
    17  
    18  from base64 import b64decode
    19  
    20  from aiohttp import web
    21  from aiohttp.test_utils import AioHTTPTestCase
    22  
    23  from sawtooth_rest_api.route_handlers import RouteHandler
    24  from sawtooth_rest_api.protobuf import client_batch_submit_pb2
    25  from sawtooth_rest_api.protobuf.client_state_pb2 import ClientStateListResponse
    26  from sawtooth_rest_api.protobuf.client_list_control_pb2 \
    27      import ClientPagingControls
    28  from sawtooth_rest_api.protobuf.client_list_control_pb2 \
    29      import ClientPagingResponse
    30  from sawtooth_rest_api.protobuf.client_list_control_pb2 \
    31      import ClientSortControls
    32  from sawtooth_rest_api.protobuf.block_pb2 import Block
    33  from sawtooth_rest_api.protobuf.block_pb2 import BlockHeader
    34  from sawtooth_rest_api.protobuf.batch_pb2 import BatchList
    35  from sawtooth_rest_api.protobuf.batch_pb2 import Batch
    36  from sawtooth_rest_api.protobuf.batch_pb2 import BatchHeader
    37  from sawtooth_rest_api.protobuf.transaction_pb2 import Transaction
    38  from sawtooth_rest_api.protobuf.transaction_pb2 import TransactionHeader
    39  
    40  from sawtooth_rest_api.protobuf.validator_pb2 import Message
    41  
    42  TEST_TIMEOUT = 5
    43  
    44  
    45  class MockConnection(object):
    46      """Replaces a route handler's connection to allow tests to preset the
    47      response to send back as well as run asserts on the protobufs sent
    48      to the connection.
    49  
    50      Methods can be accessed using `self.connection` within a test case.
    51      MockConnection should not be initialized directly.
    52      """
    53  
    54      def __init__(self, test_case, request_type, request_proto, response_proto):
    55          self._tests = test_case
    56          self._request_type = request_type
    57          self._request_proto = request_proto
    58          self._response_proto = response_proto
    59          self._reset_sent_request()
    60          self._reset_response()
    61  
    62      def preset_response(self, status=None, proto=None, **response_data):
    63          """Sets the response that will be returned by the next `send` call.
    64          Should be set once before every test call the Rest Api makes to
    65          Connection.
    66  
    67          Args:
    68              status (int, optional): Enum of the response status, defaults to OK
    69              response_data (kwargs): Other data to add to the Protobuf response
    70          """
    71          if proto is None:
    72              proto = self._response_proto
    73          if status is None:
    74              status = proto.OK
    75          self._response.append(proto(status=status, **response_data))
    76  
    77      def assert_valid_request_sent(self, **request_data):
    78          """Asserts that the last sent request matches the expected data.
    79  
    80          Args:
    81              request_data (kwargs): The data expected to be in the last request
    82  
    83          Raises:
    84              AssertionError: Raised if no new request was sent previous to call
    85          """
    86          if not self._sent_request_type or not self._sent_request:
    87              raise AssertionError('You must send a request before testing it!')
    88  
    89          self._tests.assertEqual(self._sent_request_type, self._request_type)
    90  
    91          expected_request = self._request_proto(**request_data)
    92          self._tests.assertEqual(self._sent_request, expected_request)
    93  
    94          self._reset_sent_request()
    95  
    96      async def send(self, message_type, message_content, timeout):
    97          """Replaces send method on Connection. Should not be called directly.
    98          """
    99          request = self._request_proto()
   100          request.ParseFromString(message_content)
   101  
   102          self._sent_request_type = message_type
   103          self._sent_request = request
   104  
   105          try:
   106              response_bytes = self._response.pop().SerializeToString()
   107          except AttributeError:
   108              raise AssertionError("Preset a response before sending a request!")
   109  
   110          return Message(content=response_bytes)
   111  
   112      def _reset_sent_request(self):
   113          self._sent_request_type = None
   114          self._sent_request = None
   115  
   116      def _reset_response(self):
   117          self._response = []
   118  
   119      class _MockFuture(object):
   120          class Response(object):
   121              def __init__(self, content):
   122                  # message_type must be set, but is not important
   123                  self.message_type = 0
   124                  self.content = content
   125  
   126          def __init__(self, response_content):
   127              self._response = self.Response(response_content)
   128  
   129          def result(self, timeout):
   130              return self._response
   131  
   132  
   133  class BaseApiTest(AioHTTPTestCase):
   134      """A parent class for Rest Api test cases, providing common functionality.
   135      """
   136      async def get_application(self):
   137          """Each child must implement this method which similar to __init__
   138          sets up aiohttp's async test cases.
   139  
   140          Additionally, within this method each child should run the methods
   141          `set_status_and_connection`, `build_handlers`, and `build_app` as part
   142          of the setup process.
   143  
   144          Args:
   145              loop (object): Provided by aiohttp for async operations,
   146                  will be needed in order to `build_app`.
   147  
   148          Returns:
   149              web.Application: the individual app for this test case
   150          """
   151          raise NotImplementedError('Rest Api tests need get_application method')
   152  
   153      def set_status_and_connection(self, req_type, req_proto, resp_proto):
   154          """Sets the `status` and `connection` properties for the test case.
   155  
   156          Args:
   157              req_type (int): Expected enum of the type of Message sent to
   158                  connection
   159              req_proto (class): Protobuf of requests that will be sent to
   160                  connection
   161              resp_proto (class): Protobuf of responses to send back from
   162                  connection
   163          """
   164          self.status = resp_proto
   165          self.connection = MockConnection(self, req_type, req_proto, resp_proto)
   166  
   167      @staticmethod
   168      def build_handlers(loop, connection):
   169          """Returns Rest Api route handlers modified with some a mock
   170          connection.
   171  
   172          Args:
   173              connection (object): The MockConnection set to `self.connection`
   174  
   175          Returns:
   176              RouteHandler: The route handlers to handle test queries
   177          """
   178          handlers = RouteHandler(loop, connection, TEST_TIMEOUT)
   179          return handlers
   180  
   181      @staticmethod
   182      def build_app(loop, endpoint, handler):
   183          """Returns the final app for `get_application`, with routes set up
   184          to be test queried.
   185  
   186          Args:
   187              loop (object): The loop provided to `get_application`
   188              endpoint (str): The path that will be queried by this test case
   189              handler (function): Rest Api handler for queries to the endpoint
   190  
   191          Returns:
   192              web.Application: the individual app for this test case
   193          """
   194          app = web.Application(loop=loop)
   195          app.router.add_get(endpoint, handler)
   196          app.router.add_post(endpoint, handler)
   197          return app
   198  
   199      async def post_batches(self, batches):
   200          """POSTs batches to '/batches' with an optional wait parameter
   201          """
   202          batch_bytes = BatchList(batches=batches).SerializeToString()
   203  
   204          return await self.client.post(
   205              '/batches',
   206              data=batch_bytes,
   207              headers={'content-type': 'application/octet-stream'})
   208  
   209      async def get_assert_status(self, endpoint, status):
   210          """GETs from endpoint, asserts an HTTP status, returns parsed response
   211          """
   212          request = await self.client.get(endpoint)
   213          self.assertEqual(status, request.status)
   214          return await request.json()
   215  
   216      async def get_assert_200(self, endpoint):
   217          """GETs from endpoint, asserts a 200 status, returns a parsed response
   218          """
   219          return await self.get_assert_status(endpoint, 200)
   220  
   221      def assert_all_instances(self, items, cls):
   222          """Asserts that all items in a collection are instances of a class
   223          """
   224          for item in items:
   225              self.assertIsInstance(item, cls)
   226  
   227      def assert_has_valid_head(self, response, expected):
   228          """Asserts a response has a head string with an expected value
   229          """
   230          self.assertIn('head', response)
   231          head = response['head']
   232          self.assertIsInstance(head, str)
   233          self.assertEqual(head, expected)
   234  
   235      def assert_has_valid_link(self, response, expected_ending):
   236          """Asserts a response has a link url string with an expected ending
   237          """
   238          self.assertIn('link', response)
   239          link = response['link']
   240          self.assert_valid_url(link, expected_ending)
   241  
   242      def assert_has_valid_paging(self, js_response, pb_paging,
   243                                  next_link=None, previous_link=None):
   244          """Asserts a response has a paging dict with the expected values.
   245          """
   246          self.assertIn('paging', js_response)
   247          js_paging = js_response['paging']
   248  
   249          if pb_paging.next:
   250              self.assertIn('next_position', js_paging)
   251  
   252          if next_link is not None:
   253              self.assertIn('next', js_paging)
   254              self.assert_valid_url(js_paging['next'], next_link)
   255          else:
   256              self.assertNotIn('next', js_paging)
   257  
   258      def assert_has_valid_error(self, response, expected_code):
   259          """Asserts a response has only an error dict with an expected code
   260          """
   261          self.assertIn('error', response)
   262          self.assertEqual(1, len(response))
   263  
   264          error = response['error']
   265          self.assertIn('code', error)
   266          self.assertEqual(error['code'], expected_code)
   267          self.assertIn('title', error)
   268          self.assertIsInstance(error['title'], str)
   269          self.assertIn('message', error)
   270          self.assertIsInstance(error['message'], str)
   271  
   272      def assert_has_valid_data_list(self, response, expected_length):
   273          """Asserts a response has a data list of dicts of an expected length.
   274          """
   275          self.assertIn('data', response)
   276          data = response['data']
   277          self.assertIsInstance(data, list)
   278          self.assert_all_instances(data, dict)
   279          self.assertEqual(expected_length, len(data))
   280  
   281      def assert_valid_url(self, url, expected_ending=''):
   282          """Asserts a url is valid, and ends with the expected value
   283          """
   284          self.assertIsInstance(url, str)
   285          self.assertTrue(url.startswith('http'))
   286          try:
   287              self.assertTrue(url.endswith(expected_ending))
   288          except AssertionError:
   289              raise AssertionError(
   290                  'Expected "{}" to end with "{}"'.format(url, expected_ending))
   291  
   292      def assert_entries_match(self, proto_entries, json_entries):
   293          """Asserts that each JSON leaf matches the original Protobuf entries
   294          """
   295          self.assertEqual(len(proto_entries), len(json_entries))
   296          for pb_leaf, js_leaf in zip(proto_entries, json_entries):
   297              self.assertIn('address', js_leaf)
   298              self.assertIn('data', js_leaf)
   299              self.assertEqual(pb_leaf.address, js_leaf['address'])
   300              self.assertEqual(pb_leaf.data, b64decode(js_leaf['data']))
   301  
   302      def assert_statuses_match(self, proto_statuses, json_statuses):
   303          """Asserts that JSON statuses match the original enum statuses dict
   304          """
   305          self.assertEqual(len(proto_statuses), len(json_statuses))
   306          for pb_status, js_status in zip(proto_statuses, json_statuses):
   307              self.assertEqual(pb_status.batch_id, js_status['id'])
   308              pb_enum_name = \
   309                  client_batch_submit_pb2.ClientBatchStatus.Status.Name(
   310                      pb_status.status)
   311              self.assertEqual(pb_enum_name, js_status['status'])
   312  
   313              if pb_status.invalid_transactions:
   314                  txn_info = zip(pb_status.invalid_transactions,
   315                                 js_status['invalid_transactions'])
   316                  for pb_txn, js_txn in txn_info:
   317                      self.assertEqual(pb_txn.transaction_id, js_txn['id'])
   318                      self.assertEqual(pb_txn.message, js_txn.get('message', ''))
   319                      self.assertEqual(
   320                          pb_txn.extended_data, b64decode(
   321                              js_txn.get(
   322                                  'extended_data', b'')))
   323  
   324      def assert_blocks_well_formed(self, blocks, *expected_ids):
   325          """Asserts a block dict or list of block dicts have expanded headers
   326          and match the expected ids. Assumes each block contains one batch and
   327          transaction which share its id.
   328          """
   329          if not isinstance(blocks, list):
   330              blocks = [blocks]
   331  
   332          for block, expected_id in zip(blocks, expected_ids):
   333              self.assertIsInstance(block, dict)
   334              self.assertEqual(expected_id, block['header_signature'])
   335              self.assertIsInstance(block['header'], dict)
   336              self.assertEqual(b'consensus', b64decode(
   337                  block['header']['consensus']))
   338  
   339              batches = block['batches']
   340              self.assertIsInstance(batches, list)
   341              self.assertEqual(1, len(batches))
   342              self.assert_all_instances(batches, dict)
   343              self.assert_batches_well_formed(batches, expected_id)
   344  
   345      def assert_batches_well_formed(self, batches, *expected_ids):
   346          """Asserts a batch dict or list of batch dicts have expanded headers
   347          and match the expected ids. Assumes each batch contains one transaction
   348          which shares its id.
   349          """
   350          if not isinstance(batches, list):
   351              batches = [batches]
   352  
   353          for batch, expected_id in zip(batches, expected_ids):
   354              self.assertEqual(expected_id, batch['header_signature'])
   355              self.assertIsInstance(batch['header'], dict)
   356              self.assertEqual(
   357                  'public_key', batch['header']['signer_public_key'])
   358  
   359              txns = batch['transactions']
   360              self.assertIsInstance(txns, list)
   361              self.assertEqual(1, len(txns))
   362              self.assert_all_instances(txns, dict)
   363              self.assert_txns_well_formed(txns, expected_id)
   364  
   365      def assert_txns_well_formed(self, txns, *expected_ids):
   366          """Asserts a transaction dict or list of transactions dicts have
   367          expanded headers and match the expected ids.
   368          """
   369  
   370          if not isinstance(txns, list):
   371              txns = [txns]
   372  
   373          for txn, expected_id in zip(txns, expected_ids):
   374              self.assertEqual(expected_id, txn['header_signature'])
   375              self.assertEqual(b'payload', b64decode(txn['payload']))
   376              self.assertIsInstance(txn['header'], dict)
   377              self.assertEqual(expected_id, txn['header']['nonce'])
   378  
   379  
   380  class Mocks(object):
   381      """A static class with methods that return lists of mock Protobuf objects.
   382      """
   383      @staticmethod
   384      def make_paging_controls(limit=None, start=None):
   385          """Returns a ClientPagingControls Protobuf
   386          """
   387          return ClientPagingControls(
   388              limit=limit,
   389              start=start
   390          )
   391  
   392      @staticmethod
   393      def make_paging_response(next_id=None, start=None, limit=None):
   394          """Returns a ClientPagingResponse Protobuf
   395          """
   396          return ClientPagingResponse(
   397              next=next_id,
   398              start=start,
   399              limit=limit
   400          )
   401  
   402      @staticmethod
   403      def make_sort_controls(keys, reverse=False):
   404          """Returns a ClientSortControls Protobuf in a list. Use concatenation
   405          to combine multiple sort controls.
   406          """
   407          return [ClientSortControls(
   408              keys=[keys],
   409              reverse=reverse
   410          )]
   411  
   412      @staticmethod
   413      def make_entries(**leaf_data):
   414          """Returns Entry objects with specfied kwargs turned into
   415          addresses and data
   416          """
   417          return [ClientStateListResponse.Entry(address=a, data=d)
   418                  for a, d in leaf_data.items()]
   419  
   420      @classmethod
   421      def make_blocks(cls, *block_ids):
   422          """Returns Block objects with the specified ids, and each with
   423          one Batch with one Transaction with matching ids.
   424          """
   425          blocks = []
   426  
   427          for block_id in block_ids:
   428              batches = cls.make_batches(block_id)
   429  
   430              blk_header = BlockHeader(
   431                  block_num=len(blocks),
   432                  previous_block_id=blocks[-1].header_signature
   433                  if blocks else '', signer_public_key='public_key',
   434                  batch_ids=[b.header_signature for b in batches],
   435                  consensus=b'consensus', state_root_hash='root_hash')
   436  
   437              block = Block(
   438                  header=blk_header.SerializeToString(),
   439                  header_signature=block_id,
   440                  batches=batches)
   441  
   442              blocks.append(block)
   443  
   444          return blocks
   445  
   446      @classmethod
   447      def make_batches(cls, *batch_ids):
   448          """Returns Batch objects with the specified ids, and each with
   449          one Transaction with matching ids.
   450          """
   451          batches = []
   452  
   453          for batch_id in batch_ids:
   454              txns = cls.make_txns(batch_id)
   455  
   456              batch_header = BatchHeader(
   457                  signer_public_key='public_key',
   458                  transaction_ids=[t.header_signature for t in txns])
   459  
   460              batch = Batch(
   461                  header=batch_header.SerializeToString(),
   462                  header_signature=batch_id,
   463                  transactions=txns)
   464  
   465              batches.append(batch)
   466  
   467          return batches
   468  
   469      @staticmethod
   470      def make_txns(*txn_ids):
   471          """Returns Transaction objects with the specified ids and a header
   472          nonce that matches its id.
   473          """
   474          txns = []
   475  
   476          for txn_id in txn_ids:
   477              txn_header = TransactionHeader(
   478                  batcher_public_key='public_key',
   479                  family_name='family',
   480                  family_version='0.0',
   481                  nonce=txn_id,
   482                  signer_public_key='public_key')
   483  
   484              txn = Transaction(
   485                  header=txn_header.SerializeToString(),
   486                  header_signature=txn_id,
   487                  payload=b'payload')
   488  
   489              txns.append(txn)
   490  
   491          return txns