"""Test models.""" import datetime from typing import Any from zoneinfo import ZoneInfo import freezegun import pytest from src.models import ( create_dummy_m2m_token, DUMMY_EXPIRES_IN_SECONDS, M2MToken, ClientCredentials, OAuthToken, ) from freezegun import freeze_time @pytest.fixture def token_string() -> str: """Return fake token string.""" return "fake token" @pytest.mark.parametrize( "expiration_string, expiration_datetime", [ pytest.param( "2024-07-22 16:55:30.13455+0000", datetime.datetime( 2024, 7, 22, 16, 55, 30, 134550, datetime.timezone.utc, ), id="Has TZ and timestamp with microseconds", ), pytest.param( "2024-07-22 16:55:30.13455-0500", datetime.datetime( 2024, 7, 22, 16, 55, 30, 134550, ZoneInfo("EST"), ), id="Has TZ with non-zero offset and timestamp with microseconds", ), pytest.param( "2024-07-22 16:55:30+0000", datetime.datetime( 2024, 7, 22, 16, 55, 30, 0, datetime.timezone.utc, ), id="Has TZ and timestamp without microseconds", ), pytest.param( "2024-07-22 16:55+0000", datetime.datetime( 2024, 7, 22, 16, 55, 0, 0, datetime.timezone.utc, ), id="Has TZ and timestamp without milliseconds, microseconds", ), pytest.param( "2024-07-22T16:55+0000", datetime.datetime( 2024, 7, 22, 16, 55, 0, 0, datetime.timezone.utc, ), id="Has TZ and optional isoformat separator 'T'", ), ], ) def test_m2m_token_model_validate( expiration_string: str, expiration_datetime: datetime.datetime, token_string: str ) -> None: """Test M2MToken schema model_validate.""" client_credentials_secret_arn = "M2M_TOKEN_CLIENT_CREDENTIALS" token = M2MToken.model_validate( { "token": token_string, "expires_at": expiration_string, "client_credentials_secret_arn": client_credentials_secret_arn, } ) assert token.token == token_string assert token.expires_at == expiration_datetime assert token.client_credentials_secret_arn == client_credentials_secret_arn token_from_datetime = M2MToken.model_validate( { "token": token_string, "expires_at": expiration_datetime, "client_credentials_secret_arn": client_credentials_secret_arn, } ) assert token_from_datetime.token == token_string assert token_from_datetime.expires_at == expiration_datetime assert ( token_from_datetime.client_credentials_secret_arn == client_credentials_secret_arn ) @pytest.mark.parametrize( "invalid_expiration", [ pytest.param("bs bs bs bs", id="not a date."), pytest.param("2024-07-22 16:55:30.13455-2400", id="invalid tz offset"), pytest.param("2024-07-22 16:55:30.13455", id="missing tz"), pytest.param("2024-07-22 16:55:30.13455+00000", id="invalid tz format"), pytest.param( "2024-07-22 16:55:30.+0000", id="missing microseconds after decimal" ), pytest.param("2024-07-22 16:55:.0+0000", id="missing seconds after colon"), pytest.param("2024-07-22 16::30.0+0000", id="missing minutes between colons"), pytest.param("2024-07-22 :55:30.0+0000", id="missing hours before 1st colon"), ], ) def test_m2m_token_invalid_expiration( invalid_expiration: str, token_string: str, ) -> None: """Test M2MToken with invalid timestamps.""" client_credentials_secret_arn = "M2M_TOKEN_CLIENT_CREDENTIALS" with pytest.raises(ValueError): M2MToken.model_validate( { "token": token_string, "expires_at": invalid_expiration, "client_credentials_secret_arn": client_credentials_secret_arn, } ) @pytest.mark.parametrize( "invalid_token", [ pytest.param("", id="Empty string not allowed."), pytest.param(" ", id="Token with all spaces is not allowed"), ], ) def test_m2m_token_invalid_token( invalid_token: str, expiration_string: str, ) -> None: """Test M2MToken schema with invalid tokens.""" client_credentials_secret_arn = "M2M_TOKEN_CLIENT_CREDENTIALS" with pytest.raises(ValueError): M2MToken.model_validate( { "token": invalid_token, "expires_at": expiration_string, "client_credentials_secret_arn": client_credentials_secret_arn, } ) @pytest.mark.parametrize( "invalid_arn", [ pytest.param("", id="Empty ARN not allowed."), pytest.param(" ", id="ARN with all spaces is not allowed"), ], ) def test_m2m_token_invalid_arn( invalid_arn: str, expiration_string: str, token_string: str, ) -> None: """Test M2MToken schema with invalid arns.""" with pytest.raises(ValueError): M2MToken.model_validate( { "token": token_string, "expires_at": expiration_string, "client_credentials_secret_arn": invalid_arn, } ) def test_m2m_token_model_dump_json( expiration_string: str, mock_arn: str, ) -> None: """Test M2MToken model_dump_json output is standard string.""" token_string = "1" token = M2MToken.model_validate( { "token": token_string, "expires_at": expiration_string, "client_credentials_secret_arn": mock_arn, } ) actual = token.model_dump_json() assert ( actual == '{"token":"1","expires_at":"2024-07-22 16:55:30.134550+0000","client_credentials_secret_arn":"%s"}' % mock_arn ) token.expires_at = datetime.datetime( 2025, 4, 23, 16, 55, 30, 143130, datetime.timezone.utc, ) new_token = token.model_dump_json() assert ( new_token == '{"token":"1","expires_at":"2025-04-23 16:55:30.143130+0000","client_credentials_secret_arn":"%s"}' % mock_arn ) def test_is_expired(expiration_string: str, mock_arn: str) -> None: """Test token is expired happy path.""" token = M2MToken.model_validate( { "token": "happy path", "expires_at": expiration_string, "client_credentials_secret_arn": mock_arn, } ) assert token.is_expired() def test_is_not_expired(mock_arn: str) -> None: """Test token is not expired.""" expires_at = datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=3) expiration_string = expires_at.strftime("%Y-%m-%d %H:%M:%S.%f%z") token = M2MToken.model_validate( { "token": "TOKEN", "expires_at": expiration_string, "client_credentials_secret_arn": mock_arn, } ) assert not token.is_expired() @freezegun.freeze_time("2022-01-02 00:00:00") def test_is_expired_leeway_seconds(mock_arn: str) -> None: """Test token is_expired with leeway_seconds.""" # expired_at is 2022-01-02 00:00:00 expires_at = datetime.datetime.now(datetime.UTC) expiration_string = expires_at.strftime("%Y-%m-%d %H:%M:%S.%f%z") token = M2MToken.model_validate( { "token": "TOKEN", "expires_at": expiration_string, "client_credentials_secret_arn": mock_arn, } ) with freezegun.freeze_time("2022-01-02 00:00:00"): # the token is expired now() with 0 leeway assert token.is_expired(leeway_seconds=0) with freezegun.freeze_time("2022-01-01 23:59:59"): # the token will expire in 1 minute. # a leeway of 60 seconds should return is_expired == True assert token.is_expired(leeway_seconds=60) with freezegun.freeze_time("2022-01-01 23:59:59"): # the token will expire in 1 minute. # a leeway of negative 60 seconds should return is_expired == False assert not token.is_expired(leeway_seconds=-60) with freezegun.freeze_time("2022-01-01 23:55:00"): # The token will expire in 5 minutes # a leeway of 60 seconds should return is_expired == False assert not token.is_expired(leeway_seconds=60) @freeze_time("2024-09-17 00:00:00.000000+0000") def test_create_dummy_m2m_token(mock_arn: str) -> None: """Test create_dummy_m2m_token creates a token with dummy value and 12-hour expiration.""" dummy_token = create_dummy_m2m_token(mock_arn) assert dummy_token.token == "dummy" # Verify client_credentials_secret_arn is set correctly assert dummy_token.client_credentials_secret_arn == mock_arn # Verify expiration is 12 hours in the future (DUMMY_EXPIRES_IN_SECONDS) expected_expires_at = datetime.datetime( 2024, 9, 17, 12, 0, 0, 0, datetime.timezone.utc ) assert dummy_token.expires_at == expected_expires_at # Verify it's 12 hours (43200 seconds) assert DUMMY_EXPIRES_IN_SECONDS == 60 * 60 * 12 # Verify the token is not expired assert not dummy_token.is_expired() def test_client_credentials_create() -> None: """Test ClientCredentials schema.""" audience = "https://fake.audience.io" client_id = "fake_client_id" client_secret = "fake_client_secret" grant_type = "client_credentials" client_credentials = ClientCredentials.model_validate( { "audience": audience, "client_id": client_id, "client_secret": client_secret, "grant_type": grant_type, } ) assert client_credentials.audience == audience assert client_credentials.client_id == client_id assert client_credentials.client_secret == client_secret assert client_credentials.grant_type == grant_type @pytest.mark.parametrize( "invalid_client_credentials", [ pytest.param( { "client_id": "fake_client_id", "client_secret": "fake_client_secret", "grant_type": "fake_grant_type", }, id="Empty audience not allowed.", ), pytest.param( { "audience": "fake_audience", "client_id": "", "client_secret": "fake_client_secret", "grant_type": "fake_grant_type", }, id="Empty client_id not allowed.", ), pytest.param( { "audience": "fake_audience", "client_id": "fake_client_id", "grant_type": "fake_grant_type", }, id="Empty client_secret not allowed.", ), pytest.param( { "audience": "fake_audience", "client_id": "fake_client_id", "client_secret": "fake_client_secret", }, id="Empty grant_type not allowed.", ), ], ) def test_client_credentials_invalid_schema( invalid_client_credentials: dict[str, Any], ) -> None: """Test ClientCredentials with invalid schema.""" with pytest.raises(ValueError): ClientCredentials.model_validate(invalid_client_credentials) def test_client_credentials_model_dump_json() -> None: """Test ClientCredentials model_dump_json output is standard string.""" client_credentials = ClientCredentials.model_validate( { "audience": "https://fake.audience.io", "client_id": "fake_client_id", "client_secret": "fake_client_secret", "grant_type": "fake_grant_type", } ) output = client_credentials.model_dump_json() assert ( output == '{"audience":"https://fake.audience.io","client_id":"fake_client_id","client_secret":"fake_client_secret",' '"grant_type":"fake_grant_type"}' ) def test_oauth_token_create(mock_valid_auth0_response: dict[str, Any]) -> None: """Test OAuthToken schema.""" oauth_token = OAuthToken.model_validate(mock_valid_auth0_response) assert oauth_token.access_token == mock_valid_auth0_response["access_token"] assert oauth_token.expires_in == mock_valid_auth0_response["expires_in"] assert oauth_token.token_type == mock_valid_auth0_response["token_type"] @pytest.mark.parametrize( "invalid_oauth_token", [ pytest.param( { "expires_in": 36000, "token_type": "Bearer", }, id="Empty access_token not allowed.", ), pytest.param( { "access_token": "fake access token", "token_type": "Bearer", }, id="Empty expires_in not allowed.", ), pytest.param( { "access_token": "fake access token", "expires_in": 36000, "token_type": "", }, id="Empty token_type not allowed.", ), pytest.param( { "access_token": "fake access token", "expires_in": 0, "token_type": "Bearer", }, id="Expires in should be greater than 0.", ), pytest.param( { "access_token": "fake access token", "expires_in": -10, "token_type": "Bearer", }, id="Expires in should certainly not be in the past.", ), ], ) def test_oauth_token_invalid_schema( invalid_oauth_token: dict[str, Any], ) -> None: """Test OAuthToken with invalid schema.""" with pytest.raises(ValueError): OAuthToken.model_validate(invalid_oauth_token) def test_oauth_token_model_dump_json(mock_valid_auth0_response: dict[str, Any]) -> None: """Test 0AuthToken model_dump_json output is standard string.""" oauth_token = OAuthToken.model_validate(mock_valid_auth0_response) output = oauth_token.model_dump_json() assert ( output == '{"access_token":"some_token","expires_in":36000,"token_type":"Bearer"}' ) @freeze_time("2024-09-11 00:00:00.000000+0000") def test_convert_to_m2m_token_is_successful( mock_valid_auth0_response: dict[str, Any], ) -> None: """Test convert_to_m2m_token is successful.""" oauth_token = OAuthToken.model_validate(mock_valid_auth0_response) m2m_token = oauth_token.convert_to_m2m_token("M2M_TOKEN_CLIENT_CREDENTIALS") assert m2m_token.token == mock_valid_auth0_response["access_token"] assert m2m_token.expires_at == datetime.datetime.now( datetime.UTC ) + datetime.timedelta(seconds=mock_valid_auth0_response["expires_in"]) assert isinstance(m2m_token, M2MToken) def test_m2m_token_str_strip_whitespace(expiration_string: str) -> None: """Test pydantic str_strip_whitespace config attr with M2MToken model.""" token_string = " ws token " client_credentials_secret_arn = "M2M_TOKEN_CLIENT_CREDENTIALS" token = M2MToken.model_validate( { "token": token_string, "expires_at": expiration_string, "client_credentials_secret_arn": client_credentials_secret_arn, } ) assert token.token == "ws token"