diff --git a/yoti_python_sdk/doc_scan/__init__.py b/yoti_python_sdk/doc_scan/__init__.py index b9068ee9..76be8753 100644 --- a/yoti_python_sdk/doc_scan/__init__.py +++ b/yoti_python_sdk/doc_scan/__init__.py @@ -13,6 +13,7 @@ from .session.create.notification_config import NotificationConfigBuilder from .session.create.sdk_config import SdkConfigBuilder from .session.create.session_spec import SessionSpecBuilder +from .session.create.filter.required_share_code import RequiredShareCodeBuilder from .client import DocScanClient __all__ = [ @@ -25,5 +26,6 @@ "SessionSpecBuilder", "NotificationConfigBuilder", "SdkConfigBuilder", + "RequiredShareCodeBuilder", "DocScanClient", ] diff --git a/yoti_python_sdk/doc_scan/constants.py b/yoti_python_sdk/doc_scan/constants.py index 279d4e5e..bfa10b4a 100644 --- a/yoti_python_sdk/doc_scan/constants.py +++ b/yoti_python_sdk/doc_scan/constants.py @@ -12,6 +12,7 @@ SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION = ( "SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION" ) +VERIFY_SHARE_CODE_TASK = "VERIFY_SHARE_CODE_TASK" CAMERA = "CAMERA" CAMERA_AND_UPLOAD = "CAMERA_AND_UPLOAD" diff --git a/yoti_python_sdk/doc_scan/session/create/filter/__init__.py b/yoti_python_sdk/doc_scan/session/create/filter/__init__.py index aa9998d6..71a80deb 100644 --- a/yoti_python_sdk/doc_scan/session/create/filter/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/filter/__init__.py @@ -5,6 +5,7 @@ from .orthogonal_restrictions_filter import OrthogonalRestrictionsFilterBuilder from .required_id_document import RequiredIdDocumentBuilder from .required_supplementary_document import RequiredSupplementaryDocumentBuilder +from .required_share_code import RequiredShareCodeBuilder __all__ = [ "DocumentRestrictionsFilterBuilder", @@ -12,4 +13,5 @@ "OrthogonalRestrictionsFilterBuilder", "RequiredIdDocumentBuilder", "RequiredSupplementaryDocumentBuilder", + "RequiredShareCodeBuilder", ] diff --git a/yoti_python_sdk/doc_scan/session/create/filter/required_share_code.py b/yoti_python_sdk/doc_scan/session/create/filter/required_share_code.py new file mode 100644 index 00000000..ad1212c3 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/required_share_code.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.utils import YotiSerializable, remove_null_values + + +class RequiredShareCode(YotiSerializable): + """ + Represents a required share code for session creation + """ + + def __init__(self, issuer=None, scheme=None): + """ + :param issuer: the issuer of the share code + :type issuer: str or None + :param scheme: the scheme of the share code + :type scheme: str or None + """ + self.__issuer = issuer + self.__scheme = scheme + + @property + def issuer(self): + """ + The issuer of the share code + + :return: the issuer + :rtype: str or None + """ + return self.__issuer + + @property + def scheme(self): + """ + The scheme of the share code + + :return: the scheme + :rtype: str or None + """ + return self.__scheme + + def to_json(self): + return remove_null_values({ + "issuer": self.issuer, + "scheme": self.scheme, + }) + + +class RequiredShareCodeBuilder(object): + """ + Builder used to assist the creation of a required share code. + + Example:: + + required_share_code = (RequiredShareCodeBuilder() + .with_issuer("some-issuer") + .with_scheme("some-scheme") + .build()) + + """ + + def __init__(self): + self.__issuer = None + self.__scheme = None + + def with_issuer(self, issuer): + """ + Sets the issuer of the required share code + + :param issuer: the issuer + :type issuer: str + :return: the builder + :rtype: RequiredShareCodeBuilder + """ + self.__issuer = issuer + return self + + def with_scheme(self, scheme): + """ + Sets the scheme of the required share code + + :param scheme: the scheme + :type scheme: str + :return: the builder + :rtype: RequiredShareCodeBuilder + """ + self.__scheme = scheme + return self + + def build(self): + """ + Builds a required share code, using the values supplied to the builder + + :return: the required share code + :rtype: RequiredShareCode + """ + return RequiredShareCode(self.__issuer, self.__scheme) diff --git a/yoti_python_sdk/doc_scan/session/create/session_spec.py b/yoti_python_sdk/doc_scan/session/create/session_spec.py index 75a2b8be..df96e23d 100644 --- a/yoti_python_sdk/doc_scan/session/create/session_spec.py +++ b/yoti_python_sdk/doc_scan/session/create/session_spec.py @@ -22,6 +22,7 @@ def __init__( required_documents=None, block_biometric_consent=None, session_deadline=None, + required_share_codes=None, ): """ :param client_session_token_ttl: the client session token TTL @@ -43,7 +44,9 @@ def __init__( :param block_biometric_consent: block the collection of biometric consent :type block_biometric_consent: bool :param session_deadline: session deadline using a Zoned timestamp - "type session_deadline: str + :type session_deadline: str + :param required_share_codes: the list of required share codes + :type required_share_codes: list[RequiredShareCode] or None """ if requested_tasks is None: requested_tasks = [] @@ -51,6 +54,8 @@ def __init__( requested_checks = [] if required_documents is None: required_documents = [] + if required_share_codes is None: + required_share_codes = [] self.__client_session_token_ttl = client_session_token_ttl self.__resources_ttl = resources_ttl @@ -62,6 +67,7 @@ def __init__( self.__required_documents = required_documents self.__block_biometric_consent = block_biometric_consent self.__session_deadline = session_deadline + self.__required_share_codes = required_share_codes @property def client_session_token_ttl(self): @@ -166,6 +172,16 @@ def session_deadline(self): """ return self.__session_deadline + @property + def required_share_codes(self): + """ + List of required share codes for the session + + :return: the list of required share codes + :rtype: list[RequiredShareCode] + """ + return self.__required_share_codes + def to_json(self): return remove_null_values( { @@ -179,6 +195,7 @@ def to_json(self): "required_documents": self.required_documents, "block_biometric_consent": self.block_biometric_consent, "session_deadline": self.session_deadline, + "required_share_codes": self.required_share_codes, } ) @@ -199,6 +216,7 @@ def __init__(self): self.__required_documents = [] self.__block_biometric_consent = None self.__session_deadline = None + self.__required_share_codes = [] def with_client_session_token_ttl(self, value): """ @@ -321,6 +339,18 @@ def with_block_biometric_consent(self, block_biometric_consent): self.__block_biometric_consent = block_biometric_consent return self + def with_required_share_code(self, required_share_code): + """ + Adds a required share code to the session specification + + :param required_share_code: the required share code + :type required_share_code: RequiredShareCode + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__required_share_codes.append(required_share_code) + return self + def build(self): """ Builds a :class:`SessionSpec` using the supplied values @@ -339,4 +369,5 @@ def build(self): self.__required_documents, self.__block_biometric_consent, self.__session_deadline, + self.__required_share_codes, ) diff --git a/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py index 8295e503..793ba221 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py @@ -11,6 +11,9 @@ LivenessResourceResponse, ZoomLivenessResourceResponse, ) +from yoti_python_sdk.doc_scan.session.retrieve.share_code_resource_response import ( + ShareCodeResourceResponse, +) class ResourceContainer(object): @@ -42,6 +45,11 @@ def __init__(self, data=None): for liveness in data.get("liveness_capture", []) ] + self.__share_codes = [ + ShareCodeResourceResponse(share_code) + for share_code in data.get("share_codes", []) + ] + @staticmethod def __parse_liveness_capture(liveness_capture): """ @@ -105,3 +113,13 @@ def zoom_liveness_resources(self): for liveness in self.__liveness_capture if isinstance(liveness, ZoomLivenessResourceResponse) ] + + @property + def share_codes(self): + """ + Return a list of share code resources + + :return: list of share codes + :rtype: list[ShareCodeResourceResponse] + """ + return self.__share_codes diff --git a/yoti_python_sdk/doc_scan/session/retrieve/share_code_resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/share_code_resource_response.py new file mode 100644 index 00000000..9d7adb93 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/share_code_resource_response.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import iso8601 +from iso8601 import ParseError + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.doc_scan.session.retrieve.resource_response import ResourceResponse +from yoti_python_sdk.doc_scan.session.retrieve.file_response import FileResponse +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class ShareCodeProfile(object): + """ + Represents a share code profile (lookup or returned) + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__media = MediaResponse(data["media"]) if "media" in data.keys() else None + + @property + def media(self): + """ + Returns the media associated with the profile + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media + + +class ShareCodeIdPhoto(object): + """ + Represents a share code ID photo + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__media = MediaResponse(data["media"]) if "media" in data.keys() else None + + @property + def media(self): + """ + Returns the media associated with the ID photo + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media + + +class VerifyShareCodeTaskResponse(object): + """ + Represents a verify share code task + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__type = data.get("type", None) + self.__id = data.get("id", None) + self.__state = data.get("state", None) + self.__created = self.__parse_date(data.get("created", None)) + self.__last_updated = self.__parse_date(data.get("last_updated", None)) + + from yoti_python_sdk.doc_scan.session.retrieve.generated_media import GeneratedMedia + self.__generated_media = [ + GeneratedMedia(media) for media in data.get("generated_media", []) + ] + + @staticmethod + def __parse_date(date): + """ + Parse a date using the iso8601 library, + returning None if there was an error + + :param date: the date string to parse + :type date: str + :return: the parsed date + :rtype: datetime.datetime or None + """ + if date is None: + return None + + try: + return iso8601.parse_date(date) + except ParseError: + return None + + @property + def type(self): + """ + Return the type of the task + + :return: the type + :rtype: str or None + """ + return self.__type + + @property + def id(self): + """ + Return the ID of the task + + :return: the ID + :rtype: str or None + """ + return self.__id + + @property + def state(self): + """ + Return the state of the task + + :return: the state + :rtype: str or None + """ + return self.__state + + @property + def created(self): + """ + Return the date the task was created + + :return: the created date + :rtype: datetime.datetime or None + """ + return self.__created + + @property + def last_updated(self): + """ + Return the date the task was last updated + + :return: the last updated date + :rtype: datetime.datetime or None + """ + return self.__last_updated + + @property + def generated_media(self): + """ + Return the list of media that has been generated by the task + + :return: the list of generated media + :rtype: list[GeneratedMedia] + """ + return self.__generated_media + + +class ShareCodeResourceResponse(ResourceResponse): + """ + Represents a share code resource for a given session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + # Don't call ResourceResponse.__init__ as we have custom task parsing + self.__id = data.get("id", None) + self.__source = data.get("source", None) + self.__created_at = data.get("created_at", None) + self.__last_updated = data.get("last_updated", None) + + self.__lookup_profile = ( + ShareCodeProfile(data["lookup_profile"]) + if "lookup_profile" in data.keys() + else None + ) + self.__returned_profile = ( + ShareCodeProfile(data["returned_profile"]) + if "returned_profile" in data.keys() + else None + ) + self.__id_photo = ( + ShareCodeIdPhoto(data["id_photo"]) + if "id_photo" in data.keys() + else None + ) + self.__file = ( + FileResponse(data["file"]) + if "file" in data.keys() + else None + ) + + # Parse tasks specifically for share code resources + self.__tasks = [ + self.__parse_task(task) for task in data.get("tasks", []) + ] + + @staticmethod + def __parse_task(task): + """ + Parse a task into a VerifyShareCodeTaskResponse + + :param task: the raw task + :type task: dict + :return: the parsed task + :rtype: VerifyShareCodeTaskResponse + """ + task_type = task.get("type", None) + if task_type == constants.VERIFY_SHARE_CODE_TASK: + return VerifyShareCodeTaskResponse(task) + # Fallback to generic task response if type doesn't match + return VerifyShareCodeTaskResponse(task) + + @property + def id(self): + """ + The ID of the resource + + :return: the id + :rtype: str or None + """ + return self.__id + + @property + def source(self): + """ + The source of the share code + + :return: the source + :rtype: str or None + """ + return self.__source + + @property + def created_at(self): + """ + When the share code resource was created + + :return: the created_at timestamp + :rtype: str or None + """ + return self.__created_at + + @property + def last_updated(self): + """ + When the share code resource was last updated + + :return: the last_updated timestamp + :rtype: str or None + """ + return self.__last_updated + + @property + def lookup_profile(self): + """ + The lookup profile of the share code + + :return: the lookup profile + :rtype: ShareCodeProfile or None + """ + return self.__lookup_profile + + @property + def returned_profile(self): + """ + The returned profile of the share code + + :return: the returned profile + :rtype: ShareCodeProfile or None + """ + return self.__returned_profile + + @property + def id_photo(self): + """ + The ID photo from the share code + + :return: the ID photo + :rtype: ShareCodeIdPhoto or None + """ + return self.__id_photo + + @property + def file(self): + """ + The file associated with the share code + + :return: the file + :rtype: FileResponse or None + """ + return self.__file + + @property + def tasks(self): + """ + Tasks associated with the share code resource + + :return: the list of tasks + :rtype: list[VerifyShareCodeTaskResponse] + """ + return self.__tasks + + @property + def verify_share_code_tasks(self): + """ + Returns a list of verify share code tasks associated with the share code + + :return: list of verify share code tasks + :rtype: list[VerifyShareCodeTaskResponse] + """ + return [ + task for task in self.tasks if isinstance(task, VerifyShareCodeTaskResponse) + ] diff --git a/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_share_code.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_share_code.py new file mode 100644 index 00000000..4eb23e9e --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_share_code.py @@ -0,0 +1,76 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create.filter.required_share_code import ( + RequiredShareCode, + RequiredShareCodeBuilder, +) +from yoti_python_sdk.utils import YotiEncoder + + +class RequiredShareCodeTest(unittest.TestCase): + SOME_ISSUER = "some-issuer" + SOME_SCHEME = "some-scheme" + + def test_should_build_correctly_with_issuer_and_scheme(self): + result = ( + RequiredShareCodeBuilder() + .with_issuer(self.SOME_ISSUER) + .with_scheme(self.SOME_SCHEME) + .build() + ) + + assert result.issuer == self.SOME_ISSUER + assert result.scheme == self.SOME_SCHEME + + def test_should_build_correctly_with_only_issuer(self): + result = RequiredShareCodeBuilder().with_issuer(self.SOME_ISSUER).build() + + assert result.issuer == self.SOME_ISSUER + assert result.scheme is None + + def test_should_build_correctly_with_only_scheme(self): + result = RequiredShareCodeBuilder().with_scheme(self.SOME_SCHEME).build() + + assert result.issuer is None + assert result.scheme == self.SOME_SCHEME + + def test_should_build_correctly_with_no_fields(self): + result = RequiredShareCodeBuilder().build() + + assert result.issuer is None + assert result.scheme is None + + def test_should_serialize_to_json_without_error(self): + result = ( + RequiredShareCodeBuilder() + .with_issuer(self.SOME_ISSUER) + .with_scheme(self.SOME_SCHEME) + .build() + ) + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + parsed = json.loads(s) + assert parsed["issuer"] == self.SOME_ISSUER + assert parsed["scheme"] == self.SOME_SCHEME + + def test_should_not_include_null_values_in_json(self): + result = RequiredShareCodeBuilder().with_issuer(self.SOME_ISSUER).build() + + s = json.dumps(result, cls=YotiEncoder) + parsed = json.loads(s) + + assert parsed["issuer"] == self.SOME_ISSUER + assert "scheme" not in parsed + + def test_should_create_directly(self): + result = RequiredShareCode(issuer=self.SOME_ISSUER, scheme=self.SOME_SCHEME) + + assert result.issuer == self.SOME_ISSUER + assert result.scheme == self.SOME_SCHEME + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py b/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py index 034c4b05..52f43ee9 100644 --- a/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py +++ b/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py @@ -8,6 +8,9 @@ from yoti_python_sdk.doc_scan.session.create.filter.required_id_document import ( RequiredIdDocument, ) +from yoti_python_sdk.doc_scan.session.create.filter.required_share_code import ( + RequiredShareCode, +) from yoti_python_sdk.doc_scan.session.create.notification_config import ( NotificationConfig, ) @@ -123,6 +126,33 @@ def test_should_build_correctly_without_block_biometric_consent_false(self): assert result.block_biometric_consent is None + def test_should_add_required_share_code(self): + required_share_code_mock = Mock(spec=RequiredShareCode) + + result = ( + SessionSpecBuilder() + .with_required_share_code(required_share_code_mock) + .build() + ) + + assert len(result.required_share_codes) == 1 + assert result.required_share_codes[0] == required_share_code_mock + + def test_should_default_empty_array_for_required_share_codes(self): + result = SessionSpec( + client_session_token_ttl=1, + resources_ttl=1, + user_tracking_id="someTrackingId", + notifications=None, + sdk_config=None, + requested_checks=None, + requested_tasks=None, + required_documents=None, + required_share_codes=None, + ) + + assert len(result.required_share_codes) == 0 + if __name__ == "__main__": unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py index 5a08c5a0..b44b1dc0 100644 --- a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py @@ -9,6 +9,9 @@ from yoti_python_sdk.doc_scan.session.retrieve.resource_container import ( ResourceContainer, ) +from yoti_python_sdk.doc_scan.session.retrieve.share_code_resource_response import ( + ShareCodeResourceResponse, +) class ResourceContainerTest(unittest.TestCase): @@ -20,6 +23,10 @@ def test_should_parse_correctly(self): {"liveness_type": "ZOOM"}, {"liveness_type": "someUnknown"}, ], + "share_codes": [ + {"id": "share-code-1", "source": "source-1"}, + {"id": "share-code-2", "source": "source-2"}, + ], } result = ResourceContainer(data) @@ -29,12 +36,15 @@ def test_should_parse_correctly(self): assert len(result.liveness_capture) == 2 assert isinstance(result.liveness_capture[0], ZoomLivenessResourceResponse) assert isinstance(result.liveness_capture[1], LivenessResourceResponse) + assert len(result.share_codes) == 2 + assert isinstance(result.share_codes[0], ShareCodeResourceResponse) def test_should_parse_with_none(self): result = ResourceContainer(None) assert len(result.id_documents) == 0 assert len(result.liveness_capture) == 0 + assert len(result.share_codes) == 0 def test_should_filter_zoom_liveness_resources(self): data = { @@ -49,6 +59,24 @@ def test_should_filter_zoom_liveness_resources(self): assert len(result.liveness_capture) == 2 assert len(result.zoom_liveness_resources) == 1 + def test_should_parse_share_codes(self): + data = { + "share_codes": [ + { + "id": "share-code-id", + "source": "share-code-source", + "created_at": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + ] + } + + result = ResourceContainer(data) + + assert len(result.share_codes) == 1 + assert result.share_codes[0].id == "share-code-id" + assert result.share_codes[0].source == "share-code-source" + if __name__ == "__main__": unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_share_code_resource_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_share_code_resource_response.py new file mode 100644 index 00000000..679e9b57 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_share_code_resource_response.py @@ -0,0 +1,213 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.share_code_resource_response import ( + ShareCodeResourceResponse, + ShareCodeProfile, + ShareCodeIdPhoto, + VerifyShareCodeTaskResponse, +) + + +class ShareCodeResourceResponseTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = { + "id": "some-id", + "source": "some-source", + "created_at": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + "lookup_profile": { + "media": { + "id": "media-id-1", + "type": "JSON", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + }, + "returned_profile": { + "media": { + "id": "media-id-2", + "type": "JSON", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + }, + "id_photo": { + "media": { + "id": "media-id-3", + "type": "IMAGE", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + }, + "file": { + "media": { + "id": "media-id-4", + "type": "JSON", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + }, + "tasks": [ + { + "type": "VERIFY_SHARE_CODE_TASK", + "id": "task-id-1", + "state": "DONE", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + "generated_media": [ + {"id": "gen-media-1", "type": "JSON"} + ], + } + ], + } + + result = ShareCodeResourceResponse(data) + + assert result.id == "some-id" + assert result.source == "some-source" + assert result.created_at == "2023-01-01T00:00:00Z" + assert result.last_updated == "2023-01-02T00:00:00Z" + assert result.lookup_profile is not None + assert result.returned_profile is not None + assert result.id_photo is not None + assert result.file is not None + assert len(result.tasks) == 1 + + def test_should_parse_with_none(self): + result = ShareCodeResourceResponse(None) + + assert result.id is None + assert result.source is None + assert result.created_at is None + assert result.last_updated is None + assert result.lookup_profile is None + assert result.returned_profile is None + assert result.id_photo is None + assert result.file is None + assert len(result.tasks) == 0 + + def test_should_parse_tasks(self): + data = { + "tasks": [ + { + "type": "VERIFY_SHARE_CODE_TASK", + "id": "task-id-1", + "state": "DONE", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + "generated_media": [], + }, + { + "type": "VERIFY_SHARE_CODE_TASK", + "id": "task-id-2", + "state": "IN_PROGRESS", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + "generated_media": [], + }, + ] + } + + result = ShareCodeResourceResponse(data) + + assert len(result.tasks) == 2 + assert len(result.verify_share_code_tasks) == 2 + assert isinstance(result.tasks[0], VerifyShareCodeTaskResponse) + assert result.tasks[0].type == "VERIFY_SHARE_CODE_TASK" + assert result.tasks[0].id == "task-id-1" + assert result.tasks[0].state == "DONE" + + +class ShareCodeProfileTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = { + "media": { + "id": "media-id", + "type": "JSON", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + } + + result = ShareCodeProfile(data) + + assert result.media is not None + assert result.media.id == "media-id" + assert result.media.type == "JSON" + + def test_should_parse_with_none(self): + result = ShareCodeProfile(None) + + assert result.media is None + + +class ShareCodeIdPhotoTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = { + "media": { + "id": "photo-media-id", + "type": "IMAGE", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + } + } + + result = ShareCodeIdPhoto(data) + + assert result.media is not None + assert result.media.id == "photo-media-id" + assert result.media.type == "IMAGE" + + def test_should_parse_with_none(self): + result = ShareCodeIdPhoto(None) + + assert result.media is None + + +class VerifyShareCodeTaskResponseTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = { + "type": "VERIFY_SHARE_CODE_TASK", + "id": "task-id", + "state": "DONE", + "created": "2023-01-01T00:00:00Z", + "last_updated": "2023-01-02T00:00:00Z", + "generated_media": [ + {"id": "gen-media-1", "type": "JSON"}, + {"id": "gen-media-2", "type": "IMAGE"}, + ], + } + + result = VerifyShareCodeTaskResponse(data) + + assert result.type == "VERIFY_SHARE_CODE_TASK" + assert result.id == "task-id" + assert result.state == "DONE" + assert result.created is not None + assert result.last_updated is not None + assert len(result.generated_media) == 2 + + def test_should_parse_with_none(self): + result = VerifyShareCodeTaskResponse(None) + + assert result.type is None + assert result.id is None + assert result.state is None + assert result.created is None + assert result.last_updated is None + assert len(result.generated_media) == 0 + + def test_should_handle_invalid_date(self): + data = { + "created": "invalid-date", + "last_updated": "also-invalid", + } + + result = VerifyShareCodeTaskResponse(data) + + assert result.created is None + assert result.last_updated is None + + +if __name__ == "__main__": + unittest.main()