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