From ea4bea69d777dda076ef27070d876116f2bdda84 Mon Sep 17 00:00:00 2001 From: piotrm3ter Date: Wed, 8 Jul 2026 08:29:00 +0200 Subject: [PATCH] BR-3276 Fix bearer token expiry calculation --- src/m3ter/_client.py | 4 ++-- tests/test_client.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/m3ter/_client.py b/src/m3ter/_client.py index 8824009..39c034d 100644 --- a/src/m3ter/_client.py +++ b/src/m3ter/_client.py @@ -5,7 +5,7 @@ import os import base64 from typing import TYPE_CHECKING, Any, Mapping -from datetime import datetime +from datetime import datetime, timedelta from typing_extensions import Self, override import httpx @@ -1038,7 +1038,7 @@ def _prepare_options( ) self.token = token.access_token # expiry minus 5 minutes from effective refreshing - self.token_expiry = datetime.fromtimestamp(token.expires_in - 300) + self.token_expiry = datetime.now() + timedelta(seconds=token.expires_in - 300) return options def copy( diff --git a/tests/test_client.py b/tests/test_client.py index b309f7b..59ac85d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,6 +10,7 @@ import inspect import dataclasses import tracemalloc +from datetime import datetime, timedelta from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock from typing_extensions import Literal, AsyncIterator, override @@ -127,6 +128,44 @@ def test_raw_response_for_binary(self, respx_mock: MockRouter, client: M3ter) -> assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} + @pytest.mark.respx(base_url=base_url) + def test_bearer_token_expiry_uses_expires_in_duration(self, respx_mock: MockRouter) -> None: + token_route = respx_mock.post("/oauth/token").mock( + return_value=httpx.Response( + 200, + json={ + "access_token": "refreshed-token", + "expires_in": 3600, + "token_type": "bearer", + }, + ) + ) + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + with update_env(**{"M3TER_API_TOKEN": Omit()}): + client = M3ter( + base_url=base_url, + api_key=api_key, + api_secret=api_secret, + token=None, + org_id=org_id, + _strict_response_validation=True, + ) + + now = datetime.now() + + response = client.get("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert client.token == "refreshed-token" + assert client.token_expiry is not None + assert client.token_expiry > now + timedelta(minutes=50) + + response = client.get("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert token_route.call_count == 1 + + client.close() + def test_copy(self, client: M3ter) -> None: copied = client.copy() assert id(copied) != id(client)