Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ChargePointConfiguration {
json custom_schema;
json internal_schema;
bool core_schema_unlock_connector_on_ev_side_disconnect_ro_value;
fs::path ocpp_main_path;
fs::path user_config_path;

std::set<SupportedFeatureProfiles> supported_feature_profiles;
Expand All @@ -43,6 +44,8 @@ class ChargePointConfiguration {
void setChargepointInformationProperty(json& user_config, const std::string& key,
const std::optional<std::string>& value);

bool validate(const std::string_view& schema_file, const json& object);

public:
ChargePointConfiguration(const std::string& config, const fs::path& ocpp_main_path,
const fs::path& user_config_path);
Expand Down
51 changes: 48 additions & 3 deletions lib/everest/ocpp/lib/ocpp/v16/charge_point_configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const std::int32_t MAX_WAIT_FOR_SET_USER_PRICE_TIMEOUT_MS = 30000;

ChargePointConfiguration::ChargePointConfiguration(const std::string& config, const fs::path& ocpp_main_path,
const fs::path& user_config_path) :
user_config_path(user_config_path) {
ocpp_main_path(ocpp_main_path), user_config_path(user_config_path) {

if (!fs::exists(this->user_config_path)) {
EVLOG_critical << "User config file does not exist";
Expand Down Expand Up @@ -312,6 +312,24 @@ void ChargePointConfiguration::setChargepointInformationProperty(json& user_conf
}
}

bool ChargePointConfiguration::validate(const std::string_view& schema_file, const json& object) {
bool result{false};
try {
fs::path schema_path = ocpp_main_path / "profile_schemas" / schema_file;
std::ifstream ifs(schema_path);
auto schema_json = json::parse(ifs);
ocpp::Schemas schema(std::move(schema_json));
auto validator = schema.get_validator();
if (validator) {
validator->validate(object);
result = true;
}
} catch (const std::exception& e) {
EVLOG_error << "Error validating against schema " << schema_file << ": " << e.what();
}
return result;
}

void ChargePointConfiguration::setChargepointInformation(const std::string& chargePointVendor,
const std::string& chargePointModel,
const std::optional<std::string>& chargePointSerialNumber,
Expand Down Expand Up @@ -2855,8 +2873,15 @@ ConfigurationStatus ChargePointConfiguration::setDefaultPriceText(const CiString
default_price = this->config["CostAndPrice"]["DefaultPriceText"];
}

json j;
try {
j = json::parse(value.get());
} catch (const std::exception& e) {
EVLOG_error << "Configuration DefaultPriceText is set, but is invalid: " << e.what();
return ConfigurationStatus::Rejected;
}

// priceText is mandatory
json j = json::parse(value.get());
if (!j.contains("priceText")) {
EVLOG_error << "Configuration DefaultPriceText is set, but does not contain 'priceText'";
return ConfigurationStatus::Rejected;
Expand Down Expand Up @@ -2888,6 +2913,16 @@ ConfigurationStatus ChargePointConfiguration::setDefaultPriceText(const CiString
default_price["priceTexts"].push_back(j);
}

// perform schema validation on default_price
json test_value;
test_value["CustomDisplayCostAndPrice"] = true;
test_value["DefaultPriceText"] = default_price;

if (!validate("CostAndPrice.json", test_value)) {
EVLOG_error << "DefaultPriceText value is invalid: " << value;
return ConfigurationStatus::Rejected;
}

this->config["CostAndPrice"]["DefaultPriceText"] = default_price;
this->setInUserConfig("CostAndPrice", "DefaultPriceText", default_price);

Expand Down Expand Up @@ -2955,14 +2990,24 @@ std::optional<std::string> ChargePointConfiguration::getDefaultPrice() {

ConfigurationStatus ChargePointConfiguration::setDefaultPrice(const std::string& value) {

json default_price = json::object();
json default_price;
try {
default_price = json::parse(value);
} catch (const std::exception& e) {
EVLOG_error << "Default price json not correct, can not store default price : " << e.what();
return ConfigurationStatus::Rejected;
}

// perform schema validation on value
json test_value;
test_value["CustomDisplayCostAndPrice"] = true;
test_value["DefaultPrice"] = default_price;

if (!validate("CostAndPrice.json", test_value)) {
EVLOG_error << "DefaultPrice is invalid: " << value;
return ConfigurationStatus::Rejected;
}

this->config["CostAndPrice"]["DefaultPrice"] = default_price;
this->setInUserConfig("CostAndPrice", "DefaultPrice", default_price);

Expand Down
107 changes: 106 additions & 1 deletion lib/everest/ocpp/tests/lib/ocpp/v16/test_configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest

#include <filesystem>

Check warning on line 5 in lib/everest/ocpp/tests/lib/ocpp/v16/test_configuration.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/everest/ocpp/tests/lib/ocpp/v16/test_configuration.cpp#L5

Include file: <filesystem> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <fstream>
#include <memory>

#include <gtest/gtest.h>

#include <ocpp/common/schemas.hpp>

Check warning on line 11 in lib/everest/ocpp/tests/lib/ocpp/v16/test_configuration.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

lib/everest/ocpp/tests/lib/ocpp/v16/test_configuration.cpp#L11

Include file: <ocpp/common/schemas.hpp> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <ocpp/v16/charge_point_configuration.hpp>

namespace {
Expand All @@ -16,7 +18,9 @@
std::unique_ptr<ChargePointConfiguration> config;

void SetUp() override {
std::ifstream ifs(CONFIG_FILE_LOCATION_V16);
fs::path cfg{CONFIG_DIR_V16};
cfg /= "config-full.json";
std::ifstream ifs(cfg);
const std::string config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
config = std::make_unique<ChargePointConfiguration>(config_file, CONFIG_DIR_V16, USER_CONFIG_FILE_LOCATION_V16);
}
Expand Down Expand Up @@ -45,4 +49,105 @@
EXPECT_TRUE(set_result.has_value());
}

TEST(PartialSchemaValidator, CostAndPrice) {
fs::path schema_file = CONFIG_DIR_V16;
schema_file /= "profile_schemas/CostAndPrice.json";
std::ifstream ifs(schema_file);
auto schema_json = json::parse(ifs);
ocpp::Schemas schema(schema_json);

const char* valid_str = R"({"priceText":"1"})";
const char* invalid_str = R"({"priceText":null,"priceTextOffline":null,"chargingPrice":null})";

auto valid_json = json::parse(valid_str);
auto invalid_json = json::parse(invalid_str);

auto validator = schema.get_validator();

json to_test;
to_test["CustomDisplayCostAndPrice"] = false;
to_test["DefaultPrice"] = valid_json;
EXPECT_NO_THROW(validator->validate(to_test));
to_test["DefaultPrice"] = invalid_json;
EXPECT_ANY_THROW(validator->validate(to_test));
}

TEST_F(ConfigurationTester, BadPriceText) {
// PriceText value is JSON encoded - check that malformed and invalid
// messages are correctly handled
const char* valid = R"({"priceText":"default"})";
const char* invalid = R"({"priceText":null,"priceTextOffline":null,"chargingPrice":null})";

auto set_result = config->set("DefaultPrice", valid);
EXPECT_TRUE(set_result.has_value());
EXPECT_EQ(set_result.value(), ConfigurationStatus::Accepted);

auto get_result = config->getDefaultPrice();
ASSERT_TRUE(get_result.has_value());
auto get_json = json::parse(get_result.value());
EXPECT_EQ(get_json["priceText"], "default");

set_result = config->set("DefaultPrice", invalid);
EXPECT_TRUE(set_result.has_value());
EXPECT_EQ(set_result.value(), ConfigurationStatus::Rejected);

auto get_result2 = config->getDefaultPrice();
ASSERT_TRUE(get_result2.has_value());
EXPECT_EQ(get_result2, get_result);
get_json = json::parse(get_result.value());
EXPECT_EQ(get_json["priceText"], "default");

auto set_result2 = config->setDefaultPrice(invalid);
EXPECT_EQ(set_result2, ConfigurationStatus::Rejected);

get_result2 = config->getDefaultPrice();
ASSERT_TRUE(get_result2.has_value());
EXPECT_EQ(get_result2, get_result);
get_json = json::parse(get_result.value());
EXPECT_EQ(get_json["priceText"], "default");
}

TEST_F(ConfigurationTester, DefaultPriceTextEmptyArray) {
const char* empty = R"([])";
auto set_result = config->set("DefaultPriceText,en", empty);
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
set_result = config->setDefaultPriceText("DefaultPriceText,en", empty);
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
}

TEST_F(ConfigurationTester, DefaultPriceTextEmptyObject) {
const char* empty = R"({})";
auto set_result = config->set("DefaultPriceText,en", empty);
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
set_result = config->setDefaultPriceText("DefaultPriceText,en", empty);
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
}

TEST_F(ConfigurationTester, DefaultPriceInvalid) {
const char* minimal = R"("priceText":[])";

auto set_result = config->set("DefaultPriceText,en", minimal);
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
set_result = config->setDefaultPriceText("DefaultPriceText,en", minimal);
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
}

TEST_F(ConfigurationTester, DefaultPriceTextMinimal) {
const char* minimal = R"({"priceText":"Default"})";

auto set_result = config->set("DefaultPriceText,en", minimal);
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
set_result = config->setDefaultPriceText("DefaultPriceText,en", minimal);
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
}

TEST_F(ConfigurationTester, DefaultPriceText) {
const char* minimal = R"({"priceText":"Default","priceTextOffline":"Offline"})";

auto set_result = config->set("DefaultPriceText,en", minimal);
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
set_result = config->setDefaultPriceText("DefaultPriceText,en", minimal);
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
}

} // namespace
Loading