Skip to content

Commit 0acf364

Browse files
Alfonso Sastreclaude
andcommitted
feat: add OCPI 2.3.0 support to push functionality
send_push_request now matches endpoints by RECEIVER role for 2.3.x (same as 2.2.x), and push_object already base64-encodes tokens for any version beyond 2.1.x. Previously 2.3.0 pushes would silently use an empty base_url because the version check only covered "2.2". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5999dc9 commit 0acf364

2 files changed

Lines changed: 87 additions & 2 deletions

File tree

ocpi/core/push.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def send_push_request(
6262
base_url = ""
6363
for endpoint in endpoints:
6464
if (
65-
version.value.startswith("2.2")
65+
(version.value.startswith("2.2") or version.value.startswith("2.3"))
6666
and endpoint["identifier"] == module_id
6767
and endpoint["role"] == InterfaceRole.receiver
6868
) or (version.value.startswith("2.1") and endpoint["identifier"] == module_id):
@@ -92,7 +92,7 @@ async def push_object(
9292
# get client endpoints
9393
if version.value.startswith("2.1") or version.value.startswith("2.0"):
9494
token = receiver.auth_token
95-
else:
95+
else: # 2.2.x and 2.3.x use base64-encoded tokens
9696
token = encode_string_base64(receiver.auth_token)
9797

9898
client_auth_token = f"Token {token}"

tests/test_push.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from tests.test_modules.utils import (
1414
ENCODED_AUTH_TOKEN,
15+
ENCODED_AUTH_TOKEN_V_2_3_0,
1516
ClientAuthenticator,
1617
)
1718

@@ -488,3 +489,87 @@ def test_push_token_module_uses_emsp_role():
488489
crud.get.assert_awaited_once()
489490
call_kwargs = crud.get.call_args
490491
assert call_kwargs[0][1] == enums.RoleEnum.emsp
492+
493+
494+
def test_push_v_2_3_0_uses_receiver_role_and_base64_token():
495+
"""Push for v2.3.0 matches endpoints by RECEIVER role and base64-encodes the token."""
496+
crud = AsyncMock()
497+
adapter = MagicMock()
498+
crud.get.return_value = LOCATIONS[0]
499+
adapter.location_adapter.return_value.model_dump.return_value = LOCATIONS[0]
500+
501+
app = get_application(
502+
version_numbers=[VersionNumber.v_2_3_0],
503+
roles=[enums.RoleEnum.cpo],
504+
crud=crud,
505+
adapter=adapter,
506+
authenticator=ClientAuthenticator,
507+
modules=[],
508+
http_push=True,
509+
)
510+
511+
client = TestClient(app)
512+
push_data = schemas.Push(
513+
module_id=enums.ModuleID.locations,
514+
object_id="loc-1",
515+
receivers=[
516+
schemas.Receiver(
517+
endpoints_url="http://example.com/versions", auth_token="token"
518+
),
519+
],
520+
).model_dump()
521+
522+
with patch("ocpi.core.push.httpx.AsyncClient") as mock_client:
523+
mock_endpoints_response = MagicMock()
524+
mock_endpoints_response.status_code = 200
525+
mock_endpoints_response.json.return_value = {
526+
"data": {
527+
"endpoints": [
528+
# SENDER endpoint — should be ignored
529+
{
530+
"identifier": enums.ModuleID.locations,
531+
"role": "SENDER",
532+
"url": "http://example.com/sender/locations/",
533+
},
534+
# RECEIVER endpoint — should be picked up
535+
{
536+
"identifier": enums.ModuleID.locations,
537+
"role": "RECEIVER",
538+
"url": "http://example.com/locations/",
539+
},
540+
]
541+
}
542+
}
543+
544+
mock_push_response = MagicMock()
545+
mock_push_response.status_code = 200
546+
mock_push_response.json.return_value = {"status_code": 1000}
547+
548+
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
549+
return_value=mock_endpoints_response
550+
)
551+
mock_client.return_value.__aenter__.return_value.send = AsyncMock(
552+
return_value=mock_push_response
553+
)
554+
build_request = MagicMock()
555+
mock_client.return_value.__aenter__.return_value.build_request = build_request
556+
557+
response = client.post(
558+
"/push/2.3.0",
559+
json=push_data,
560+
headers={"Authorization": f"Token {ENCODED_AUTH_TOKEN_V_2_3_0}"},
561+
)
562+
563+
assert response.status_code == 200
564+
565+
# Token must be base64-encoded for 2.3.0
566+
call_args = build_request.call_args
567+
auth_header = call_args[1]["headers"]["Authorization"]
568+
assert auth_header.startswith("Token ")
569+
# The raw "token" string encoded in base64 is "dG9rZW4="
570+
assert auth_header == "Token dG9rZW4="
571+
572+
# Must use the RECEIVER url, not the SENDER url
573+
url_arg = call_args[0][1]
574+
assert "sender" not in url_arg
575+
assert "locations" in url_arg

0 commit comments

Comments
 (0)