diff --git a/examples/edutap_wallet_google_example_callback/README.md b/examples/edutap_wallet_google_example_callback/README.md index f356a2b..6280769 100644 --- a/examples/edutap_wallet_google_example_callback/README.md +++ b/examples/edutap_wallet_google_example_callback/README.md @@ -76,9 +76,11 @@ We hope you get the idea. There is an example swarm deployment in here in `swarm.yml`. It can be deployed on the cluster. -The public domain is configured using the environment variable `EDUTAP_WALLET_GOOGLE_EXAMPLE_DOMAIN`. +The public domain must be configured using the environment variable `EDUTAP_WALLET_GOOGLE_EXAMPLE_DOMAIN`. A TLS certificate will be issued automatically using Lets Encrypt. +Example: `export EDUTAP_WALLET_GOOGLE_EXAMPLE_DOMAIN=edutap-wallet-google-callback.example.com` + ```shell docker stack deploy swarm.yml -c swarm.yml edutap_wallet_google_example_callback ``` diff --git a/pyproject.toml b/pyproject.toml index 9b47b55..43d0776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,12 +47,13 @@ callback = [ "httpx", ] test = [ - "pytest", + "edutap.wallet_google[callback]", + "freezegun", "pytest-cov", "pytest-explicit", + "pytest", "requests-mock", "tox", - "edutap.wallet_google[callback]", ] typecheck = [ "google-auth-stubs", diff --git a/src/edutap/wallet_google/api.py b/src/edutap/wallet_google/api.py index 0835895..9d6292b 100644 --- a/src/edutap/wallet_google/api.py +++ b/src/edutap/wallet_google/api.py @@ -20,6 +20,7 @@ import json import logging +import time import typing @@ -338,6 +339,38 @@ def listing( break +def _create_payload(models: list[ClassModel | ObjectModel | Reference]) -> JWTPayload: + """Creates a payload for the JWT.""" + payload = JWTPayload() + + for model in models: + if isinstance(model, Reference): + if model.model_name is not None: + name = lookup_metadata_by_name(model.model_name)["plural"] + elif model.model_type is not None: + name = lookup_metadata_by_model_type(model.model_type)["plural"] + else: + name = lookup_metadata_by_model_instance(model)["plural"] + if getattr(payload, name) is None: + setattr(payload, name, []) + getattr(payload, name).append(model) + return payload + + +def _create_claims( + issuer: str, + origins: list[str], + models: list[ClassModel | ObjectModel | Reference], +) -> JWTClaims: + """Creates a JWTClaims instance based on the given issuer, origins and models.""" + return JWTClaims( + iss=issuer, + iat=str(int(time.time())), + origins=origins, + payload=_create_payload(models), + ) + + def save_link( models: list[ClassModel | ObjectModel | Reference], *, @@ -362,23 +395,11 @@ def save_link( messages in the browser console when the origins field is not defined. :return: Link with JWT to save the resources to the wallet. """ - payload = JWTPayload() - for model in models: - if isinstance(model, Reference): - if model.model_name is not None: - name = lookup_metadata_by_name(model.model_name)["plural"] - elif model.model_type is not None: - name = lookup_metadata_by_model_type(model.model_type)["plural"] - else: - name = lookup_metadata_by_model_instance(model)["plural"] - if getattr(payload, name) is None: - setattr(payload, name, []) - getattr(payload, name).append(model) - claims = JWTClaims( - iss=session_manager.settings.credentials_info["client_email"], - origins=origins, - payload=payload, + claims = _create_claims( + session_manager.settings.credentials_info["client_email"], + origins, + models, ) signer = crypt.RSASigner.from_service_account_file( session_manager.settings.credentials_file diff --git a/tests/test_api_save_link.py b/tests/test_api_save_link.py index 253cfaa..9b05b80 100644 --- a/tests/test_api_save_link.py +++ b/tests/test_api_save_link.py @@ -1,3 +1,70 @@ +from freezegun import freeze_time + + +def test_create_payload(): + + from edutap.wallet_google import api + + payload = api._create_payload( + [ + api.new( + "Reference", {"id": "test-1.edutap.eu", "model_name": "GenericObject"} + ), + api.new( + "OfferObject", + {"id": "test-2.edutap.eu", "classId": "test-class-1.edutap.eu"}, + ), + ] + ) + expected = '{"offerObjects":[{"id":"test-2.edutap.eu","classId":"test-class-1.edutap.eu","state":"STATE_UNSPECIFIED","hasLinkedDevice":false,"disableExpirationNotification":false,"notifyPreference":"NOTIFICATION_SETTINGS_FOR_UPDATES_UNSPECIFIED"}],"genericObjects":[{"id":"test-1.edutap.eu"}]}' + assert payload.model_dump_json(exclude_none=True) == expected + + +@freeze_time("2025-01-17 12:54:00") +def test_create_claims(): + + from edutap.wallet_google import api + + models = [ + api.new("Reference", {"id": "test-1.edutap.eu", "model_name": "GenericObject"}), + api.new( + "OfferObject", + {"id": "test-2.edutap.eu", "classId": "test-class-1.edutap.eu"}, + ), + ] + expected = { + "iss": "123456789", + "aud": "google", + "typ": "savettowallet", + "iat": "1737118440", + "payload": { + "offerObjects": [ + { + "id": "test-2.edutap.eu", + "classId": "test-class-1.edutap.eu", + "state": "STATE_UNSPECIFIED", + "hasLinkedDevice": False, + "disableExpirationNotification": False, + "notifyPreference": "NOTIFICATION_SETTINGS_FOR_UPDATES_UNSPECIFIED", + } + ], + "genericObjects": [{"id": "test-1.edutap.eu"}], + }, + "origins": [], + } + + claims = api._create_claims("123456789", [], models) + + dumped = claims.model_dump( + mode="json", + exclude_unset=False, + exclude_defaults=False, + exclude_none=True, + ) + assert expected == dumped + + +@freeze_time("2025-01-17 12:54:00") def test_api_save_link(mock_settings): from edutap.wallet_google.settings import ROOT_DIR @@ -21,5 +88,5 @@ def test_api_save_link(mock_settings): ) assert ( link - == "https://pay.google.com/gp/v/save/eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3OCJ9.eyJpc3MiOiAiZWR1dGFwLXRlc3QtZXhhbXBsZUBzb2RpdW0tcmF5LTEyMzQ1Ni5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsICJhdWQiOiAiZ29vZ2xlIiwgInR5cCI6ICJzYXZldHRvd2FsbGV0IiwgImlhdCI6ICIiLCAicGF5bG9hZCI6IHsib2ZmZXJPYmplY3RzIjogW3siaWQiOiAidGVzdC0yLmVkdXRhcC5ldSIsICJjbGFzc0lkIjogInRlc3QtY2xhc3MtMS5lZHV0YXAuZXUiLCAic3RhdGUiOiAiU1RBVEVfVU5TUEVDSUZJRUQiLCAiaGFzTGlua2VkRGV2aWNlIjogZmFsc2UsICJkaXNhYmxlRXhwaXJhdGlvbk5vdGlmaWNhdGlvbiI6IGZhbHNlLCAibm90aWZ5UHJlZmVyZW5jZSI6ICJOT1RJRklDQVRJT05fU0VUVElOR1NfRk9SX1VQREFURVNfVU5TUEVDSUZJRUQifV0sICJnZW5lcmljT2JqZWN0cyI6IFt7ImlkIjogInRlc3QtMS5lZHV0YXAuZXUifV19LCAib3JpZ2lucyI6IFtdfQ.LEPJBlt7ic9cPWKUvpoxUWe5yvdK0_kqPlBFkHmqFBfO5eeYN-owTHCElCGhnHeE730D4U3XjQWeZXfcaEAQcdBKB8udoT2Tja7Rw_M8M18kpBrSdGDRKT_uXG_-RkG3uVB30Lu5otlJiX2VOJWg9H6NR7wD_pfUt67cLjiBeMILuIVi-h0CDUV0dObEjnOHrRhj6KeKdfqq6izwwmw4iSQxsaQrDxWZtwCZ__pV5UK54Od6-lNrsBQwz241SDYv9kJTXrImrjRZXdoht6xgwqxg-GcuqUJgcczG-TLyN_9aI4FtA2cz8PCXyKPXnd-_HTe9nohi05dfMDeVWsmP6g" + == "https://pay.google.com/gp/v/save/eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3OCJ9.eyJpc3MiOiAiZWR1dGFwLXRlc3QtZXhhbXBsZUBzb2RpdW0tcmF5LTEyMzQ1Ni5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsICJhdWQiOiAiZ29vZ2xlIiwgInR5cCI6ICJzYXZldHRvd2FsbGV0IiwgImlhdCI6ICIxNzM3MTE4NDQwIiwgInBheWxvYWQiOiB7Im9mZmVyT2JqZWN0cyI6IFt7ImlkIjogInRlc3QtMi5lZHV0YXAuZXUiLCAiY2xhc3NJZCI6ICJ0ZXN0LWNsYXNzLTEuZWR1dGFwLmV1IiwgInN0YXRlIjogIlNUQVRFX1VOU1BFQ0lGSUVEIiwgImhhc0xpbmtlZERldmljZSI6IGZhbHNlLCAiZGlzYWJsZUV4cGlyYXRpb25Ob3RpZmljYXRpb24iOiBmYWxzZSwgIm5vdGlmeVByZWZlcmVuY2UiOiAiTk9USUZJQ0FUSU9OX1NFVFRJTkdTX0ZPUl9VUERBVEVTX1VOU1BFQ0lGSUVEIn1dLCAiZ2VuZXJpY09iamVjdHMiOiBbeyJpZCI6ICJ0ZXN0LTEuZWR1dGFwLmV1In1dfSwgIm9yaWdpbnMiOiBbXX0.X5nE4Zh4kvxGZ4LxnEw8_9i2tCq-JJUmSCRxpf8ckQJVNumsA_uQze0uyuCPTBSVsdHcnMxozFdiCktrwIdvBYDjSLf91acoADMtY1n-HIPbCexlpTpHgyDpyC8giVx27lAvkjaDtHzvEoJ1nAkwh-_-322uKwEWOllR_voV162lNLMIE5QY2eDJ9yfcFa5LXqxSW64UDTZSYX0DsJtGf-Oa1HOgnG77D6RZfMYUq18duNmcBp5wREVrE27vPjnVRC2kEsrWfQNw4gkN_aSp6ZhEAG-exQqUQQXeR7nPqYm-DM7uVOCh8pbk8WqwA7fKcsZIRK8dsWnqnDuNdwP7Gw" )