diff --git a/agrirouter/auth/auth.py b/agrirouter/auth/auth.py index 6cf1902b..b33527c0 100644 --- a/agrirouter/auth/auth.py +++ b/agrirouter/auth/auth.py @@ -12,10 +12,10 @@ class Authorization(EnvironmentalService): TOKEN_KEY = "token" ERROR_KEY = "error" - def __init__(self, *args, **kwargs): - self._public_key = kwargs.pop("public_key") - self._private_key = kwargs.pop("private_key") - super(Authorization, self).__init__(*args, **kwargs) + def __init__(self, env, public_key, private_key): + self._public_key = public_key + self._private_key = private_key + super(Authorization, self).__init__(env) def get_auth_request_url(self, parameters: AuthUrlParameter) -> str: auth_parameters = parameters.get_parameters() @@ -34,4 +34,3 @@ def verify_auth_response(self, response, public_key=None): def _extract_query_params(query_params: str) -> dict: qp_pairs = parse_qs(query_params) return {k: v[0] for k, v in qp_pairs.items()} - diff --git a/agrirouter/auth/dto.py b/agrirouter/auth/dto.py new file mode 100644 index 00000000..55d1bbbb --- /dev/null +++ b/agrirouter/auth/dto.py @@ -0,0 +1,118 @@ +import json +from typing import Union + +from agrirouter.messaging.exceptions import WrongFieldError + + +class AuthorizationToken: + ACCOUNT = 'account' + REGISTRATION_CODE = 'regcode' + EXPIRES = 'expires' + + def __init__(self, + *, + account: str = None, + regcode: str = None, + expires: str = None + ): + self.account = account + self.regcode = regcode + self.expires = expires + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.ACCOUNT: + self.account = value + elif key == self.REGISTRATION_CODE: + self.regcode = value + elif key == self.EXPIRES: + self.expires = value + else: + raise WrongFieldError(f"Unknown field {key} for AuthorizationToken class") + + def get_account(self) -> str: + return self.account + + def set_account(self, account: str) -> None: + self.account = account + + def get_regcode(self) -> str: + return self.regcode + + def set_regcode(self, regcode: str) -> None: + self.regcode = regcode + + def get_expires(self) -> str: + return self.expires + + def set_expires(self, expires: str) -> None: + self.expires = expires + + +class AuthorizationResultUrl: + def __init__(self, + *, + state: str = None, + signature: str = None, + token: str = None, + decoded_token: AuthorizationToken = None, + error: str = None + ): + self.state = state + self.signature = signature + self.token = token + self.decoded_token = decoded_token + self.error = error + + def get_state(self) -> str: + return self.state + + def set_state(self, state: str) -> None: + self.state = state + + def get_signature(self) -> str: + return self.signature + + def set_signature(self, signature: str) -> None: + self.signature = signature + + def get_token(self) -> str: + return self.token + + def set_token(self, token: str) -> None: + self.token = token + + def get_error(self) -> str: + return self.error + + def set_error(self, error: str) -> None: + self.error = error + + def get_decoded_token(self) -> AuthorizationToken: + return self.decoded_token + + def set_decoded_token(self, decoded_token: AuthorizationToken) -> None: + self.decoded_token = decoded_token + + +class AuthorizationResult: + def __init__(self, + *, + authorization_url: str = None, + state: str = None, + ): + self.authorization_url = authorization_url + self.state = state + + def get_authorization_url(self) -> str: + return self.authorization_url + + def set_authorization_url(self, authorization_url: str) -> None: + self.authorization_url = authorization_url + + def get_state(self) -> str: + return self.state + + def set_state(self, state: str) -> None: + self.state = state diff --git a/agrirouter/auth/response.py b/agrirouter/auth/response.py index a711c4a6..a4b87f7f 100644 --- a/agrirouter/auth/response.py +++ b/agrirouter/auth/response.py @@ -5,6 +5,7 @@ from cryptography.exceptions import InvalidSignature +from agrirouter.auth.dto import AuthorizationToken, AuthorizationResultUrl from agrirouter.onboarding.signature import verify_signature @@ -17,11 +18,11 @@ class AuthResponse: CRED_KEY = "credentials" def __init__(self, query_params): - self._state = query_params.get(self.STATE_KEY, None) - self._signature = query_params.get(self.SIGNATURE_KEY, None) - self._token = query_params.get(self.TOKEN_KEY, None) - self._error = query_params.get(self.ERROR_KEY, None) - self.is_successful = not bool(self._error) + self.state = query_params.get(self.STATE_KEY, None) + self.signature = query_params.get(self.SIGNATURE_KEY, None) + self.token = query_params.get(self.TOKEN_KEY, None) + self.error = query_params.get(self.ERROR_KEY, None) + self.is_successful = not bool(self.error) self._was_verified = False self._is_valid = False @@ -43,33 +44,56 @@ def verify(self, public_key) -> None: :return: """ - encoded_data = self._state + self._token - unquoted_signature = unquote(self._signature) + encoded_data = self.state + self.token + unquoted_signature = unquote(self.signature) encoded_signature = base64.b64decode(unquoted_signature.encode("utf-8")) - self._was_verified = True + try: verify_signature(encoded_data, encoded_signature, public_key) except InvalidSignature: print("Response is invalid: invalid signature.") self._is_valid = False + finally: + self._was_verified = True self._is_valid = True @staticmethod - def decode_token(token: Union[str, bytes]) -> dict: + def decode_token(token: Union[str, bytes]) -> AuthorizationToken: if type(token) == str: token = token.encode("utf-8") base_64_decoded_token = base64.b64decode(token) decoded_token = base_64_decoded_token.decode("utf-8") - return json.loads(decoded_token) - - def get_auth_result(self) -> dict: - if not self.is_successful: - return {self.ERROR_KEY: self._error} - decoded_token = self.decode_token(self._token) - return { - self.SIGNATURE_KEY: self._signature, - self.STATE_KEY: self._state, - self.TOKEN_KEY: self._token, - self.CRED_KEY: decoded_token - } + + auth_token = AuthorizationToken() + auth_token.json_deserialize(json.loads(decoded_token)) + return auth_token + + def get_auth_result(self) -> AuthorizationResultUrl: + decoded_token = self.decode_token(self.token) + + return AuthorizationResultUrl( + signature=self.signature, + state=self.state, + token=self.token, + decoded_token=decoded_token, + error=self.error + ) + + def get_signature(self): + return self.signature + + def set_signature(self, signature): + self.signature = signature + + def get_state(self): + return self.state + + def set_state(self, state): + self.state = state + + def get_token(self): + return self.token + + def set_token(self, token): + self.token = token diff --git a/agrirouter/environments/environments.py b/agrirouter/environments/environments.py index 503ecd6b..6b05b99e 100644 --- a/agrirouter/environments/environments.py +++ b/agrirouter/environments/environments.py @@ -6,7 +6,7 @@ class BaseEnvironment: _MQTT_URL_TEMPLATE = "ssl://{host}:{port}" _SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE = \ "/application/{application_id}/authorize" \ - "?response_type={response_type}&state={state}&redirect_uri={redirect_uri}" + "?response_type={response_type}&state={state}" _ENV_BASE_URL = "" _API_PREFIX = "" @@ -38,13 +38,13 @@ def get_revoke_url(self) -> str: def get_agrirouter_login_url(self) -> str: return self.get_base_url() + self._AGRIROUTER_LOGIN_URL - def get_secured_onboarding_authorization_url(self, application_id, response_type, state, redirect_uri) -> str: - return self.get_base_url() + self._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( + def get_secured_onboarding_authorization_url(self, application_id, response_type, state, redirect_uri=None) -> str: + auth_url = self.get_base_url() + self._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( application_id=application_id, response_type=response_type, - state=state, - redirect_uri=redirect_uri + state=state ) + return auth_url + f"&redirect_uri={redirect_uri}" if redirect_uri is not None else auth_url def get_mqtt_server_url(self, host, port) -> str: return self._MQTT_URL_TEMPLATE.format(host=host, port=port) diff --git a/agrirouter/messaging/builders.py b/agrirouter/messaging/builders.py new file mode 100644 index 00000000..d7815042 --- /dev/null +++ b/agrirouter/messaging/builders.py @@ -0,0 +1,201 @@ +from typing import List + +from agrirouter.generated.messaging.request.payload.endpoint.capabilities_pb2 import CapabilitySpecification +from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription +from agrirouter.messaging.enums import CapabilityType + + +class SubscriptionItemBuilder: + + def __init__(self): + self._subscription_items = [] + + def build(self): + return self._subscription_items + + def clear(self): + self._subscription_items = [] + + def with_task_data(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.ISO_11783_TASKDATA_ZIP.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_device_description(self, ddis: List[int] = None, position: bool = None): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.ISO_11783_DEVICE_DESCRIPTION_PROTOBUF.value, + ddis=ddis, + position=position + ) + self._subscription_items.append(subscription_item) + return self + + def with_time_log(self, ddis: List[int] = None, position: bool = None): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.ISO_11783_TIMELOG_PROTOBUF.value, + ddis=ddis, + position=position + ) + self._subscription_items.append(subscription_item) + return self + + def with_bmp(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.IMG_BMP.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_jpg(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.IMG_JPEG.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_png(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.IMG_PNG.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_shape(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.SHP_SHAPE_ZIP.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_pdf(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.DOC_PDF.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_avi(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.VID_AVI.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_mp4(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.VID_MP4.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_wmv(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.VID_WMV.value + ) + self._subscription_items.append(subscription_item) + return self + + def with_gps_info(self): + subscription_item = Subscription.MessageTypeSubscriptionItem( + technical_message_type=CapabilityType.GPS_INFO.value + ) + self._subscription_items.append(subscription_item) + return self + + +class CapabilityBuilder: + + def __init__(self): + self._capabilities = [] + + def build(self) -> list: + return self._capabilities + + def clear(self): + self._capabilities = [] + + def with_task_data(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.ISO_11783_TASKDATA_ZIP.value + self._capabilities.append(capability) + return self + + def with_device_description(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.ISO_11783_DEVICE_DESCRIPTION_PROTOBUF.value + self._capabilities.append(capability) + return self + + def with_time_log(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.ISO_11783_TIMELOG_PROTOBUF.value + self._capabilities.append(capability) + return self + + def with_bmp(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.IMG_BMP.value + self._capabilities.append(capability) + return self + + def with_jpg(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.IMG_JPEG.value + self._capabilities.append(capability) + return self + + def with_png(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.IMG_PNG.value + self._capabilities.append(capability) + return self + + def with_shape(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.SHP_SHAPE_ZIP.value + self._capabilities.append(capability) + return self + + def with_pdf(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.DOC_PDF.value + self._capabilities.append(capability) + return self + + def with_avi(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.VID_AVI.value + self._capabilities.append(capability) + return self + + def with_mp4(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.VID_MP4.value + self._capabilities.append(capability) + return self + + def with_wmv(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.VID_WMV.value + self._capabilities.append(capability) + return self + + def with_gps_info(self, direction: int): + capability = CapabilitySpecification.Capability() + capability.direction = direction + capability.technical_message_type = CapabilityType.GPS_INFO.value + self._capabilities.append(capability) + return self diff --git a/agrirouter/messaging/certification.py b/agrirouter/messaging/certification.py index 5579b870..c87afc83 100644 --- a/agrirouter/messaging/certification.py +++ b/agrirouter/messaging/certification.py @@ -1,14 +1,12 @@ -import json import os -import pathlib -from pathlib import Path import tempfile -from agrirouter.onboarding.response import BaseOnboardingResonse +from agrirouter.onboarding.response import SoftwareOnboardingResponse -def create_certificate_file(onboard_response: BaseOnboardingResonse): +def create_certificate_file_from_pen(onboard_response: SoftwareOnboardingResponse): + dir_ = tempfile.mkdtemp() prefix = onboard_response.get_sensor_alternate_id() data = onboard_response.get_authentication().get_certificate() diff --git a/agrirouter/messaging/clients/constants.py b/agrirouter/messaging/clients/constants.py new file mode 100644 index 00000000..78de8628 --- /dev/null +++ b/agrirouter/messaging/clients/constants.py @@ -0,0 +1,2 @@ +ASYNC = "ASYNC" +SYNC = "SYNC" diff --git a/agrirouter/messaging/clients/http.py b/agrirouter/messaging/clients/http.py index 08530c77..cf1e400f 100644 --- a/agrirouter/messaging/clients/http.py +++ b/agrirouter/messaging/clients/http.py @@ -1,22 +1,81 @@ +import http.client +import json +import os +import ssl +from urllib.parse import urlparse + +from agrirouter.messaging.certification import create_certificate_file_from_pen +from agrirouter.onboarding.response import SoftwareOnboardingResponse + + class HttpClient: - headers = {"Content-Type": "application/json"} + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + + def make_connection(self, certificate_file_path: str, uri: str, onboard_response: SoftwareOnboardingResponse): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.load_cert_chain( + certfile=certificate_file_path, + keyfile=certificate_file_path, + password=onboard_response.get_authentication().get_secret(), + ) + connection = http.client.HTTPSConnection( + host=self.get_host(uri), + port=self.get_port(uri), + context=context + ) + return connection + + def send_measure(self, onboard_response: SoftwareOnboardingResponse, request_body=None): + return self.send( + method="POST", + uri=onboard_response.get_connection_criteria().get_measures(), + onboard_response=onboard_response, + request_body=request_body + ) + + def send_command(self, onboard_response: SoftwareOnboardingResponse, request_body=None): + return self.send( + method="GET", + uri=onboard_response.get_connection_criteria().get_commands(), + onboard_response=onboard_response, + request_body=request_body + ) - def __init__(self, - on_message_callback: callable, - timeout=20 - ): - self.on_message_callback = on_message_callback - self.timeout = timeout + def send(self, method: str, uri: str, onboard_response: SoftwareOnboardingResponse, request_body=None): + certificate_file_path = create_certificate_file_from_pen(onboard_response) + try: + connection = self.make_connection(certificate_file_path, uri, onboard_response) + if request_body is not None: + connection.request( + method=method, + url=self.get_path(uri), + headers=self.headers, + body=json.dumps(request_body.json_serialize()) + ) + else: + connection.request( + method=method, + url=self.get_path(uri), + headers=self.headers, + ) + response = connection.getresponse() + finally: + os.remove(certificate_file_path) - def publish(self): - pass + return response - def subscribe(self): - pass + @staticmethod + def get_host(uri): + return urlparse(uri).netloc - def unsubscribe(self): - pass + @staticmethod + def get_port(uri): + return urlparse(uri).port if urlparse(uri).port else None - def _start_loop(self): - pass + @staticmethod + def get_path(uri): + return urlparse(uri).path diff --git a/agrirouter/messaging/clients/mqtt.py b/agrirouter/messaging/clients/mqtt.py index 4e8dd555..423897d0 100644 --- a/agrirouter/messaging/clients/mqtt.py +++ b/agrirouter/messaging/clients/mqtt.py @@ -1,16 +1,22 @@ +import time +import ssl from typing import Any, List, Tuple -from paho.mqtt import client as mqtt_client +import paho.mqtt.client as mqtt_client from paho.mqtt.client import MQTTv31, MQTTMessageInfo +from agrirouter.messaging.certification import create_certificate_file_from_pen +from agrirouter.messaging.clients.constants import SYNC, ASYNC + class MqttClient: def __init__(self, - client_id: str = "", + onboard_response, + client_id: str, on_message_callback: callable = None, userdata: Any = None, - clean_session: bool = True + clean_session: bool = False ): # TODO: Implement on_message_callback parameter validation: # must take params as described at https://pypi.org/project/paho-mqtt/#callbacks @@ -22,26 +28,54 @@ def __init__(self, protocol=MQTTv31, transport="tcp" ) + self.mqtt_client.on_message = on_message_callback if on_message_callback else self._get_on_message_callback() - self.mqtt_client.on_connect = self._get_on_connect_callback() + self.mqtt_client.on_connect = self._get_on_connect_callback(onboard_response) self.mqtt_client.on_disconnect = self._get_on_disconnect_callback() self.mqtt_client.on_subscribe = self._get_on_subscribe_callback() self.mqtt_client.on_unsubscribe = self._get_on_unsubscribe_callback() + certificate_file_path = create_certificate_file_from_pen(onboard_response) + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.load_cert_chain( + certfile=certificate_file_path, + keyfile=certificate_file_path, + password=onboard_response.get_authentication().get_secret(), + ) + self.mqtt_client.tls_set_context(context) + + self._mode = None + def connect(self, host: str, port: str) -> None: + self.mqtt_client.connect( + host=host, + port=int(port) + ) + self.mqtt_client.loop() + + self._mode = SYNC + + def connect_async(self, host: str, port: str): self.mqtt_client.connect_async( host=host, - port=port + port=int(port) ) self.mqtt_client.loop_start() + self._mode = ASYNC + + while self.mqtt_client._state == 0: + time.sleep(1) + def disconnect(self): self.mqtt_client.loop_stop() self.mqtt_client.disconnect() - def publish(self, topic, payload, qos=0) -> MQTTMessageInfo: - """ + def receive_outbox_messages(self): + self.mqtt_client.loop() + def publish(self, topic, payload, qos=2) -> MQTTMessageInfo: + """ :param topic: str representing unique name of the topic that the message should be published on :param payload: The actual message to send :param qos: int representing the quality of service level to use. May be [0, 1, 2] @@ -52,6 +86,10 @@ def publish(self, topic, payload, qos=0) -> MQTTMessageInfo: payload=payload, qos=qos ) + if self._mode == SYNC: + self.mqtt_client.loop() + time.sleep(3) + self.mqtt_client.loop() return message_info def subscribe(self, topics: List[Tuple[str, int]]) -> tuple: @@ -67,7 +105,7 @@ def subscribe(self, topics: List[Tuple[str, int]]) -> tuple: :return: tuple """ - result, mid = self.mqtt_client.subscribe(topics) + result, mid = self.mqtt_client.subscribe(topics, qos=2) return result, mid def unsubscribe(self, topics: List[str]) -> tuple: @@ -85,15 +123,12 @@ def unsubscribe(self, topics: List[str]) -> tuple: return result, mid @staticmethod - def _get_on_connect_callback() -> callable: + def _get_on_connect_callback(onboard_response) -> callable: - def on_connect(client, userdata, flags, rc, properties=None): + def on_connect(client: mqtt_client.Client, userdata, flags, rc, properties=None): if rc == 0: - print("Connected to MQTT Broker!") - else: - print(f"Failed to connect, return code: {rc}") - - return client, userdata, flags, rc, properties + client.subscribe(topic=onboard_response.connection_criteria.commands) + time.sleep(3) return on_connect @@ -101,8 +136,6 @@ def on_connect(client, userdata, flags, rc, properties=None): def _get_on_message_callback() -> callable: def on_message(client, userdata, msg): - # print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") - return client, userdata, msg return on_message @@ -110,29 +143,23 @@ def on_message(client, userdata, msg): @staticmethod def _get_on_subscribe_callback() -> callable: - def on_subscribe(client, userdata, mid, granted_qos, properties=None): - # print(f"Subscribed {userdata} to `{properties}`") - - return client, userdata, mid, granted_qos, properties + def on_subscribe(*args, **kwargs): + return args, kwargs return on_subscribe @staticmethod def _get_on_disconnect_callback() -> callable: - def on_disconnect(client, userdata, rc): - # print(f"Disconnected from from `{properties}`") - - return client, userdata, rc + def on_disconnect(*args, **kwargs): + return args, kwargs return on_disconnect @staticmethod def _get_on_unsubscribe_callback() -> callable: - def on_unsubscribe(client, userdata, mid): - # print(f"Unsubscribed `{userdata}` from `{properties}`") - - return client, userdata, mid + def on_unsubscribe(*args, **kwargs): + return args, kwargs return on_unsubscribe diff --git a/agrirouter/messaging/decode.py b/agrirouter/messaging/decode.py index 839a0e48..0d1a4e12 100644 --- a/agrirouter/messaging/decode.py +++ b/agrirouter/messaging/decode.py @@ -1,5 +1,4 @@ import base64 -from ctypes import Union from google.protobuf.any_pb2 import Any from google.protobuf.internal.decoder import _DecodeVarint @@ -9,6 +8,7 @@ from agrirouter.generated.messaging.response.payload.feed.feed_response_pb2 import HeaderQueryResponse, \ MessageQueryResponse from agrirouter.generated.messaging.response.response_pb2 import ResponseEnvelope, ResponsePayloadWrapper +from agrirouter.messaging.exceptions import DecodeMessageException from agrirouter.messaging.messages import DecodedMessage from agrirouter.utils.type_url import TypeUrl @@ -31,8 +31,10 @@ def decode_response(message: bytes) -> DecodedMessage: input_stream = base64.b64decode(message) response_envelope_buffer, response_payload_buffer = read_properties_buffers_from_input_stream(input_stream) - envelope = ResponseEnvelope().MergeFromString(response_envelope_buffer) - payload = ResponsePayloadWrapper().MergeFromString(response_payload_buffer) + envelope = ResponseEnvelope() + envelope.ParseFromString(response_envelope_buffer) + payload = ResponsePayloadWrapper() + payload.ParseFromString(response_payload_buffer) message = DecodedMessage(envelope, payload) @@ -40,11 +42,21 @@ def decode_response(message: bytes) -> DecodedMessage: def decode_details(details: Any): - if details.type_url == TypeUrl.get_type_url(Messages.__name__): - return Messages().MergeFromString(details.value) - elif details.type_url == TypeUrl.get_type_url(ListEndpointsResponse.__name__): - return ListEndpointsResponse().MergeFromString(details.value) - elif details.type_url == TypeUrl.get_type_url(HeaderQueryResponse.__name__): - return HeaderQueryResponse().MergeFromString(details.value) - elif details.type_url == TypeUrl.get_type_url(MessageQueryResponse.__name__): - return MessageQueryResponse().MergeFromString(details.value) + if details.type_url == TypeUrl.get_type_url(Messages): + messages = Messages() + messages.MergeFromString(details.value) + return messages + elif details.type_url == TypeUrl.get_type_url(ListEndpointsResponse): + list_endpoints_response = ListEndpointsResponse() + list_endpoints_response.MergeFromString(details.value) + return list_endpoints_response + elif details.type_url == TypeUrl.get_type_url(HeaderQueryResponse): + header_query_response = HeaderQueryResponse() + header_query_response.MergeFromString(details.value) + return header_query_response + elif details.type_url == TypeUrl.get_type_url(MessageQueryResponse): + message_query_response = MessageQueryResponse() + message_query_response.MergeFromString(details.value) + return message_query_response + else: + raise DecodeMessageException(f"Could not handle type {details.type_url} while decoding details.") diff --git a/agrirouter/messaging/encode.py b/agrirouter/messaging/encode.py index fe74c229..f1f40513 100644 --- a/agrirouter/messaging/encode.py +++ b/agrirouter/messaging/encode.py @@ -18,23 +18,26 @@ def write_proto_parts_to_buffer(parts: list, buffer: bytes = b""): return buffer -def encode_message(header_parameters: MessageHeaderParameters, payload_parameters: MessagePayloadParameters) -> bytes: +def encode_message(header_parameters: MessageHeaderParameters, payload_parameters: MessagePayloadParameters) -> str: request_envelope = encode_header(header_parameters) request_payload = encode_payload(payload_parameters) raw_data = write_proto_parts_to_buffer([request_envelope, request_payload]) - return base64.b64encode(raw_data) + return base64.b64encode(raw_data).decode() def encode_header(header_parameters: MessageHeaderParameters) -> RequestEnvelope: request_envelope = RequestEnvelope() - request_envelope.application_id = header_parameters.get_application_message_id() \ + request_envelope.application_message_id = header_parameters.get_application_message_id() \ if header_parameters.get_application_message_id() else new_uuid() request_envelope.application_message_seq_no = header_parameters.get_application_message_seq_no() request_envelope.technical_message_type = header_parameters.get_technical_message_type() request_envelope.mode = header_parameters.get_mode() - request_envelope.timestamp = now_as_utc_timestamp() + if header_parameters.get_team_set_context_id() is not None: + request_envelope.team_set_context_id = header_parameters.get_team_set_context_id() + request_envelope.timestamp.FromDatetime(now_as_utc_timestamp()) + return request_envelope @@ -42,5 +45,5 @@ def encode_payload(payload_parameters: MessagePayloadParameters) -> RequestPaylo any_proto_wrapper = Any() any_proto_wrapper.type_url = payload_parameters.get_type_url() any_proto_wrapper.value = payload_parameters.get_value() - request_payload = RequestPayloadWrapper(any_proto_wrapper) + request_payload = RequestPayloadWrapper(details=any_proto_wrapper) return request_payload diff --git a/agrirouter/messaging/enums.py b/agrirouter/messaging/enums.py index 1247118f..53265ec5 100644 --- a/agrirouter/messaging/enums.py +++ b/agrirouter/messaging/enums.py @@ -13,3 +13,18 @@ class TechnicalMessageType(BaseEnum): FEED_MESSAGE_QUERY = "dke:feed_message_query" CLOUD_ONBOARD_ENDPOINTS = "dke:cloud_onboard_endpoints" CLOUD_OFFBOARD_ENDPOINTS = "dke:cloud_offboard_endpoints" + + +class CapabilityType(BaseEnum): + ISO_11783_TASKDATA_ZIP = "iso:11783:-10:taskdata:zip" + ISO_11783_DEVICE_DESCRIPTION_PROTOBUF = "iso:11783:-10:device_description:protobuf" + ISO_11783_TIMELOG_PROTOBUF = "iso:11783:-10:time_log:protobuf" + IMG_BMP = "img:bmp" + IMG_JPEG = "img:jpeg" + IMG_PNG = "img:png" + SHP_SHAPE_ZIP = "shp:shape:zip" + DOC_PDF = "doc:pdf" + VID_AVI = "vid:avi" + VID_MP4 = "vid:mp4" + VID_WMV = "vid:wmv" + GPS_INFO = "gps:info" diff --git a/agrirouter/messaging/exceptions.py b/agrirouter/messaging/exceptions.py index 3df29d4c..2bc8b9a7 100644 --- a/agrirouter/messaging/exceptions.py +++ b/agrirouter/messaging/exceptions.py @@ -7,3 +7,11 @@ class TypeUrlNotFoundError(AgriRouuterBaseException): class WrongFieldError(AgriRouuterBaseException): _message = "Unknown field" + + +class DecodeMessageException(AgriRouuterBaseException): + _message = "Can't decode message" + + +class OutboxException(AgriRouuterBaseException): + _message = "Can't fetch outbox message" diff --git a/agrirouter/messaging/messages.py b/agrirouter/messaging/messages.py index 1e5228fd..7db4e461 100644 --- a/agrirouter/messaging/messages.py +++ b/agrirouter/messaging/messages.py @@ -1,8 +1,8 @@ import json -from datetime import datetime, timezone -from typing import Union, List, Dict +from typing import Union, Dict from agrirouter.messaging.exceptions import WrongFieldError +from agrirouter.utils.utc_time_util import now_as_utc_str class EncodedMessage: @@ -36,12 +36,12 @@ class Message: def __init__(self, content): self.content = content - self.timestamp = datetime.utcnow() + self.timestamp = now_as_utc_str() def json_serialize(self) -> dict: return { self.MESSAGE: self.content, - self.TIMESTAMP: self.timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + self.TIMESTAMP: self.timestamp } @@ -52,8 +52,8 @@ def __init__(self, message: str = None): self.message = message def json_deserialize(self, data: Union[Dict[str, str], str]): - messages = data if type(data) == list else json.loads(data) - for key, value in messages.keys(): + messages = data if type(data) == dict else json.loads(data) + for key, value in messages.items(): if key == self.MESSAGE: self.message = value else: @@ -81,15 +81,17 @@ def __init__(self, self.sensor_alternate_id = sensor_alternate_id self.command = command - def json_deserialize(self, data: Union[list, str]): - data = data if type(data) == list else json.loads(data) - for key, value in data.keys(): + def json_deserialize(self, data: Union[dict, str]): + data = data if type(data) == dict else json.loads(data) + for (key, value) in data.items(): if key == self.CAPABILITY_ALTERNATE_ID: self.capability_alternate_id = value elif key == self.SENSOR_ALTERNATE_ID: self.sensor_alternate_id = value elif key == self.COMMAND: - self.command = Command.json_deserialize(value) + command = Command() + command.json_deserialize(value) + self.command = command else: raise WrongFieldError(f"Unknown field `{key}` for {self.__class__}") @@ -110,6 +112,3 @@ def get_command(self) -> Command: def set_command(self, command: Command) -> None: self.command = command - - def json_deserialize(self): - pass diff --git a/agrirouter/messaging/parameters/dto.py b/agrirouter/messaging/parameters/dto.py index 3c1404de..17998bf0 100644 --- a/agrirouter/messaging/parameters/dto.py +++ b/agrirouter/messaging/parameters/dto.py @@ -8,8 +8,8 @@ class Parameters: def __init__(self, *, - application_message_seq_no: str, - application_message_id: int = None, + application_message_seq_no: int, + application_message_id: str = None, team_set_context_id: str ): self.application_message_seq_no = application_message_seq_no @@ -41,9 +41,9 @@ def validate(self): class MessageParameters(Parameters): def __init__(self, *, - application_message_seq_no: str, - application_message_id: int, - team_set_context_id: str, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, onboarding_response: BaseOnboardingResonse ): super(MessageParameters, self).__init__( @@ -54,7 +54,7 @@ def __init__(self, self.onboarding_response = onboarding_response - def get_onboarding_response(self): + def get_onboarding_response(self) -> BaseOnboardingResonse: return self.onboarding_response @@ -63,7 +63,7 @@ class MessagingParameters(MessageParameters): def __init__(self, *, application_message_seq_no: str = None, - application_message_id: int = None, + application_message_id: str = None, team_set_context_id: str = None, onboarding_response: BaseOnboardingResonse, encoded_messages=None diff --git a/agrirouter/messaging/parameters/service.py b/agrirouter/messaging/parameters/service.py index 0769fa08..758df834 100644 --- a/agrirouter/messaging/parameters/service.py +++ b/agrirouter/messaging/parameters/service.py @@ -1,11 +1,12 @@ -from abc import ABC, abstractmethod from copy import deepcopy from typing import List from agrirouter.generated.commons.chunk_pb2 import ChunkComponent +from agrirouter.generated.messaging.request.payload.endpoint.capabilities_pb2 import CapabilitySpecification from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription from agrirouter.generated.messaging.request.payload.feed.feed_requests_pb2 import ValidityPeriod from agrirouter.messaging.parameters.dto import MessageParameters, Parameters +from agrirouter.onboarding.response import BaseOnboardingResonse class MessageHeaderParameters(Parameters): @@ -15,10 +16,10 @@ def __init__(self, technical_message_type: str = None, mode: str = None, team_set_context_id: str = None, - application_message_seq_no: str = None, + application_message_seq_no: int = None, recipients: list = None, chunk_component: ChunkComponent = None, - application_message_id: int = None, + application_message_id: str = None, ): super(MessageHeaderParameters, self).__init__( application_message_seq_no=application_message_seq_no, @@ -65,13 +66,20 @@ def get_value(self) -> str: class CloudOnboardParameters(MessageParameters): def __init__(self, - # List[EndpointRegistrationDetails] - must be defined in generated by by proto schemes, - # but they are not + *, onboarding_requests: list = None, - **kwargs + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse ): self.onboarding_requests = onboarding_requests if onboarding_requests else [] - super(CloudOnboardParameters, self).__init__(**kwargs) + super(CloudOnboardParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_onboarding_requests(self) -> list: return self.onboarding_requests @@ -89,11 +97,20 @@ def extend_onboarding_requests(self, onboarding_requests: list) -> None: class CloudOffboardParameters(MessageParameters): def __init__(self, + *, endpoints: List[str] = None, - **kwargs + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse ): self.endpoints = endpoints if endpoints else [] - super(CloudOffboardParameters, self).__init__(**kwargs) + super(CloudOffboardParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_endpoints(self) -> List[str]: return self.endpoints @@ -111,17 +128,26 @@ def extend_endpoints(self, endpoints: List[str]) -> None: class CapabilityParameters(MessageParameters): def __init__(self, - application_id, - certification_version_id, - enable_push_notification, - capability_parameters: list = None, - **kwargs + *, + application_id: str, + certification_version_id: str, + enable_push_notification: int = CapabilitySpecification.PushNotification.Value("DISABLED"), + capability_parameters: List[CapabilitySpecification.Capability] = None, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse ): self.application_id = application_id self.certification_version_id = certification_version_id self.enable_push_notification = enable_push_notification self.capability_parameters = capability_parameters if capability_parameters else [] - super(CapabilityParameters, self).__init__(**kwargs) + super(CapabilityParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_application_id(self): return self.application_id @@ -155,9 +181,21 @@ def extend_capability_parameters(self, capability_parameters: list): class FeedConfirmParameters(MessageParameters): - def __init__(self, message_ids: list = None, **kwargs): + def __init__(self, + *, + message_ids: list = None, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse + ): self.message_ids = message_ids if message_ids else [] - super(FeedConfirmParameters, self).__init__(**kwargs) + super(FeedConfirmParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_message_ids(self): return deepcopy(self.message_ids) @@ -174,14 +212,24 @@ def extend_message_ids(self, message_ids): class FeedDeleteParameters(MessageParameters): def __init__(self, + *, message_ids: list = None, receivers: list = None, validity_period: ValidityPeriod = None, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.message_ids = message_ids if message_ids else [] self.receivers = receivers if receivers else [] self.validity_period = validity_period - super(FeedDeleteParameters, self).__init__(**kwargs) + super(FeedDeleteParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_message_ids(self): return deepcopy(self.message_ids) @@ -216,14 +264,24 @@ def set_validity_period(self, validity_period: ValidityPeriod): class ListEndpointsParameters(MessageParameters): def __init__(self, + *, technical_message_type: str = None, - direction: str = None, + direction: int = None, filtered: bool = False, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.technical_message_type = technical_message_type self.direction = direction self.filtered = filtered - super(ListEndpointsParameters, self).__init__(**kwargs) + super(ListEndpointsParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_technical_message_type(self) -> str: return self.technical_message_type @@ -231,10 +289,10 @@ def get_technical_message_type(self) -> str: def set_technical_message_type(self, technical_message_type: str): self.technical_message_type = technical_message_type - def get_direction(self) -> str: + def get_direction(self) -> int: return self.direction - def set_direction(self, direction: str): + def set_direction(self, direction: int): self.direction = direction def is_filtered(self): @@ -246,14 +304,24 @@ def set_filtered(self, filtered: bool): class QueryMessageParameters(MessageParameters): def __init__(self, + *, senders: list = None, message_ids: list = None, validity_period: ValidityPeriod = None, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.senders = senders self.message_ids = message_ids self.validity_period = validity_period - super(QueryMessageParameters, self).__init__(**kwargs) + super(QueryMessageParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_senders(self) -> list: return self.senders @@ -288,14 +356,24 @@ def set_validity_period(self, validity_period: list) -> None: class QueryHeaderParameters(MessageParameters): def __init__(self, + *, senders: list = None, message_ids: list = None, validity_period: ValidityPeriod = None, - **kwargs): + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, + ): self.senders = senders self.message_ids = message_ids self.validity_period = validity_period - super(QueryHeaderParameters, self).__init__(**kwargs) + super(QueryHeaderParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_senders(self) -> list: return self.senders @@ -330,10 +408,20 @@ def set_validity_period(self, validity_period: list) -> None: class SubscriptionParameters(MessageParameters): def __init__(self, + *, + application_message_seq_no: int, + application_message_id: str, + team_set_context_id: str = None, + onboarding_response: BaseOnboardingResonse, subscription_items: List[Subscription.MessageTypeSubscriptionItem] = None, - **kwargs): + ): self.subscription_items = subscription_items if subscription_items else [] - super(SubscriptionParameters, self).__init__(**kwargs) + super(SubscriptionParameters, self).__init__( + application_message_seq_no=application_message_seq_no, + application_message_id=application_message_id, + team_set_context_id=team_set_context_id, + onboarding_response=onboarding_response + ) def get_subscription_items(self) -> List[Subscription.MessageTypeSubscriptionItem]: return self.subscription_items diff --git a/agrirouter/messaging/request.py b/agrirouter/messaging/request.py index 4de0a69e..14766318 100644 --- a/agrirouter/messaging/request.py +++ b/agrirouter/messaging/request.py @@ -1,7 +1,5 @@ from typing import List -from agrirouter.messaging.messages import Message - class MessageRequest: SENSOR_ALTERNATE_ID = "sensorAlternateId" @@ -11,7 +9,7 @@ class MessageRequest: def __init__(self, sensor_alternate_id: str, capability_alternate_id: str, - messages: List[Message] + messages: List[dict] ): self.sensor_alternate_id = sensor_alternate_id self.capability_alternate_id = capability_alternate_id diff --git a/agrirouter/messaging/result.py b/agrirouter/messaging/result.py index ba2df495..d08c7f4b 100644 --- a/agrirouter/messaging/result.py +++ b/agrirouter/messaging/result.py @@ -26,7 +26,13 @@ def __init__(self, def json_deserialize(self, data: Union[list, str]): messages = data if type(data) == list else json.loads(data) - self.set_messages([OutboxMessage.json_deserialize(message) for message in messages]) + outbox_message_list = [] + for message in messages: + outbox_message = OutboxMessage() + outbox_message.json_deserialize(message) + outbox_message_list.append(outbox_message) + + self.set_messages(outbox_message_list) def get_status_code(self) -> int: return self.status_code diff --git a/agrirouter/messaging/services/cloud.py b/agrirouter/messaging/services/cloud.py index cd40a465..217462e2 100644 --- a/agrirouter/messaging/services/cloud.py +++ b/agrirouter/messaging/services/cloud.py @@ -28,7 +28,7 @@ def encode(parameters: CloudOnboardParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(OnboardingRequest.__name__), + type_url=TypeUrl.get_type_url(OnboardingRequest), value=onboarding_request.SerializeToString() ) @@ -58,7 +58,7 @@ def encode(parameters: CloudOffboardParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(OffboardingRequest.__name__), + type_url=TypeUrl.get_type_url(OffboardingRequest), value=offboarding_request.SerializeToString() ) diff --git a/agrirouter/messaging/services/commons.py b/agrirouter/messaging/services/commons.py index 3e368632..8f4f6db5 100644 --- a/agrirouter/messaging/services/commons.py +++ b/agrirouter/messaging/services/commons.py @@ -1,13 +1,13 @@ -import os +import json from abc import ABC, abstractmethod -import requests - -from agrirouter.messaging.certification import create_certificate_file +from agrirouter.messaging.clients.http import HttpClient from agrirouter.messaging.clients.mqtt import MqttClient from agrirouter.messaging.messages import Message from agrirouter.messaging.request import MessageRequest from agrirouter.messaging.result import MessagingResult +from agrirouter.onboarding.exceptions import BadMessagingResult +from agrirouter.onboarding.response import SoftwareOnboardingResponse class AbstractMessagingClient(ABC): @@ -17,10 +17,10 @@ def create_message_request(parameters) -> MessageRequest: messages = [] for encoded_message in parameters.get_encoded_messages(): message = Message(encoded_message) - messages.append(message) + messages.append(message.json_serialize()) message_request = MessageRequest( - parameters.get_sensor_alternate_id(), - parameters.get_capability_alternate_id(), + parameters.get_onboarding_response().get_sensor_alternate_id(), + parameters.get_onboarding_response().get_capability_alternate_id(), messages ) return message_request @@ -32,59 +32,50 @@ def send(self, parameters): class HttpMessagingService(AbstractMessagingClient): + def __init__(self): + self.client = HttpClient() + def send(self, parameters) -> MessagingResult: request = self.create_message_request(parameters) - cert_file_path = create_certificate_file(parameters.get_onboarding_response()) - try: - response = requests.post( - url=parameters.get_onboarding_response().get_connection_criteria().get_measures(), - headers={"Content-type": "application/json"}, - data=request.json_serialize(), - cert=( - cert_file_path, - parameters.get_onboarding_response().get_authentication().get_secret() - ), - ) - finally: - os.remove(cert_file_path) - result = MessagingResult([parameters.get_message_id()]) + response = self.client.send_measure(parameters.get_onboarding_response(), request) + if response.status != 200: + raise BadMessagingResult(f"Messaging Request failed with status code {response.status}") + result = MessagingResult([parameters.get_application_message_id()]) return result - def subscribe(self): - pass - - def unsubscribe(self): - pass - class MqttMessagingService(AbstractMessagingClient): def __init__(self, - onboarding_response, + onboarding_response: SoftwareOnboardingResponse, on_message_callback: callable = None, + client_async: bool = True ): self.onboarding_response = onboarding_response self.client = MqttClient( - client_id=self.onboarding_response.get_client_id(), + onboard_response=onboarding_response, + client_id=onboarding_response.get_connection_criteria().get_client_id(), on_message_callback=on_message_callback, ) - self.client.connect( - self.onboarding_response.get_connection_criteria().get_host(), - self.onboarding_response.get_connection_criteria().get_port() - ) + if client_async: + self.client.connect_async( + self.onboarding_response.get_connection_criteria().get_host(), + self.onboarding_response.get_connection_criteria().get_port() + ) + else: + self.client.connect( + self.onboarding_response.get_connection_criteria().get_host(), + self.onboarding_response.get_connection_criteria().get_port() + ) def send(self, parameters, qos: int = 0) -> MessagingResult: - mqtt_payload = self.create_message_request(parameters) + message_request = self.create_message_request(parameters) + mqtt_payload = message_request.json_serialize() self.client.publish( - self.onboarding_response.get_connection_criteria().get_measures(), mqtt_payload, + topic=self.onboarding_response.get_connection_criteria().get_measures(), + payload=json.dumps(mqtt_payload), qos=qos ) - result = MessagingResult([parameters.get_message_id()]) + result = MessagingResult([parameters.get_application_message_id()]) return result - - def subscribe(self): - pass - - def unsubscribe(self): - pass diff --git a/agrirouter/messaging/services/http/outbox.py b/agrirouter/messaging/services/http/outbox.py index b81bfcbf..7985f914 100644 --- a/agrirouter/messaging/services/http/outbox.py +++ b/agrirouter/messaging/services/http/outbox.py @@ -1,30 +1,21 @@ -import os - -import requests - +from agrirouter.messaging.clients.http import HttpClient +from agrirouter.messaging.exceptions import OutboxException from agrirouter.messaging.result import OutboxResponse -from agrirouter.messaging.certification import create_certificate_file - class OutboxService: + def __init__(self): + self.client = HttpClient() + def fetch(self, onboarding_response) -> OutboxResponse: - cert_file_path = create_certificate_file(onboarding_response) - try: - response = requests.get( - url=onboarding_response.get_connection_criteria().get_commands(), - headers={"Content-type": "application/json"}, - cert=( - cert_file_path, - onboarding_response.get_authentication().get_secret() - ), - ) - finally: - os.remove(cert_file_path) + response = self.client.send_command(onboarding_response, None) - outbox_response = OutboxResponse(status_code=response.status_code) - outbox_response.json_deserialize(response.json()["contents"]) + if response.status == 200: + outbox_response = OutboxResponse(status_code=response.status) + response_body = response.read() + outbox_response.json_deserialize(response_body) + else: + raise OutboxException(f"Could not fetch messages from outbox. Status code was {response.status}") return outbox_response - diff --git a/agrirouter/messaging/services/messaging.py b/agrirouter/messaging/services/messaging.py index e071b028..81f60d6f 100644 --- a/agrirouter/messaging/services/messaging.py +++ b/agrirouter/messaging/services/messaging.py @@ -6,7 +6,7 @@ from agrirouter.messaging.encode import encode_message from agrirouter.messaging.enums import TechnicalMessageType from agrirouter.messaging.messages import EncodedMessage -from agrirouter.messaging.parameters.dto import MessageParameters, MessagingParameters +from agrirouter.messaging.parameters.dto import MessagingParameters from agrirouter.messaging.parameters.service import MessageHeaderParameters, MessagePayloadParameters, \ CapabilityParameters, FeedConfirmParameters, FeedDeleteParameters, ListEndpointsParameters, \ SubscriptionParameters, QueryHeaderParameters, QueryMessageParameters @@ -52,10 +52,10 @@ def encode(parameters: CapabilityParameters) -> EncodedMessage: enable_push_notifications=parameters.get_enable_push_notification() ) if parameters.get_capability_parameters(): - capability_specification.capabilities = parameters.get_capability_parameters() + capability_specification.capabilities.extend(parameters.get_capability_parameters()) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(CapabilitySpecification.__name__), + type_url=TypeUrl.get_type_url(CapabilitySpecification), value=capability_specification.SerializeToString() ) @@ -85,7 +85,7 @@ def encode(parameters: FeedConfirmParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageConfirm.__name__), + type_url=TypeUrl.get_type_url(MessageConfirm), value=message_confirm.SerializeToString() ) @@ -115,7 +115,7 @@ def encode(parameters: FeedDeleteParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageConfirm.__name__), + type_url=TypeUrl.get_type_url(MessageConfirm), value=message_confirm.SerializeToString() ) @@ -147,7 +147,7 @@ def encode(parameters: ListEndpointsParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(ListEndpointsQuery.__name__), + type_url=TypeUrl.get_type_url(ListEndpointsQuery), value=list_endpoints_query.SerializeToString() ) @@ -179,7 +179,7 @@ def encode(parameters: QueryMessageParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageQuery.__name__), + type_url=TypeUrl.get_type_url(MessageQuery), value=message_query.SerializeToString() ) @@ -211,7 +211,7 @@ def encode(parameters: QueryHeaderParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(MessageQuery.__name__), + type_url=TypeUrl.get_type_url(MessageQuery), value=message_query.SerializeToString() ) @@ -241,7 +241,7 @@ def encode(parameters: SubscriptionParameters) -> EncodedMessage: ) message_payload_parameters = MessagePayloadParameters( - type_url=TypeUrl.get_type_url(Subscription.__name__), + type_url=TypeUrl.get_type_url(Subscription), value=subscription.SerializeToString() ) diff --git a/agrirouter/onboarding/dto.py b/agrirouter/onboarding/dto.py new file mode 100644 index 00000000..224f8b5c --- /dev/null +++ b/agrirouter/onboarding/dto.py @@ -0,0 +1,223 @@ +import json +from typing import Union + +from agrirouter.messaging.exceptions import WrongFieldError + + +class ConnectionCriteria: + CLIENT_ID = 'clientId' + COMMANDS = 'commands' + GATEWAY_ID = 'gatewayId' + HOST = 'host' + MEASURES = 'measures' + PORT = 'port' + + def __init__(self, + *, + gateway_id: str = None, + measures: str = None, + commands: str = None, + host: str = None, + port: str = None, + client_id: str = None + ): + self.gateway_id = gateway_id + self.measures = measures + self.commands = commands + self.host = host + self.port = port + self.client_id = client_id + + def json_serialize(self) -> dict: + return { + self.GATEWAY_ID: self.gateway_id, + self.MEASURES: self.measures, + self.COMMANDS: self.commands, + self.HOST: self.host, + self.PORT: self.port, + self.CLIENT_ID: self.client_id + } + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.GATEWAY_ID: + self.gateway_id = value + elif key == self.MEASURES: + self.measures = value + elif key == self.COMMANDS: + self.commands = value + elif key == self.HOST: + self.host = value + elif key == self.PORT: + self.port = value + elif key == self.CLIENT_ID: + self.client_id = value + else: + raise WrongFieldError(f"Unknown field {key} for Connection Criteria class") + + def get_gateway_id(self) -> str: + return self.gateway_id + + def set_gateway_id(self, gateway_id: str) -> None: + self.gateway_id = gateway_id + + def get_measures(self) -> str: + return self.measures + + def set_measures(self, measures: str) -> None: + self.measures = measures + + def get_commands(self) -> str: + return self.commands + + def set_commands(self, commands: str) -> None: + self.commands = commands + + def get_host(self) -> str: + return self.host + + def set_host(self, host: str) -> None: + self.host = host + + def get_port(self) -> str: + return self.port + + def set_port(self, port: str) -> None: + self.port = port + + def get_client_id(self) -> str: + return self.client_id + + def set_client_id(self, client_id: str) -> None: + self.client_id = client_id + + def __str__(self): + return str(self.json_serialize()) + + def __repr__(self): + return str(self.json_serialize()) + + +class Authentication: + TYPE = 'type' + SECRET = 'secret' + CERTIFICATE = 'certificate' + + def __init__(self, + *, + type: str = None, + secret: str = None, + certificate: str = None, + ): + self.type = type + self.secret = secret + self.certificate = certificate + + def json_serialize(self) -> dict: + return { + self.TYPE: self.type, + self.SECRET: self.secret, + self.CERTIFICATE: self.certificate, + } + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.TYPE: + self.type = value + elif key == self.SECRET: + self.secret = value + elif key == self.CERTIFICATE: + self.certificate = value + else: + raise WrongFieldError(f"Unknown field {key} for Authentication class") + + def get_type(self) -> str: + return self.type + + def set_type(self, type: str) -> None: + self.type = type + + def get_secret(self) -> str: + return self.secret + + def set_secret(self, secret: str) -> None: + self.secret = secret + + def get_certificate(self) -> str: + return self.certificate + + def set_certificate(self, certificate: str) -> None: + self.certificate = certificate + + def __str__(self): + return str(self.json_serialize()) + + def __repr__(self): + return str(self.json_serialize()) + + +class ErrorResponse: + CODE = "code" + MESSAGE = "message" + TARGET = "target" + DETAILS = "details" + + def __init__(self, + *, + code: str = None, + message: str = None, + target: str = None, + details: str = None + ): + self.code = code + self.message = message + self.target = target + self.details = details + + def json_serialize(self) -> dict: + return { + self.CODE: self.code, + self.MESSAGE: self.message, + self.TARGET: self.target, + self.DETAILS: self.details + } + + def json_deserialize(self, data: Union[str, dict]) -> None: + data = data if type(data) == dict else json.loads(data) + for key, value in data.items(): + if key == self.CODE: + self.code = value + elif key == self.MESSAGE: + self.message = value + elif key == self.TARGET: + self.target = value + elif key == self.DETAILS: + self.details = value + else: + raise WrongFieldError(f"Unknown field {key} for ErrorResponse class") + + def get_code(self) -> str: + return self.code + + def set_code(self, code: str) -> None: + self.code = code + + def get_message(self) -> str: + return self.message + + def set_message(self, message: str) -> None: + self.message = message + + def get_target(self) -> str: + return self.target + + def set_target(self, target: str) -> None: + self.target = target + + def get_details(self) -> str: + return self.details + + def set_details(self, details: str) -> None: + self.details = details diff --git a/agrirouter/onboarding/exceptions.py b/agrirouter/onboarding/exceptions.py index 490e51e3..fb3b024a 100644 --- a/agrirouter/onboarding/exceptions.py +++ b/agrirouter/onboarding/exceptions.py @@ -21,3 +21,7 @@ class RequestNotSigned(AgriRouuterBaseException): Details on: https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/ integration/onboarding.html#signing-requests """ + + +class BadMessagingResult(AgriRouuterBaseException): + _message = "Messaging Request failed" diff --git a/agrirouter/onboarding/headers.py b/agrirouter/onboarding/headers.py index a1e0d61c..7843e2ad 100644 --- a/agrirouter/onboarding/headers.py +++ b/agrirouter/onboarding/headers.py @@ -1,28 +1,7 @@ -import base64 -from abc import ABC, abstractmethod - from agrirouter.constants.media_types import ContentTypes -class BaseOnboardingHeader(ABC): - @abstractmethod - def __init__(self, *args, **kwargs): - self._set_params(*args, **kwargs) - - @abstractmethod - def get_header(self) -> dict: - ... - - @abstractmethod - def _set_params(self, *args, **kwargs): - ... - - @abstractmethod - def sign(self, *args, **kwargs): - ... - - -class SoftwareOnboardingHeader(BaseOnboardingHeader): +class SoftwareOnboardingHeader: def __init__(self, reg_code, application_id, @@ -35,10 +14,8 @@ def __init__(self, def get_header(self) -> dict: return self.params - def sign(self, signature: bytes): - print(signature) - self.params["X-Agrirouter-Signature"] = base64.b64encode(signature).decode() - print(self.params["X-Agrirouter-Signature"]) + def sign(self, signature: str): + self.params["X-Agrirouter-Signature"] = signature def _set_params(self, reg_code: str, application_id: str, signature: str, content_type: str): header = dict() @@ -48,7 +25,3 @@ def _set_params(self, reg_code: str, application_id: str, signature: str, conten header["X-Agrirouter-Signature"] = signature if signature else "" self.params = header - - -class CUOnboardingHeader(BaseOnboardingHeader): - pass diff --git a/agrirouter/onboarding/onboarding.py b/agrirouter/onboarding/onboarding.py index 155d08dc..3119e181 100644 --- a/agrirouter/onboarding/onboarding.py +++ b/agrirouter/onboarding/onboarding.py @@ -1,15 +1,12 @@ -import json - import requests from agrirouter.environments.environmental_services import EnvironmentalService from agrirouter.onboarding.exceptions import RequestNotSigned -from agrirouter.onboarding.headers import SoftwareOnboardingHeader, CUOnboardingHeader -from agrirouter.onboarding.parameters import SoftwareOnboardingParameter, BaseOnboardingParameter, CUOnboardingParameter -from agrirouter.onboarding.request import SoftwareOnboardingRequest, BaseOnboardingRequest, CUOnboardingRequest -from agrirouter.onboarding.request_body import SoftwareOnboardingBody, CUOnboardingBody -from agrirouter.onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse, \ - CUOnboardingResponse +from agrirouter.onboarding.headers import SoftwareOnboardingHeader +from agrirouter.onboarding.parameters import SoftwareOnboardingParameter +from agrirouter.onboarding.request import SoftwareOnboardingRequest +from agrirouter.onboarding.request_body import SoftwareOnboardingBody +from agrirouter.onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse class SoftwareOnboarding(EnvironmentalService): @@ -19,7 +16,7 @@ def __init__(self, *args, **kwargs): self._private_key = kwargs.pop("private_key") super(SoftwareOnboarding, self).__init__(*args, **kwargs) - def _create_request(self, params: BaseOnboardingParameter, url: str) -> SoftwareOnboardingRequest: + def _create_request(self, params: SoftwareOnboardingParameter, url: str) -> SoftwareOnboardingRequest: body_params = params.get_body_params() request_body = SoftwareOnboardingBody(**body_params) @@ -28,59 +25,25 @@ def _create_request(self, params: BaseOnboardingParameter, url: str) -> Software return SoftwareOnboardingRequest(header=request_header, body=request_body, url=url) - def _perform_request(self, params: BaseOnboardingParameter, url: str) -> requests.Response: + def _perform_request(self, params: SoftwareOnboardingParameter, url: str) -> requests.Response: request = self._create_request(params, url) - request.sign(self._private_key) + request.sign(self._private_key, self._public_key) if request.is_signed: return requests.post( url=request.get_url(), - data=json.dumps(request.get_data()), + data=request.get_body_content(), headers=request.get_header() ) raise RequestNotSigned - def verify(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: + def verify(self, params: SoftwareOnboardingParameter) -> SoftwareVerifyOnboardingResponse: url = self._environment.get_verify_onboard_request_url() http_response = self._perform_request(params=params, url=url) - return SoftwareOnboardingResponse(http_response) + return SoftwareVerifyOnboardingResponse(http_response) def onboard(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: url = self._environment.get_secured_onboard_url() http_response = self._perform_request(params=params, url=url) return SoftwareOnboardingResponse(http_response) - - -class CUOnboarding(EnvironmentalService): - - def __init__(self, *args, **kwargs): - self._public_key = kwargs.pop("public_key") - self._private_key = kwargs.pop("private_key") - super(CUOnboarding, self).__init__(*args, **kwargs) - - def _create_request(self, params: CUOnboardingParameter, url: str) -> CUOnboardingRequest: - body_params = params.get_body_params() - request_body = CUOnboardingBody(**body_params) - - header_params = params.get_header_params() - request_header = CUOnboardingHeader(**header_params) - - return CUOnboardingRequest(header=request_header, body=request_body, url=url) - - def _perform_request(self, params: CUOnboardingParameter, url: str) -> requests.Response: - request = self._create_request(params, url) - request.sign(self._private_key) - if request.is_signed: - return requests.post( - url=request.get_url(), - data=request.get_data(), - headers=request.get_header() - ) - raise RequestNotSigned - - def onboard(self, params: CUOnboardingParameter) -> CUOnboardingResponse: - url = self._environment.get_onboard_url() - http_response = self._perform_request(params=params, url=url) - - return CUOnboardingResponse(http_response) diff --git a/agrirouter/onboarding/parameters.py b/agrirouter/onboarding/parameters.py index 1ca75312..1895c9fe 100644 --- a/agrirouter/onboarding/parameters.py +++ b/agrirouter/onboarding/parameters.py @@ -1,25 +1,9 @@ -from abc import ABC, abstractmethod -from datetime import datetime - from agrirouter.constants.media_types import ContentTypes from agrirouter.onboarding.enums import CertificateTypes +from agrirouter.utils.utc_time_util import now_as_utc_str -class BaseOnboardingParameter(ABC): - @abstractmethod - def __init__(self, *args, **kwargs): - ... - - @abstractmethod - def get_header_params(self, *args, **kwargs): - ... - - @abstractmethod - def get_body_params(self, *args, **kwargs): - ... - - -class SoftwareOnboardingParameter(BaseOnboardingParameter): +class SoftwareOnboardingParameter: def __init__(self, *, id_, @@ -39,8 +23,7 @@ def __init__(self, self.certification_version_id = certification_version_id self.gateway_id = str(gateway_id) self.certificate_type = certificate_type - self.utc_timestamp = str(utc_timestamp) if utc_timestamp \ - else datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + self.utc_timestamp = str(utc_timestamp) if utc_timestamp else now_as_utc_str() self.time_zone = str(time_zone) self.reg_code = reg_code @@ -61,38 +44,3 @@ def get_body_params(self): "utc_timestamp": self.utc_timestamp, "time_zone": self.time_zone, } - - -class CUOnboardingParameter(BaseOnboardingParameter): - def __init__(self, - id_, - application_id, - certification_version_id, - gateway_id, - reg_code, - content_type=ContentTypes.APPLICATION_JSON.value, - certificate_type=CertificateTypes.P12.value, - ): - - self.id_ = id_ - self.application_id = application_id - self.content_type = content_type - self.certification_version_id = certification_version_id - self.gateway_id = gateway_id - self.certificate_type = certificate_type - self.reg_code = reg_code - - def get_header_params(self): - return { - "content_type": self.content_type, - "reg_code": self.reg_code, - } - - def get_body_params(self): - return { - "id_": self.id_, - "application_id": self.application_id, - "certification_version_id": self.certification_version_id, - "gateway_id": self.gateway_id, - "certificate_type": self.certificate_type, - } diff --git a/agrirouter/onboarding/request.py b/agrirouter/onboarding/request.py index 1e7113b0..0728f149 100644 --- a/agrirouter/onboarding/request.py +++ b/agrirouter/onboarding/request.py @@ -1,10 +1,10 @@ -from agrirouter.onboarding.headers import SoftwareOnboardingHeader, BaseOnboardingHeader -from agrirouter.onboarding.request_body import SoftwareOnboardingBody, BaseOnboardingBody -from agrirouter.onboarding.signature import create_signature +from agrirouter.onboarding.headers import SoftwareOnboardingHeader +from agrirouter.onboarding.request_body import SoftwareOnboardingBody +from agrirouter.onboarding.signature import create_signature, verify_signature -class BaseOnboardingRequest: - def __init__(self, header: BaseOnboardingHeader, body: BaseOnboardingBody, url: str): +class SoftwareOnboardingRequest: + def __init__(self, header: SoftwareOnboardingHeader, body: SoftwareOnboardingBody, url: str): self.header = header self.body = body self.url = url @@ -18,8 +18,13 @@ def get_data(self): def get_header(self): return self.header.get_header() - def sign(self, private_key): - signature = create_signature(self.body.json(), private_key) + def get_body_content(self): + return self.body.json().replace("\n", "") + + def sign(self, private_key, public_key): + body = self.get_body_content() + signature = create_signature(body, private_key) + verify_signature(body, bytes.fromhex(signature), public_key) self.header.sign(signature) @property @@ -28,17 +33,3 @@ def is_signed(self): if header_has_signature: return True return False - - -class SoftwareOnboardingRequest(BaseOnboardingRequest): - """ - Request must be used to onboard Farming Software or Telemetry Platform - """ - pass - - -class CUOnboardingRequest(BaseOnboardingRequest): - """ - Request must be used to onboard CUs - """ - pass diff --git a/agrirouter/onboarding/request_body.py b/agrirouter/onboarding/request_body.py index 951ab01d..a730cf6e 100644 --- a/agrirouter/onboarding/request_body.py +++ b/agrirouter/onboarding/request_body.py @@ -1,30 +1,11 @@ import json -from abc import ABC, abstractmethod from datetime import datetime from agrirouter.onboarding.enums import CertificateTypes, GateWays from agrirouter.onboarding.exceptions import WrongCertificationType, WrongGateWay -class BaseOnboardingBody(ABC): - @abstractmethod - def __init__(self, *args, **kwargs): - ... - - @abstractmethod - def get_parameters(self, *args, **kwargs) -> dict: - ... - - @abstractmethod - def _set_params(self, *args, **kwargs): - ... - - @abstractmethod - def json(self, *args, **kwargs): - ... - - -class SoftwareOnboardingBody(BaseOnboardingBody): +class SoftwareOnboardingBody: def __init__(self, *, id_, @@ -75,7 +56,7 @@ def _set_params(self, } def json(self) -> str: - return json.dumps(self.get_parameters()) + return json.dumps(self.get_parameters(), separators=(',', ':')) @staticmethod def _validate_certificate_type(certificate_type: str) -> None: @@ -86,21 +67,3 @@ def _validate_certificate_type(certificate_type: str) -> None: def _validate_gateway_id(gateway_id: str) -> None: if gateway_id not in GateWays.values_list(): raise WrongGateWay - - -class CUOnboardingBody(BaseOnboardingBody): - - def __init__(self, *args, **kwargs): - ... - - def get_parameters(self, *args, **kwargs) -> dict: - ... - - def _set_params(self, *args, **kwargs): - ... - - def json(self, new_lines: bool = True) -> str: - result = json.dumps(self.get_parameters(), indent="") - if not new_lines: - return result.replace("\n", "") - return result diff --git a/agrirouter/onboarding/response.py b/agrirouter/onboarding/response.py index cab6bca0..43e42a8d 100644 --- a/agrirouter/onboarding/response.py +++ b/agrirouter/onboarding/response.py @@ -1,64 +1,174 @@ +import json +from typing import Union + from requests import Response +from agrirouter.messaging.exceptions import WrongFieldError +from agrirouter.onboarding.dto import ErrorResponse, ConnectionCriteria, Authentication + class BaseOnboardingResonse: def __init__(self, http_response: Response): - self.response: Response = http_response - - def get_connection_criteria(self) -> dict: - response_data = self.data() - return response_data.get("connectionCriteria") - def get_sensor_alternate_id(self): - response_data = self.data() - return response_data.get("sensorAlternateId") - - def get_authentication(self): - response_data = self.data() - return response_data.get("authentication") - - @property - def data(self): - return self.response.json() + self._status_code = http_response.status_code + self._text = http_response.text @property def status_code(self): - return self.response.status_code + return self._status_code @property def text(self): - return self.response.text + return self._text class SoftwareVerifyOnboardingResponse(BaseOnboardingResonse): """ Response from verify request used for Farming Software or Telemetry Platform before onboarding """ - pass + def __init__(self, http_response: Response = None): + if http_response: + super(SoftwareVerifyOnboardingResponse, self).__init__(http_response) + response_body = http_response.json() + else: + self._text = None + self._status_code = None + response_body = {} -class SoftwareOnboardingResponse(BaseOnboardingResonse): - """ - Response from onboarding request used for Farming Software or Telemetry Platform - """ + self.account_id = response_body.get("accountId", None) - def get_connection_criteria(self) -> dict: - response_data = self.data() - return response_data.get("connectionCriteria") + self.error = ErrorResponse( + code=response_body.get("error").get("code"), + message=response_body.get("error").get("message"), + target=response_body.get("error").get("target"), + details=response_body.get("error").get("details"), + ) if response_body.get("error", None) else None - def get_sensor_alternate_id(self): - response_data = self.data() - return response_data.get("sensorAlternateId") + def get_account_id(self) -> str: + return self.account_id - def get_authentication(self): - response_data = self.data() - return response_data.get("authentication") + def set_account_id(self, account_id: str): + self.account_id = account_id -class CUOnboardingResponse(BaseOnboardingResonse): +class SoftwareOnboardingResponse(BaseOnboardingResonse): """ - Response from onboarding request used for CUs + Response from onboarding request used for Farming Software or Telemetry Platform """ - pass + DEVICE_ALTERNATE_ID = "deviceAlternateId" + CAPABILITY_ALTERNATE_ID = "capabilityAlternateId" + SENSOR_ALTERNATE_ID = "sensorAlternateId" + CONNECTION_CRITERIA = "connectionCriteria" + AUTHENTICATION = "authentication" + ERROR = "error" + + def __init__(self, http_response: Response = None): + if http_response: + super(SoftwareOnboardingResponse, self).__init__(http_response) + response_body = http_response.json() + else: + self._text = None + self._status_code = None + response_body = {} + + self.connection_criteria = ConnectionCriteria( + gateway_id=response_body.get("connectionCriteria").get("gatewayId"), + measures=response_body.get("connectionCriteria").get("measures"), + commands=response_body.get("connectionCriteria").get("commands"), + host=response_body.get("connectionCriteria").get("host"), + port=response_body.get("connectionCriteria").get("port"), + client_id=response_body.get("connectionCriteria").get("clientId") + ) if response_body.get("connectionCriteria", None) else None + + self.authentication = Authentication( + type=response_body.get("authentication").get("type"), + secret=response_body.get("authentication").get("secret"), + certificate=response_body.get("authentication").get("certificate") + ) if response_body.get("authentication", None) else None + + self.capability_alternate_id = response_body.get("capabilityAlternateId", None) + self.device_alternate_id = response_body.get("deviceAlternateId", None) + self.sensor_alternate_id = response_body.get("sensorAlternateId", None) + + self.error = ErrorResponse( + code=response_body.get("error").get("code"), + message=response_body.get("error").get("message"), + target=response_body.get("error").get("target"), + details=response_body.get("error").get("details"), + ) if response_body.get("error", None) else None + + def get_connection_criteria(self) -> ConnectionCriteria: + return self.connection_criteria + + def set_connection_criteria(self, connection_criteria: ConnectionCriteria): + self.connection_criteria = connection_criteria + + def get_authentication(self) -> Authentication: + return self.authentication + + def set_authentication(self, authentication: Authentication): + self.authentication = authentication + + def get_sensor_alternate_id(self) -> str: + return self.sensor_alternate_id + + def set_sensor_alternate_id(self, sensor_alternate_id: str): + self.sensor_alternate_id = sensor_alternate_id + + def get_device_alternate_id(self) -> str: + return self.device_alternate_id + + def set_device_alternate_id(self, device_alternate_id: str): + self.device_alternate_id = device_alternate_id + + def get_capability_alternate_id(self) -> str: + return self.capability_alternate_id + + def set_capability_alternate_id(self, capability_alternate_id: str): + self.capability_alternate_id = capability_alternate_id + + def json_serialize(self): + if self.error: + return { + self.ERROR: self.error + } + return { + self.DEVICE_ALTERNATE_ID: self.device_alternate_id, + self.CAPABILITY_ALTERNATE_ID: self.capability_alternate_id, + self.SENSOR_ALTERNATE_ID: self.sensor_alternate_id, + self.CONNECTION_CRITERIA: self.connection_criteria.json_serialize(), + self.AUTHENTICATION: self.authentication.json_serialize() + } + + def json_deserialize(self, data: Union[dict, str]): + data_dict = data if type(data) == dict else json.loads(data) + for (key, value) in data_dict.items(): + if key == self.DEVICE_ALTERNATE_ID: + self.device_alternate_id = value + elif key == self.CAPABILITY_ALTERNATE_ID: + self.capability_alternate_id = value + elif key == self.SENSOR_ALTERNATE_ID: + self.sensor_alternate_id = value + elif key == self.CONNECTION_CRITERIA: + connection_criteria = ConnectionCriteria() + connection_criteria.json_deserialize(value) + self.connection_criteria = connection_criteria + elif key == self.AUTHENTICATION: + authentication = Authentication() + authentication.json_deserialize(value) + self.authentication = authentication + elif key == self.ERROR: + error_response = ErrorResponse() + error_response.json_deserialize(value) + self.error = error_response + else: + raise WrongFieldError(f"Unknown field `{key}` for {self.__class__}") + + def __str__(self): + return str(self.json_serialize()) + + def __repr__(self): + return str(self.json_serialize()) diff --git a/agrirouter/onboarding/signature.py b/agrirouter/onboarding/signature.py index 6bedc3bd..64174adc 100644 --- a/agrirouter/onboarding/signature.py +++ b/agrirouter/onboarding/signature.py @@ -2,20 +2,24 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding -from pprint import pprint SIGNATURE_ALGORITHM = "SHA256withRSA" -def create_signature(request_body: str, private_key: str) -> bytes: +def to_hex(sign: bytes): + return sign.hex() + + +def create_signature(request_body: str, private_key: str) -> str: private_key_bytes = bytearray(private_key.encode('utf-8')) private_key_data = load_pem_private_key(private_key_bytes, None) signature = private_key_data.sign( - request_body.encode('utf-8'), + request_body.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256() ) - return signature + + return to_hex(signature) def verify_signature(request_body: str, signature: bytes, public_key: str) -> None: diff --git a/agrirouter/revoking/headers.py b/agrirouter/revoking/headers.py index 0c1770f2..50652d5a 100644 --- a/agrirouter/revoking/headers.py +++ b/agrirouter/revoking/headers.py @@ -23,4 +23,4 @@ def _set_params(self, application_id: str, signature: str, content_type: str): if signature: header["X-Agrirouter-Signature"] = signature - self.params = header \ No newline at end of file + self.params = header diff --git a/agrirouter/revoking/parameters.py b/agrirouter/revoking/parameters.py index 95a73332..cf706b02 100644 --- a/agrirouter/revoking/parameters.py +++ b/agrirouter/revoking/parameters.py @@ -8,7 +8,7 @@ def __init__(self, account_id, endpoint_ids, utc_timestamp, - timestamp, + time_zone, content_type=ContentTypes.APPLICATION_JSON.value ): @@ -17,7 +17,7 @@ def __init__(self, self.account_id = account_id self.endpoint_ids = endpoint_ids self.utc_timestamp = utc_timestamp - self.timestamp = timestamp + self.time_zone = time_zone def get_header_params(self): return { @@ -30,5 +30,5 @@ def get_body_params(self): "account_id": self.account_id, "endpoint_ids": self.endpoint_ids, "utc_timestamp": self.utc_timestamp, - "timestamp": self.timestamp, + "time_zone": self.time_zone, } diff --git a/agrirouter/revoking/request.py b/agrirouter/revoking/request.py index d45b7787..1fea09e1 100644 --- a/agrirouter/revoking/request.py +++ b/agrirouter/revoking/request.py @@ -18,8 +18,12 @@ def get_data(self): def get_header(self): return self.header.get_header() + def get_body_content(self): + return self.body.json().replace("\n", "") + def sign(self, private_key): - signature = create_signature(self.body.json(new_lines=False), private_key) + body = self.get_body_content() + signature = create_signature(body, private_key) self.header.sign(signature) @property @@ -28,10 +32,3 @@ def is_signed(self) -> bool: if header_has_signature: return True return False - - @property - def is_valid(self) -> bool: - if not self.is_signed: - return False - signature = self.get_header().get("X-Agrirouter-Signature") - # return validate_signature(signature) diff --git a/agrirouter/revoking/request_body.py b/agrirouter/revoking/request_body.py index 6ec70b2d..7bdea6e5 100644 --- a/agrirouter/revoking/request_body.py +++ b/agrirouter/revoking/request_body.py @@ -26,14 +26,11 @@ def _set_params(self, ) -> None: self.params = { - "account_id": account_id, - "endpoint_ids": endpoint_ids, + "accountId": account_id, + "endpointIds": endpoint_ids, "UTCTimestamp": utc_timestamp, "timeZone": time_zone, } - def json(self, new_lines: bool = True) -> str: - result = json.dumps(self.get_parameters(), indent="") - if not new_lines: - return result.replace("\n", "") - return result + def json(self) -> str: + return json.dumps(self.get_parameters(), separators=(',', ':')) diff --git a/agrirouter/revoking/revoking.py b/agrirouter/revoking/revoking.py index aaa8f871..b835bc8f 100644 --- a/agrirouter/revoking/revoking.py +++ b/agrirouter/revoking/revoking.py @@ -29,9 +29,9 @@ def _perform_request(self, params: RevokingParameter, url: str) -> requests.Resp request = self._create_request(params, url) request.sign(self._private_key) if request.is_signed: - return requests.post( + return requests.delete( url=request.get_url(), - data=request.get_data(), + json=request.get_data(), headers=request.get_header() ) raise RequestNotSigned @@ -40,4 +40,4 @@ def revoke(self, params: RevokingParameter) -> RevokingResponse: url = self._environment.get_revoke_url() http_response = self._perform_request(params=params, url=url) - return RevokingResponse(http_response) \ No newline at end of file + return RevokingResponse(http_response) diff --git a/agrirouter/utils/type_url.py b/agrirouter/utils/type_url.py index 71f4c6df..2457b9da 100644 --- a/agrirouter/utils/type_url.py +++ b/agrirouter/utils/type_url.py @@ -17,29 +17,29 @@ class TypeUrl: @classmethod def get_type_url(cls, class_): - if class_.__name__ == Messages.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == ListEndpointsResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == HeaderQueryResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageQueryResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageDelete.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageConfirm.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == OnboardingResponse.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == OnboardingRequest.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == CapabilitySpecification.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == Subscription.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == MessageQuery.__name__: - return cls.prefix + class_.__name__ - elif class_.__name__ == ListEndpointsQuery.__name__: - return cls.prefix + class_.__name__ + if class_ == Messages: + return cls.prefix + Messages.DESCRIPTOR.full_name + elif class_ == ListEndpointsResponse: + return cls.prefix + ListEndpointsResponse.DESCRIPTOR.full_name + elif class_ == HeaderQueryResponse: + return cls.prefix + HeaderQueryResponse.DESCRIPTOR.full_name + elif class_ == MessageQueryResponse: + return cls.prefix + MessageQueryResponse.DESCRIPTOR.full_name + elif class_ == MessageDelete: + return cls.prefix + MessageDelete.DESCRIPTOR.full_name + elif class_ == MessageConfirm: + return cls.prefix + MessageConfirm.DESCRIPTOR.full_name + elif class_ == OnboardingResponse: + return cls.prefix + OnboardingResponse.DESCRIPTOR.full_name + elif class_ == OnboardingRequest: + return cls.prefix + OnboardingRequest.DESCRIPTOR.full_name + elif class_ == CapabilitySpecification: + return cls.prefix + CapabilitySpecification.DESCRIPTOR.full_name + elif class_ == Subscription: + return cls.prefix + Subscription.DESCRIPTOR.full_name + elif class_ == MessageQuery: + return cls.prefix + MessageQuery.DESCRIPTOR.full_name + elif class_ == ListEndpointsQuery: + return cls.prefix + ListEndpointsQuery.DESCRIPTOR.full_name else: raise TypeUrlNotFoundError(f"The {class_} type url not found") diff --git a/agrirouter/utils/utc_time_util.py b/agrirouter/utils/utc_time_util.py index b86f3afb..b87ca808 100644 --- a/agrirouter/utils/utc_time_util.py +++ b/agrirouter/utils/utc_time_util.py @@ -2,5 +2,9 @@ def now_as_utc_timestamp(): + return datetime.utcnow() + + +def now_as_utc_str(): timestamp = datetime.utcnow() return timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/agrirouter/utils/uuid_util.py b/agrirouter/utils/uuid_util.py index 0c9bd872..8e500ead 100644 --- a/agrirouter/utils/uuid_util.py +++ b/agrirouter/utils/uuid_util.py @@ -2,4 +2,4 @@ def new_uuid(): - return uuid.uuid4() + return str(uuid.uuid4()) diff --git a/example_script.py b/example_script.py new file mode 100644 index 00000000..50e4ae3f --- /dev/null +++ b/example_script.py @@ -0,0 +1,310 @@ +from pprint import pprint + +from google.protobuf.timestamp_pb2 import Timestamp + +from agrirouter.generated.messaging.request.payload.account.endpoints_pb2 import ListEndpointsQuery +from agrirouter.generated.messaging.request.payload.feed.feed_requests_pb2 import ValidityPeriod +from agrirouter.onboarding.response import SoftwareOnboardingResponse +import time + +public_key = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzGt41/+kSOTlO1sJvLIN +6RAFaOn6GiCNX/Ju0oVT4VMDHfuQMI5t9+ZgBxFmUhtY5/eykQfYJVGac/cy5xyk +F/1xpMzltK7pfo7XZpfHjkHLPUOeaHW0zE+g2vopQOARKE5LSguCBUhdtfFuiheR +IP0EU+MtEQDhlfiqYLAJkAvZHluCH9q6hawn0t/G873jlzsrXBqIgKboXqyz1lRE +SvMyqX04Xwaq1CgAZjHXBVWvbuOriCR0P2n13/nkCgBgLd/ORwVilb4GQDXkkCSg +uOVcRU3s/KG/OVJTonHVlLvDzBA5GLrpZMpzC4EfzXBM98s4Vj6IOAIQeY84Sppj +qwIDAQAB +-----END PUBLIC KEY-----""" + +private_key = """-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDMa3jX/6RI5OU7 +Wwm8sg3pEAVo6foaII1f8m7ShVPhUwMd+5Awjm335mAHEWZSG1jn97KRB9glUZpz +9zLnHKQX/XGkzOW0rul+jtdml8eOQcs9Q55odbTMT6Da+ilA4BEoTktKC4IFSF21 +8W6KF5Eg/QRT4y0RAOGV+KpgsAmQC9keW4If2rqFrCfS38bzveOXOytcGoiApuhe +rLPWVERK8zKpfThfBqrUKABmMdcFVa9u46uIJHQ/afXf+eQKAGAt385HBWKVvgZA +NeSQJKC45VxFTez8ob85UlOicdWUu8PMEDkYuulkynMLgR/NcEz3yzhWPog4AhB5 +jzhKmmOrAgMBAAECggEAEEr6mUCzb+nqiWYSqxsH980CmV+Yww9YJU8V3SqqSlnK +9E9SKUSY6DrQ6Y9N9/pdBjQcY+nbpPHRnS+VO41xWMYnEisQneuZCbDJ40/ypFiD +IfFrRUkobWZlXD63Hggd5fgDkTXEmbYwXemN1WzWcOopt6PyOho3YLQupEEzqerb +XkzBFWwWO9589fbWnlaSoJPtgA8gFxeJJkU3kG10Epj6wV17yo6DuyVZpemGPTUL +uVl7yNx9O/Lp8UXRlBtSEEBQqoJaGy9mzVZyobXNKvdlZxwlkbJQpZB/m4dzqbyn +Wv+lSJdmbOnOzc67FfRqHf/irIdg6aInJd6WxZ3rPQKBgQDlxrcePlzpNwJOWtXb +sneHU50Lx73u183q5dtKlH/FudhOgP4aot6+q1KDu3b9rRakGJUKQYoLgGNwhl/7 +5CF0iKQE+5JZ5R9YpwFoDuALjPfic5vFN76G851ccz5pfThLjCMV1NgKJskaefP0 +OdV+UW9qOIxR8UAMntWTTrQzFwKBgQDjv+2Kz1/KsXSPaw+mJKsmUnC2YbqeAr+9 +Dwm7Hr0RZWkkS2EjqcMxvq0D8bYcuJvrlZFmB/r6Ly0MKlfsUT+64LAQnKHhlCUi +vlE7VuDOR16lC4ZCPeWtjrL45fpj+Lhe54m7rCT8F+Ocdxv2yNQrSBbQ6epOVuDz +XJaSRt/AjQKBgQCrBZPIS+yFnO73eP6SLixvKhnK6dmBi1h1zK3CvfK4LZJFJBd9 +pdoampOo/wAa4hjm/HD6GDvyQZZB65JHfs4z2XwTRVfx1urU5kDSvbeegUcDYr7/ +NHV4JpzqcdBzXcNn359BoZFHRQUL0tdz4RP5mA1QR1SRrPnaKuKWaM8Q8wKBgQC5 +mY9br+PAqxzyQ61dGETh1g1ElCAg5NyclcS4WTR7GMm2ajefeJk50MnujOx8O3XV +Zu422AoQGKH9aAR+8Teec70HzJ2f17rrtW09jm9lq4PVvK6NDSQ/bCst6z1Ce07F +CKuV5ZO+XTmAKREA7Gj7XKQ7XGU1sldf+/Q5AMkXgQKBgQC4lXL9zLV/vfWUTPSR +qlGcS2+WYtjWPapDZa+7zlxGdPgOTri4nJO69Bs9ReLlzsYKSBihfpWPxcl9sS65 +KFrlBkR/vzKYjCFXB6cmMP61mUrgGQRoYJQBetAyEiXZL3zjt1R/Dndk0kHkVmHr +HjmgzBRxXFy5uph6Ue6dxyszaA== +-----END PRIVATE KEY-----""" + + +onboarding_response_mqtt_data = { + "deviceAlternateId": "2145df0e-3451-46cb-bf23-23191af66fce", + "capabilityAlternateId": "523e4623-68d2-43d4-a0cc-e2ada2f68b5e", + "sensorAlternateId": "1489638c-7bed-4205-ad77-8d11efdc779f", + "connectionCriteria": { + "gatewayId": "2", + "host": "dke-qa.eu10.cp.iot.sap", + "port": 8883, + "clientId": "2145df0e-3451-46cb-bf23-23191af66fce", + "measures": "measures/2145df0e-3451-46cb-bf23-23191af66fce", + "commands": "commands/2145df0e-3451-46cb-bf23-23191af66fce" + }, + "authentication": { + "type": "PEM", + "secret": "JNKdNg8R0lwmFgvrUfOCc7inebr0h?!7Z9wL", + "certificate": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIE6zAdBgoqhkiG9w0BDAEDMA8ECMkL85F+LbPbAgMCAAAEggTI1CmRlnDUStBv\nTycvaRVFMCk1OuynhiOYRF6HBFFXBCxWKZa3WqTShLdf9iCel/NgtdZIiQsoD1LL\nMxVyh8pWAfLQ+pDJLvM6suQjHALt8dW5iTeCZ7R1gzFvPJ+xnDGFFytN7HmGSvHM\nQbcCOuEeIu8U6ENa6/+WmUwK9/ZMkLNqDHVKEGpI+lSJs8JWEE+S3Klmsxuq0dvz\nh6o3V7RKFwMfUZOQLHezGBDjLfEBdP+d2G87CY+LSzinL8pFhLwyrXFKfYWYoT0m\n5PkDdjfiVq3SJIUoQWnGrjaVVw4TV3WSxmhQnWbDwOQydr8DAiBxDMYoeK3rePpC\nwh6KATnBrovq1icqjonYDE0T+3Rs2SUbG+3+m9Zj4j46L2Sh9bUB6qxdw74Ck2/z\nAzJ1N+tB+RL7UvOpMOhmndMBl5qpx9dFFy8Z/N7w4YTQLZLN7chD8ApeFhCgvppt\nAGh8/VeWO54OC9ZOSHpxEl7sJz97jaHYNbw/lGbDk7cOZezwpA0NCWZ/Bb1vRDzy\n8EDX9s1hOA3jiy2T1RSyk2Rj/12pWdKtdSO8lMhMKC0B32Zr1F8rBJKDVzqFWuTt\nn+pXOKedyOA/ggyvYJdsltP8O4XB2oBN3WBdFK7Y1FG/tN30LsaqcnFTxab5v1Pp\ngq2dHu6Xy0TCMAw/DH3RmGXlGnDDWu86Zad7TjjrEZvpSIv4TTSCqqTvc4IN0xFX\nbKZCrY6JSkJWWnDMKrsRYOijUDvpAbYwZuTV9PAljYbt5YX778qxV9O0fNBQdaww\nNlfxU93jgr4g3E9nIzRxLu9S98hPbxKUnVYiQmYvP7vJUcUSo5F0LmUU/nvHY1pi\nr4tZDp8Xu1aZy7cOd3sTbf/68IjiZMZlF5/PVlOFOo40yGqW600j/qEqXoY/492h\nONXUCpHKaG/Pkjtg9THuYoaw1773gxYYsYLt+c6NkQCCsydOr2BMZQ4Qy4bZV67D\n2RNDeZzSBY6jEX6dnfY0FJqIsSiw28Ek5NXx0HTEGN8txPkx/1dfu3RfZnzUqT/0\nmS9xcWVYRmlip3vm48fMecqP/DNIHyjVLC39SsFdeXa+De76z/S3+or0t7HGlUim\nNVkIcWqm/sD2ia8hYberaRRTbUQ1iObNToIg8dA/xna6D61sYK8jkf1GVPpKsCTA\nOVW5u9XrE1f5YQEovE9kFgvtzs0u6jSeI9edqVadH1u6hX4QWQSTrcTb3raqAKpK\nl67cQ96eXI1WQPSdPhQPTjqzOPZDbot3qMkGFijHar7FdQjDx/cNhqhvxv0LWsvl\njgep1czUFoo1BS3wTUiO0qyloNGOQdgmlTOHbMFk1wgoNyAohfZtfn6LH/zlJnE3\nQ0YkUKgAG+1N/PmkQFO0k5qAflUV7h+HAzT1ZAZcscjHNbQFDc0Zjq9nE9sfhxE8\nOFpnF9Jp3fQVekyyC/dsCxtJdYfhxqYe+BzZu0SlsLCmc1JoK5lkiXQwv6+cFpKW\nwfHMTTrCoOetJyiF7oJX+t4adzmLmnujiw5izxObWQJ7avHC1oYNHfRejrOtlu34\n0nDPRFiSDyEbDCBXPe9dIafqjJVLQGFOeXC8/VN9cGSZp2JV8rqumWOr9E+Wd5zU\n8MRZpevo0i3rPgdyFRpw\n-----END ENCRYPTED PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIIEaDCCA1CgAwIBAgIPANHZYxYlOc+wEAEDDWDpMA0GCSqGSIb3DQEBCwUAMFYx\nCzAJBgNVBAYTAkRFMSMwIQYDVQQKExpTQVAgSW9UIFRydXN0IENvbW11bml0eSBJ\nSTEiMCAGA1UEAxMZU0FQIEludGVybmV0IG9mIFRoaW5ncyBDQTAeFw0yMTExMTIw\nNzMyMjNaFw0yMjExMTIwNzMyMjNaMIG1MQswCQYDVQQGEwJERTEcMBoGA1UEChMT\nU0FQIFRydXN0IENvbW11bml0eTEVMBMGA1UECxMMSW9UIFNlcnZpY2VzMXEwbwYD\nVQQDFGhkZXZpY2VBbHRlcm5hdGVJZDoyMTQ1ZGYwZS0zNDUxLTQ2Y2ItYmYyMy0y\nMzE5MWFmNjZmY2V8Z2F0ZXdheUlkOjJ8dGVuYW50SWQ6MTExNjkwMzQ5MHxpbnN0\nYW5jZUlkOmRrZS1xYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJeF\naxjV7Xk1R2dFjadN6WsUkrmcVu44vZRCJEbR7Chkg1xcXT6cgIlokO/V4lTgaD6i\neCMKMFegjXzEJQy0dyIWncozcmt6HJFxpdjVQtdCtDtCWykGscNDgvv5ukykOOKI\nMzWJ4d2cJRlostpNe4FYZoPp6cArSHTl9DvfYqjZ/ykeTa1w157dgVxPxezHrJMl\n+z2XgO37mq6CJLw8J6W8RBHbCADgB8c6qGHgJnBURyxnoHHi/yqdIKC6cOs8NAnc\nyVmnvLDu8RUWu9pWkqFHhMvSqdkUCTYORZ9mUTm/Kmv6ss2NaYT4uUBZTskwnAa9\nFLdj+DV2NG0OQl3NYr8CAwEAAaOB0jCBzzBIBgNVHR8EQTA/MD2gO6A5hjdodHRw\nczovL3Rjcy5teXNhcC5jb20vY3JsL1RydXN0Q29tbXVuaXR5SUkvU0FQSW9UQ0Eu\nY3JsMAwGA1UdEwEB/wQCMAAwJQYDVR0SBB4wHIYaaHR0cDovL3NlcnZpY2Uuc2Fw\nLmNvbS9UQ1MwDgYDVR0PAQH/BAQDAgbAMB0GA1UdDgQWBBSRf8DUjowgQ+6amVIs\njd7zM7VWqjAfBgNVHSMEGDAWgBSVt7P1WN7VtLNYRuDypsl4Tr0tdTANBgkqhkiG\n9w0BAQsFAAOCAQEARzSc9GLpSU3pRJPIfgadHrZ+2KQsPsQ1/fLlASlt4V1Rlxn7\n/tn0gk3sP0X5/TrkO+N0kx1qrLarxWSDiVfaXoPa6Lit30SBPnPLUPPPZeTJOz5r\nTW9PkPPuC39GlM1biVoil2cLZrTr9DMSUoBvR4IVKQoJveQsLwn7Ea+SDPE0uvZV\nbDN6UPGZ2yIiCXO1MODJ6r3A4EDD2MArGgfhGdbvJNAY36ShFJhzfzi0t8linEAA\nxh0vcaEEIkVeEiwiguyGWB69X88cjZ0Q5cCf0r6iu3oQnB57uM5TW12OwXQN1NpQ\neK3EMFSoM6BYJu/3B8TXhNmpNBvD7KYozw9XaA==\n-----END CERTIFICATE-----\n" + } +} + + + + +import agrirouter as ar +from agrirouter.onboarding.enums import GateWays +from agrirouter.messaging.enums import CapabilityType +from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription +from agrirouter.messaging.services.commons import HttpMessagingService, MqttMessagingService +from agrirouter import ListEndpointsParameters, ListEndpointsService, SubscriptionService, SubscriptionParameters, \ + QueryHeaderService, QueryHeaderParameters +from agrirouter.utils.uuid_util import new_uuid + + +application_id = "8c947a45-c57d-42d2-affc-206e21d63a50" # # store here your application id. You can find it in AR UI + + +def example_auth(): + print("Authorization...\n") + + auth_params = ar.AuthUrlParameter(application_id=application_id, response_type="onboard") + auth_client = ar.Authorization("QA", public_key=public_key, private_key=private_key) + auth_url = auth_client.get_auth_request_url( + auth_params) # use this url to authorize the user as described at https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/integration/authorization.html#perform-authorization + print(f"auth_url={auth_url}") + + auth_result_url = input( + "Enter auth_url (the url the user was redirected to after his authorization, see above): ") # the url the user was redirected to after his authorization. + auth_response = auth_client.extract_auth_response( + auth_result_url) # auth_response contains the results of the auth process + auth_client.verify_auth_response(auth_response) # you may verify auth_response to ensure answer was from AR + + print( + f"auth_response is successful: {auth_response.is_successful}") # True if user accepted application, False if he rejected + + print( + f"auth_response is valid: {auth_response.is_valid}") # Result of verification, if False, response was not validated by public key. Doesn't indicate the auth was successfull. Accessible only after response verifying + + # Get dict containing data from auth process you will use for futher communication. + # If auth was rejected, contains {"error"} key. + # If auth was accepted, contains {signature, state, token, credentials{account, expires, regcode}} keys + # Even if response verifying was not processed or failed, the results will be returned. But in that case you act on your risk. + auth_data = auth_response.get_auth_result() + print(f"auth_data: {auth_data}") + + return auth_data + + +def example_onboarding(gateway_id): + + auth_data = example_auth() + + print("Onboarding...\n") + + id_ = "urn:myapp:snr00003234" # just unique + certification_version_id = "edd5d6b7-45bb-4471-898e-ff9c2a7bf56f" # get from AR UI + time_zone = "+03:00" + + onboarding_client = ar.SoftwareOnboarding("QA", public_key=public_key, private_key=private_key) + onboarding_parameters = ar.SoftwareOnboardingParameter(id_=id_, application_id=application_id, + certification_version_id=certification_version_id, + gateway_id=gateway_id, time_zone=time_zone, + reg_code=auth_data.get_decoded_token().regcode) + onboarding_verifying_response = onboarding_client.verify(onboarding_parameters) + print(f"onboarding_verifying_response.status_code: {onboarding_verifying_response.status_code}") + print(f"onboarding_verifying_response.text: {onboarding_verifying_response.text}") + onboarding_response = onboarding_client.onboard(onboarding_parameters) + print(f"onboarding_response.status_code: {onboarding_response.status_code}") + print(f"onboarding_response.text: {onboarding_response.text}") + + return onboarding_response + + +def example_list_endpoints_mqtt(onboarding_response_data, foo): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = MqttMessagingService( + onboarding_response=onboarding_response, + on_message_callback=foo + + ) + list_endpoint_parameters = ListEndpointsParameters( + technical_message_type=CapabilityType.ISO_11783_TASKDATA_ZIP.value, + direction=ListEndpointsQuery.Direction.Value("SEND_RECEIVE"), + filtered=False, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + list_endpoint_service = ListEndpointsService(messaging_service) + + messaging_result = list_endpoint_service.send(list_endpoint_parameters) + print("Sent message: ", messaging_result) + + # Is needed for waiting of messaging responses from outbox + while True: + time.sleep(1) + + +def example_list_endpoints_http(onboarding_response_data): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = HttpMessagingService() + list_endpoint_parameters = ListEndpointsParameters( + technical_message_type=CapabilityType.ISO_11783_TASKDATA_ZIP.value, + direction=2, + filtered=False, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + list_endpoint_service = ListEndpointsService(messaging_service) + + messaging_result = list_endpoint_service.send(list_endpoint_parameters) + print("Sent message: ", messaging_result) + + return messaging_result + + +def example_subscription_http(onboarding_response_data): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = HttpMessagingService() + subscription_service = SubscriptionService(messaging_service) + tmt = CapabilityType.ISO_11783_TASKDATA_ZIP.value + subscription_item = Subscription.MessageTypeSubscriptionItem(technical_message_type=tmt) + subscription_parameters = SubscriptionParameters( + subscription_items=[subscription_item], + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + + messaging_result = subscription_service.send(subscription_parameters) + print("Sent message: ", messaging_result) + + return messaging_result + + +def example_subscription_mqtt(onboarding_response_data, on_msg_callback): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = MqttMessagingService(onboarding_response, on_message_callback=on_msg_callback) + subscription_service = SubscriptionService(messaging_service) + tmt = CapabilityType.ISO_11783_TASKDATA_ZIP.value + subscription_item = Subscription.MessageTypeSubscriptionItem(technical_message_type=tmt) + subscription_parameters = SubscriptionParameters( + subscription_items=[subscription_item], + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + + messaging_result = subscription_service.send(subscription_parameters) + print("Sent message: ", messaging_result) + + # Is needed for waiting of messaging responses from outbox + while True: + time.sleep(1) + + +def example_query_header_message_http(onboarding_response_data): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = HttpMessagingService() + query_header_service = QueryHeaderService(messaging_service) + sent_from = Timestamp() + sent_to = Timestamp() + validity_period = ValidityPeriod(sent_from=sent_from, sent_to=sent_to) + query_header_parameters = QueryHeaderParameters( + message_ids=[new_uuid(), new_uuid()], + senders=[new_uuid(), new_uuid()], + validity_period=validity_period, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + messaging_result = query_header_service.send(query_header_parameters) + print("Sent message: ", messaging_result) + + return messaging_result + + +def example_query_header_message_mqtt(onboarding_response_data, on_msg_callback): + onboarding_response = SoftwareOnboardingResponse() + onboarding_response.json_deserialize(onboarding_response_data) + + messaging_service = MqttMessagingService(onboarding_response, on_message_callback=on_msg_callback) + query_header_service = QueryHeaderService(messaging_service) + sent_from = Timestamp() + sent_to = Timestamp() + validity_period = ValidityPeriod(sent_from=sent_from, sent_to=sent_to) + query_header_parameters = QueryHeaderParameters( + message_ids=[new_uuid(), new_uuid()], + senders=[new_uuid(), new_uuid()], + validity_period=validity_period, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) + messaging_result = query_header_service.send(query_header_parameters) + print("Sent message: ", messaging_result) + + # Is needed for waiting of messaging responses from outbox + while True: + time.sleep(1) + + +def on_message_callback(client, userdata, msg): + + # Define here the way receiving messages will be processed + + from agrirouter.messaging.decode import decode_response + from agrirouter.messaging.decode import decode_details + from agrirouter.messaging.messages import OutboxMessage + + outbox_message = OutboxMessage() + outbox_message.json_deserialize(msg.payload.decode().replace("'", '"')) + + print(outbox_message.command.message) + + decoded_message = decode_response(outbox_message.command.message) + print(decoded_message.response_envelope) + + try: + decoded_details = decode_details(decoded_message.response_payload.details) + print(decoded_details) + except Exception as exc: + print("Error in decoding details: ", exc) + + +if __name__ == "__main__": + onboarding_response_mqtt = example_onboarding(GateWays.MQTT.value) + example_list_endpoints_mqtt(onboarding_response_mqtt.json_serialize(), on_message_callback) + + # or for http + # onboarding_response_mqtt = example_onboarding(GateWays.REST.value) + # example_list_endpoints_http(onboarding_response_mqtt.json_serialize()) diff --git a/examples.txt b/examples.txt index f0fa895a..3ca0f065 100644 --- a/examples.txt +++ b/examples.txt @@ -1,3 +1,44 @@ + +public_key = "-----BEGIN PUBLIC KEY-----\n" \ + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFlVFgYRIkGL6Ay/av2e\n" \ + "S2yIag7XHRWFgVFewPegFyWjUQPSe5t2unrNOUu6Ucp7/ck/Ivm4c/6g39fDDzmq\n" \ + "i4JU8OfMpVbUxpiJSGa/OSiXnDuWkJyjdac/C8ip0EpOCFjAWdE+pnGhDny1XAwp\n" \ + "i4t0/WtO8U+IOYtjxpyyOp3daX97C7ihM1I6eOecVN6Caz9B38EnPg12UGA5NkZO\n" \ + "pnz4BHMwYUZqgxaeOPlh4MquAnF5fdjOV3TkmFWkbP1un3BJkU6owcadbjN5DQCG\n" \ + "jguFzX8VVfJEgn2VtIFbbhqsRivvNDmWst1XNZ0GRpviFFQRymz1WroV0lB9P9vK\n" \ + "mwIDAQAB\n" \ + "-----END PUBLIC KEY-----" + +private_key = "-----BEGIN PRIVATE KEY-----\n" \ + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8WVUWBhEiQYvo\n" \ + "DL9q/Z5LbIhqDtcdFYWBUV7A96AXJaNRA9J7m3a6es05S7pRynv9yT8i+bhz/qDf\n" \ + "18MPOaqLglTw58ylVtTGmIlIZr85KJecO5aQnKN1pz8LyKnQSk4IWMBZ0T6mcaEO\n" \ + "fLVcDCmLi3T9a07xT4g5i2PGnLI6nd1pf3sLuKEzUjp455xU3oJrP0HfwSc+DXZQ\n" \ + "YDk2Rk6mfPgEczBhRmqDFp44+WHgyq4CcXl92M5XdOSYVaRs/W6fcEmRTqjBxp1u\n" \ + "M3kNAIaOC4XNfxVV8kSCfZW0gVtuGqxGK+80OZay3Vc1nQZGm+IUVBHKbPVauhXS\n" \ + "UH0/28qbAgMBAAECggEANVX8vMBaEL/L/RnDCOqp7UTeOl5adx91j2G5+d4FhRiA\n" \ + "73usGpmzHOqSe/OgXvH+e6cGDIL3w00rREgGsiSL0XbGU/PoJTf6CAUA9zI1W1vN\n" \ + "1w2evPPGbBZAybb4s4WfJEjxq12QJrUNvRr+hoLhLuV+axb8o2P4uQbqab9Mz0ER\n" \ + "lczCbHi4VDs1fwmNR3o47T1J4Qffzv1nMlor3pSrDzRDebic7/DC5JFkYZNGUtHk\n" \ + "jKDF5Uv7Vzxgb4Of+i3JA5mRMqvG33pdenvvetwl9X69WOiC29bVlymSHyybBE4A\n" \ + "ItfCAHIiY3nUL7UqzoIXpsyPs3ftkiy3Hn7isVSpLQKBgQDjadkGlqIgXCKZ8RS6\n" \ + "a4iLTTTlh8Ur+vMrejBLPul1oxz2dRWZy8zykfNN2MPz7q2xT8wXGuxgj+jei/fi\n" \ + "Gk08+UudMhV5Dtshb3fFq0NFCBe1ZUEX/wAcKC4Ed9xuuHpe7HOKAG0AsnzS8MPC\n" \ + "lcMiL1/vz0GuRbsiyMY6hXweZQKBgQDUBmQNqOBWDTQkO/8MFHopo6Ju9iNvZ4fC\n" \ + "u4SWqL+5BO3nnQHAQyslsj8FNilqhgMI+zaFFbZMZPv5opBSaAR0CQanKxMe3c9I\n" \ + "XYkAJH2+M0fpp80LtxwShD411UDhIypzumfKe8vUXRW/8TWfl6VidfEVjxw6Rc2D\n" \ + "g9btI4k0/wKBgQC42plnGZq/4yTdLXJD9pUPZrrQuQQ1M8/mT3RiNclfri8kxxe/\n" \ + "5EG8C5dSeBkQd7sInmyve1sZQuFvxSbBy89s+NfV95gsxz6odwtMymHsAyACe0Pm\n" \ + "VYmpWZ/OUgAEoEAYWOuyCZaRMoT0knEOAt6TMx8wt7AUEOqE497+QvMZYQKBgQC6\n" \ + "ARlJenvEQjUaDKBFYrmBShK4MasIktThG0zINyZrFE35wR3GI6b4nRT4Z3mSABst\n" \ + "h+Vef5u8DWOYrurZwHMXsMtrYDiX/ZNZMuV7gIfnkmlmLFWQD4XLIMTKyVjvqcAW\n" \ + "YtOnKU+58CeiieO3LHxkkn97oF7tKEuRMtock+5M1QKBgC2fquqxXMrBEIoMGCZs\n" \ + "ooU5V9gOjFVKC52VWnTNgmOWTqgZuqxPJtCTN5wPvhOSggQuHPwBHa9ioshJ0dGE\n" \ + "6jdxGaJjAc82q2KZu9VEqoH/Xa2aS8dPEHwfJtzUVTia6WkrFtMFNaDMFd6byWDQ\n" \ + "ai+T4i2J3/SDL0BfsFWdQuje\n" \ + "-----END PRIVATE KEY-----" + + >>> private_key = ... # store here your private key you get in AR UI during application creation >>> public_key = ... # store here your public key you get in AR UI during application creation >>> application_id = "8c947a45-c57d-4fd2-affc-206e2sdg3a50" # # store here your application id. You can find it in AR UI @@ -12,13 +53,13 @@ >>> auth_client = ar.Authorization("QA", public_key=public_key, private_key=private_key) >>> auth_url = auth_client.get_auth_request_url(auth_params) # use this url to authorize the user as described at https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/integration/authorization.html#perform-authorization ->>> auth_result_url = ... # the url the user was redirected after his authorization. ->>> auth_response = auth_client.extract_auth_response(auth_result_url) # auth_response containing the results of the auth process +>>> auth_result_url = ... # the url the user was redirected to after his authorization. +>>> auth_response = auth_client.extract_auth_response(auth_result_url) # auth_response contains the results of the auth process >>> auth_client.verify_auth_response(auth_response) # you may verify auth_response to ensure answer was from AR ->>> auth_response.is_successfull # True if user accepted application, False if he rejected +>>> auth_response.is_successful # True if user accepted application, False if he rejected True ->>> auth_response.is_valid # Result of verification, if False, response was not validated by public key. Doesn't indicate was the auth successfull. Accessible only after response verifying +>>> auth_response.is_valid # Result of verification, if False, response was not validated by public key. Doesn't indicate the auth was successfull. Accessible only after response verifying True >>> # Get dict containing data from auth process you will use for futher communication. @@ -45,15 +86,16 @@ True >>> id_ = "mydeviceid" >>> certification_version_id = ... # get from AR UI ->>> utc_timestamp = "2018-06-20T07:29:23.457Z" >>> time_zone = "+03:00" >>> onboarding_client = ar.SoftwareOnboarding("QA", public_key=public_key, private_key=private_key) ->>> onboarding_parameters = ar.SoftwareOnboardingParameter(id_=id_, application_id=application_id, certification_version_id=certification_version_id, gateway_id=GateWays.REST.value, utc_timestamp=utc_timestamp, time_zone=time_zone, reg_code=auth_data["credentials"]["regcode"]) +>>> onboarding_parameters = ar.SoftwareOnboardingParameter(id_=id_, application_id=application_id, certification_version_id=certification_version_id, gateway_id=GateWays.REST.value, time_zone=time_zone, reg_code=auth_data.get_decoded_token().regcode) >>> onboarding_verifying_response = onboarding_client.verify(onboarding_parameters) +>>> onboarding_verifying_response.status_code +>>> onboarding_verifying_response.text >>> onboarding_response = onboarding_client.onboard(onboarding_parameters) ->>> onboarding_response.status_code() ->>> onboarding_response.data() # or onboarding_response.text() +>>> onboarding_response.status_code +>>> onboarding_response.text { "authentication": { @@ -69,4 +111,44 @@ True }, "deviceAlternateId": "c067272a-d3a7-4dcf-ab58-5c45ba66ad60", "sensorAlternateId": "5564ce96-385f-448a-9502-9ea3c940a259" -} \ No newline at end of file +} + + +>>> ########################## +>>> # Messaging + + +>>> from agrirouter.messaging.enums import CapabilityTypeDefinitions +>>> from agrirouter.generated.messaging.request.payload.endpoint.subscription_pb2 import Subscription +>>> from agrirouter.messaging.services.commons import HttpMessagingService, MqttMessagingService +>>> from agrirouter import ListEndpointsParameters, ListEndpointsService, SubscriptionService, SubscriptionParameters +>>> from agrirouter.utils.uuid_util import new_uuid + +>>> # List Endpoints + +>>> messaging_service = HttpMessagingService() +>>> list_endpoint_parameters = ListEndpointsParameters( + technical_message_type=CapabilityTypeDefinitions.ISO_11783_TASKDATA_ZIP.value, + direction=2, + filtered=False, + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, + ) +>>> list_endpoint_service = ListEndpointsService(messaging_service) +>>> list_endpoint_service.send(list_endpoint_parameters) + +>>> # Subscription + +>>> messaging_service = HttpMessagingService() +>>> subscription_service = SubscriptionService(messaging_service) + +>>> tmt = CapabilityTypeDefinitions.ISO_11783_TASKDATA_ZIP.value +>>> subscription_item = Subscription.MessageTypeSubscriptionItem(technical_message_type=tmt) +>>> subscription_parameters = SubscriptionParameters( + subscription_items=[subscription_item], + onboarding_response=onboarding_response, + application_message_id=new_uuid(), + application_message_seq_no=1, +) +>>> subscription_service.send(subscription_parameters) diff --git a/tests/auth_test/test_auth.py b/tests/auth_test/test_auth.py new file mode 100644 index 00000000..350e951e --- /dev/null +++ b/tests/auth_test/test_auth.py @@ -0,0 +1,58 @@ +"""Tests agrirouter/auth/auth.py""" + +from agrirouter import AuthUrlParameter +from agrirouter.auth.auth import Authorization +from tests.constants import ( + public_key, + private_key, + auth_result_url, + ENV, + application_id, +) +import re + + +class TestAuthorization: + def test_extract_query_params(self): + auth_client = Authorization(ENV, public_key=public_key, private_key=private_key) + test_uri = "key1=val1&key2=val2&key3=val3" + params = auth_client._extract_query_params(test_uri) + assert params == {"key1": "val1", "key2": "val2", "key3": "val3"} + + def test_get_auth_request_url(self): + auth_params = AuthUrlParameter( + application_id="8c947a45-c57d-42d2-affc-206e21d63a50", response_type="onboard" + ) + assert auth_params.state + + auth_client = Authorization( + "QA", public_key=public_key, private_key=private_key + ) + + check_url = "https://agrirouter-qa.cfapps.eu10.hana.ondemand.com/application/" \ + "8c947a45-c57d-42d2-affc-206e21d63a50/authorize?response_type=onboard&" + result_url = auth_client.get_auth_request_url(auth_params) + assert check_url == result_url.split("state")[0] + + def test_extract_auth_response(self): + auth_client = Authorization(ENV, public_key=public_key, private_key=private_key) + state = "3770a15d-adf3-4900-a435-464978fe8054" + token = "token" + signature = "signature" + + test_uri = f"www.my_response.com/app?state={state}&token={token}&signature={signature}" + response = auth_client.extract_auth_response(test_uri) + + assert response.state == state + assert response.signature == signature + assert response.token == token + assert not response.error + assert response.is_successful + + def test_get_auth_result(self): + auth_client = Authorization( + "QA", public_key=public_key, private_key=private_key + ) + auth_response = auth_client.extract_auth_response(auth_result_url) + auth_client.verify_auth_response(auth_response) + assert auth_response.get_auth_result().get_decoded_token() diff --git a/tests/auth_test/test_auth_dto.py b/tests/auth_test/test_auth_dto.py new file mode 100644 index 00000000..9497abbd --- /dev/null +++ b/tests/auth_test/test_auth_dto.py @@ -0,0 +1,76 @@ +"""Tests agrirouter/auth/dto.py""" +from agrirouter.auth.dto import AuthorizationToken +from agrirouter.messaging.exceptions import WrongFieldError +import pytest + + +class TestAuthorizationToken: + def test_json_deserialize_from_valid_dict(self): + account = "account" + regcode = "regcode" + expires = "01-01-2021" + test_object = AuthorizationToken( + account=account, + ) + test_object.json_deserialize({"regcode": regcode, "expires": expires}) + assert test_object + assert test_object.account == account + assert test_object.regcode == regcode + assert test_object.expires == expires + + test_object_1 = AuthorizationToken( + regcode=regcode, + ) + test_object_1.json_deserialize({"account": account}) + assert test_object_1 + assert test_object_1.account == account + assert test_object_1.regcode == regcode + assert test_object_1.expires is None + + test_object_2 = AuthorizationToken( + account=account, + ) + test_object_2.json_deserialize({"expires": expires}) + assert test_object_2 + assert test_object_2.account == account + assert test_object_2.regcode is None + assert test_object_2.expires == expires + + test_object_3 = AuthorizationToken( + expires=expires, + ) + test_object_3.json_deserialize({"regcode": regcode}) + assert test_object_3 + assert test_object_3.account is None + assert test_object_3.regcode == regcode + assert test_object_3.expires == expires + + def test_json_deserialize_from_invalid_dict(self): + account = "account" + regcode = "regcode" + expires = "01-01-2021" + test_object = AuthorizationToken() + + with pytest.raises(WrongFieldError): + test_object.json_deserialize({"regcode": regcode, "expires": expires, "wrong_key": account}) + + def test_json_deserialize_from_valid_json(self): + account = "account" + regcode = "regcode" + expires = "01-01-2021" + + json_data = '{"account": "account", "regcode": "regcode", "expires": "01-01-2021"}' + + test_object = AuthorizationToken() + test_object.json_deserialize(json_data) + assert test_object + assert test_object.account == account + assert test_object.regcode == regcode + assert test_object.expires == expires + + def test_json_deserialize_from_invalid_json(self): + json_data = '{"account": "account", "regcode": "regcode", "wrong_key": "01-01-2021"}' + test_object = AuthorizationToken() + + with pytest.raises(WrongFieldError): + assert test_object.json_deserialize(json_data) diff --git a/tests/auth_test/test_response.py b/tests/auth_test/test_response.py index 75d0f679..886ca293 100644 --- a/tests/auth_test/test_response.py +++ b/tests/auth_test/test_response.py @@ -1,22 +1,57 @@ """Tests agrirouter/auth/response.py""" import re + +import pytest + from agrirouter.auth.response import AuthResponse +from tests.constants import valid_response_signature, valid_response_token, public_key def test_decode_token(): - token = ( - "eyJhY2NvdW50IjoiMGJhMjRlZWUtYzMwYi00N2U1LWJkYzktNzcwM" - "2NmYjEzNmEwIiwicmVnY29kZSI6IjhlYWNiMTk4ZmMiLCJleHBpcm" - "VzIjoiMjAyMS0wOS0yM1QxNjowODo0My44ODhaIn0=" - ) - decoded_token = AuthResponse.decode_token(token) - assert isinstance(decoded_token["account"], str) - assert isinstance(decoded_token["expires"], str) - assert re.search(r"[\w]", decoded_token["regcode"]) - assert re.search(r"[\w]", decoded_token["account"]) + + decoded_token = AuthResponse.decode_token(valid_response_token) + assert re.search(r"[\w]", decoded_token.regcode) + assert re.search(r"[\w]", decoded_token.account) + assert decoded_token.expires + + +def test_verify(authorization): + state = "46c81f94-d117-4658-9a38-a85692448219" + token = valid_response_token + signature = valid_response_signature + + auth_response = AuthResponse({"state": state, + "signature": signature, + "token": token}) + + assert auth_response.signature + assert auth_response.token + assert not auth_response.error + + with pytest.raises(PermissionError): + auth_response.is_valid + auth_response.verify(public_key) + assert auth_response.is_valid def test_get_auth_result(authorization): - assert isinstance(AuthResponse(authorization).get_auth_result(), dict) - assert AuthResponse(authorization).get_auth_result()["credentials"] + state = "46c81f94-d117-4658-9a38-a85692448219" + token = valid_response_token + signature = valid_response_signature + + auth_response = AuthResponse({"state": state, + "signature": signature, + "token": token}) + + auth_result = auth_response.get_auth_result() + + assert auth_result.token == token + assert auth_result.state == state + assert auth_result.signature == signature + assert not auth_result.error + assert auth_result.decoded_token + + assert re.search(r"[\w]", auth_result.decoded_token.regcode) + assert re.search(r"[\w]", auth_result.decoded_token.account) + assert auth_result.decoded_token.expires diff --git a/tests/constants.py b/tests/constants.py index c07044eb..5651f23d 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -58,3 +58,70 @@ "HbEeY0F96nioXArdQWXcjUQsTch+p0p9eqh23Ak4ef5oGcZhNd4yp\nY8M6ppvIMiXkgWSPJevCJjhxRJRmndY+ajYGx7CLePx7wN" "vxXWtkng3yh+7WiZ/Y\nqwIDAQAB\n-----END PUBLIC KEY-----" ) + +wrong_auth_result_url = ( + "http://fuf.me/?state=46c81f94-d117-4658-9a38-a85692448219&token=eyJhY2NvdW50IjoiMGJhMjRlZWUtYzMwY" + "i00N2U1LWJkYzktNzcwM2NmYjEzNmEwIiwicmVnY29kZSI6IjhlYWNiMTk4ZmMiLCJleHBpcmVzIjoiMjAyMS0wOS0yM1QxNj" + "owODo0My44ODhaIn0%3D&signature=SUL9SQMWAfG4%2FEyT0rejkRfAyioxJIOs4sxI5wxeB8TkIiv0MR6YFKw1tPIkM4ll" + "uZKHEIgr5WvM3b3SvII9TtEbzZf995R8GIlNP6yyP51TF%2F4vZMbkMjq%2B2g1o0qw%2FyuDQcGz1RpOJWCuBOjMXu9quzGO" + "8xvDW7LjrN%2BMA9rzJZYb1toNf51O0eO4BDWL5L1oLvrKrqvaErKcIoRJtTVJ51awOWMARDkGZahcRdWrZbdGUbQwIyKJQu4" + "vH8%2B4ytlyXPSWEYwKE2VFoAjhzWsKODdRRxDbNNLWsW8sxKamdXjSOC8inHUFsFNoxLbwZEnKROm2s3OfKGYuibXOpXw%3D%3C" +) + +wrong_private_key = ( + "-----BEGIN PRIVATE KEY-----\n" + "BIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8WVUWBhEiQYvo\n" + "DL9q/Z5LbIhqDtcdFYWBUV7A96AXJaNRA9J7m3a6es05S7pRynv9yT8i+bhz/qDf\n" + "18MPOaqLglTw58ylVtTGmIlIZr85KJecO5aQnKN1pz8LyKnQSk4IWMBZ0T6mcaEO\n" + "fLVcDCmLi3T9a07xT4g5i2PGnLI6nd1pf3sLuKEzUjp455xU3oJrP0HfwSc+DXZQ\n" + "YDk2Rk6mfPgEczBhRmqDFp44+WHgyq4CcXl92M5XdOSYVaRs/W6fcEmRTqjBxp1u\n" + "M3kNAIaOC4XNfxVV8kSCfZW0gVtuGqxGK+80OZay3Vc1nQZGm+IUVBHKbPVauhXS\n" + "UH0/28qbAgMBAAECggEANVX8vMBaEL/L/RnDCOqp7UTeOl5adx91j2G5+d4FhRiA\n" + "73usGpmzHOqSe/OgXvH+e6cGDIL3w00rREgGsiSL0XbGU/PoJTf6CAUA9zI1W1vN\n" + "1w2evPPGbBZAybb4s4WfJEjxq12QJrUNvRr+hoLhLuV+axb8o2P4uQbqab9Mz0ER\n" + "lczCbHi4VDs1fwmNR3o47T1J4Qffzv1nMlor3pSrDzRDebic7/DC5JFkYZNGUtHk\n" + "jKDF5Uv7Vzxgb4Of+i3JA5mRMqvG33pdenvvetwl9X69WOiC29bVlymSHyybBE4A\n" + "ItfCAHIiY3nUL7UqzoIXpsyPs3ftkiy3Hn7isVSpLQKBgQDjadkGlqIgXCKZ8RS6\n" + "a4iLTTTlh8Ur+vMrejBLPul1oxz2dRWZy8zykfNN2MPz7q2xT8wXGuxgj+jei/fi\n" + "Gk08+UudMhV5Dtshb3fFq0NFCBe1ZUEX/wAcKC4Ed9xuuHpe7HOKAG0AsnzS8MPC\n" + "lcMiL1/vz0GuRbsiyMY6hXweZQKBgQDUBmQNqOBWDTQkO/8MFHopo6Ju9iNvZ4fC\n" + "u4SWqL+5BO3nnQHAQyslsj8FNilqhgMI+zaFFbZMZPv5opBSaAR0CQanKxMe3c9I\n" + "XYkAJH2+M0fpp80LtxwShD411UDhIypzumfKe8vUXRW/8TWfl6VidfEVjxw6Rc2D\n" + "g9btI4k0/wKBgQC42plnGZq/4yTdLXJD9pUPZrrQuQQ1M8/mT3RiNclfri8kxxe/\n" + "5EG8C5dSeBkQd7sInmyve1sZQuFvxSbBy89s+NfV95gsxz6odwtMymHsAyACe0Pm\n" + "VYmpWZ/OUgAEoEAYWOuyCZaRMoT0knEOAt6TMx8wt7AUEOqE497+QvMZYQKBgQC6\n" + "ARlJenvEQjUaDKBFYrmBShK4MasIktThG0zINyZrFE35wR3GI6b4nRT4Z3mSABst\n" + "h+Vef5u8DWOYrurZwHMXsMtrYDiX/ZNZMuV7gIfnkmlmLFWQD4XLIMTKyVjvqcAW\n" + "YtOnKU+58CeiieO3LHxkkn97oF7tKEuRMtock+5M1QKBgC2fquqxXMrBEIoMGCZs\n" + "ooU5V9gOjFVKC52VWnTNgmOWTqgZuqxPJtCTN5wPvhOSggQuHPwBHa9ioshJ0dGE\n" + "6jdxGaJjAc82q2KZu9VEqoH/Xa2aS8dPEHwfJtzUVTia6WkrFtMFNaDMFd6byWDQ\n" + "ai+T4i2J3/SDL0BfsFWdQuje\n" + "-----END PRIVATE KEY-----" +) + +wrong_public_key = ( + "-----BEGIN PUBLIC KEY-----\n" + "BIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFlVFgYRIkGL6Ay/av2e\n" + "S2yIag7XHRWFgVFewPegFyWjUQPSe5t2unrNOUu6Ucp7/ck/Ivm4c/6g39fDDzmq\n" + "i4JU8OfMpVbUxpiJSGa/OSiXnDuWkJyjdac/C8ip0EpOCFjAWdE+pnGhDny1XAwp\n" + "i4t0/WtO8U+IOYtjxpyyOp3daX97C7ihM1I6eOecVN6Caz9B38EnPg12UGA5NkZO\n" + "pnz4BHMwYUZqgxaeOPlh4MquAnF5fdjOV3TkmFWkbP1un3BJkU6owcadbjN5DQCG\n" + "jguFzX8VVfJEgn2VtIFbbhqsRivvNDmWst1XNZ0GRpviFFQRymz1WroV0lB9P9vK\n" + "mwIDAQAB\n" + "-----END PUBLIC KEY-----" +) + +# taken from auth_result_url +valid_response_signature = ( + "SUL9SQMWAfG4%2FEyT0rejkRfAyioxJIOs4sxI5wxeB8TkIiv0MR6YFKw1tPIkM4ll" + "uZKHEIgr5WvM3b3SvII9TtEbzZf995R8GIlNP6yyP51TF%2F4vZMbkMjq%2B2g1o0qw%2FyuDQcGz1RpOJWCuBOjMXu9quzGO" + "8xvDW7LjrN%2BMA9rzJZYb1toNf51O0eO4BDWL5L1oLvrKrqvaErKcIoRJtTVJ51awOWMARDkGZahcRdWrZbdGUbQwIyKJQu4" + "vH8%2B4ytlyXPSWEYwKE2VFoAjhzWsKODdRRxDbNNLWsW8sxKamdXjSOC8inHUFsFNoxLbwZEnKROm2s3OfKGYuibXOpXw%3D%3D" +) + +# taken from auth_result_url +valid_response_token = ( + "eyJhY2NvdW50IjoiMGJhMjRlZWUtYzMwYi00N2U1LWJkYzktNzcwM" + "2NmYjEzNmEwIiwicmVnY29kZSI6IjhlYWNiMTk4ZmMiLCJleHBpcm" + "VzIjoiMjAyMS0wOS0yM1QxNjowODo0My44ODhaIn0=" + ) diff --git a/tests/enviroments_test/test_environmental_services.py b/tests/enviroments_test/test_environmental_services.py index 611e3172..0f8ffeb4 100644 --- a/tests/enviroments_test/test_environmental_services.py +++ b/tests/enviroments_test/test_environmental_services.py @@ -7,5 +7,7 @@ def test_arclient_set_env(): + assert EnvironmentalService(ENV)._set_env(ENV) is None + assert EnvironmentalService("Production")._set_env("Production") is None with pytest.raises(InvalidEnvironmentSetup): assert EnvironmentalService("WRONG")._set_env("WRONG") diff --git a/tests/enviroments_test/test_environments.py b/tests/enviroments_test/test_environments.py index 8619bdc4..c866f1ad 100644 --- a/tests/enviroments_test/test_environments.py +++ b/tests/enviroments_test/test_environments.py @@ -64,32 +64,14 @@ def test_get_agrirouter_login_url(self): ) def test_get_secured_onboarding_authorization_url(self): + redirect_uri = "www.my_redirect.com" + response_type = "response_type" assert ProductionEnvironment().get_secured_onboarding_authorization_url( - application_id, str, "state", auth_result_url - ) == ProductionEnvironment._ENV_BASE_URL + ProductionEnvironment._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( + application_id, response_type, "state", redirect_uri + ) == "https://goto.my-agrirouter.com/application/{application_id}/authorize?response_type={response_type}&state={state}".format( application_id=application_id, - response_type=str, - state="state", - redirect_uri=auth_result_url, - ) - with pytest.raises(AssertionError): - assert ProductionEnvironment().get_secured_onboarding_authorization_url( - application_id, str, "state", auth_result_url - ) == ProductionEnvironment._ENV_BASE_URL + ProductionEnvironment._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( - application_id=application_id, - response_type=str, - state="123", - redirect_uri=auth_result_url, - ) - with pytest.raises(AssertionError): - assert ProductionEnvironment().get_secured_onboarding_authorization_url( - application_id, dict, "state", auth_result_url - ) == ProductionEnvironment._ENV_BASE_URL + ProductionEnvironment._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( - application_id=application_id, - response_type=str, - state="state", - redirect_uri=auth_result_url, - ) + response_type=response_type, + state="state") + f"&redirect_uri={redirect_uri}" def test_get_mqtt_server_url(self): assert ProductionEnvironment().get_mqtt_server_url( @@ -97,18 +79,6 @@ def test_get_mqtt_server_url(self): ) == ProductionEnvironment._MQTT_URL_TEMPLATE.format( host="localhost", port="5000" ) - with pytest.raises(AssertionError): - assert ProductionEnvironment().get_mqtt_server_url( - "localhost", "5000" - ) == ProductionEnvironment._MQTT_URL_TEMPLATE.format( - host="127.0.0.1", port="5000" - ) - with pytest.raises(AssertionError): - assert ProductionEnvironment().get_mqtt_server_url( - "localhost", "5000" - ) == ProductionEnvironment._MQTT_URL_TEMPLATE.format( - host="localhost", port="80" - ) def test_get_env_public_key(self): assert ( @@ -169,45 +139,19 @@ def test_get_agrirouter_login_url(self): ) def test_get_secured_onboarding_authorization_url(self): + redirect_uri = "www.my_redirect.com" + response_type = "response_type" assert QAEnvironment().get_secured_onboarding_authorization_url( - application_id, str, "state", auth_result_url + application_id, response_type, "state", redirect_uri ) == QAEnvironment._ENV_BASE_URL + QAEnvironment._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( application_id=application_id, - response_type=str, - state="state", - redirect_uri=auth_result_url, - ) - with pytest.raises(AssertionError): - assert QAEnvironment().get_secured_onboarding_authorization_url( - application_id, str, "state", auth_result_url - ) == QAEnvironment._ENV_BASE_URL + QAEnvironment._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( - application_id=application_id, - response_type=str, - state="123", - redirect_uri=auth_result_url, - ) - with pytest.raises(AssertionError): - assert QAEnvironment().get_secured_onboarding_authorization_url( - application_id, dict, "state", auth_result_url - ) == QAEnvironment._ENV_BASE_URL + QAEnvironment._SECURED_ONBOARDING_AUTHORIZATION_LINK_TEMPLATE.format( - application_id=application_id, - response_type=str, - state="state", - redirect_uri=auth_result_url, - ) + response_type=response_type, + state="state") + f"&redirect_uri={redirect_uri}" def test_get_mqtt_server_url(self): assert QAEnvironment().get_mqtt_server_url( "localhost", "5000" ) == QAEnvironment._MQTT_URL_TEMPLATE.format(host="localhost", port="5000") - with pytest.raises(AssertionError): - assert QAEnvironment().get_mqtt_server_url( - "localhost", "5000" - ) == QAEnvironment._MQTT_URL_TEMPLATE.format(host="127.0.0.1", port="5000") - with pytest.raises(AssertionError): - assert QAEnvironment().get_mqtt_server_url( - "localhost", "5000" - ) == QAEnvironment._MQTT_URL_TEMPLATE.format(host="localhost", port="80") def test_get_env_public_key(self): assert QAEnvironment().get_env_public_key() == QAEnvironment.AR_PUBLIC_KEY diff --git a/tests/messaging_test/test_decode.py b/tests/messaging_test/test_decode.py new file mode 100644 index 00000000..bd2aa5da --- /dev/null +++ b/tests/messaging_test/test_decode.py @@ -0,0 +1,51 @@ +import json +import pytest + +from agrirouter.generated.messaging.response.response_pb2 import ResponseEnvelope +from agrirouter.messaging.decode import decode_response +from agrirouter.messaging.decode import decode_details + + +MESSAGING_RESULT = b'[{"sensorAlternateId":"185cd97b-ed0b-4e75-a6e2-6be1cdd38a06","capabilityAlternateId":"bbe9f361-b551-48d9-9fca-1b4dc768287c","command":{"message":"XwjIARAKGiQ5NWUzNWE0Zi1jNWM4LTQ1NDEtODE4OS03NmJlMzM0OTc0NDUiJDUzNzYyM2ZjLWY2NmYtNDc5Yi1hMmJhLWVjZjNlNWM3ZjhlMCoMCNTV5YsGEICI8LIDzQIKygIKTnR5cGVzLmFncmlyb3V0ZXIuY29tL2Fncmlyb3V0ZXIucmVzcG9uc2UucGF5bG9hZC5hY2NvdW50Lkxpc3RFbmRwb2ludHNSZXNwb25zZRL3AQp4CiRkNzA0YTQ0My05OWY3LTQ3YjQtYmU1NS1lMmZhMDk2ODllYmUSJFB5dGhvblNES19kZXYgLSAyMDIxLTEwLTI1LCAxMDo1MToxOBoLYXBwbGljYXRpb24iBmFjdGl2ZTIVdXJuOm15YXBwOnNucjAwMDAzMjM0CnsKJDE4NWNkOTdiLWVkMGItNGU3NS1hNmUyLTZiZTFjZGQzOGEwNhIkUHl0aG9uU0RLX2RldiAtIDIwMjEtMTAtMjEsIDIxOjQxOjI0GgthcHBsaWNhdGlvbiIGYWN0aXZlMhh1cm46bXlhcHA6c25yMDAwMDMyMzRzZGY="}}]' + + +def test_decode_response(): + json_response = json.loads(MESSAGING_RESULT) + message = decode_response(json_response[0]["command"]["message"].encode()) + assert message.response_payload + assert message.response_envelope + + assert message.response_payload.details + + assert message.response_payload.details.type_url == "types.agrirouter.com/agrirouter.response.payload.account.ListEndpointsResponse" + assert message.response_payload.details.value == b'\nx\n$d704a443-99f7-47b4-be55-e2fa09689ebe\x12$PythonSDK_dev - 2021-10-25, 10:51:18\x1a\x0bapplication"\x06active2\x15urn:myapp:snr00003234\n{\n$185cd97b-ed0b-4e75-a6e2-6be1cdd38a06\x12$PythonSDK_dev - 2021-10-21, 21:41:24\x1a\x0bapplication"\x06active2\x18urn:myapp:snr00003234sdf' + + assert message.response_envelope.response_code == 200 + assert message.response_envelope.type == ResponseEnvelope.ResponseBodyType.Value("ENDPOINTS_LISTING") + assert message.response_envelope.application_message_id == "95e35a4f-c5c8-4541-8189-76be33497445" + assert message.response_envelope.message_id == "537623fc-f66f-479b-a2ba-ecf3e5c7f8e0" + + assert message.response_envelope.timestamp + assert message.response_envelope.timestamp.seconds == 1635347156 + assert message.response_envelope.timestamp.nanos == 912000000 + + +def test_decode_details(): + json_response = json.loads(MESSAGING_RESULT) + message = decode_response(json_response[0]["command"]["message"].encode()) + details = message.response_payload.details + decoded_details = decode_details(details) + + assert decoded_details.endpoints + assert len(decoded_details.endpoints) == 2 + assert decoded_details.endpoints[0].endpoint_id == "d704a443-99f7-47b4-be55-e2fa09689ebe" + assert decoded_details.endpoints[0].endpoint_name == "PythonSDK_dev - 2021-10-25, 10:51:18" + assert decoded_details.endpoints[0].endpoint_type == "application" + assert decoded_details.endpoints[0].status == "active" + assert decoded_details.endpoints[0].external_id == "urn:myapp:snr00003234" + + assert decoded_details.endpoints[1].endpoint_id == "185cd97b-ed0b-4e75-a6e2-6be1cdd38a06" + assert decoded_details.endpoints[1].endpoint_name == "PythonSDK_dev - 2021-10-21, 21:41:24" + assert decoded_details.endpoints[1].endpoint_type == "application" + assert decoded_details.endpoints[1].status == "active" + assert decoded_details.endpoints[1].external_id == "urn:myapp:snr00003234sdf" diff --git a/tests/messaging_test/test_encode.py b/tests/messaging_test/test_encode.py new file mode 100644 index 00000000..886f4965 --- /dev/null +++ b/tests/messaging_test/test_encode.py @@ -0,0 +1,26 @@ +from google.protobuf.any_pb2 import Any + +from agrirouter.generated.commons.message_pb2 import Message, Messages +from agrirouter.generated.messaging.request.request_pb2 import RequestEnvelope, RequestPayloadWrapper +from agrirouter.messaging.decode import read_properties_buffers_from_input_stream +from agrirouter.messaging.encode import write_proto_parts_to_buffer + + +def test_write_proto_parts_to_buffer(): + mode = 1 + tmt = "TMT" + team_set_context_id = "team_set_context_id" + type_url = "type_url" + + message = Message(message="Test message", message_code="Test message code") + messages = Messages(messages=[message]) + + envelope = RequestEnvelope(mode=mode, technical_message_type=tmt, team_set_context_id=team_set_context_id) + payload = RequestPayloadWrapper(details=Any(type_url=type_url, value=messages.SerializeToString())) + + buffer = write_proto_parts_to_buffer([envelope, payload]) + result = read_properties_buffers_from_input_stream(buffer) + + assert len(result) == 2 + assert len(result[0]) == envelope.ByteSize() + assert len(result[1]) == payload.ByteSize() diff --git a/tests/messaging_test/test_request.py b/tests/messaging_test/test_request.py index 5b978ec6..25771cef 100644 --- a/tests/messaging_test/test_request.py +++ b/tests/messaging_test/test_request.py @@ -10,6 +10,6 @@ def test_json_serialize(): capability_alternate_id="1", messages=[Message(content="content")], ).json_serialize() - assert isinstance(message_request, dict) assert message_request["capabilityAlternateId"] == "1" assert message_request["sensorAlternateId"] == "1" + assert message_request["measures"] diff --git a/tests/onboarding_test/test_headers.py b/tests/onboarding_test/test_headers.py index 4a412082..210eb7e9 100644 --- a/tests/onboarding_test/test_headers.py +++ b/tests/onboarding_test/test_headers.py @@ -11,7 +11,6 @@ class TestSoftwareOnboardingHeader: ) def test_get_header(self): - assert isinstance(self.test_object.get_header(), dict) assert ( self.test_object.get_header()["Authorization"] == "Bearer " + self.reg_code ) diff --git a/tests/onboarding_test/test_onboarding.py b/tests/onboarding_test/test_onboarding.py new file mode 100644 index 00000000..d14e8b15 --- /dev/null +++ b/tests/onboarding_test/test_onboarding.py @@ -0,0 +1,61 @@ +"""Test agrirouter/onboarding/onboarding.py""" + +from agrirouter.onboarding.exceptions import WrongCertificationType, WrongGateWay +from agrirouter.onboarding.onboarding import SoftwareOnboarding +from agrirouter.onboarding.parameters import SoftwareOnboardingParameter +from agrirouter.onboarding.enums import GateWays, CertificateTypes +from tests.constants import public_key, private_key, ENV, application_id +import pytest + + +class TestSoftwareOnboarding: + def test__create_request(self): + params = SoftwareOnboardingParameter( + id_=1, + application_id=application_id, + content_type="json", + certification_version_id="13", + gateway_id=GateWays.MQTT.value, + certificate_type=CertificateTypes.PEM.value, + utc_timestamp="+03:00", + time_zone="01-01-2021", + reg_code="8eloz190fd", + ) + onboarding = SoftwareOnboarding( + public_key=public_key, private_key=private_key, env=ENV + ) + assert onboarding._create_request(params, "localhost") + + params = SoftwareOnboardingParameter( + id_=2, + application_id=application_id, + content_type="json", + certification_version_id="13", + gateway_id=GateWays.MQTT.value, + certificate_type="wrong_certificate", + utc_timestamp="+03:00", + time_zone="01-01-2021", + reg_code="8eloz190fd", + ) + onboarding = SoftwareOnboarding( + public_key=public_key, private_key=private_key, env=ENV + ) + with pytest.raises(WrongCertificationType): + assert onboarding._create_request(params, "localhost") + + params = SoftwareOnboardingParameter( + id_=3, + application_id=application_id, + content_type="content_type", + certification_version_id="13", + gateway_id="wrong_gateway_id", + certificate_type=CertificateTypes.PEM.value, + utc_timestamp="+03:00", + time_zone="01-01-2021", + reg_code="8eloz190fd", + ) + onboarding = SoftwareOnboarding( + public_key=public_key, private_key=private_key, env=ENV + ) + with pytest.raises(WrongGateWay): + assert onboarding._create_request(params, "localhost") diff --git a/tests/onboarding_test/test_onboarding_dto.py b/tests/onboarding_test/test_onboarding_dto.py new file mode 100644 index 00000000..2ddbdb74 --- /dev/null +++ b/tests/onboarding_test/test_onboarding_dto.py @@ -0,0 +1,281 @@ +"""Tests agrirouter/onboarding/dto.py""" +from agrirouter.onboarding.dto import ConnectionCriteria, Authentication, ErrorResponse +from agrirouter.messaging.exceptions import WrongFieldError +import pytest + + +class TestConnectionCriteria: + + def test_json_deserialize_from_valid_dict(self): + client_id = "1" + commands = "commands" + gateway_id = "3" + host = "localhost" + measures = "test_measures" + port = "80" + + test_object = ConnectionCriteria() + + data = {"clientId": "1", "commands": "commands", "gatewayId": "3", "host": "localhost", "port": "80", + "measures": "test_measures"} + + test_object.json_deserialize(data) + assert test_object + assert test_object.gateway_id == gateway_id + assert test_object.client_id == client_id + assert test_object.commands == commands + assert test_object.host == host + assert test_object.measures == measures + assert test_object.port == port + + def test_json_deserialize_from_invalid_dict(self): + test_object = ConnectionCriteria() + + with pytest.raises(WrongFieldError): + test_object.json_deserialize({"clientId": "1", "commands": "commands", "wrong_key": "localhost"}) + + def test_json_deserialize_from_valid_json(self): + client_id = "1" + commands = "commands" + gateway_id = "3" + host = "localhost" + measures = "test_measures" + port = "80" + + json_data = '{"clientId": "1", "commands": "commands", "gatewayId": "3", "host": "localhost", "port": "80",' \ + '"measures": "test_measures"}' + + test_object = ConnectionCriteria() + test_object.json_deserialize(json_data) + assert test_object + assert test_object.gateway_id == gateway_id + assert test_object.client_id == client_id + assert test_object.commands == commands + assert test_object.host == host + assert test_object.measures == measures + assert test_object.port == port + + def test_json_deserialize_from_invalid_json(self): + json_data = '{"client_id": "1", "commands": "commands", "wrong_key": "localhost"}' + test_object = ConnectionCriteria() + + with pytest.raises(WrongFieldError): + assert test_object.json_deserialize(json_data) + + def test_json_serialize(self): + client_id = "1" + commands = "commands" + gateway_id = "3" + host = "localhost" + measures = "test_measures" + port = "80" + + test_object = ConnectionCriteria( + client_id=client_id, + commands=commands, + gateway_id=gateway_id, + host=host, + measures=measures, + port=port + ) + + serialized_data = test_object.json_serialize() + assert serialized_data["gatewayId"] == gateway_id + assert serialized_data["clientId"] == client_id + assert serialized_data["commands"] == commands + assert serialized_data["host"] == host + assert serialized_data["measures"] == measures + assert serialized_data["port"] == port + + +class TestAuthentication: + def test_json_deserialize(self): + type = "type" + secret = "secret" + certificate = "certificate" + test_object = Authentication( + type=type, + secret=secret, + ) + test_object.json_deserialize({"certificate": certificate}) + assert test_object + assert test_object.type == type + assert test_object.secret == secret + assert test_object.certificate == certificate + + test_object_1 = Authentication(type=type, certificate=certificate) + test_object_1.json_deserialize({"secret": secret}) + assert test_object_1 + assert test_object_1.type == type + assert test_object_1.secret == secret + assert test_object_1.certificate == certificate + + test_object_2 = Authentication(secret=secret, certificate=certificate) + test_object_2.json_deserialize({"type": type}) + assert test_object_2 + assert test_object_2.type == type + assert test_object_2.secret == secret + assert test_object_2.certificate == certificate + + test_object_2 = Authentication( + secret=secret, + ) + test_object_2.json_deserialize({"type": type}) + assert test_object_2 + assert test_object_2.type == type + assert test_object_2.secret == secret + assert test_object_2.certificate is None + + with pytest.raises(WrongFieldError): + assert test_object_2.json_deserialize({"wrong_key": certificate}) + + def test_json_deserialize_from_valid_dict(self): + type = "type" + secret = "secret" + certificate = "certificate" + + test_object = Authentication() + + test_object.json_deserialize({"certificate": certificate, "type": type, "secret": secret}) + + assert test_object + assert test_object.type == type + assert test_object.secret == secret + assert test_object.certificate == certificate + + test_object_1 = Authentication(type=type, certificate=certificate) + test_object_1.json_deserialize({"secret": secret}) + assert test_object_1 + assert test_object_1.type == type + assert test_object_1.secret == secret + assert test_object_1.certificate == certificate + + test_object_2 = Authentication(secret=secret, certificate=certificate) + test_object_2.json_deserialize({"type": type}) + assert test_object_2 + assert test_object_2.type == type + assert test_object_2.secret == secret + assert test_object_2.certificate == certificate + + test_object_2 = Authentication( + secret=secret, + ) + test_object_2.json_deserialize({"type": type}) + assert test_object_2 + assert test_object_2.type == type + assert test_object_2.secret == secret + assert test_object_2.certificate is None + + def test_json_deserialize_from_invalid_dict(self): + test_object = Authentication() + + with pytest.raises(WrongFieldError): + test_object.json_deserialize({"type": "type", "secret": "secret", "wrong_key": "certificate"}) + + def test_json_deserialize_from_valid_json(self): + type = "type" + secret = "secret" + certificate = "certificate" + + test_object = Authentication() + json_data = '{"certificate": "certificate", "type": "type", "secret": "secret"}' + + test_object.json_deserialize(json_data) + + assert test_object + assert test_object.type == type + assert test_object.secret == secret + assert test_object.certificate == certificate + + def test_json_deserialize_from_invalid_json(self): + json_data = '{"type": "type", "secret": "secret", "wrong_key": "certificate"}' + test_object = ConnectionCriteria() + + with pytest.raises(WrongFieldError): + assert test_object.json_deserialize(json_data) + + def test_json_serialize(self): + type = "type" + secret = "secret" + certificate = "certificate" + + test_object = Authentication( + type=type, + secret=secret, + certificate=certificate + ) + + serialized_data = test_object.json_serialize() + + assert serialized_data + assert serialized_data["type"] == type + assert serialized_data["secret"] == secret + assert serialized_data["certificate"] == certificate + + +class TestErrorResponse: + + def test_json_deserialize_from_valid_dict(self): + code = "400" + message = "message" + target = "target" + details = "details" + + data = {"code": code, "message": message, "target": target, "details": details} + + test_object = ErrorResponse() + test_object.json_deserialize(data) + assert test_object + assert test_object.code == code + assert test_object.message == message + assert test_object.target == target + assert test_object.details == details + + def test_json_deserialize_from_invalid_dict(self): + data = {"code": "401", "message": "message", "wrong_field": "target"} + test_object = ErrorResponse() + + with pytest.raises(WrongFieldError): + assert test_object.json_deserialize(data) + + def test_json_deserialize_from_valid_json(self): + code = "400" + message = "message" + target = "target" + details = "details" + + json_data = '{"code": "400", "message": "message", "target": "target", "details": "details"}' + + test_object = ErrorResponse() + test_object.json_deserialize(json_data) + assert test_object + assert test_object.code == code + assert test_object.message == message + assert test_object.target == target + assert test_object.details == details + + def test_json_deserialize_from_invalid_json(self): + json_data = '{"code": "401", "message": "message", "wrong_field": "target"}' + test_object = ErrorResponse() + + with pytest.raises(WrongFieldError): + assert test_object.json_deserialize(json_data) + + def test_json_serialize(self): + code = "400" + message = "message" + target = "target" + details = "details" + + test_object = ErrorResponse( + code=code, + message=message, + target=target, + details=details + ) + + serialized_data = test_object.json_serialize() + assert serialized_data["code"] == code + assert serialized_data["message"] == message + assert serialized_data["target"] == target + assert serialized_data["details"] == details diff --git a/tests/onboarding_test/test_request_onboarding.py b/tests/onboarding_test/test_request_onboarding.py new file mode 100644 index 00000000..17ab1eea --- /dev/null +++ b/tests/onboarding_test/test_request_onboarding.py @@ -0,0 +1,51 @@ +"""Test agrirouter/onboarding/request.py""" + +from agrirouter import SoftwareOnboardingParameter, SoftwareOnboarding +from agrirouter.onboarding.enums import GateWays, CertificateTypes +from tests.constants import application_id, public_key, private_key, ENV + + +class TestBaseOnboardingRequest: + reg_code = "8eloz190fd" + content_type = "json" + certification_version_id = "13" + utc_timestamp = "+03:00" + time_zone = "01-01-2021" + url = "localhost" + params = SoftwareOnboardingParameter( + id_=1, + application_id=application_id, + content_type=content_type, + certification_version_id=certification_version_id, + gateway_id=GateWays.MQTT.value, + certificate_type=CertificateTypes.PEM.value, + utc_timestamp=utc_timestamp, + time_zone=time_zone, + reg_code=reg_code, + ) + onboarding = SoftwareOnboarding( + public_key=public_key, private_key=private_key, env=ENV + ) + test_object = onboarding._create_request(params, url) + + def test_get_url(self): + assert self.test_object.get_url() == self.url + + def test_get_data(self): + assert self.test_object.get_data()["applicationId"] == application_id + assert ( + self.test_object.get_data()["certificateType"] == CertificateTypes.PEM.value + ) + assert ( + self.test_object.get_data()["certificateType"] == CertificateTypes.PEM.value + ) + + def test_get_header(self): + assert ( + self.test_object.get_header()["Authorization"] == "Bearer " + self.reg_code + ) + assert self.test_object.get_header()["Content-Type"] == self.content_type + assert ( + self.test_object.get_header()["X-Agrirouter-ApplicationId"] + == application_id + ) diff --git a/tests/onboarding_test/test_signature.py b/tests/onboarding_test/test_signature.py new file mode 100644 index 00000000..e275d5d9 --- /dev/null +++ b/tests/onboarding_test/test_signature.py @@ -0,0 +1,27 @@ +"""Test agrirouter/onboarding/signature.py""" + +import pytest +import re + +from cryptography.exceptions import InvalidSignature + +from agrirouter.onboarding.signature import create_signature, verify_signature +from tests.constants import private_key, wrong_private_key, public_key, valid_response_signature + + +def test_create_signature_ok(): + signature = create_signature( + "REQUEST CONTENT", private_key) + raised = False + try: + verify_signature( + "REQUEST CONTENT", bytes.fromhex(signature), public_key) + except InvalidSignature: + raised = True + assert not raised + + +def test_verify_signature_fail(): + with pytest.raises(InvalidSignature): + verify_signature( + "REQUEST CONTENT", b"wrong_signature", public_key) diff --git a/tests/sleeper.py b/tests/sleeper.py new file mode 100644 index 00000000..9d4179ac --- /dev/null +++ b/tests/sleeper.py @@ -0,0 +1,5 @@ +import time + + +def let_agrirouter_process_the_message(seconds: int = 3): + time.sleep(seconds) diff --git a/tests/test_revoking/test_parameters.py b/tests/test_revoking/test_parameters.py new file mode 100644 index 00000000..2c1c737e --- /dev/null +++ b/tests/test_revoking/test_parameters.py @@ -0,0 +1,30 @@ +"""Test agrirouter/revoking/parameters.py""" + +from agrirouter import RevokingParameter +from tests.constants import application_id + + +class TestRevokingParameter: + content_type = "json" + account_id = "111" + endpoint_ids = "endpoint_1" + time_zone = "+03:00" + utc_timestamp = "01-01-2021" + test_object = RevokingParameter( + application_id=application_id, + content_type=content_type, + account_id=account_id, + endpoint_ids=endpoint_ids, + utc_timestamp=utc_timestamp, + time_zone=time_zone + ) + + def test_get_header_params(self): + assert self.test_object.get_header_params()["application_id"] == application_id + assert self.test_object.get_header_params()["content_type"] == self.content_type + + def test_get_body_params(self): + assert self.test_object.get_body_params()["account_id"] == self.account_id + assert self.test_object.get_body_params()["endpoint_ids"] == self.endpoint_ids + assert self.test_object.get_body_params()["utc_timestamp"] == self.utc_timestamp + assert self.test_object.get_body_params()["time_zone"] == self.time_zone