- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
972 lines
32 KiB
Python
972 lines
32 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright Pionix GmbH and Contributors to EVerest
|
|
|
|
import pytest
|
|
from datetime import datetime, timezone
|
|
|
|
import traceback
|
|
# fmt: off
|
|
import logging
|
|
|
|
from everest.testing.core_utils.controller.test_controller_interface import TestController
|
|
|
|
from ocpp.v21 import call as call21
|
|
from ocpp.v21 import call_result as call_result21
|
|
from ocpp.v21.enums import *
|
|
from ocpp.v21.datatypes import *
|
|
from ocpp.routing import on, create_route_map
|
|
from everest.testing.ocpp_utils.fixtures import *
|
|
from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v21 Action enum with the v16 one
|
|
from ocpp.v21.enums import (
|
|
Action,
|
|
ConnectorStatusEnumType,
|
|
AttributeEnumType,
|
|
)
|
|
from validations import validate_status_notification_201
|
|
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment
|
|
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, OcppTestConfiguration
|
|
# fmt: on
|
|
|
|
log = logging.getLogger("provisioningTest")
|
|
|
|
|
|
def validate_set_variables_success(response, expected_count=1):
|
|
"""Validate SetVariables response indicates success.
|
|
response is a call_result.SetVariables with set_variable_result as list of dicts.
|
|
"""
|
|
if not response or not response.set_variable_result:
|
|
return False
|
|
|
|
success_count = sum(
|
|
1
|
|
for r in response.set_variable_result
|
|
if r.get("attribute_status") == "Accepted"
|
|
)
|
|
return success_count >= expected_count
|
|
|
|
|
|
def validate_set_variables_rejected(response, expected_reason=None):
|
|
"""Validate SetVariables response indicates rejection.
|
|
response is a call_result.SetVariables with set_variable_result as list of dicts.
|
|
"""
|
|
if not response or not response.set_variable_result:
|
|
return False
|
|
|
|
for result in response.set_variable_result:
|
|
status = result.get("attribute_status")
|
|
if status != "Accepted":
|
|
if expected_reason:
|
|
status_info = result.get("attribute_status_info") or {}
|
|
reason = status_info.get("reason_code", "").upper()
|
|
return expected_reason.upper() in reason
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_cold_boot_01(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
test_utility: TestUtility,
|
|
):
|
|
"""
|
|
B01.FR.01
|
|
...
|
|
"""
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
|
|
|
try:
|
|
# expect StatusNotification with status available
|
|
assert await wait_for_and_validate(
|
|
test_utility,
|
|
charge_point_v21,
|
|
"StatusNotification",
|
|
call21.StatusNotification(
|
|
1, ConnectorStatusEnumType.available, 1, datetime.now().isoformat()
|
|
),
|
|
validate_status_notification_201,
|
|
)
|
|
except Exception as e:
|
|
traceback.print_exc()
|
|
logging.critical(e)
|
|
|
|
# TOOD(piet): Check configured HeartbeatInterval of BootNotificationResponse
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_cold_boot_pending_01(
|
|
test_config: OcppTestConfiguration,
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
test_utility: TestUtility,
|
|
):
|
|
|
|
@on(Action.boot_notification)
|
|
def on_boot_notification_pending(**kwargs):
|
|
return call_result21.BootNotification(
|
|
current_time=datetime.now().isoformat(),
|
|
interval=5,
|
|
status=RegistrationStatusEnumType.pending,
|
|
)
|
|
|
|
@on(Action.boot_notification)
|
|
def on_boot_notification_accepted(**kwargs):
|
|
return call_result21.BootNotification(
|
|
current_time=datetime.now(timezone.utc).isoformat(),
|
|
interval=5,
|
|
status=RegistrationStatusEnumType.accepted,
|
|
)
|
|
|
|
test_utility.forbidden_actions.append("SecurityEventNotification")
|
|
|
|
central_system_v21.function_overrides.append(
|
|
("on_boot_notification", on_boot_notification_pending)
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
|
|
|
setattr(charge_point_v21, "on_boot_notification",
|
|
on_boot_notification_accepted)
|
|
central_system_v21.chargepoint.route_map = create_route_map(
|
|
central_system_v21.chargepoint
|
|
)
|
|
|
|
assert await wait_for_and_validate(
|
|
test_utility, charge_point_v21, "BootNotification", {}
|
|
)
|
|
|
|
test_utility.forbidden_actions.clear()
|
|
|
|
test_controller.plug_in()
|
|
|
|
assert await wait_for_and_validate(
|
|
test_utility, charge_point_v21, "SecurityEventNotification", {}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_cold_boot_rejected_01(
|
|
test_config: OcppTestConfiguration,
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
test_utility: TestUtility,
|
|
):
|
|
|
|
@on(Action.boot_notification)
|
|
def on_boot_notification_pending(**kwargs):
|
|
return call_result21.BootNotification(
|
|
current_time=datetime.now().isoformat(),
|
|
interval=5,
|
|
status=RegistrationStatusEnumType.rejected,
|
|
)
|
|
|
|
@on(Action.boot_notification)
|
|
def on_boot_notification_accepted(**kwargs):
|
|
return call_result21.BootNotification(
|
|
current_time=datetime.now(timezone.utc).isoformat(),
|
|
interval=5,
|
|
status=RegistrationStatusEnumType.accepted,
|
|
)
|
|
|
|
central_system_v21.function_overrides.append(
|
|
("on_boot_notification", on_boot_notification_pending)
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
|
|
|
setattr(charge_point_v21, "on_boot_notification",
|
|
on_boot_notification_accepted)
|
|
central_system_v21.chargepoint.route_map = create_route_map(
|
|
central_system_v21.chargepoint
|
|
)
|
|
|
|
assert await wait_for_and_validate(
|
|
test_utility, charge_point_v21, "BootNotification", {}
|
|
)
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_get_network_configuration_slot_1(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
B09.FR.01 - Get NetworkConfiguration for slot 1 (primary configuration)
|
|
Verify that GetVariables can retrieve NetworkConfiguration[1] variables
|
|
"""
|
|
log.info(
|
|
"##################### B09.FR.01: Get NetworkConfiguration Slot 1 #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# Request OcppCsmsUrl for NetworkConfiguration slot 1
|
|
get_var = GetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="1"
|
|
),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=[get_var]
|
|
)
|
|
|
|
assert response and response.get_variable_result, "No get variable result"
|
|
results = response.get_variable_result
|
|
assert len(results) > 0, "Empty get variable result"
|
|
assert (
|
|
results[0].get("attribute_status") == "Accepted"
|
|
), f"Failed to get OcppCsmsUrl: {results[0]}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_set_network_configuration_slot_2(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
B09.FR.09 - Set NetworkConfiguration for slot 2 (backup configuration)
|
|
Verify that SetVariables can update NetworkConfiguration[2] variables
|
|
which are not in the priority list
|
|
"""
|
|
log.info(
|
|
"##################### B09.FR.09: Set NetworkConfiguration Slot 2 #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# Set OcppCsmsUrl for NetworkConfiguration slot 2
|
|
set_var = SetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="2"
|
|
),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="wss://backup-csms.example.com/ocpp",
|
|
)
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=[set_var]
|
|
)
|
|
|
|
assert validate_set_variables_success(response, 1), \
|
|
f"Failed to set OcppCsmsUrl on slot 2: {response}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_get_all_network_configuration_variables_slot_1(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
Verify GetVariables can retrieve all NetworkConfiguration[1] variables
|
|
"""
|
|
log.info(
|
|
"##################### Get All NetworkConfiguration[1] Variables #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# Request all main configuration variables for slot 1
|
|
variable_names = [
|
|
"OcppCsmsUrl",
|
|
"SecurityProfile",
|
|
"OcppInterface",
|
|
"OcppTransport",
|
|
"MessageTimeout",
|
|
]
|
|
|
|
get_vars = [
|
|
GetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="1"
|
|
),
|
|
variable=VariableType(name=var_name),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
for var_name in variable_names
|
|
]
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=get_vars
|
|
)
|
|
|
|
assert response and response.get_variable_result, "No get variable result"
|
|
results = response.get_variable_result
|
|
assert len(results) == len(variable_names), "Not all variables returned"
|
|
|
|
# Count successful retrievals
|
|
success_count = sum(
|
|
1
|
|
for r in results
|
|
if r.get("attribute_status") == "Accepted"
|
|
)
|
|
assert (
|
|
success_count == len(variable_names)
|
|
), f"Some variables failed: {results}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_network_configuration_priority_list_management(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
TC_B_107_CS: Add and remove slots from NetworkConfigurationPriority list
|
|
B09.FR.21, B09.FR.22, B09.FR.23 - Verify that CSMS can manage which configuration slots
|
|
are in the priority list, and verify that configuration values are consistent when
|
|
slots are added/removed from the priority list.
|
|
"""
|
|
log.info(
|
|
"##################### TC_B_107_CS: NetworkConfigurationPriority List Management #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# First, set a configuration on slot 2 (which is not in priority by default)
|
|
set_vars = [
|
|
SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="wss://backup-csms.example.com/ocpp",
|
|
),
|
|
SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="1",
|
|
),
|
|
]
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=set_vars
|
|
)
|
|
|
|
assert validate_set_variables_success(
|
|
response, len(set_vars)
|
|
), f"Failed to set initial configuration on slot 2: {response}"
|
|
|
|
# Verify the configuration was set
|
|
get_var = GetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=[get_var]
|
|
)
|
|
|
|
assert response and response.get_variable_result, "No get variable result"
|
|
results = response.get_variable_result
|
|
assert len(results) > 0, "Empty get variable result"
|
|
assert (
|
|
results[0].get("attribute_status") == "Accepted"
|
|
), f"Failed to get OcppCsmsUrl from slot 2: {results[0]}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_network_configuration_reject_priority_slot_changes(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
TC_B_108_CS: Reject SetVariables on the currently active NetworkConfiguration slot
|
|
B09.FR.22 - Verify that attempts to change configuration on the active slot
|
|
are rejected with "PriorityNetworkConf" reason code.
|
|
"""
|
|
log.info(
|
|
"##################### TC_B_108_CS: Reject Changes to Priority Slots #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# Attempt to change NetworkConfiguration slot 1 (which is in priority by default)
|
|
# This should be rejected with "PriorityNetworkConf" reason
|
|
set_var = SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="1"),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="wss://new-csms.example.com/ocpp",
|
|
)
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=[set_var]
|
|
)
|
|
|
|
# Validate that the change was rejected with PriorityNetworkConf reason
|
|
assert validate_set_variables_rejected(
|
|
response, "PriorityNetworkConf"
|
|
), f"Expected SetVariables to be rejected with PriorityNetworkConf, but got: {response}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_network_configuration_security_downgrade_rejection_set_network_profile(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
TC_B_110_CS: Reject security profile downgrade via SetNetworkProfileRequest
|
|
B09.FR.31 - Verify that SetNetworkProfileRequest is rejected with "NoSecurityDowngrade"
|
|
reason when attempting to downgrade the active security profile.
|
|
"""
|
|
log.info(
|
|
"##################### TC_B_110_CS: Reject Security Downgrade via SetNetworkProfileRequest #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# First, get the current active security profile from slot 1
|
|
get_var = GetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="1"),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=[get_var]
|
|
)
|
|
|
|
assert response and response.get_variable_result, "No get variable result"
|
|
results = response.get_variable_result
|
|
assert len(results) > 0, "Empty get variable result"
|
|
|
|
# Try to set a lower security profile on slot 2
|
|
# Assuming the current active profile is 1, try to set slot 2 to 0 (which would be lower)
|
|
set_vars = [
|
|
SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="0", # Downgrade attempt
|
|
),
|
|
]
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=set_vars
|
|
)
|
|
|
|
# B09.FR.31: Downgrade attempt should be rejected with NoSecurityDowngrade
|
|
assert validate_set_variables_rejected(
|
|
response, "NoSecurityDowngrade"
|
|
), f"Expected security downgrade to be rejected with NoSecurityDowngrade, but got: {response}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_network_configuration_security_downgrade_rejection_set_variables(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
TC_B_111_CS: Reject security profile downgrade via SetVariables on NetworkConfiguration
|
|
B09.FR.32 - Verify that SetVariables is rejected with "NoSecurityDowngrade" reason
|
|
when attempting to downgrade the SecurityProfile variable on an active configuration slot.
|
|
"""
|
|
log.info(
|
|
"##################### TC_B_111_CS: Reject Security Downgrade via SetVariables #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# First, set a higher security profile on slot 2
|
|
set_var = SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="2", # Set to profile 2
|
|
)
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=[set_var]
|
|
)
|
|
|
|
# This should succeed if slot 2 is not active
|
|
assert validate_set_variables_success(
|
|
response, 1
|
|
), f"Failed to set initial security profile: {response}"
|
|
|
|
# Now try to downgrade the security profile on slot 2
|
|
set_var = SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="0", # Attempt downgrade
|
|
)
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=[set_var]
|
|
)
|
|
|
|
# B09.FR.32: Downgrade attempt on slot 2 after setting profile 2 should be rejected
|
|
assert validate_set_variables_rejected(
|
|
response, "NoSecurityDowngrade"
|
|
), f"Expected security downgrade to be rejected with NoSecurityDowngrade, but got: {response}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_network_configuration_allow_security_downgrade_flag(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
TC_B_112_CS: AllowSecurityDowngrade configuration flag behavior
|
|
B09.FR.04, B09.FR.31 - Verify that when AllowSecurityDowngrade is set to false,
|
|
all security profile downgrades are rejected regardless of whether they occur
|
|
via SetNetworkProfileRequest or SetVariables on NetworkConfiguration.
|
|
"""
|
|
log.info(
|
|
"##################### TC_B_112_CS: AllowSecurityDowngrade Flag Enforcement #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# Check the current AllowSecurityDowngrade setting
|
|
get_var = GetVariableDataType(
|
|
component=ComponentType(name="SecurityCtrlr"),
|
|
variable=VariableType(name="AllowSecurityDowngrade"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=[get_var]
|
|
)
|
|
|
|
# Verify we can read the AllowSecurityDowngrade variable
|
|
assert response and response.get_variable_result, "No get variable result"
|
|
results = response.get_variable_result
|
|
assert len(results) > 0, "Empty get variable result"
|
|
|
|
# When AllowSecurityDowngrade is false, any downgrade should be rejected
|
|
# Try to set a lower security profile
|
|
set_var = SetVariableDataType(
|
|
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="0",
|
|
)
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=[set_var]
|
|
)
|
|
|
|
# B09.FR.04, B09.FR.31: When AllowSecurityDowngrade is false, any downgrade should be rejected
|
|
assert validate_set_variables_rejected(
|
|
response, "NoSecurityDowngrade"
|
|
), f"Expected security downgrade to be rejected with NoSecurityDowngrade, but got: {response}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.ocpp_version("ocpp2.1")
|
|
@pytest.mark.everest_core_config(
|
|
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
|
)
|
|
@pytest.mark.ocpp_config_adaptions(
|
|
GenericOCPP2XConfigAdjustment(
|
|
[
|
|
(
|
|
OCPP2XConfigVariableIdentifier(
|
|
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
|
),
|
|
"ocpp2.1",
|
|
)
|
|
]
|
|
)
|
|
)
|
|
async def test_network_configuration_dm_end_to_end(
|
|
central_system_v21: CentralSystem,
|
|
test_controller: TestController,
|
|
):
|
|
"""
|
|
US-006: End-to-end integration test for NetworkConfiguration device model.
|
|
|
|
Verifies the full lifecycle:
|
|
1. Migration from legacy NetworkConnectionProfiles blob to DM variables on boot
|
|
2. GetVariables reads correct values from NetworkConfiguration[1]
|
|
3. SetVariables updates a non-active slot and the change persists (read-back)
|
|
4. SetVariables on the active slot is rejected with PriorityNetworkConf
|
|
"""
|
|
log.info(
|
|
"##################### US-006: NetworkConfiguration DM End-to-End #################"
|
|
)
|
|
|
|
test_controller.start()
|
|
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
|
wait_for_bootnotification=True
|
|
)
|
|
|
|
# ── Step 1 & 2: Verify migration happened by reading slot 1 DM variables ──
|
|
# The charge point booted with a legacy NetworkConnectionProfiles blob.
|
|
# Migration should have populated NetworkConfiguration[1] with those values.
|
|
variable_names = ["OcppCsmsUrl", "SecurityProfile", "OcppInterface", "OcppTransport"]
|
|
|
|
get_vars = [
|
|
GetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="1"
|
|
),
|
|
variable=VariableType(name=var_name),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
for var_name in variable_names
|
|
]
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=get_vars
|
|
)
|
|
|
|
assert response and response.get_variable_result, "No get variable result for slot 1"
|
|
results = response.get_variable_result
|
|
assert len(results) == len(variable_names), (
|
|
f"Expected {len(variable_names)} results, got {len(results)}"
|
|
)
|
|
|
|
# Build a map of variable name → value for easier assertions
|
|
slot1_values = {}
|
|
for r in results:
|
|
var_name = r.get("variable", {}).get("name")
|
|
status = r.get("attribute_status")
|
|
assert status == "Accepted", (
|
|
f"GetVariables failed for {var_name}: status={status}"
|
|
)
|
|
slot1_values[var_name] = r.get("attribute_value")
|
|
|
|
# Verify migration produced correct values from the legacy blob
|
|
# The legacy blob has: securityProfile=1, ocppInterface=Wired0, ocppTransport=JSON
|
|
# OcppCsmsUrl is injected by the test framework to point to the test CSMS
|
|
assert slot1_values["OcppCsmsUrl"], "OcppCsmsUrl should not be empty after migration"
|
|
assert "ws" in slot1_values["OcppCsmsUrl"].lower(), (
|
|
f"OcppCsmsUrl should be a websocket URL, got: {slot1_values['OcppCsmsUrl']}"
|
|
)
|
|
assert slot1_values["SecurityProfile"] == "1", (
|
|
f"SecurityProfile should be 1, got: {slot1_values['SecurityProfile']}"
|
|
)
|
|
assert slot1_values["OcppInterface"] == "Wired0", (
|
|
f"OcppInterface should be Wired0, got: {slot1_values['OcppInterface']}"
|
|
)
|
|
assert slot1_values["OcppTransport"] == "JSON", (
|
|
f"OcppTransport should be JSON, got: {slot1_values['OcppTransport']}"
|
|
)
|
|
|
|
log.info("Step 1-2 PASSED: Migration verified — slot 1 DM variables match legacy blob")
|
|
|
|
# ── Step 3: SetVariables on non-active slot 2, then read back to verify persistence ──
|
|
new_url = "wss://updated-backup.example.com/ocpp"
|
|
new_security_profile = "2"
|
|
new_interface = "Wired0"
|
|
new_transport = "JSON"
|
|
|
|
set_vars = [
|
|
SetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="2"
|
|
),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value=new_url,
|
|
),
|
|
SetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="2"
|
|
),
|
|
variable=VariableType(name="SecurityProfile"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value=new_security_profile,
|
|
),
|
|
SetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="2"
|
|
),
|
|
variable=VariableType(name="OcppInterface"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value=new_interface,
|
|
),
|
|
SetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="2"
|
|
),
|
|
variable=VariableType(name="OcppTransport"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value=new_transport,
|
|
),
|
|
]
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=set_vars
|
|
)
|
|
|
|
assert validate_set_variables_success(response, len(set_vars)), (
|
|
f"SetVariables on slot 2 should succeed: {response}"
|
|
)
|
|
|
|
# Read back slot 2 to verify persistence
|
|
get_vars_slot2 = [
|
|
GetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="2"
|
|
),
|
|
variable=VariableType(name=var_name),
|
|
attribute_type=AttributeEnumType.actual,
|
|
)
|
|
for var_name in variable_names
|
|
]
|
|
|
|
response = await charge_point_v21.get_variables_req(
|
|
get_variable_data=get_vars_slot2
|
|
)
|
|
|
|
assert response and response.get_variable_result, "No get variable result for slot 2"
|
|
results = response.get_variable_result
|
|
|
|
slot2_values = {}
|
|
for r in results:
|
|
var_name = r.get("variable", {}).get("name")
|
|
status = r.get("attribute_status")
|
|
assert status == "Accepted", (
|
|
f"GetVariables failed for slot 2 {var_name}: status={status}"
|
|
)
|
|
slot2_values[var_name] = r.get("attribute_value")
|
|
|
|
assert slot2_values["OcppCsmsUrl"] == new_url, (
|
|
f"Slot 2 OcppCsmsUrl should be {new_url}, got: {slot2_values['OcppCsmsUrl']}"
|
|
)
|
|
assert slot2_values["SecurityProfile"] == new_security_profile, (
|
|
f"Slot 2 SecurityProfile should be {new_security_profile}, got: {slot2_values['SecurityProfile']}"
|
|
)
|
|
assert slot2_values["OcppInterface"] == new_interface, (
|
|
f"Slot 2 OcppInterface should be {new_interface}, got: {slot2_values['OcppInterface']}"
|
|
)
|
|
assert slot2_values["OcppTransport"] == new_transport, (
|
|
f"Slot 2 OcppTransport should be {new_transport}, got: {slot2_values['OcppTransport']}"
|
|
)
|
|
|
|
log.info("Step 3 PASSED: SetVariables on slot 2 persisted and verified via GetVariables")
|
|
|
|
# ── Step 4: SetVariables on the active slot (1) should be rejected ──
|
|
set_var_active = SetVariableDataType(
|
|
component=ComponentType(
|
|
name="NetworkConfiguration", instance="1"
|
|
),
|
|
variable=VariableType(name="OcppCsmsUrl"),
|
|
attribute_type=AttributeEnumType.actual,
|
|
attribute_value="wss://should-be-rejected.example.com/ocpp",
|
|
)
|
|
|
|
response = await charge_point_v21.set_variables_req(
|
|
set_variable_data=[set_var_active]
|
|
)
|
|
|
|
assert validate_set_variables_rejected(
|
|
response, "PriorityNetworkConf"
|
|
), (
|
|
f"SetVariables on active slot 1 should be rejected with "
|
|
f"PriorityNetworkConf, but got: {response}"
|
|
)
|
|
|
|
log.info("Step 4 PASSED: SetVariables on active slot 1 rejected with PriorityNetworkConf")
|