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))