diff --git a/ocpi/core/endpoints/v_2_1_1/cpo.py b/ocpi/core/endpoints/v_2_1_1/cpo.py index c938486..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, @@ -15,11 +19,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: list[Endpoint] = [ + 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..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,12 +18,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: list[Endpoint] = [ + 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..e507b38 100644 --- a/ocpi/core/endpoints/v_2_2_1/cpo.py +++ b/ocpi/core/endpoints/v_2_2_1/cpo.py @@ -1,12 +1,17 @@ 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, 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: list[Endpoint] = [ + 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..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,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: list[Endpoint] = [ + 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..881f0e4 100644 --- a/ocpi/core/endpoints/v_2_3_0/cpo.py +++ b/ocpi/core/endpoints/v_2_3_0/cpo.py @@ -2,13 +2,18 @@ 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, 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: list[Endpoint] = [ + 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..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,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: list[Endpoint] = [ + 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: 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