diff --git a/duniterpy/api/ws2p/requests.py b/duniterpy/api/ws2p/requests.py new file mode 100644 index 0000000000000000000000000000000000000000..66f5b60e77e627b120fbbfa791fd7915ce957176 --- /dev/null +++ b/duniterpy/api/ws2p/requests.py @@ -0,0 +1,52 @@ +import json +import re +from typing import Optional + +from duniterpy.helpers import get_ws2p_challenge +from duniterpy.api.bma.blockchain import BLOCK_SCHEMA + +ERROR_RESPONSE_SCHEMA = { + "type": "object", + "properties": { + "resId": { + "type": "string", + "pattern": "^[0-9,a-z,A-Z]{8}$" + }, + "err": {"type": "string"} + }, + "required": ["resId", "err"] +} + + +def get_current(request_id: Optional[str] = None) -> str: + """ + Return ws2p getCurrent() request as json string + + :return: + """ + + if request_id is None: + request_id = get_ws2p_challenge()[:8] + else: + if not re.fullmatch("^[0-9a-zA-Z]{8}$", request_id): + raise Exception("Invalid ws2p request unique id") + return json.dumps({ + "reqId": request_id, + "body": { + "name": "CURRENT", + "params": {} + } + }) + + +GET_CURRENT_RESPONSE_SCHEMA = { + "type": "object", + "properties": { + "resId": { + "type": "string", + "pattern": "^[0-9,a-z,A-Z]{8}$" + }, + "body": BLOCK_SCHEMA + }, + "required": ["resId", "body"] +} diff --git a/examples/request_ws2p.py b/examples/request_ws2p.py index da108805378a009e9e43258ae83048f84cab6c7c..6975054e73335016dd69f17b643e6d48b3dd87a8 100644 --- a/examples/request_ws2p.py +++ b/examples/request_ws2p.py @@ -4,8 +4,10 @@ from _socket import gaierror import aiohttp import jsonschema +from jsonschema import ValidationError from duniterpy.api import ws2p +from duniterpy.api.ws2p import requests from duniterpy.documents.ws2p.messages import Connect, Ack, Ok from duniterpy.api.client import Client, parse_text @@ -107,7 +109,8 @@ async def main(): Ok(CURRENCY, remote_connect_document.pubkey, connect_document.challenge, data["sig"]) print("Received OK message signature is valid") - + # do not wait for messages anymore + break except jsonschema.exceptions.ValidationError: pass @@ -118,6 +121,27 @@ async def main(): # Connection error print("Web socket connection error !") + # send ws2p request + print("Send getCurrent() request") + await ws.send_str(requests.get_current()) + # receive response as string + response = await ws.receive_str() + try: + # check response format + parse_text(response, requests.GET_CURRENT_RESPONSE_SCHEMA) + # if valid display response + print("Response: " + response) + except ValidationError as exception: + # if invalid response... + try: + # check error response format + parse_text(response, requests.ERROR_RESPONSE_SCHEMA) + # if valid, display error response + print("Error response: " + response) + except ValidationError as e: + # if invalid, display exception on response validation + print(exception) + # Close session await client.close() diff --git a/tests/api/ws2p/test_ws2p.py b/tests/api/ws2p/test_ws2p.py new file mode 100644 index 0000000000000000000000000000000000000000..07158c8085772dc33283d90c441cecc168458897 --- /dev/null +++ b/tests/api/ws2p/test_ws2p.py @@ -0,0 +1,83 @@ +import unittest + +import jsonschema + +from duniterpy.api.client import Client, parse_text +from duniterpy.api.endpoint import BMAEndpoint +from duniterpy.api.ws2p.network import heads, WS2P_HEADS_SCHEMA +from duniterpy.api.ws2p.requests import GET_CURRENT_RESPONSE_SCHEMA, ERROR_RESPONSE_SCHEMA +from tests.api.webserver import WebFunctionalSetupMixin, web + + +class TestWs2pHeads(WebFunctionalSetupMixin, unittest.TestCase): + + def test_block(self): + json_sample = { + "heads": [ + { + "message": "WS2POCAIC:HEAD:1:8iVdpXqFLCxGyPqgVx5YbFSkmWKkceXveRd2yvBKeARL:\ + 102102-000002C0694C7D373A78B095419C86584B81804CFB9641B7EBC3A18040B6FEE6:e66254bf:duniter:1.6.20:1", + "sig": "ZO5gSUMK6IaUEwU4K40nhuHOfnJ6Zfn8VS+4Ko2FM7t+mDsHf+3gDRT9PgV2p0fz81mF6jVYWpq2UYEsnK/gCg==", + "messageV2": "WS2POCAIC:HEAD:2:8iVdpXqFLCxGyPqgVx5YbFSkmWKkceXveRd2yvBKeARL:\ + 102102-000002C0694C7D373A78B095419C86584B81804CFB9641B7EBC3A18040B6FEE6:e66254bf:\ + duniter:1.6.20:1:15:14", + "sigV2": "ReXzbgUya9jo4dL/R4g19Y+RE9BGB0xDkw7mrBWoldlRLkq3KFyRkAf9VthVx1UUb/AINr3nxImZKVQiVH9+DQ==", + "step": 0 + }, + { + "message": "WS2POCAIC:HEAD:1:2ny7YAdmzReQxAayyJZsyVYwYhVyax2thKcGknmQy5nQ:\ + 102102-000002C0694C7D373A78B095419C86584B81804CFB9641B7EBC3A18040B6FEE6:a0a45ed2:duniter:1.6.21:1", + "sig": "pXLMmOpyEMdWihT183g/rnCvMzA2gHki5Cxg7rEl3psQu0RuK0ObCv5YFhmQnRlg+QZ1CWfbYEEbm3G1eGplAQ==", + "messageV2": "WS2POCAIC:HEAD:2:2ny7YAdmzReQxAayyJZsyVYwYhVyax2thKcGknmQy5nQ:\ + 102102-000002C0694C7D373A78B095419C86584B81804CFB9641B7EBC3A18040B6FEE6:a0a45ed2:\ + duniter:1.6.21:1:34:28", + "sigV2": "p5f7/KfQqjTaCYSMUXpjUDH7uF2DafetHNgphGzkOXgxM+Upeii0Fz2RFBwnZvN+Gjp81hAqSuH48PJP6HJSAw==", + "step": 1 + }, + { + "message": "WS2POCA:HEAD:1:GRBPV3Y7PQnB9LaZhSGuS3BqBJbSHyibzYq65kTh1nQ4:\ + 102102-000002C0694C7D373A78B095419C86584B81804CFB9641B7EBC3A18040B6FEE6:6d0e96f9:duniter:1.6.21:1", + "sig": "h9o1XBEV18gUzbvj1jdQB1M7U8ifZprIyVwLdlSQEfeG9WZLvZAjYzLGA2nD6h/9RkJLOJPzIQJXysHUHJ2dDQ==", + "messageV2": "WS2POCA:HEAD:2:GRBPV3Y7PQnB9LaZhSGuS3BqBJbSHyibzYq65kTh1nQ4:\ + 102102-000002C0694C7D373A78B095419C86584B81804CFB9641B7EBC3A18040B6FEE6:6d0e96f9:\ + duniter:1.6.21:1:20:20", + "sigV2": "VsyQmXOUYrfHWy0FeS4rJrIJCUBI+3BergbSYQ78icJWV6MQzZSw7Z+Yl7urujCYZriDQM76D6GW+6F0EELpBQ==", + "step": 2 + }, + ] + } + + jsonschema.validate(json_sample, WS2P_HEADS_SCHEMA) + + def test_ws2p_heads_bad(self): + async def handler(request): + await request.read() + return web.Response(body=b'{}', content_type='application/json') + + async def go(): + _, port, url = await self.create_server('GET', '/network/ws2p/heads', handler) + with self.assertRaises(jsonschema.ValidationError): + client = Client(BMAEndpoint("127.0.0.1", "", "", port)) + await client(heads) + await client.close() + + self.loop.run_until_complete(go()) + + def test_get_current_validation(self): + response_string = """{"resId":"cfe10cc4","body":{"wrong":false,"version":11,"number":367572, + "currency":"g1-test","hash":"000024399D612753E59D44415CFA61F3A663919110CD2EB8D30C93F49C61E07F", + "previousHash":"00007A2931B1B33351151058E8FE5C8368C9A7C6F13F37FEB92AA67B17B7EC46", + "issuer":"7BGpV28HzE6fyZtteuPmwHf6fHwHkQ9Ssww3Cxq82NnT", + "previousIssuer":"CrznBiyq8G4RVUprH9jHmAw1n1iuzw8y9FdJbrESnaX7","dividend":null,"time":1557357655, + "powMin":65,"unitbase":1,"membersCount":18,"issuersCount":4,"issuersFrame":21,"issuersFrameVar":0, + "identities":[],"joiners":[],"actives":[],"leavers":[],"revoked":[],"excluded":[],"certifications":[], + "transactions":[],"medianTime":1557355021,"fork":false,"parameters":"", + "inner_hash":"BA4D939F40D3B6D036659F6B7E0881D69054ADFF399533B44E1D5A9983235721", + "signature":"Ks0ugrWCZ/jBDyFQ77TnzTIKJrv2lBJKwQqVW64ZEESgD++J4pjPCEP0WDmcbm65VAomKbnkWOJsThdAIgj2DA==", + "nonce":10400000002073,"monetaryMass":144418724,"writtenOn":367572, + "written_on":"367572-000024399D612753E59D44415CFA61F3A663919110CD2EB8D30C93F49C61E07F"}} """ + response = parse_text(response_string, GET_CURRENT_RESPONSE_SCHEMA) + self.assertIsInstance(response, dict) + error_response_string = """{"resId":"cfe10cc4","err":"Error message"}""" + error_response = parse_text(error_response_string, ERROR_RESPONSE_SCHEMA) + self.assertIsInstance(error_response, dict)