def test_create_request_with_content_claims( client: Client, endorser: PrivateIdentity, mocked_requests_200: respx.MockTransport, entity: Entity, ) -> None: """Create endorsement request provided by a 3rd party endorser.""" claims = [b"claim-1", b"claim-2"] # Create endorsement request with authorisation of endorser content, authorisation = endorser.endorse(entity, claims) # This will also add the subject holders authorisation client.put( entity, claims=claims, content=content, authorisations=[authorisation], create_claims=True # endorse=True # TODO - provide test that this does not have any effect ) http_request, _ = mocked_requests_200["create_entity"].calls[0] claims_header = json.loads( iov42_decode(http_request.headers["x-iov42-claims"])) assert claims_header == {hashed_claim(c): c.decode() for c in claims}
def read_endorsement( ctx: click.core.Context, identity: str, endorser_id: str, entity_type: str, entity_id: str, asset_type_id: str, claim: str, ) -> None: """Read specific endorsement of a claim (identity, asset type, unique asset).""" if entity_type.lower() == "asset": entity = Asset(asset_type_id=asset_type_id, asset_id=entity_id) else: raise NotImplementedError # pragma: no cover id = _load_identity(identity) client = Client(ctx.obj["url"], id) response = client.get( entity, claim=claim.encode(), endorser_id=endorser_id, request_id=ctx.obj["request_id"], ) print(f"{entity!r}") print(f"{claim!r}") print(f"endorser: {response.endorser_id!r}") # type: ignore[union-attr]
def test_raise_identity_already_exists_2(client: Client) -> None: """Raise exception when an identity (with an other key) already exists.""" client.identity = PrivateIdentity( CryptoProtocol.SHA256WithECDSA.generate_private_key(), "test-1234") with respx.mock(base_url=PLATFORM_URL) as respx_mock: respx_mock.put( re.compile("/api/v1/requests/.*$"), status_code=400, content= ('{"errors":[{"errorCode":2503,"errorType":"Authorisation",' '"message":"Signature L-IjTeba3wvJn4hHR40GPCG-H7iIeDWOzBo3hCK7x1mLZgif' "SdgR-YVxOZtvPzHaI86WdhIL3y-sNOwYUf2c0j7OfT31dAX71W9le-Cp2Mx1PgjjqI09f" "i0Nku-h5lgipQ07VKAm3gUx0foeG9GdDQe_I85QuCqtJsaAXWDVc8r0NeWpa3dnQEflIm" "W0-gecjO6pYDeyXPALcvp9h8Q_TxkuGVvreqpWvgKzdPMlXHMbN3wYoLNNLM3gpqrqAp" "Eze1aTqtlK6gCQUuhsJlKe4Bb2Nj8MRxXXXNpxIJqjJHM0IRps5J0U8gsnEEcny8Zf0tB" 'h7NGkTteNv554QUbNVA cannot be verified with public credentials of identity test-1234."},' '{"errorCode":2503,"errorType":"Authorisation","message":"Signature ' "L2PIREIx1MZsjV-j0fSMoN3u1eHP2wyqUpAs1mOWdp8k8yrnoBTbyH2Uxw8_9zYTzDHrz" "rI16fNKeRFuLlHosWqzoUf41M0Nip5zbW6gmPYiL05AWPdH1pg9qS-cgQa9IFXiMUkZh9" "EZltT7HHl9aRn35kcwoJYAoPm96Up1YPI0JWISx1iXXEAcxVOA1N_k-l0tT5Tb7lWNOI4" "5eh6flW_vVEeBQDjQhkl94rlP3qDFlDYZ9HZS2A3lTkiIo6MsU57pxeTD9FqwZ8uofJ3O" "Yx05TJKl106GPsscf2mnpnQGEzgS20QsJyqUs_u7dpZbAcjfBsaHucVz8gwkz_PoNg " 'cannot be verified with public credentials of identity test-1234."},' '{"errorCode":2602,"errorType":"AssetExistence",' '"message":"Another identity with address test-1234 already exists"}],' '"requestId":"23343439","proof":"/api/v1/proofs/23343439"}'), ) with pytest.raises(EntityAlreadyExists) as excinfo: client.put(client.identity.public_identity, request_id="1234567") assert str(excinfo.value) == "identity 'test-1234' already exists" assert excinfo.value.request_id == "1234567"
def test_create_request_with_content( client: Client, endorser: PrivateIdentity, mocked_requests_200: respx.MockTransport, entity: Entity, ) -> None: """Create endorsement request provided by a 3rd party endorser.""" claims = [b"claim-1", b"claim-2"] # Create endorsement request with authorisation of endorser content, authorisation = endorser.endorse(entity, claims) # This will also add the subject holders authorisation client.put( entity, claims=claims, content=content, authorisations=[authorisation], # endorse=True # TODO - provide test that this does not have any effect ) http_request, _ = mocked_requests_200["create_entity"].calls[0] request_id = json.loads(content.decode())["requestId"] assert http_request.url.path.rsplit("/", 1)[1] == request_id claims_header = json.loads( iov42_decode(http_request.headers["x-iov42-claims"])) assert claims_header == {} authorisations = json.loads( iov42_decode(http_request.headers["x-iov42-authorisations"].encode())) expected_identities = [a["identityId"] for a in authorisations] assert client.identity.identity_id in expected_identities assert endorser.identity_id in expected_identities
def test_raises_claims_missing(client: Client, entity: Entity) -> None: """Raise TyepError if no claims are provided for endorsement.""" with pytest.raises(TypeError) as excinfo: client.put(entity, endorse=True) assert (str( excinfo.value ) == "missing required keyword argument needed for endorsement: 'claims'")
def create_asset_type(ctx: click.core.Context, identity: str, asset_type_id: str, scale: int) -> None: """Create an asset type.""" asset_type = AssetType(asset_type_id) id = _load_identity(identity) client = Client(ctx.obj["url"], id) _ = client.put(asset_type, request_id=ctx.obj["request_id"]) print(f"asset_type_id: {asset_type_id}")
def create_asset(ctx: click.core.Context, identity: str, asset_type_id: str, asset_id: str) -> None: """Create an asset.""" asset = Asset(asset_type_id=asset_type_id, asset_id=asset_id) id = _load_identity(identity) client = Client(ctx.obj["url"], id) _ = client.put(asset, request_id=ctx.obj["request_id"]) print(f"asset_id: {asset}")
def test_propagate_close(identity: PrivateIdentity) -> None: """Propagate close to the wrapped HTTP client implementation.""" with mock.patch.object(HttpClient, "close") as mocked_close: client = Client("https://example.org", identity) client.close() mocked_close.assert_called_once_with()
def register_product(client: Client, tag_type: AssetType, product: Tuple[str, str]) -> None: """Register product on iov42 platform and add product information as claim.""" tag_id, claim = product tag = Asset(asset_id=tag_id, asset_type_id=tag_type.asset_type_id) client.put(tag) print(f"Created tag: {tag_id}") client.put(tag, claims=[claim.encode()], endorse=True) print(f"Tag [{tag_id}]: added enrosement on claim '{claim}'")
def existing_asset_claims(alice_client: Client, existing_asset: Asset) -> List[bytes]: """Return a list of claims endorsed against an asset owned by Alice.""" claims = [b"asset-claim-1", b"asset-claim-2"] alice_client.put(existing_asset, claims=claims, endorse=True, create_claims=True) return claims
def create_identity(ctx: click.core.Context, identity_id: str, crypto_protocol: str) -> None: """Create an identity.""" private_key = generate_private_key(crypto_protocol) identity = PrivateIdentity(private_key, identity_id) client = Client(ctx.obj["url"], identity) _ = client.put(identity.public_identity, request_id=ctx.obj["request_id"]) print(_identity_json(identity))
def test_invalid_request_id(client: Client, entity: Entity, invalid_request_id: str) -> None: """Raise exception if the provided request ID contains invalid charatcers.""" with pytest.raises(ValueError) as excinfo: client.put(entity, request_id=invalid_request_id) # No request is sent assert not respx.calls assert ( str(excinfo.value) == f"invalid identifier '{invalid_request_id}' - valid characters are [a-zA-Z0-9._\\-+]" )
def test_read_unique_asset_endorsement_header( client: Client, endorser: PublicIdentity, mocked_requests_200: respx.MockTransport, ) -> None: """GET request has only x-iov42-authentication header.""" asset = Asset(asset_type_id="1234567") client.get(asset, claim=b"claim-1", endorser_id=endorser.identity_id) assert mocked_requests_200["read_asset_endorsement"].call_count == 1 http_request, _ = mocked_requests_200["read_asset_endorsement"].calls[0] assert "x-iov42-authentication" in [*http_request.headers]
def test_raise_on_request_error(client: Client) -> None: """If raise exception on a request error.""" respx.put( re.compile(PLATFORM_URL + "/api/v1/requests/.*$"), content=httpcore.ConnectError(), ) # TODO: do we really want to leak httpx to our clients? # We could catch all exception thrown by httpx, wrap it in a few library # exceptions and rethrow those. with pytest.raises(httpx.ConnectError): client.put(client.identity.public_identity)
def test_get_node_id( client: Client, mocked_requests_200: respx.MockTransport, ) -> None: """Retrieve node_id on the very first GET request.""" client.get( Asset(asset_type_id="1234567"), claim=b"claim-1", endorser_id=client.identity.identity_id, ) assert client.node_id == "node-1" assert mocked_requests_200["read_node_info"].call_count == 1
def test_raise_duplicate_request_id(client: Client) -> None: """Raise exception when the request_id already exists.""" with respx.mock(base_url=PLATFORM_URL) as respx_mock: respx_mock.put( re.compile("/api/v1/requests/.*$"), status_code=409, content= ('{"errors":[{"errorCode":2701,"errorType":"RequestId",' '"message":"Found duplicate request id"}],"requestId":"1234567"}' ), ) with pytest.raises(DuplicateRequestId) as excinfo: client.put(client.identity.public_identity, request_id="1234567") assert str(excinfo.value) == "request ID already exists" assert excinfo.value.request_id == "1234567"
def test_authentication_header( client: Client, endorser: PublicIdentity, mocked_requests_200: respx.MockTransport, ) -> None: """The x-iov42-authentication header is signed by the identity.""" asset = Asset(asset_type_id="1234567") client.get(asset, claim=b"claim-1", endorser_id=endorser.identity_id) http_request, _ = mocked_requests_200["read_asset_endorsement"].calls[0] authentication = json.loads( iov42_decode(http_request.headers["x-iov42-authentication"].encode())) assert len(authentication) == 3 assert authentication["identityId"] == client.identity.identity_id assert authentication[ "protocolId"] == client.identity.private_key.protocol.name
def test_response(client: Client, mocked_requests_200: respx.MockTransport, entity: Entity) -> None: """Platform response to the create an entity request.""" request_id = str(uuid.uuid4()) response = client.put(entity, request_id=request_id) assert response.proof == "/api/v1/proofs/" + request_id assert len(response.resources) == 1 # type: ignore[union-attr]
def test_header_content_type(client: Client, mocked_requests_200: respx.MockTransport, entity: Entity) -> None: """PUT request content-type is JSON.""" _ = client.put(entity) http_request, _ = mocked_requests_200["create_entity"].calls[0] assert http_request.headers["content-type"] == "application/json"
def test_context_manager(identity: PrivateIdentity) -> None: """Close connection when leaving the context manager.""" with mock.patch.object(HttpClient, "close") as mocked_close: with Client("https://example.org", identity) as _: pass mocked_close.assert_called_once_with()
def test_create_asset_claims_with_endorsement(alice_client: Client, existing_asset: Asset) -> None: """Create asset claims and (self-) endorsements on an unique asset all at once.""" claims = [b"claim-1", b"claim-2"] response = alice_client.put(existing_asset, claims=claims, endorse=True, create_claims=True) prefix = "/".join(( "/api/v1/asset-types", existing_asset.asset_type_id, "assets", existing_asset.asset_id, "claims", )) # Affected resources: for each endorsements we also created the claim. assert len( response.resources) == 2 * len(claims) # type: ignore[union-attr] for c in [hashed_claim(c) for c in claims]: assert "/".join( (prefix, c)) in response.resources # type: ignore[union-attr] assert ("/".join( (prefix, c, "endorsements", alice_client.identity.identity_id)) in response.resources # type: ignore[union-attr] )
def main() -> None: """Create identity, asset type and register products.""" product_data = read_product_data("nfc-tags.csv") # Usually we would store the identity (ID and key) on a safe place. manufacturer = PrivateIdentity( CryptoProtocol.SHA256WithECDSA.generate_private_key()) with Client(cfg.iov42_platform["url"], manufacturer) as client: # Create the identity client.put(manufacturer.public_identity) print(f"Created manufacturer identity: {manufacturer.identity_id}") # Create the asset typ used for the NFC tags. tag_type = AssetType() client.put(tag_type) print(f"Created tag asset type: {tag_type}") # Register the NFC tags on the distributed ledger in parallel. with ThreadPoolExecutor(max_workers=20) as executor: _ = executor.map( register_product, [client] * len(product_data), [tag_type] * len(product_data), product_data, ) executor.shutdown(wait=True)
def test_raise_identity_already_exists(client: Client) -> None: """Raise exception when an identity already exists.""" client.identity = PrivateIdentity( CryptoProtocol.SHA256WithECDSA.generate_private_key(), "test-1234") with respx.mock(base_url=PLATFORM_URL) as respx_mock: respx_mock.put( re.compile("/api/v1/requests/.*$"), status_code=400, content= ('{"errors":[{"errorCode":2602,"errorType":"AssetExistence",' '"message":"Another identity with address test-1234 already exists"}],' '"requestId":"1234567","proof":"/api/v1/proofs/23343456"}'), ) with pytest.raises(EntityAlreadyExists) as excinfo: client.put(client.identity.public_identity, request_id="1234567") assert str(excinfo.value) == "identity 'test-1234' already exists" assert excinfo.value.request_id == "1234567"
def test_authentication_header_signature( client: Client, endorser: PublicIdentity, mocked_requests_200: respx.MockTransport, ) -> None: """Signature of x-iov42-authentication header is the signed request URL.""" asset = Asset(asset_type_id="1234567") client.get(asset, claim=b"claim-1", endorser_id=endorser.identity_id) http_request, _ = mocked_requests_200["read_asset_endorsement"].calls[0] authentication = json.loads( iov42_decode(http_request.headers["x-iov42-authentication"].encode())) try: content = http_request.url.raw_path client.identity.verify_signature(authentication["signature"], content) except InvalidSignature: pytest.fail("Signature verification failed")
def test_call_to_put_endpoint(client: Client, mocked_requests_200: respx.MockTransport, entity: Entity) -> None: """Corret endpoint is called once.""" request_id = str(uuid.uuid4()) _ = client.put(entity, request_id=request_id) http_request, _ = mocked_requests_200["create_entity"].calls[0] assert str(http_request.url).rsplit("/", 1)[1] == request_id
def test_close_on_del(identity: PrivateIdentity) -> None: """Resources are freed on deleting the object.""" with mock.patch.object(HttpClient, "close") as mocked_close: client = Client("https://example.org", identity) del client mocked_close.assert_called_once_with()
def test_node_id_cached( client: Client, mocked_requests_200: respx.MockTransport, ) -> None: """node_id is cached after the first GET request.""" client.get( Asset(asset_type_id="1234567"), claim=b"claim-1", endorser_id=client.identity.identity_id, ) client.get( Asset(asset_type_id="1234567"), claim=b"claim-1", endorser_id=client.identity.identity_id, ) assert mocked_requests_200["read_node_info"].call_count == 1
def test_create_asset_type(alice_client: Client, asset_type: AssetType) -> None: """Create an asset types on an iov42 platform.""" response = alice_client.put(asset_type) assert ("/".join(("/api/v1/asset-types", asset_type.asset_type_id )) == response.resources[0] # type: ignore[union-attr] )
def test_response_identity( client: Client, mocked_requests_200: respx.MockTransport, ) -> None: """Platform response to the request to create an identity.""" response = client.put(client.identity.public_identity) assert response.resources == [ # type: ignore[union-attr] "/api/v1/identities/" + client.identity.identity_id ]
def test_iov42_headers(client: Client, mocked_requests_200: respx.MockTransport, entity: Entity) -> None: """Authentication and authorisations are created with the request.""" _ = client.put(entity) http_request, _ = mocked_requests_200["create_entity"].calls[0] assert "x-iov42-authorisations" in [*http_request.headers] assert "x-iov42-authentication" in [*http_request.headers]