From c94a3266e7cb78ac472cc3e00d25fa1fa4224de2 Mon Sep 17 00:00:00 2001 From: Alfonso Sastre Date: Wed, 25 Feb 2026 11:24:00 +0100 Subject: [PATCH 1/2] feat: add Commands (receiver) to CPO version details for all OCPI versions Add ModuleID.commands with InterfaceRole.receiver to the CPO ENDPOINTS_LIST for OCPI 2.1.1, 2.2.1, and 2.3.0. Without this, Payter and other eMSP/PTP partners could not discover the Commands endpoint via GET version details, blocking START_SESSION/STOP_SESSION flows. Also add credentials SENDER and payments SENDER roles to CPO endpoints for 2.2.1 and 2.3.0, as recommended by Payter to support proactive credential updates and payment data push. To support multiple interface roles per module (e.g. credentials with both SENDER and RECEIVER), convert ENDPOINTS_LIST from dict[ModuleID, Endpoint] to list[Endpoint] in all six endpoint files (CPO + eMSP for each version). Update main.py to iterate the list instead of using dict.get(). All existing tests pass. Co-authored-by: Cursor --- ocpi/core/endpoints/v_2_1_1/cpo.py | 19 +++++++------ ocpi/core/endpoints/v_2_1_1/emsp.py | 18 ++++++------- ocpi/core/endpoints/v_2_2_1/cpo.py | 32 +++++++++++++++------- ocpi/core/endpoints/v_2_2_1/emsp.py | 22 +++++++-------- ocpi/core/endpoints/v_2_3_0/cpo.py | 42 ++++++++++++++++++++--------- ocpi/core/endpoints/v_2_3_0/emsp.py | 24 ++++++++--------- ocpi/main.py | 16 ++++++----- 7 files changed, 105 insertions(+), 68 deletions(-) diff --git a/ocpi/core/endpoints/v_2_1_1/cpo.py b/ocpi/core/endpoints/v_2_1_1/cpo.py index c938486..233ec15 100644 --- a/ocpi/core/endpoints/v_2_1_1/cpo.py +++ b/ocpi/core/endpoints/v_2_1_1/cpo.py @@ -15,11 +15,14 @@ TOKENS = cpo_generator.generate_endpoint(ModuleID.tokens) -ENDPOINTS_LIST = { - ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, - ModuleID.locations: LOCATIONS, - ModuleID.cdrs: CDRS, - ModuleID.tariffs: TARIFFS, - ModuleID.sessions: SESSIONS, - ModuleID.tokens: TOKENS, -} +COMMANDS = cpo_generator.generate_endpoint(ModuleID.commands) + +ENDPOINTS_LIST = [ + CREDENTIALS_AND_REGISTRATION, + LOCATIONS, + CDRS, + TARIFFS, + SESSIONS, + TOKENS, + COMMANDS, +] diff --git a/ocpi/core/endpoints/v_2_1_1/emsp.py b/ocpi/core/endpoints/v_2_1_1/emsp.py index 99a7731..17c28cd 100644 --- a/ocpi/core/endpoints/v_2_1_1/emsp.py +++ b/ocpi/core/endpoints/v_2_1_1/emsp.py @@ -17,12 +17,12 @@ COMMANDS = emsp_generator.generate_endpoint(ModuleID.commands) -ENDPOINTS_LIST = { - ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, - ModuleID.locations: LOCATIONS, - ModuleID.cdrs: CDRS, - ModuleID.tariffs: TARIFFS, - ModuleID.sessions: SESSIONS, - ModuleID.tokens: TOKENS, - ModuleID.commands: COMMANDS, -} +ENDPOINTS_LIST = [ + CREDENTIALS_AND_REGISTRATION, + LOCATIONS, + CDRS, + TARIFFS, + SESSIONS, + TOKENS, + COMMANDS, +] diff --git a/ocpi/core/endpoints/v_2_2_1/cpo.py b/ocpi/core/endpoints/v_2_2_1/cpo.py index 9f4d2bb..3565d8e 100644 --- a/ocpi/core/endpoints/v_2_2_1/cpo.py +++ b/ocpi/core/endpoints/v_2_2_1/cpo.py @@ -7,6 +7,11 @@ InterfaceRole.receiver, ) +CREDENTIALS_SENDER = cpo_generator.generate_endpoint( + ModuleID.credentials_and_registration, + InterfaceRole.sender, +) + LOCATIONS = cpo_generator.generate_endpoint( ModuleID.locations, InterfaceRole.sender, @@ -32,6 +37,11 @@ InterfaceRole.receiver, ) +COMMANDS = cpo_generator.generate_endpoint( + ModuleID.commands, + InterfaceRole.receiver, +) + HUB_CLIENT_INFO = cpo_generator.generate_endpoint( ModuleID.hub_client_info, InterfaceRole.receiver, @@ -43,13 +53,15 @@ ) -ENDPOINTS_LIST = { - ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, - ModuleID.locations: LOCATIONS, - ModuleID.sessions: SESSIONS, - ModuleID.cdrs: CDRS, - ModuleID.tariffs: TARIFFS, - ModuleID.tokens: TOKENS, - ModuleID.hub_client_info: HUB_CLIENT_INFO, - ModuleID.charging_profile: CHARGING_PROFILE, -} +ENDPOINTS_LIST = [ + CREDENTIALS_AND_REGISTRATION, + CREDENTIALS_SENDER, + LOCATIONS, + SESSIONS, + CDRS, + TARIFFS, + TOKENS, + COMMANDS, + HUB_CLIENT_INFO, + CHARGING_PROFILE, +] diff --git a/ocpi/core/endpoints/v_2_2_1/emsp.py b/ocpi/core/endpoints/v_2_2_1/emsp.py index 0b46cc3..e9d2327 100644 --- a/ocpi/core/endpoints/v_2_2_1/emsp.py +++ b/ocpi/core/endpoints/v_2_2_1/emsp.py @@ -47,14 +47,14 @@ InterfaceRole.sender, ) -ENDPOINTS_LIST = { - ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, - ModuleID.locations: LOCATIONS, - ModuleID.sessions: SESSIONS, - ModuleID.cdrs: CDRS, - ModuleID.tariffs: TARIFFS, - ModuleID.commands: COMMANDS, - ModuleID.tokens: TOKENS, - ModuleID.hub_client_info: HUB_CLIENT_INFO, - ModuleID.charging_profile: CHARGING_PROFILE, -} +ENDPOINTS_LIST = [ + CREDENTIALS_AND_REGISTRATION, + LOCATIONS, + SESSIONS, + CDRS, + TARIFFS, + COMMANDS, + TOKENS, + HUB_CLIENT_INFO, + CHARGING_PROFILE, +] diff --git a/ocpi/core/endpoints/v_2_3_0/cpo.py b/ocpi/core/endpoints/v_2_3_0/cpo.py index 4e26606..8fb1595 100644 --- a/ocpi/core/endpoints/v_2_3_0/cpo.py +++ b/ocpi/core/endpoints/v_2_3_0/cpo.py @@ -9,6 +9,11 @@ InterfaceRole.receiver, ) +CREDENTIALS_SENDER = cpo_generator.generate_endpoint( + ModuleID.credentials_and_registration, + InterfaceRole.sender, +) + LOCATIONS = cpo_generator.generate_endpoint( ModuleID.locations, InterfaceRole.sender, @@ -34,6 +39,11 @@ InterfaceRole.receiver, ) +COMMANDS = cpo_generator.generate_endpoint( + ModuleID.commands, + InterfaceRole.receiver, +) + HUB_CLIENT_INFO = cpo_generator.generate_endpoint( ModuleID.hub_client_info, InterfaceRole.receiver, @@ -50,6 +60,11 @@ InterfaceRole.receiver, ) +PAYMENTS_SENDER = cpo_generator.generate_endpoint( + ModuleID.payments, + InterfaceRole.sender, +) + # New in OCPI 2.3.0 - Booking extension BOOKINGS = cpo_generator.generate_endpoint( ModuleID.bookings, @@ -57,15 +72,18 @@ ) -ENDPOINTS_LIST = { - ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, - ModuleID.locations: LOCATIONS, - ModuleID.sessions: SESSIONS, - ModuleID.cdrs: CDRS, - ModuleID.tariffs: TARIFFS, - ModuleID.tokens: TOKENS, - ModuleID.hub_client_info: HUB_CLIENT_INFO, - ModuleID.charging_profile: CHARGING_PROFILE, - ModuleID.payments: PAYMENTS, - ModuleID.bookings: BOOKINGS, -} +ENDPOINTS_LIST = [ + CREDENTIALS_AND_REGISTRATION, + CREDENTIALS_SENDER, + LOCATIONS, + SESSIONS, + CDRS, + TARIFFS, + TOKENS, + COMMANDS, + HUB_CLIENT_INFO, + CHARGING_PROFILE, + PAYMENTS, + PAYMENTS_SENDER, + BOOKINGS, +] diff --git a/ocpi/core/endpoints/v_2_3_0/emsp.py b/ocpi/core/endpoints/v_2_3_0/emsp.py index 2253451..3694993 100644 --- a/ocpi/core/endpoints/v_2_3_0/emsp.py +++ b/ocpi/core/endpoints/v_2_3_0/emsp.py @@ -56,15 +56,15 @@ ) -ENDPOINTS_LIST = { - ModuleID.credentials_and_registration: CREDENTIALS_AND_REGISTRATION, - ModuleID.locations: LOCATIONS, - ModuleID.sessions: SESSIONS, - ModuleID.cdrs: CDRS, - ModuleID.tariffs: TARIFFS, - ModuleID.tokens: TOKENS, - ModuleID.commands: COMMANDS, - ModuleID.hub_client_info: HUB_CLIENT_INFO, - ModuleID.charging_profile: CHARGING_PROFILE, - ModuleID.bookings: BOOKINGS, -} +ENDPOINTS_LIST = [ + CREDENTIALS_AND_REGISTRATION, + LOCATIONS, + SESSIONS, + CDRS, + TARIFFS, + TOKENS, + COMMANDS, + HUB_CLIENT_INFO, + CHARGING_PROFILE, + BOOKINGS, +] diff --git a/ocpi/main.py b/ocpi/main.py index b9104a6..529bcc2 100644 --- a/ocpi/main.py +++ b/ocpi/main.py @@ -225,6 +225,7 @@ def get_application( version_endpoints[version] = [] if RoleEnum.cpo in roles: + cpo_modules_with_router: set[ModuleID] = set() for module in modules: cpo_router = mapped_version["cpo_router"].get(module) # type: ignore[attr-defined] if cpo_router: @@ -233,11 +234,13 @@ def get_application( prefix=f"/{settings.OCPI_PREFIX}/cpo/{version.value}", tags=[f"CPO {version.value}"], ) - endpoint = ENDPOINTS[version][RoleEnum.cpo].get(module) # type: ignore[index] - if endpoint: - version_endpoints[version].append(endpoint) + cpo_modules_with_router.add(module) + for endpoint in ENDPOINTS[version][RoleEnum.cpo]: # type: ignore[index] + if endpoint.identifier in cpo_modules_with_router: + version_endpoints[version].append(endpoint) if RoleEnum.emsp in roles: + emsp_modules_with_router: set[ModuleID] = set() for module in modules: emsp_router = mapped_version["emsp_router"].get(module) # type: ignore[attr-defined] if emsp_router: @@ -246,9 +249,10 @@ def get_application( prefix=f"/{settings.OCPI_PREFIX}/emsp/{version.value}", tags=[f"EMSP {version.value}"], ) - endpoint = ENDPOINTS[version][RoleEnum.emsp].get(module) # type: ignore[index] - if endpoint: - version_endpoints[version].append(endpoint) + emsp_modules_with_router.add(module) + for endpoint in ENDPOINTS[version][RoleEnum.emsp]: # type: ignore[index] + if endpoint.identifier in emsp_modules_with_router: + version_endpoints[version].append(endpoint) if RoleEnum.ptp in roles: for module in modules: From ba9e7d96c737ec8a550d2dc330a3a7f29a700083 Mon Sep 17 00:00:00 2001 From: Alfonso Sastre Date: Wed, 25 Feb 2026 13:31:02 +0100 Subject: [PATCH 2/2] fix: address review suggestions for commands CPO endpoint PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add list[Endpoint] type annotation to ENDPOINTS_LIST in all 6 endpoint files (CPO + eMSP × 3 versions) so mypy catches accidental reversion - Add comment in v_2_1_1/cpo.py explaining why credentials SENDER is omitted (InterfaceRole is a 2.2.1+ concept) - Add regression tests for commands RECEIVER discovery in version details for all three OCPI versions; add credentials SENDER/RECEIVER tests for 2.2.1 and 2.3.0; add payments SENDER/RECEIVER test for 2.3.0 - Fix tests/test_modules/mocks/async_client.py to use list iteration instead of dict key lookup after ENDPOINTS_LIST dict→list migration All 457 tests pass. Co-authored-by: Cursor --- ocpi/core/endpoints/v_2_1_1/cpo.py | 6 +- ocpi/core/endpoints/v_2_1_1/emsp.py | 3 +- ocpi/core/endpoints/v_2_2_1/cpo.py | 4 +- ocpi/core/endpoints/v_2_2_1/emsp.py | 4 +- ocpi/core/endpoints/v_2_3_0/cpo.py | 4 +- ocpi/core/endpoints/v_2_3_0/emsp.py | 4 +- tests/test_modules/mocks/async_client.py | 6 +- .../test_versions/test_versions.py | 25 ++++++ .../test_versions/test_versions.py | 52 +++++++++++++ .../test_versions/test_versions.py | 78 +++++++++++++++++++ 10 files changed, 175 insertions(+), 11 deletions(-) diff --git a/ocpi/core/endpoints/v_2_1_1/cpo.py b/ocpi/core/endpoints/v_2_1_1/cpo.py index 233ec15..3b7695b 100644 --- a/ocpi/core/endpoints/v_2_1_1/cpo.py +++ b/ocpi/core/endpoints/v_2_1_1/cpo.py @@ -1,5 +1,9 @@ from ocpi.core.endpoints.v_2_1_1.utils import cpo_generator from ocpi.core.enums import ModuleID +from ocpi.modules.versions.v_2_1_1.schemas import Endpoint + +# OCPI 2.1.1 Endpoint schema has no InterfaceRole field; credentials SENDER +# role is implicit and not separately advertised (InterfaceRole is 2.2.1+). CREDENTIALS_AND_REGISTRATION = cpo_generator.generate_endpoint( ModuleID.credentials_and_registration, @@ -17,7 +21,7 @@ COMMANDS = cpo_generator.generate_endpoint(ModuleID.commands) -ENDPOINTS_LIST = [ +ENDPOINTS_LIST: list[Endpoint] = [ CREDENTIALS_AND_REGISTRATION, LOCATIONS, CDRS, diff --git a/ocpi/core/endpoints/v_2_1_1/emsp.py b/ocpi/core/endpoints/v_2_1_1/emsp.py index 17c28cd..76429b4 100644 --- a/ocpi/core/endpoints/v_2_1_1/emsp.py +++ b/ocpi/core/endpoints/v_2_1_1/emsp.py @@ -1,5 +1,6 @@ from ocpi.core.endpoints.v_2_1_1.utils import emsp_generator from ocpi.core.enums import ModuleID +from ocpi.modules.versions.v_2_1_1.schemas import Endpoint CREDENTIALS_AND_REGISTRATION = emsp_generator.generate_endpoint( ModuleID.credentials_and_registration, @@ -17,7 +18,7 @@ COMMANDS = emsp_generator.generate_endpoint(ModuleID.commands) -ENDPOINTS_LIST = [ +ENDPOINTS_LIST: list[Endpoint] = [ CREDENTIALS_AND_REGISTRATION, LOCATIONS, CDRS, diff --git a/ocpi/core/endpoints/v_2_2_1/cpo.py b/ocpi/core/endpoints/v_2_2_1/cpo.py index 3565d8e..e507b38 100644 --- a/ocpi/core/endpoints/v_2_2_1/cpo.py +++ b/ocpi/core/endpoints/v_2_2_1/cpo.py @@ -1,6 +1,6 @@ from ocpi.core.endpoints.v_2_2_1.utils import cpo_generator from ocpi.core.enums import ModuleID -from ocpi.modules.versions.v_2_2_1.schemas import InterfaceRole +from ocpi.modules.versions.v_2_2_1.schemas import Endpoint, InterfaceRole CREDENTIALS_AND_REGISTRATION = cpo_generator.generate_endpoint( ModuleID.credentials_and_registration, @@ -53,7 +53,7 @@ ) -ENDPOINTS_LIST = [ +ENDPOINTS_LIST: list[Endpoint] = [ CREDENTIALS_AND_REGISTRATION, CREDENTIALS_SENDER, LOCATIONS, diff --git a/ocpi/core/endpoints/v_2_2_1/emsp.py b/ocpi/core/endpoints/v_2_2_1/emsp.py index e9d2327..b672058 100644 --- a/ocpi/core/endpoints/v_2_2_1/emsp.py +++ b/ocpi/core/endpoints/v_2_2_1/emsp.py @@ -1,6 +1,6 @@ from ocpi.core.endpoints.v_2_2_1.utils import emsp_generator from ocpi.core.enums import ModuleID -from ocpi.modules.versions.v_2_2_1.schemas import InterfaceRole +from ocpi.modules.versions.v_2_2_1.schemas import Endpoint, InterfaceRole CREDENTIALS_AND_REGISTRATION = emsp_generator.generate_endpoint( ModuleID.credentials_and_registration, @@ -47,7 +47,7 @@ InterfaceRole.sender, ) -ENDPOINTS_LIST = [ +ENDPOINTS_LIST: list[Endpoint] = [ CREDENTIALS_AND_REGISTRATION, LOCATIONS, SESSIONS, diff --git a/ocpi/core/endpoints/v_2_3_0/cpo.py b/ocpi/core/endpoints/v_2_3_0/cpo.py index 8fb1595..881f0e4 100644 --- a/ocpi/core/endpoints/v_2_3_0/cpo.py +++ b/ocpi/core/endpoints/v_2_3_0/cpo.py @@ -2,7 +2,7 @@ from ocpi.core.endpoints.v_2_3_0.utils import cpo_generator from ocpi.core.enums import ModuleID -from ocpi.modules.versions.v_2_3_0.schemas import InterfaceRole +from ocpi.modules.versions.v_2_3_0.schemas import Endpoint, InterfaceRole CREDENTIALS_AND_REGISTRATION = cpo_generator.generate_endpoint( ModuleID.credentials_and_registration, @@ -72,7 +72,7 @@ ) -ENDPOINTS_LIST = [ +ENDPOINTS_LIST: list[Endpoint] = [ CREDENTIALS_AND_REGISTRATION, CREDENTIALS_SENDER, LOCATIONS, diff --git a/ocpi/core/endpoints/v_2_3_0/emsp.py b/ocpi/core/endpoints/v_2_3_0/emsp.py index 3694993..c7f0744 100644 --- a/ocpi/core/endpoints/v_2_3_0/emsp.py +++ b/ocpi/core/endpoints/v_2_3_0/emsp.py @@ -2,7 +2,7 @@ from ocpi.core.endpoints.v_2_3_0.utils import emsp_generator from ocpi.core.enums import ModuleID -from ocpi.modules.versions.v_2_3_0.schemas import InterfaceRole +from ocpi.modules.versions.v_2_3_0.schemas import Endpoint, InterfaceRole CREDENTIALS_AND_REGISTRATION = emsp_generator.generate_endpoint( ModuleID.credentials_and_registration, @@ -56,7 +56,7 @@ ) -ENDPOINTS_LIST = [ +ENDPOINTS_LIST: list[Endpoint] = [ CREDENTIALS_AND_REGISTRATION, LOCATIONS, SESSIONS, diff --git a/tests/test_modules/mocks/async_client.py b/tests/test_modules/mocks/async_client.py index 25ad5bc..bda6527 100644 --- a/tests/test_modules/mocks/async_client.py +++ b/tests/test_modules/mocks/async_client.py @@ -4,10 +4,14 @@ from ocpi.modules.versions.enums import VersionNumber from ocpi.modules.versions.v_2_2_1.schemas import VersionDetail +_locations_endpoint = next( + e for e in ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo] if e.identifier == ModuleID.locations +) + fake_endpoints_data = { "data": VersionDetail( version=VersionNumber.v_2_2_1, - endpoints=[ENDPOINTS[VersionNumber.v_2_2_1][RoleEnum.cpo][ModuleID.locations]], + endpoints=[_locations_endpoint], ).model_dump(), } diff --git a/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py b/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py index 0676bf2..02c323d 100644 --- a/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py +++ b/tests/test_modules/test_v_2_1_1/test_versions/test_versions.py @@ -83,6 +83,31 @@ async def do(cls, *args, **kwargs): assert len(response.json()["data"]) == 2 +def test_version_details_includes_commands_for_cpo(): + """CPO version details must advertise the commands endpoint (regression: Payter discovery).""" + + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_1_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands], + ) + client = TestClient(app) + + response = client.get(VERSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + endpoints = response.json()["data"]["endpoints"] + commands_entries = [e for e in endpoints if e["identifier"] == "commands"] + assert len(commands_entries) == 1 + + def test_get_versions_v_2_1_1_not_authenticated(): class MockCrud(Crud): @classmethod diff --git a/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py b/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py index d86b79e..b990c64 100644 --- a/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py +++ b/tests/test_modules/test_v_2_2_1/test_versions/test_versions.py @@ -83,6 +83,58 @@ async def do(cls, *args, **kwargs): assert len(response.json()["data"]) == 2 +def test_version_details_includes_commands_for_cpo(): + """CPO version details must advertise commands RECEIVER (regression: Payter discovery).""" + + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands], + ) + client = TestClient(app) + + response = client.get(VERSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + endpoints = response.json()["data"]["endpoints"] + commands_entries = [e for e in endpoints if e["identifier"] == "commands"] + assert len(commands_entries) == 1 + assert commands_entries[0]["role"] == "RECEIVER" + + +def test_version_details_includes_credentials_sender_for_cpo(): + """CPO version details must advertise both SENDER and RECEIVER for credentials.""" + + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_2_1], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + client = TestClient(app) + + response = client.get(VERSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + endpoints = response.json()["data"]["endpoints"] + cred_roles = {e["role"] for e in endpoints if e["identifier"] == "credentials"} + assert "SENDER" in cred_roles + assert "RECEIVER" in cred_roles + + def test_get_versions_v_2_2_1_not_authenticated(): class MockCrud(Crud): @classmethod diff --git a/tests/test_modules/test_v_2_3_0/test_versions/test_versions.py b/tests/test_modules/test_v_2_3_0/test_versions/test_versions.py index aab9c48..db1be59 100644 --- a/tests/test_modules/test_v_2_3_0/test_versions/test_versions.py +++ b/tests/test_modules/test_v_2_3_0/test_versions/test_versions.py @@ -85,6 +85,84 @@ async def do(cls, *args, **kwargs): assert len(response.json()["data"]) == 2 +def test_version_details_includes_commands_for_cpo(): + """CPO version details must advertise commands RECEIVER (regression: Payter discovery).""" + + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_3_0], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.commands], + ) + client = TestClient(app) + + response = client.get(VERSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + endpoints = response.json()["data"]["endpoints"] + commands_entries = [e for e in endpoints if e["identifier"] == "commands"] + assert len(commands_entries) == 1 + assert commands_entries[0]["role"] == "RECEIVER" + + +def test_version_details_includes_credentials_sender_for_cpo(): + """CPO version details must advertise both SENDER and RECEIVER for credentials.""" + + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_3_0], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.credentials_and_registration], + ) + client = TestClient(app) + + response = client.get(VERSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + endpoints = response.json()["data"]["endpoints"] + cred_roles = {e["role"] for e in endpoints if e["identifier"] == "credentials"} + assert "SENDER" in cred_roles + assert "RECEIVER" in cred_roles + + +def test_version_details_includes_payments_sender_for_cpo(): + """CPO version details must advertise payments SENDER for DirectPayment push.""" + + class MockCrud(Crud): + @classmethod + async def do(cls, *args, **kwargs): + return AUTH_TOKEN + + app = get_application( + version_numbers=[VersionNumber.v_2_3_0], + roles=[enums.RoleEnum.cpo], + crud=MockCrud, + authenticator=ClientAuthenticator, + modules=[enums.ModuleID.payments], + ) + client = TestClient(app) + + response = client.get(VERSION_URL, headers=AUTH_HEADERS) + + assert response.status_code == 200 + endpoints = response.json()["data"]["endpoints"] + payment_roles = {e["role"] for e in endpoints if e["identifier"] == "payments"} + assert "SENDER" in payment_roles + assert "RECEIVER" in payment_roles + + def test_get_versions_v_2_3_0_not_authenticated(): class MockCrud(Crud): @classmethod