Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and extend save_link #18

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/edutap/wallet_google/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from google.auth import jwt
from pydantic import ValidationError

import datetime
import json
import logging
import time
import typing


Expand Down Expand Up @@ -357,15 +357,33 @@ def _create_payload(models: list[ClassModel | ObjectModel | Reference]) -> JWTPa
return payload


def _convert_str_or_datetime_to_str(value: str | datetime.datetime) -> str:
"""convert and check the value to be a valid string for the JWT claim timestamps"""
if isinstance(value, datetime.datetime):
return str(int(value.timestamp()))
if value == "":
return value
if not value.isdecimal():
raise ValueError("string must be a decimal")
if int(value) < 0:
raise ValueError("string must be an int >= 0 number")
if int(value) > 2**32:
raise ValueError("string must be an int < 2**32 number")
return value


def _create_claims(
issuer: str,
origins: list[str],
models: list[ClassModel | ObjectModel | Reference],
iat: str | datetime.datetime,
exp: str | datetime.datetime,
) -> JWTClaims:
"""Creates a JWTClaims instance based on the given issuer, origins and models."""
return JWTClaims(
iss=issuer,
iat=str(int(time.time())),
iat=_convert_str_or_datetime_to_str(iat),
exp=_convert_str_or_datetime_to_str(exp),
origins=origins,
payload=_create_payload(models),
)
Expand All @@ -375,6 +393,8 @@ def save_link(
models: list[ClassModel | ObjectModel | Reference],
*,
origins: list[str] = [],
iat: str | datetime.datetime = "",
exp: str | datetime.datetime = "",
) -> str:
"""
Creates a link to save a Google Wallet Object to the wallet on the device.
Expand All @@ -393,14 +413,26 @@ def save_link(
The Google Wallet API button will not render when the origins field is not defined.
You could potentially get an "Load denied by X-Frame-Options" or "Refused to display"
messages in the browser console when the origins field is not defined.
:param: iat: Issued At Time. The time when the JWT was issued.
:param: exp: Expiration Time. The time when the JWT expires.
:return: Link with JWT to save the resources to the wallet.
"""

claims = _create_claims(
session_manager.settings.credentials_info["client_email"],
origins,
models,
iat=iat,
exp=exp,
)
logger.debug(
claims.model_dump_json(
indent=2,
exclude_unset=False,
exclude_defaults=False,
exclude_none=True,
)
)

signer = crypt.RSASigner.from_service_account_file(
session_manager.settings.credentials_file
)
Expand All @@ -413,6 +445,7 @@ def save_link(
exclude_none=True,
),
).decode("utf-8")
logger.debug(jwt_string)
if (jwt_len := len(jwt_string)) >= 1800:
logger.debug(
f"JWT-Length: {jwt_len} is larger than recommended 1800 bytes",
Expand Down
6 changes: 4 additions & 2 deletions src/edutap/wallet_google/models/datatypes/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..passes import retail
from ..passes import tickets_and_transit
from ..passes.bases import Reference
from datetime import datetime


class JWTPayload(Model):
Expand Down Expand Up @@ -38,7 +39,8 @@ class JWTClaims(Model):

iss: str
aud: str = "google"
typ: str = "savettowallet"
iat: str = ""
typ: str = "savetowallet"
iat: str | datetime = ""
exp: str | datetime = ""
payload: JWTPayload
origins: list[str]
58 changes: 47 additions & 11 deletions tests/test_api_save_link.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from freezegun import freeze_time
import datetime
import pytest


def test_create_payload():
Expand All @@ -20,7 +21,6 @@ def test_create_payload():
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
Expand All @@ -35,8 +35,9 @@ def test_create_claims():
expected = {
"iss": "123456789",
"aud": "google",
"typ": "savettowallet",
"iat": "1737118440",
"typ": "savetowallet",
"iat": "1737115974",
"exp": "",
"payload": {
"offerObjects": [
{
Expand All @@ -53,7 +54,13 @@ def test_create_claims():
"origins": [],
}

claims = api._create_claims("123456789", [], models)
claims = api._create_claims(
"123456789",
[],
models,
iat=datetime.datetime(2025, 1, 17, 12, 12, 54, 0, datetime.timezone.utc),
exp="",
)

dumped = claims.model_dump(
mode="json",
Expand All @@ -64,7 +71,6 @@ def test_create_claims():
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

Expand All @@ -84,9 +90,39 @@ def test_api_save_link(mock_settings):
"OfferObject",
{"id": "test-2.edutap.eu", "classId": "test-class-1.edutap.eu"},
),
]
)
assert (
link
== "https://pay.google.com/gp/v/save/eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3OCJ9.eyJpc3MiOiAiZWR1dGFwLXRlc3QtZXhhbXBsZUBzb2RpdW0tcmF5LTEyMzQ1Ni5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsICJhdWQiOiAiZ29vZ2xlIiwgInR5cCI6ICJzYXZldHRvd2FsbGV0IiwgImlhdCI6ICIxNzM3MTE4NDQwIiwgInBheWxvYWQiOiB7Im9mZmVyT2JqZWN0cyI6IFt7ImlkIjogInRlc3QtMi5lZHV0YXAuZXUiLCAiY2xhc3NJZCI6ICJ0ZXN0LWNsYXNzLTEuZWR1dGFwLmV1IiwgInN0YXRlIjogIlNUQVRFX1VOU1BFQ0lGSUVEIiwgImhhc0xpbmtlZERldmljZSI6IGZhbHNlLCAiZGlzYWJsZUV4cGlyYXRpb25Ob3RpZmljYXRpb24iOiBmYWxzZSwgIm5vdGlmeVByZWZlcmVuY2UiOiAiTk9USUZJQ0FUSU9OX1NFVFRJTkdTX0ZPUl9VUERBVEVTX1VOU1BFQ0lGSUVEIn1dLCAiZ2VuZXJpY09iamVjdHMiOiBbeyJpZCI6ICJ0ZXN0LTEuZWR1dGFwLmV1In1dfSwgIm9yaWdpbnMiOiBbXX0.X5nE4Zh4kvxGZ4LxnEw8_9i2tCq-JJUmSCRxpf8ckQJVNumsA_uQze0uyuCPTBSVsdHcnMxozFdiCktrwIdvBYDjSLf91acoADMtY1n-HIPbCexlpTpHgyDpyC8giVx27lAvkjaDtHzvEoJ1nAkwh-_-322uKwEWOllR_voV162lNLMIE5QY2eDJ9yfcFa5LXqxSW64UDTZSYX0DsJtGf-Oa1HOgnG77D6RZfMYUq18duNmcBp5wREVrE27vPjnVRC2kEsrWfQNw4gkN_aSp6ZhEAG-exQqUQQXeR7nPqYm-DM7uVOCh8pbk8WqwA7fKcsZIRK8dsWnqnDuNdwP7Gw"
],
iat=datetime.datetime(2025, 1, 22, 10, 20, 0, 0, datetime.timezone.utc)

)
expected = "https://pay.google.com/gp/v/save/eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3OCJ9.eyJpc3MiOiAiZWR1dGFwLXRlc3QtZXhhbXBsZUBzb2RpdW0tcmF5LTEyMzQ1Ni5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsICJhdWQiOiAiZ29vZ2xlIiwgInR5cCI6ICJzYXZldG93YWxsZXQiLCAiaWF0IjogIjE3Mzc1NDEyMDAiLCAiZXhwIjogIiIsICJwYXlsb2FkIjogeyJvZmZlck9iamVjdHMiOiBbeyJpZCI6ICJ0ZXN0LTIuZWR1dGFwLmV1IiwgImNsYXNzSWQiOiAidGVzdC1jbGFzcy0xLmVkdXRhcC5ldSIsICJzdGF0ZSI6ICJTVEFURV9VTlNQRUNJRklFRCIsICJoYXNMaW5rZWREZXZpY2UiOiBmYWxzZSwgImRpc2FibGVFeHBpcmF0aW9uTm90aWZpY2F0aW9uIjogZmFsc2UsICJub3RpZnlQcmVmZXJlbmNlIjogIk5PVElGSUNBVElPTl9TRVRUSU5HU19GT1JfVVBEQVRFU19VTlNQRUNJRklFRCJ9XSwgImdlbmVyaWNPYmplY3RzIjogW3siaWQiOiAidGVzdC0xLmVkdXRhcC5ldSJ9XX0sICJvcmlnaW5zIjogW119.u8xDMKKdPBB0yjYqR-uM4eAYMEskRZyv_AOBhGkZ0oswvr-nVOs4jogXZo6cOmSvzjE_tRviNf_GHDelOaND-c4AqNwTg13DRG0c-aNWKbROTlrZefG0dusPcAuhTwzG-gsDn_sCstHWy8gkKQOmb_x4RjRB-b_gsv2uhmeKtNPvofxBNLUHbOefYKL12PPII9kI00Dl0pAyh0dgqI3yew0197a2rYl6_lOlYfO4jd784b-3CDCDKpOZnEjqBBedbLSDhKdWV10eo9mz6OsgqydERuUDDzhJopkwz6BIFL_HA_IHeAaiLtoSNbuOqc7zUecgOHlqecaWBZhV_-WPkQ"
assert link == expected


def test__convert_str_or_datetime_to_str__timestamp():
from edutap.wallet_google.api import _convert_str_or_datetime_to_str

dt = datetime.datetime(2021, 1, 1, 12, 0, 0, 0, datetime.timezone.utc)
expected = "1609502400"

assert _convert_str_or_datetime_to_str(dt) == expected


def test__convert_str_or_datetime_to_str__str_int_lt_zero():
from edutap.wallet_google.api import _convert_str_or_datetime_to_str

with pytest.raises(ValueError):
_convert_str_or_datetime_to_str("-1")


def test__convert_str_or_datetime_to_str__str_int_tr_4bytes():
from edutap.wallet_google.api import _convert_str_or_datetime_to_str

with pytest.raises(ValueError):
_convert_str_or_datetime_to_str(f"{2**32+1}")


def test__convert_str_or_datetime_to_str__not_decimal():
from edutap.wallet_google.api import _convert_str_or_datetime_to_str

with pytest.raises(ValueError):
_convert_str_or_datetime_to_str("x 100")
Loading