github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-supply-chain-master/integration/sawtooth_integration/tests/test_supply_chain.py (about) 1 # Copyright 2017 Intel Corporation 2 # Copyright 2018 Cargill Incorporated 3 # 4 # Licensed under the Apache License, Version 2.0 (the "License"); 5 # you may not use this file except in compliance with the License. 6 # You may obtain a copy of the License at 7 # 8 # http://www.apache.org/licenses/LICENSE-2.0 9 # 10 # Unless required by applicable law or agreed to in writing, software 11 # distributed under the License is distributed on an "AS IS" BASIS, 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 # See the License for the specific language governing permissions and 14 # limitations under the License. 15 # ------------------------------------------------------------------------------ 16 17 import json 18 import logging 19 import unittest 20 21 from sawtooth_integration.tests.integration_tools import RestClient 22 from sawtooth_integration.tests.integration_tools import wait_for_rest_apis 23 24 from sawtooth_sc_test.supply_chain_message_factory import \ 25 SupplyChainMessageFactory 26 from sawtooth_sc_test.supply_chain_message_factory import Enum 27 from sawtooth_signing import create_context 28 from sawtooth_signing import CryptoFactory 29 30 import sawtooth_sc_test.addressing as addressing 31 from sawtooth_sc_test.protobuf.property_pb2 import PropertySchema 32 from sawtooth_sc_test.protobuf.proposal_pb2 import Proposal 33 from sawtooth_sc_test.protobuf.payload_pb2 import AnswerProposalAction 34 35 36 LOGGER = logging.getLogger(__name__) 37 LOGGER.setLevel(logging.DEBUG) 38 39 NARRATION = False 40 41 42 REST_API = 'rest-api:8008' 43 URL = 'http://' + REST_API 44 45 SERVER_URL = 'http://supply-server:3000' 46 API = SERVER_URL 47 48 49 class SupplyChainClient(RestClient): 50 def __init__(self, url=URL): 51 context = create_context('secp256k1') 52 private_key = context.new_random_private_key() 53 signer = CryptoFactory(context).new_signer(private_key) 54 self.factory = SupplyChainMessageFactory(signer=signer) 55 self.public_key = self.factory.public_key 56 self.private_key = "encryptedKey" 57 self.auth_token = None 58 59 super().__init__( 60 url=url, 61 namespace=addressing.NAMESPACE) 62 63 def _post_sc_transaction(self, transaction): 64 return self.send_batches( 65 self.factory.create_batch( 66 transaction)) 67 # 68 def create_agent(self, name): 69 return self._post_sc_transaction( 70 self.factory.create_agent( 71 name)) 72 73 def create_record_type(self, name, *properties): 74 return self._post_sc_transaction( 75 self.factory.create_record_type( 76 name, *properties)) 77 78 def create_record(self, record_id, record_type, properties_dict): 79 return self._post_sc_transaction( 80 self.factory.create_record( 81 record_id, record_type, properties_dict)) 82 83 def finalize_record(self, record_id): 84 return self._post_sc_transaction( 85 self.factory.finalize_record( 86 record_id)) 87 88 def update_properties(self, record_id, properties_dict): 89 return self._post_sc_transaction( 90 self.factory.update_properties( 91 record_id, properties_dict)) 92 93 def create_proposal(self, record_id, receiving_agent, 94 role, properties=None): 95 if properties is None: 96 properties = [] 97 98 return self._post_sc_transaction( 99 self.factory.create_proposal( 100 record_id, receiving_agent, role, properties)) 101 102 def answer_proposal(self, record_id, role, response, receiving_agent=None): 103 if receiving_agent is None: 104 receiving_agent = self.public_key 105 106 return self._post_sc_transaction( 107 self.factory.answer_proposal( 108 record_id=record_id, 109 receiving_agent=receiving_agent, 110 role=role, 111 response=response)) 112 113 def revoke_reporter(self, record_id, reporter_id, properties): 114 return self._post_sc_transaction( 115 self.factory.revoke_reporter( 116 record_id, reporter_id, properties)) 117 118 def send_empty_payload(self): 119 return self._post_sc_transaction( 120 self.factory.make_empty_payload( 121 self.public_key)) 122 123 def get_agents(self, fields=None, omit=None): 124 return self._submit_request( 125 url='{}/agents{}'.format( 126 API, 127 make_query_string(fields, omit)) 128 )[1] 129 130 def get_agent(self, public_key, fields=None, omit=None): 131 return self._submit_request( 132 url='{}/agents/{}{}'.format( 133 API, 134 public_key, 135 make_query_string(fields, omit)), 136 headers={'Authorization': self.auth_token}, 137 )[1] 138 139 def get_records(self, fields=None, omit=None): 140 return self._submit_request( 141 url='{}/records{}'.format( 142 API, 143 make_query_string(fields, omit)), 144 headers={'Authorization': self.auth_token} 145 )[1] 146 147 def get_record(self, record_id, fields=None, omit=None): 148 return self._submit_request( 149 url='{}/records/{}{}'.format( 150 API, 151 record_id, 152 make_query_string(fields, omit)), 153 headers={'Authorization': self.auth_token} 154 )[1] 155 156 def get_record_property(self, record_id, property_name, 157 fields=None, omit=None): 158 return self._submit_request( 159 url='{}/records/{}/property/{}{}'.format( 160 API, 161 record_id, 162 property_name, 163 make_query_string(fields, omit)) 164 )[1] 165 166 def post_user(self, username): 167 response = self._submit_request( 168 url=SERVER_URL + '/users', 169 method='POST', 170 headers={'Content-Type': 'application/json'}, 171 data=json.dumps({ 172 'username': username, 173 'email': '{}@website.com'.format(username), 174 'password': '{}pass'.format(username), 175 'publicKey': self.public_key, 176 'encryptedKey': str(self.private_key), 177 }), 178 ) 179 180 if self.auth_token is None: 181 self.auth_token = response[1]['authorization'] 182 183 184 class TestSupplyChain(unittest.TestCase): 185 @classmethod 186 def setUpClass(cls): 187 wait_for_rest_apis([REST_API]) 188 189 def assert_valid(self, result): 190 try: 191 self.assertEqual("COMMITTED", result[1]['data'][0]['status']) 192 self.assertIn('link', result[1]) 193 except AssertionError: 194 raise AssertionError( 195 'Transaction is unexpectedly invalid -- {}'.format( 196 result[1]['data'][0]['invalid_transactions'][0]['message'])) 197 198 def assert_invalid(self, result): 199 self.narrate('{}', result) 200 try: 201 self.assertEqual( 202 'INVALID', 203 result[1]['data'][0]['status']) 204 except (KeyError, IndexError): 205 raise AssertionError( 206 'Transaction is unexpectedly valid') 207 208 def narrate(self, message, *interpolations): 209 if NARRATION: 210 LOGGER.info( 211 message.format( 212 *interpolations)) 213 214 def test_track_and_trade(self): 215 jin = SupplyChainClient() 216 217 self.assert_invalid( 218 jin.send_empty_payload()) 219 220 self.narrate( 221 ''' 222 Jin tries to create a record for a fish with label `fish-456`. 223 ''') 224 225 self.assert_invalid( 226 jin.create_record( 227 'fish-456', 228 'fish', 229 {})) 230 231 self.narrate( 232 ''' 233 Only registered agents can create records, so 234 Jin registers as an agent with public key {}. 235 ''', 236 jin.public_key[:6]) 237 238 self.assert_valid( 239 jin.create_agent('Jin Kwon')) 240 241 self.narrate( 242 ''' 243 He can't register as an agent again because there is already an 244 agent registerd with his key (namely, himself). 245 ''') 246 247 self.assert_invalid( 248 jin.create_agent('Jin Kwon')) 249 250 self.narrate( 251 ''' 252 Jin tries to create a record for a fish with label `fish-456`. 253 ''') 254 255 self.assert_invalid( 256 jin.create_record( 257 'fish-456', 258 'fish', 259 {})) 260 261 self.narrate( 262 ''' 263 He said his fish record should have type `fish`, but that 264 type doesn't exist yet. He needs to create the `fish` 265 record type first. 266 ''') 267 268 self.narrate( 269 ''' 270 Jin creates the record type `fish` with properties {}. 271 Subsequent attempts to create a type with that name will 272 fail. 273 ''', 274 ['species', 'weight', 'temperature', 275 'location', 'is_trout', 'how_big']) 276 277 self.assert_valid( 278 jin.create_record_type( 279 'fish', 280 ('species', PropertySchema.STRING, 281 { 'required': True, 'fixed': True }), 282 ('weight', PropertySchema.NUMBER, 283 { 'required': True, 'unit': 'grams' }), 284 ('temperature', PropertySchema.NUMBER, 285 { 'number_exponent': -3, 'delayed': True, 'unit': 'Celsius' }), 286 ('location', PropertySchema.STRUCT, 287 { 'struct_properties': [ 288 ('hemisphere', PropertySchema.STRING, {}), 289 ('gps', PropertySchema.STRUCT, 290 { 'struct_properties': [ 291 ('latitude', PropertySchema.NUMBER, {}), 292 ('longitude', PropertySchema.NUMBER, {}) 293 ] }) 294 ]}), 295 ('is_trout', PropertySchema.BOOLEAN, {}), 296 ('how_big', PropertySchema.ENUM, 297 { 'enum_options': ['big', 'bigger', 'biggest']}), 298 )) 299 300 self.assert_invalid( 301 jin.create_record_type( 302 'fish', 303 ('blarg', PropertySchema.NUMBER, { 'required': True }), 304 )) 305 306 self.narrate( 307 ''' 308 Now that the `fish` record type is created, Jin can create 309 his fish record. 310 ''') 311 312 self.assert_invalid( 313 jin.create_record( 314 'fish-456', 315 'fish', 316 {})) 317 318 self.narrate( 319 ''' 320 This time, Jin's attempt to create a record failed because 321 he neglected to include initial property values for the 322 record type's require properties. In this case, a `fish` 323 record cannot be created without value for the properties 324 `species` and `weight`. 325 ''') 326 327 self.assert_invalid( 328 jin.create_record( 329 'fish-456', 330 'fish', 331 {'species': 'trout', 'weight': 'heavy'})) 332 333 self.narrate( 334 ''' 335 Jin gave the value 'heavy' for the property `weight`, but the 336 type for that property is required to be int. When he 337 provides a string value for `species` and an int value for 338 `weight`, the record can be successfully created. 339 ''') 340 341 self.assert_invalid( 342 jin.create_record( 343 'fish-456', 344 'fish', 345 {'species': 'trout', 'how_big': Enum('small')})) 346 347 self.narrate( 348 ''' 349 Jin gave the value 'small' for 'how_big', but only 'big', 'bigger', 350 and 'biggest' are valid options for this enum. 351 ''') 352 353 self.assert_invalid( 354 jin.create_record( 355 'fish-456', 356 'fish', 357 {'species': 'trout', 'location': { 358 'hemisphere': 'north', 359 'gps': {'lat': 45, 'long': 45} 360 }})) 361 362 self.narrate( 363 ''' 364 Jin used the keys "lat" and "long" for the gps, but the schema 365 specified "latitude" and "longitude". 366 ''') 367 368 self.assert_invalid( 369 jin.create_record( 370 'fish-456', 371 'fish', 372 {'species': 'trout', 'weight': 5, 373 'temperature': -1000})) 374 375 self.narrate( 376 ''' 377 Jin gave an initial value for temperature, but temperature is a 378 "delayed" property, and may not be set at creation time. 379 ''') 380 381 self.assert_valid( 382 jin.create_record( 383 'fish-456', 384 'fish', 385 {'species': 'trout', 'weight': 5, 386 'is_trout': True, 'how_big': Enum('bigger'), 387 'location': { 388 'hemisphere': 'north', 389 'gps': {'longitude': 45, 'latitude': 45}}})) 390 391 self.narrate( 392 ''' 393 Jin updates the fish record's temperature. Updates, of 394 course, can only be made to the type-specified properties 395 of existing records, and the type of the update value must 396 match the type-specified type. 397 ''') 398 399 self.assert_valid( 400 jin.update_properties( 401 'fish-456', 402 {'temperature': -1414})) 403 404 self.assert_invalid( 405 jin.update_properties( 406 'fish-456', 407 {'temperature': '-1414'})) 408 409 self.assert_invalid( 410 jin.update_properties( 411 'fish-456', 412 {'splecies': 'tluna'})) 413 414 self.assert_invalid( 415 jin.update_properties( 416 'fish-???', 417 {'species': 'flounder'})) 418 419 self.narrate( 420 ''' 421 Jin attempts to update species, but it is a static property. 422 ''') 423 424 self.assert_invalid( 425 jin.update_properties( 426 'fish-456', 427 {'species': 'bass'})) 428 429 self.narrate( 430 ''' 431 Jin updates is_trout. 432 ''') 433 434 self.assert_valid( 435 jin.update_properties( 436 'fish-456', 437 {'is_trout': False})) 438 439 self.narrate( 440 ''' 441 Jin updates how_big. 442 ''') 443 444 self.assert_valid( 445 jin.update_properties( 446 'fish-456', 447 {'how_big': Enum('biggest')})) 448 449 self.narrate( 450 ''' 451 Jin updates the temperature again. 452 ''') 453 454 self.assert_valid( 455 jin.update_properties( 456 'fish-456', 457 {'temperature': -3141})) 458 459 self.assert_invalid( 460 jin.update_properties( 461 'fish-456', 462 {'location': { 463 'hemisphere': 'north', 464 'gps': {'latitude': 50, 'longitude': False}}})) 465 466 self.assert_invalid( 467 jin.update_properties( 468 'fish-456', 469 {'location': {'hemisphere': 'south'}})) 470 471 self.assert_valid( 472 jin.update_properties( 473 'fish-456', 474 {'location': { 475 'hemisphere': 'south', 476 'gps': {'latitude': 50, 'longitude': 45}}})) 477 478 self.narrate( 479 ''' 480 Jin gets tired of sending fish updates himself, so he decides to 481 get an autonomous IoT sensor to do it for him. 482 ''') 483 484 sensor_stark = SupplyChainClient() 485 486 self.assert_invalid( 487 sensor_stark.update_properties( 488 'fish-456', 489 {'temperature': -3141})) 490 491 self.narrate( 492 ''' 493 To get his sensor to be able to send updates, Jin has to send it a 494 proposal to authorize it as a reporter for some properties 495 for his record. 496 ''') 497 498 self.assert_invalid( 499 jin.create_proposal( 500 record_id='fish-456', 501 receiving_agent=sensor_stark.public_key, 502 role=Proposal.REPORTER, 503 properties=['temperature'], 504 )) 505 506 self.narrate( 507 ''' 508 This requires that the sensor be registered as an agent, 509 just like Jin. 510 ''') 511 512 self.assert_valid( 513 sensor_stark.create_agent( 514 'sensor-stark')) 515 516 self.assert_valid( 517 jin.create_proposal( 518 record_id='fish-456', 519 receiving_agent=sensor_stark.public_key, 520 role=Proposal.REPORTER, 521 properties=['temperature'], 522 )) 523 524 self.assert_invalid( 525 sensor_stark.update_properties( 526 'fish-456', 527 {'temperature': -3141})) 528 529 self.narrate( 530 ''' 531 There's one last step before the sensor can send updates: 532 it has to "accept" the proposal. 533 ''') 534 535 self.assert_valid( 536 sensor_stark.answer_proposal( 537 record_id='fish-456', 538 role=Proposal.REPORTER, 539 response=AnswerProposalAction.ACCEPT, 540 )) 541 542 self.assert_invalid( 543 sensor_stark.answer_proposal( 544 record_id='fish-456', 545 role=Proposal.REPORTER, 546 response=AnswerProposalAction.ACCEPT, 547 )) 548 549 self.narrate( 550 ''' 551 Now that it is an authorized reporter, the sensor can 552 start sending updates. 553 ''') 554 555 for i in range(5): 556 self.assert_valid( 557 sensor_stark.update_properties( 558 'fish-456', 559 {'temperature': -i * 1000})) 560 561 self.narrate( 562 ''' 563 Jin would like to sell his fish to Sun, a fish dealer. Of 564 course, Sun must also be registered as an agent. After she 565 registers, Jin can propose to transfer ownership to her 566 (with payment made off-chain). 567 ''') 568 569 sun = SupplyChainClient() 570 571 self.assert_invalid( 572 jin.create_proposal( 573 record_id='fish-456', 574 role=Proposal.OWNER, 575 receiving_agent=sun.public_key, 576 )) 577 578 self.assert_valid( 579 sun.create_agent(name='Sun Kwon')) 580 581 self.assert_valid( 582 jin.create_proposal( 583 record_id='fish-456', 584 role=Proposal.OWNER, 585 receiving_agent=sun.public_key, 586 )) 587 588 self.narrate( 589 ''' 590 Jin has second thoughts and cancels his proposal. Sun and 591 her lawyers convince him to change his mind back, so he 592 opens a new proposal. 593 ''') 594 595 self.assert_valid( 596 jin.answer_proposal( 597 record_id='fish-456', 598 role=Proposal.OWNER, 599 response=AnswerProposalAction.CANCEL, 600 receiving_agent=sun.public_key, 601 )) 602 603 self.assert_invalid( 604 sun.answer_proposal( 605 record_id='fish-456', 606 role=Proposal.OWNER, 607 response=AnswerProposalAction.ACCEPT, 608 )) 609 610 self.assert_valid( 611 jin.create_proposal( 612 record_id='fish-456', 613 role=Proposal.OWNER, 614 receiving_agent=sun.public_key, 615 )) 616 617 self.assert_valid( 618 sun.answer_proposal( 619 record_id='fish-456', 620 role=Proposal.OWNER, 621 response=AnswerProposalAction.ACCEPT, 622 )) 623 624 self.assert_invalid( 625 sun.answer_proposal( 626 record_id='fish-456', 627 role=Proposal.OWNER, 628 response=AnswerProposalAction.ACCEPT, 629 )) 630 631 self.assert_invalid( 632 jin.create_proposal( 633 record_id='fish-456', 634 role=Proposal.OWNER, 635 receiving_agent=sun.public_key, 636 )) 637 638 self.narrate( 639 ''' 640 Upon transfer of ownership, Sun became a reporter on all 641 of the record's properties and Jin reporter authorization 642 was revoked. Jin's sensor remains authorized. 643 ''') 644 645 self.assert_invalid( 646 jin.update_properties( 647 'fish-456', 648 {'temperature': -6282})) 649 650 self.assert_valid( 651 sun.update_properties( 652 'fish-456', 653 {'temperature': -6282})) 654 655 self.assert_valid( 656 sensor_stark.update_properties( 657 'fish-456', 658 {'temperature': -7071})) 659 660 self.narrate( 661 ''' 662 Sun decides to revoke the reporter authorization of Jin's 663 sensor and authorize her own sensor. 664 ''') 665 666 sensor_dollars = SupplyChainClient() 667 668 self.assert_valid( 669 sensor_dollars.create_agent( 670 'sensor-dollars')) 671 672 self.assert_valid( 673 sun.create_proposal( 674 record_id='fish-456', 675 receiving_agent=sensor_dollars.public_key, 676 role=Proposal.REPORTER, 677 properties=['temperature'], 678 )) 679 680 self.assert_valid( 681 sensor_dollars.answer_proposal( 682 record_id='fish-456', 683 role=Proposal.REPORTER, 684 response=AnswerProposalAction.ACCEPT, 685 )) 686 687 self.assert_valid( 688 sensor_dollars.update_properties( 689 'fish-456', 690 {'temperature': -8485})) 691 692 self.assert_valid( 693 sun.revoke_reporter( 694 record_id='fish-456', 695 reporter_id=sensor_stark.public_key, 696 properties=['temperature'])) 697 698 self.assert_invalid( 699 sensor_stark.update_properties( 700 'fish-456', 701 {'temperature': -9899})) 702 703 self.narrate( 704 ''' 705 Sun wants to finalize the record to prevent any further updates. 706 ''') 707 708 self.assert_invalid( 709 sun.finalize_record('fish-456')) 710 711 self.narrate( 712 ''' 713 In order to finalize a record, the owner and the custodian 714 must be the same person. Sun is the owner of the fish in a 715 legal sense, but Jin is still the custodian (that is, he 716 has physical custody of it). Jin hands over the fish. 717 ''') 718 719 self.assert_valid( 720 jin.create_proposal( 721 record_id='fish-456', 722 role=Proposal.CUSTODIAN, 723 receiving_agent=sun.public_key, 724 )) 725 726 self.assert_invalid( 727 jin.create_proposal( 728 record_id='fish-456', 729 role=Proposal.CUSTODIAN, 730 receiving_agent=sun.public_key, 731 )) 732 733 self.assert_valid( 734 sun.answer_proposal( 735 record_id='fish-456', 736 role=Proposal.CUSTODIAN, 737 response=AnswerProposalAction.ACCEPT, 738 )) 739 740 self.assert_valid( 741 sun.finalize_record('fish-456')) 742 743 self.assert_invalid( 744 sun.finalize_record('fish-456')) 745 746 self.assert_invalid( 747 sun.update_properties( 748 'fish-456', 749 {'temperature': -2828})) 750 751 ## 752 753 agents_endpoint = jin.get_agents() 754 755 log_json(agents_endpoint) 756 757 agents_assertion = [ 758 [ 759 agent['name'], 760 agent['owns'], 761 agent['custodian'], 762 agent['reports'], 763 ] 764 for agent in 765 sorted( 766 agents_endpoint, 767 key=lambda agent: agent['name'] 768 ) 769 ] 770 771 self.assertEqual( 772 agents_assertion, 773 [ 774 [ 775 'Jin Kwon', 776 [], 777 [], 778 [], 779 ], 780 [ 781 'Sun Kwon', 782 ['fish-456'], 783 ['fish-456'], 784 ['fish-456'], 785 ], 786 [ 787 'sensor-dollars', 788 [], 789 [], 790 ['fish-456'], 791 ], 792 [ 793 'sensor-stark', 794 [], 795 [], 796 [], 797 ], 798 ] 799 ) 800 801 jin.post_user('jin') 802 803 agent_auth_assertion = jin.get_agent(jin.public_key) 804 805 log_json(agent_auth_assertion) 806 807 self.assertEqual( 808 agent_auth_assertion, 809 { 810 'email': 'jin@website.com', 811 'encryptedKey': jin.private_key, 812 'name': 'Jin Kwon', 813 'publicKey': jin.public_key, 814 'username': 'jin', 815 } 816 ) 817 818 agent_no_auth_assertion = sun.get_agent(jin.public_key) 819 820 log_json(agent_no_auth_assertion) 821 822 self.assertEqual( 823 agent_no_auth_assertion, 824 { 825 'name': 'Jin Kwon', 826 'publicKey': jin.public_key, 827 } 828 ) 829 830 get_record_property = jin.get_record_property( 831 'fish-456', 'temperature') 832 833 log_json(get_record_property) 834 835 self.assertIn('dataType', get_record_property) 836 self.assertEqual(get_record_property['dataType'], 'NUMBER') 837 838 self.assertIn('name', get_record_property) 839 self.assertEqual(get_record_property['name'], 'temperature') 840 841 self.assertIn('recordId', get_record_property) 842 self.assertEqual(get_record_property['recordId'], 'fish-456') 843 844 self.assertIn('value', get_record_property) 845 846 self.assertIn('reporters', get_record_property) 847 self.assertEqual(len(get_record_property['reporters']), 2) 848 849 self.assertIn('updates', get_record_property) 850 self.assertEqual(len(get_record_property['updates']), 10) 851 852 for update in get_record_property['updates']: 853 self.assertIn('timestamp', update) 854 self.assertIn('value', update) 855 self.assertIn('reporter', update) 856 857 reporter = update['reporter'] 858 self.assertEqual(len(reporter), 2) 859 self.assertIn('name', reporter) 860 self.assertIn('publicKey', reporter) 861 862 get_record = jin.get_record('fish-456') 863 864 log_json(get_record) 865 866 self.assert_record_attributes(get_record) 867 868 self.assertEqual(get_record['custodian'], sun.public_key) 869 self.assertEqual(get_record['owner'], sun.public_key) 870 self.assertEqual(get_record['recordId'], 'fish-456') 871 872 for attr in ('species', 873 'temperature', 874 'weight', 875 'is_trout', 876 'how_big', 877 'location'): 878 self.assertIn(attr, get_record['updates']['properties']) 879 880 get_records = jin.get_records() 881 882 log_json(get_records) 883 884 for record in get_records: 885 self.assert_record_attributes(record) 886 887 self.assertEqual( 888 jin.get_agent( 889 public_key=jin.public_key, 890 fields=[ 891 'publicKey', 892 'name', 893 'email', 894 ] 895 ), 896 { 897 'email': 'jin@website.com', 898 'name': 'Jin Kwon', 899 'publicKey': jin.public_key, 900 } 901 ) 902 903 self.assertEqual( 904 sorted( 905 jin.get_agents( 906 fields=[ 907 'name', 908 'owns', 909 ] 910 ), 911 key=lambda agent: agent['name'] 912 ), 913 [ 914 { 915 'name': 'Jin Kwon', 916 'owns': [], 917 }, 918 { 919 'name': 'Sun Kwon', 920 'owns': ['fish-456'], 921 }, 922 { 923 'name': 'sensor-dollars', 924 'owns': [], 925 }, 926 { 927 'name': 'sensor-stark', 928 'owns': [], 929 }, 930 ] 931 ) 932 933 self.assertEqual( 934 jin.get_record( 935 record_id='fish-456', 936 omit=[ 937 'properties', 938 'proposals', 939 'updates', 940 ] 941 ), 942 { 943 'custodian': sun.public_key, 944 'final': True, 945 'owner': sun.public_key, 946 'recordId': 'fish-456', 947 } 948 ) 949 950 self.assertEqual( 951 jin.get_record_property( 952 record_id='fish-456', 953 property_name='weight', 954 omit=[ 955 'recordId', 956 'reporters', 957 'updates', 958 'value', 959 ] 960 ), 961 { 962 'dataType': 'NUMBER', 963 'name': 'weight', 964 } 965 ) 966 967 def assert_record_attributes(self, record): 968 for attr in ('custodian', 969 'owner', 970 'properties', 971 'proposals', 972 'recordId', 973 'updates'): 974 self.assertIn(attr, record) 975 976 for prop in record['properties']: 977 for attr in ('name', 978 'reporters', 979 'type', 980 'value'): 981 self.assertIn(attr, prop) 982 983 for prop in record['proposals']: 984 for attr in ('issuingAgent', 985 'properties', 986 'role'): 987 self.assertIn(attr, prop) 988 989 for attr in ('custodians', 990 'owners', 991 'properties'): 992 self.assertIn(attr, record['updates']) 993 994 for associated_agent in ('custodians', 'owners'): 995 for attr in ('agentId', 'timestamp'): 996 for entry in record['updates'][associated_agent]: 997 self.assertIn(attr, entry) 998 999 1000 def make_query_string(fields, omit): 1001 fields = '' if fields is None else '?fields=' + ','.join(fields) 1002 omit = '' if omit is None else '?omit=' + ','.join(omit) 1003 1004 return fields if fields else omit 1005 1006 1007 def log_json(msg): 1008 LOGGER.debug( 1009 json.dumps( 1010 msg, 1011 indent=4, 1012 sort_keys=True))