diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 2acecf83e..70bfbafa8 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -60,6 +60,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/query_api_test.cpp nmos/test/sdp_test_utils.cpp nmos/test/sdp_utils_test.cpp + nmos/test/settings_test.cpp nmos/test/slog_test.cpp nmos/test/system_resources_test.cpp nmos/test/video_jxsv_test.cpp diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 460ceb8ca..c212bfc3a 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -67,6 +67,12 @@ int main(int argc, char* argv[]) } } + // Validate the standard and example-node-specific settings (before inserting + // run-time defaults, so that errors in user-provided settings are reported with + // the offending key rather than as a bare json_exception via the field accessors) + // (throws web::json::json_exception with the offending key in the message) + validate_node_implementation_settings(node_model.settings); + // Prepare run-time default settings (different than header defaults) nmos::insert_node_default_settings(node_model.settings); diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 9d2ef4fa4..6c4303fa9 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -1,6 +1,8 @@ #include "node_implementation.h" +#include #include +#include #include #include #include @@ -10,6 +12,7 @@ #include #include "pplx/pplx_utils.h" // for pplx::complete_after, etc. #include "cpprest/host_utils.h" +#include "cpprest/json_validator.h" #ifdef HAVE_LLDP #include "lldp/lldp_manager.h" #endif @@ -2574,6 +2577,88 @@ namespace impl { web::json::push_back(resource.data[nmos::fields::tags][nmos::fields::group_hint], nmos::make_group_hint({ U("example"), resource.type.name + U(' ') + port.name + utility::conversions::details::to_string_t(index) })); } + + // JSON schema covering the example-node-specific impl::fields::* settings; combined with + // nmos::validate_node_settings, this covers all the properties this example reads from the + // settings JSON. additionalProperties is not set to false, so all other (standard) settings + // pass through unchecked, allowing the two validators to be composed. + // definitions: aliases (in nmos::details::settings_definitions_schema() source order) first, then local + static const char* node_settings_schema_text = R"-schema-( +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "definitions": { + "nonNegativeInteger": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/nonNegativeInteger" }, + "positiveInteger": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/positiveInteger" }, + "rational": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/rational" }, + "tags": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/tags" }, + "uuid": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/uuid" }, + "interlaceMode": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/interlaceMode" }, + "colorspace": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/colorspace" }, + "transferCharacteristic": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/transferCharacteristic" }, + "colorSampling": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/colorSampling" }, + "portKind": { "type": "string", "enum": ["v", "a", "d", "m", "t", "b", "s", "c", "xv", "xa", "xd"] } + }, + "properties": { + "node_tags": { "$ref": "#/definitions/tags" }, + "device_tags": { "$ref": "#/definitions/tags" }, + + "how_many": { "$ref": "#/definitions/nonNegativeInteger" }, + "activate_senders": { "type": "boolean" }, + + "senders": { "type": "array", "items": { "$ref": "#/definitions/portKind" } }, + "receivers": { "type": "array", "items": { "$ref": "#/definitions/portKind" } }, + + "frame_rate": { "$ref": "#/definitions/rational" }, + "frame_width": { "$ref": "#/definitions/positiveInteger" }, + "frame_height": { "$ref": "#/definitions/positiveInteger" }, + + "interlace_mode": { "$ref": "#/definitions/interlaceMode" }, + + "colorspace": { "$ref": "#/definitions/colorspace" }, + "transfer_characteristic": { "$ref": "#/definitions/transferCharacteristic" }, + "color_sampling": { "$ref": "#/definitions/colorSampling" }, + "component_depth": { "$ref": "#/definitions/positiveInteger" }, + "video_type": { "type": "string", "pattern": "^video\\/[^\\s\\/]+$" }, + "mxl_video_type": { "type": "string", "enum": ["video/v210", "video/v210a"] }, + + "channel_count": { "$ref": "#/definitions/positiveInteger" }, + + "smpte2022_7": { "type": "boolean" }, + "simulate_status_monitor_activity": { "type": "boolean" }, + + "mxl_domain_id": { "$ref": "#/definitions/uuid" } + } +} + )-schema-"; + + const std::pair& node_settings_schema() + { + static const std::pair instance{ + web::uri{ U("urn:x-nmos-cpp:schemas:node-settings") }, + web::json::value::parse(node_settings_schema_text) + }; + return instance; + } + +} + +void validate_node_implementation_settings(const nmos::settings& settings) +{ + // delegate validation of the standard properties (see nmos/settings.h) + nmos::validate_node_settings(settings); + + // validate the example node-specific impl::fields::* properties + static const std::map known{ + impl::node_settings_schema(), + nmos::details::settings_definitions_schema() + }; + static const web::json::experimental::json_validator validator + { + [](const web::uri& wanted) { return known.at(wanted); }, + boost::copy_range>(known | boost::adaptors::map_keys) + }; + validator.validate(settings, impl::node_settings_schema().first); } // This constructs all the callbacks used to integrate the example device-specific underlying implementation diff --git a/Development/nmos-cpp-node/node_implementation.h b/Development/nmos-cpp-node/node_implementation.h index 3c6b295c3..f793b0e39 100644 --- a/Development/nmos-cpp-node/node_implementation.h +++ b/Development/nmos-cpp-node/node_implementation.h @@ -1,6 +1,8 @@ #ifndef NMOS_CPP_NODE_NODE_IMPLEMENTATION_H #define NMOS_CPP_NODE_NODE_IMPLEMENTATION_H +#include "nmos/settings.h" + namespace slog { class base_gate; @@ -17,6 +19,13 @@ namespace nmos } } +// Validates settings for the example node, including both the standard properties (via +// nmos::validate_node_settings) and the additional impl::fields::* settings defined in +// node_implementation.cpp. +// Throws web::json::json_exception, with a message including the offending key (and where +// appropriate the actual value), for incorrect types and out-of-range or unrecognised values. +void validate_node_implementation_settings(const nmos::settings& settings); + // This is an example of how to integrate the nmos-cpp library with a device-specific underlying implementation. // It constructs and inserts a node resource and some sub-resources into the model, based on the model settings, // starts background tasks to emit regular events from the temperature event source, and then waits for shutdown. diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index a98b253b7..d560d13a9 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -60,6 +60,12 @@ int main(int argc, char* argv[]) } } + // Validate the standard settings (before inserting run-time defaults, so that + // errors in user-provided settings are reported with the offending key rather + // than as a bare json_exception via the field accessors) + // (throws web::json::json_exception with the offending key in the message) + nmos::validate_registry_settings(registry_model.settings); + // Prepare run-time default settings (different than header defaults) nmos::insert_registry_default_settings(registry_model.settings); diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index 94251cb4d..80cd97590 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -1,17 +1,322 @@ #include "nmos/settings.h" +#include #include +#include #include #include #include #include "cpprest/host_utils.h" #include "cpprest/http_utils.h" +#include "cpprest/json_validator.h" #include "cpprest/version.h" #include "nmos/id.h" #include "websocketpp/version.hpp" namespace nmos { + namespace details + { + // Useful value-type definitions for composing application-specific settings + // schemas; see settings_definitions_schema(). + static const char* settings_definitions_schema_text = R"-schema-( +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "nonNegativeInteger": { "type": "integer", "minimum": 0 }, + "positiveInteger": { "type": "integer", "minimum": 1 }, + "rational": { + "title": "nmos::rational", + "type": "object", + "required": ["numerator"], + "properties": { + "numerator": { "type": "integer" }, + "denominator": { "type": "integer", "not": { "enum": [0] } } + } + }, + "stringArray": { "type": "array", "items": { "type": "string" } }, + "tags": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/stringArray" } + }, + "uuid": { + "title": "nmos::id", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "interlaceMode": { + "title": "nmos::interlace_mode", + "type": "string", + "enum": ["progressive", "interlaced_tff", "interlaced_bff", "interlaced_psf"] + }, + "colorspace": { + "title": "nmos::colorspace", + "type": "string", + "enum": ["BT601", "BT709", "BT2020", "BT2100", "ST2065-1", "ST2065-3", "XYZ"] + }, + "transferCharacteristic": { + "title": "nmos::transfer_characteristic", + "type": "string", + "enum": ["SDR", "PQ", "HLG", "LINEAR", "BT2100LINPQ", "BT2100LINHLG", "ST2065-1", "ST428-1", "DENSITY"] + }, + "colorSampling": { + "title": "sdp::sampling", + "type": "string", + "enum": ["RGBA", "RGB", "YCbCr-4:4:4", "YCbCr-4:2:2", "YCbCr-4:2:0", "YCbCr-4:1:1", "CLYCbCr-4:4:4", "CLYCbCr-4:2:2", "CLYCbCr-4:2:0", "ICtCp-4:4:4", "ICtCp-4:2:2", "ICtCp-4:2:0", "XYZ", "KEY", "UNSPECIFIED"] + } + } +} + )-schema-"; + + const std::pair& settings_definitions_schema() + { + static const std::pair instance{ + web::uri{ U("urn:x-nmos-cpp:schemas:defs") }, + web::json::value::parse(settings_definitions_schema_text) + }; + return instance; + } + + // JSON schema covering the known properties in nmos/settings.h and nmos/certificate_settings.h + // (used by both validate_node_settings and validate_registry_settings; additionalProperties is + // not set to false, so fields not enumerated here pass through unchecked which is what we want + // both for forward compatibility and to allow downstream validators to layer their own checks) + // definitions: aliases (in settings_definitions_schema() source order) first, then local; + // properties: in the same order as in the headers; please keep them in sync + static const char* settings_schema_text = R"-schema-( +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "definitions": { + "nonNegativeInteger": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/nonNegativeInteger" }, + "positiveInteger": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/positiveInteger" }, + "stringArray": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/stringArray" }, + "tags": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/tags" }, + "uuid": { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/uuid" }, + "apiVersion": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" }, + "apiVersionArray": { "type": "array", "items": { "$ref": "#/definitions/apiVersion" } }, + "port": { "type": "integer", "minimum": -1, "maximum": 65535 } + }, + "properties": { + "error_log": { "type": "string" }, + "access_log": { "type": "string" }, + "logging_level": { "type": "integer" }, + "logging_categories": { "$ref": "#/definitions/stringArray" }, + + "host_name": { "type": "string" }, + "domain": { "type": "string" }, + "dns_sd_browse_mode": { "type": "integer", "enum": [0, 1, 2] }, + "host_address": { "type": "string" }, + "host_addresses": { "$ref": "#/definitions/stringArray" }, + + "is04_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is05_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is07_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is08_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is09_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is10_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is12_versions": { "$ref": "#/definitions/apiVersionArray" }, + "is14_versions": { "$ref": "#/definitions/apiVersionArray" }, + + "pri": { "$ref": "#/definitions/nonNegativeInteger" }, + "highest_pri": { "$ref": "#/definitions/nonNegativeInteger" }, + "lowest_pri": { "$ref": "#/definitions/nonNegativeInteger" }, + "authorization_highest_pri": { "$ref": "#/definitions/nonNegativeInteger" }, + "authorization_lowest_pri": { "$ref": "#/definitions/nonNegativeInteger" }, + + "discovery_backoff_min": { "$ref": "#/definitions/nonNegativeInteger" }, + "discovery_backoff_max": { "$ref": "#/definitions/nonNegativeInteger" }, + "discovery_backoff_factor": { "type": "number", "minimum": 1 }, + + "service_name_prefix": { "type": "string" }, + "registry_address": { "type": "string" }, + "registry_version": { "$ref": "#/definitions/apiVersion" }, + + "http_port": { "$ref": "#/definitions/port" }, + "query_port": { "$ref": "#/definitions/port" }, + "query_ws_port": { "$ref": "#/definitions/port" }, + "registration_port": { "$ref": "#/definitions/port" }, + "node_port": { "$ref": "#/definitions/port" }, + "connection_port": { "$ref": "#/definitions/port" }, + "events_port": { "$ref": "#/definitions/port" }, + "events_ws_port": { "$ref": "#/definitions/port" }, + "channelmapping_port": { "$ref": "#/definitions/port" }, + "system_port": { "$ref": "#/definitions/port" }, + "control_protocol_ws_port": { "$ref": "#/definitions/port" }, + "configuration_port": { "$ref": "#/definitions/port" }, + + "listen_backlog": { "$ref": "#/definitions/nonNegativeInteger" }, + "registration_heartbeat_interval": { "$ref": "#/definitions/positiveInteger" }, + "registration_expiry_interval": { "$ref": "#/definitions/positiveInteger" }, + "registration_request_max": { "$ref": "#/definitions/positiveInteger" }, + "registration_heartbeat_max": { "$ref": "#/definitions/positiveInteger" }, + + "query_paging_default": { "$ref": "#/definitions/positiveInteger" }, + "query_paging_limit": { "$ref": "#/definitions/positiveInteger" }, + + "ptp_announce_receipt_timeout": { "type": "integer", "minimum": 2, "maximum": 10 }, + "ptp_domain_number": { "type": "integer", "minimum": 0, "maximum": 127 }, + + "immediate_activation_max": { "$ref": "#/definitions/positiveInteger" }, + "events_heartbeat_interval": { "$ref": "#/definitions/positiveInteger" }, + "events_expiry_interval": { "$ref": "#/definitions/positiveInteger" }, + + "system_address": { "type": "string" }, + "system_version": { "$ref": "#/definitions/apiVersion" }, + "system_request_max": { "$ref": "#/definitions/positiveInteger" }, + + "seed_id": { "$ref": "#/definitions/uuid" }, + "label": { "type": "string" }, + "description": { "type": "string" }, + "registration_available": { "type": "boolean" }, + "allow_invalid_resources": { "type": "boolean" }, + + "manifest_port": { "$ref": "#/definitions/port" }, + "settings_port": { "$ref": "#/definitions/port" }, + "logging_port": { "$ref": "#/definitions/port" }, + "admin_port": { "$ref": "#/definitions/port" }, + "mdns_port": { "$ref": "#/definitions/port" }, + "schemas_port": { "$ref": "#/definitions/port" }, + + "server_address": { "type": "string" }, + "settings_address": { "type": "string" }, + "logging_address": { "type": "string" }, + "admin_address": { "type": "string" }, + "mdns_address": { "type": "string" }, + "schemas_address": { "type": "string" }, + "client_address": { "type": "string" }, + + "query_ws_paging_default": { "$ref": "#/definitions/positiveInteger" }, + "query_ws_paging_limit": { "$ref": "#/definitions/positiveInteger" }, + "logging_limit": { "$ref": "#/definitions/positiveInteger" }, + "logging_paging_default": { "$ref": "#/definitions/positiveInteger" }, + "logging_paging_limit": { "$ref": "#/definitions/positiveInteger" }, + + "http_trace": { "type": "boolean" }, + + "proxy_map": { + "type": "array", + "items": { + "type": "object", + "required": ["client_port", "server_port"], + "properties": { + "client_port": { "$ref": "#/definitions/port" }, + "server_port": { "$ref": "#/definitions/port" } + } + } + }, + "proxy_address": { "type": "string" }, + "proxy_port": { "$ref": "#/definitions/port" }, + + "href_mode": { "type": "integer", "enum": [0, 1, 2, 3] }, + + "client_secure": { "type": "boolean" }, + "server_secure": { "type": "boolean" }, + "validate_certificates": { "type": "boolean" }, + + "system_interval_min": { "$ref": "#/definitions/positiveInteger" }, + "system_interval_max": { "$ref": "#/definitions/positiveInteger" }, + + "system_label": { "type": "string" }, + "system_description": { "type": "string" }, + "system_tags": { "$ref": "#/definitions/tags" }, + + "system_syslog_host_name": { "type": "string" }, + "system_syslog_port": { "$ref": "#/definitions/port" }, + "system_syslogv2_host_name": { "type": "string" }, + "system_syslogv2_port": { "$ref": "#/definitions/port" }, + + "hsts_max_age": { "type": "integer" }, + "hsts_include_sub_domains": { "type": "boolean" }, + + "ocsp_interval_min": { "$ref": "#/definitions/positiveInteger" }, + "ocsp_interval_max": { "$ref": "#/definitions/positiveInteger" }, + "ocsp_request_max": { "$ref": "#/definitions/positiveInteger" }, + + "authorization_selector": { "type": "string" }, + "authorization_address": { "type": "string" }, + "authorization_port": { "$ref": "#/definitions/port" }, + "authorization_version": { "$ref": "#/definitions/apiVersion" }, + "authorization_request_max": { "$ref": "#/definitions/positiveInteger" }, + "fetch_authorization_public_keys_interval_min": { "$ref": "#/definitions/positiveInteger" }, + "fetch_authorization_public_keys_interval_max": { "$ref": "#/definitions/positiveInteger" }, + "access_token_refresh_interval": { "type": "integer", "minimum": -1 }, + "client_authorization": { "type": "boolean" }, + "server_authorization": { "type": "boolean" }, + "authorization_code_flow_max": { "type": "integer", "minimum": -1 }, + "authorization_flow": { "type": "string", "enum": ["authorization_code", "client_credentials"] }, + "authorization_redirect_port": { "$ref": "#/definitions/port" }, + "initial_access_token": { "type": "string" }, + "authorization_scopes": { "$ref": "#/definitions/stringArray" }, + "token_endpoint_auth_method": { + "type": "string", + "enum": ["none", "client_secret_basic", "client_secret_post", "private_key_jwt", "client_secret_jwt"] + }, + "jwks_uri_port": { "$ref": "#/definitions/port" }, + "validate_openid_client": { "type": "boolean" }, + "no_trailing_dot_for_authorization_callback_uri": { "type": "boolean" }, + "service_unavailable_retry_after": { "$ref": "#/definitions/nonNegativeInteger" }, + + "manufacturer_name": { "type": "string" }, + "product_name": { "type": "string" }, + "product_key": { "type": "string" }, + "product_revision_level": { "type": "string" }, + "serial_number": { "type": "string" }, + + "ca_certificate_file": { "type": "string" }, + "server_certificates": { + "type": "array", + "items": { + "type": "object", + "required": ["private_key_file", "certificate_chain_file"], + "properties": { + "key_algorithm": { "type": "string", "enum": ["ECDSA", "RSA"] }, + "private_key_file": { "type": "string" }, + "certificate_chain_file": { "type": "string" } + } + } + }, + "dh_param_file": { "type": "string" }, + "private_key_files": { "$ref": "#/definitions/stringArray" }, + "certificate_chain_files": { "$ref": "#/definitions/stringArray" } + } +} + )-schema-"; + + const std::pair& settings_schema() + { + static const std::pair instance{ + web::uri{ U("urn:x-nmos-cpp:schemas:settings") }, + web::json::value::parse(settings_schema_text) + }; + return instance; + } + + void validate_settings(const settings& settings) + { + static const std::map known{ + settings_schema(), + settings_definitions_schema() + }; + static const web::json::experimental::json_validator validator + { + [](const web::uri& wanted) { return known.at(wanted); }, + boost::copy_range>(known | boost::adaptors::map_keys) + }; + validator.validate(settings, settings_schema().first); + } + } + + void validate_node_settings(const settings& settings) + { + details::validate_settings(settings); + } + + void validate_registry_settings(const settings& settings) + { + details::validate_settings(settings); + } + namespace details { // Get default DNS domain name diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 02549d927..008461129 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -1,7 +1,9 @@ #ifndef NMOS_SETTINGS_H #define NMOS_SETTINGS_H +#include #include "bst/optional.h" +#include "cpprest/base_uri.h" #include "cpprest/json_utils.h" namespace web @@ -27,6 +29,32 @@ namespace nmos { typedef web::json::value settings; + // Validates the known properties (declared in nmos/settings.h and nmos/certificate_settings.h) + // against an embedded JSON schema. Unknown properties are silently ignored. Throws + // web::json::json_exception on failure. + void validate_node_settings(const settings& settings); + void validate_registry_settings(const settings& settings); + + namespace details + { + // The library's settings schema (used by validate_node_settings and + // validate_registry_settings). Exposed in the same form as + // settings_definitions_schema() so downstream code wanting to compose + // its own validator from the library's schemas can register both without + // re-parsing the schema text at every validator construction. + const std::pair& settings_schema(); + + // Useful value-type definitions (positiveInteger, nonNegativeInteger, + // stringArray, uuid, tags, rational, interlaceMode, colorspace, + // transferCharacteristic, colorSampling) for application code that wants + // to compose its own settings JSON schema via cross-schema $refs of the form + // { "$ref": "urn:x-nmos-cpp:schemas:defs#/definitions/" } + // The first member is the URI under which the fragment is registered; the second + // is the parsed schema. Pass `.first` to the json_validator's known-schemas list + // and return `.second` from the loader callback when it is invoked with that URI. + const std::pair& settings_definitions_schema(); + } + // Inserts run-time default settings for those which are impossible to determine at compile-time // if not already present in the specified settings void insert_node_default_settings(settings& settings); diff --git a/Development/nmos/test/settings_test.cpp b/Development/nmos/test/settings_test.cpp new file mode 100644 index 000000000..c621e46a1 --- /dev/null +++ b/Development/nmos/test/settings_test.cpp @@ -0,0 +1,283 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/settings.h" + +#include +#include "bst/test/test.h" +#include "cpprest/json_utils.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +// validate_node_settings/validate_registry_settings type and range checks +//////////////////////////////////////////////////////////////////////////////////////////// + +BST_TEST_CASE(testValidateNodeSettingsEmpty) +{ + // an empty settings object should be valid - defaults are used for everything + BST_CHECK_NO_THROW(nmos::validate_node_settings(web::json::value::object())); +} + +BST_TEST_CASE(testValidateRegistrySettingsEmpty) +{ + BST_CHECK_NO_THROW(nmos::validate_registry_settings(web::json::value::object())); +} + +BST_TEST_CASE(testValidateNodeSettingsAfterDefaults) +{ + // the run-time defaults shouldn't introduce any invalid values + nmos::settings settings; + nmos::insert_node_default_settings(settings); + BST_CHECK_NO_THROW(nmos::validate_node_settings(settings)); +} + +BST_TEST_CASE(testValidateRegistrySettingsAfterDefaults) +{ + nmos::settings settings; + nmos::insert_registry_default_settings(settings); + BST_CHECK_NO_THROW(nmos::validate_registry_settings(settings)); +} + +BST_TEST_CASE(testValidateNodeSettingsWrongType) +{ + using web::json::value_of; + + // all validation errors are reported as web::json::json_exception and the + // schema validator includes the JSON pointer (e.g. /http_port) in the message + { + const auto bad = value_of({ { U("http_port"), U("3210") } }); // should be an int + try + { + nmos::validate_node_settings(bad); + BST_CHECK(false); // expected json_exception + } + catch (const web::json::json_exception& e) + { + BST_CHECK(std::strstr(e.what(), "/http_port") != nullptr); + } + } + { + const auto bad = value_of({ { U("host_addresses"), U("127.0.0.1") } }); // should be an array + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto bad = value_of({ { U("http_trace"), U("true") } }); // should be a bool + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } +} + +BST_TEST_CASE(testValidateNodeSettingsRangeErrors) +{ + using web::json::value_of; + + { + const auto bad = value_of({ { U("http_port"), 65536 } }); + try + { + nmos::validate_node_settings(bad); + BST_CHECK(false); + } + catch (const web::json::json_exception& e) + { + BST_CHECK(std::strstr(e.what(), "/http_port") != nullptr); + BST_CHECK(std::strstr(e.what(), "65536") != nullptr); + } + } + { + // -1 is OK for any port (disables the corresponding feature); -2 is not + const auto ok = value_of({ { U("http_port"), -1 } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + const auto bad = value_of({ { U("http_port"), -2 } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + // logging_level is unconstrained beyond being an integer (named slog severities + // are in [-40, 40] but the sentinels never_log_severity/reset_log_severity sit + // at INT_MAX/INT_MIN) + const auto ok = value_of({ { U("logging_level"), 41 } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + const auto bad = value_of({ { U("dns_sd_browse_mode"), 3 } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto bad = value_of({ { U("discovery_backoff_factor"), 0.5 } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } +} + +BST_TEST_CASE(testValidateRegistrySettingsRangeErrors) +{ + using web::json::value_of; + + { + const auto bad = value_of({ { U("ptp_announce_receipt_timeout"), 1 } }); // valid range is 2-10 + BST_CHECK_THROW(nmos::validate_registry_settings(bad), web::json::json_exception); + } + { + const auto bad = value_of({ { U("ptp_domain_number"), 128 } }); // valid range is 0-127 + BST_CHECK_THROW(nmos::validate_registry_settings(bad), web::json::json_exception); + } + { + const auto ok = value_of({ { U("ptp_domain_number"), 0 } }); + BST_CHECK_NO_THROW(nmos::validate_registry_settings(ok)); + } +} + +BST_TEST_CASE(testValidateNodeSettingsVersions) +{ + using web::json::value_of; + + { + const auto ok = value_of({ + { U("is04_versions"), value_of({ U("v1.2"), U("v1.3") }) }, + { U("is05_versions"), value_of({ U("v1.0"), U("v1.1") }) } + }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + // any well-formed v. string is acceptable to the schema; + // whether the library actually implements it is checked at point of use + const auto ok = value_of({ { U("is04_versions"), value_of({ U("v1.99") }) } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + const auto bad = value_of({ { U("is04_versions"), value_of({ U("not-a-version") }) } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto bad = value_of({ { U("registry_version"), U("1.0") } }); // missing the "v" + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto bad = value_of({ { U("registry_version"), U("v1") } }); // missing the minor + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto bad = value_of({ { U("registry_version"), U("v1.2.3") } }); // too many parts + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } +} + +BST_TEST_CASE(testValidateNodeSettingsEnumStrings) +{ + using web::json::value_of; + + { + const auto ok = value_of({ { U("authorization_flow"), U("client_credentials") } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + const auto bad = value_of({ { U("authorization_flow"), U("password") } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto ok = value_of({ { U("authorization_scopes"), value_of({ U("registration"), U("node"), U("connection") }) } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + // authorization scope strings aren't validated against an enum (see nmos/scope.h); + // adding a new NMOS API just adds a new scope and the schema shouldn't need updating + const auto ok = value_of({ { U("authorization_scopes"), value_of({ U("some-future-scope") }) } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + const auto bad = value_of({ { U("authorization_scopes"), value_of({ 42 }) } }); // must still be strings + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + const auto ok = value_of({ { U("token_endpoint_auth_method"), U("private_key_jwt") } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + const auto bad = value_of({ { U("token_endpoint_auth_method"), U("not-a-method") } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } +} + +BST_TEST_CASE(testValidateNodeSettingsSeedId) +{ + using web::json::value_of; + + { + // a well-formed UUID is accepted + const auto ok = value_of({ { U("seed_id"), U("12345678-1234-1234-89ab-1234567890ab") } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + // an arbitrary string is rejected (seed_id is used as a v5 UUID namespace) + const auto bad = value_of({ { U("seed_id"), U("not-a-uuid") } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + // upper-case hex is rejected (NMOS UUIDs are lower-case) + const auto bad = value_of({ { U("seed_id"), U("12345678-1234-1234-89AB-1234567890ab") } }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } +} + +BST_TEST_CASE(testValidateServerCertificates) +{ + using web::json::value_of; + + { + const auto ok = value_of({ + { U("server_certificates"), value_of({ + value_of({ + { U("key_algorithm"), U("ECDSA") }, + { U("private_key_file"), U("key.pem") }, + { U("certificate_chain_file"), U("chain.pem") } + }) + }) } + }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + const auto bad = value_of({ + { U("server_certificates"), value_of({ + value_of({ { U("key_algorithm"), U("DSA") } }) + }) } + }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + // omitting key_algorithm is fine - omission is the way to say "don't care" + const auto ok = value_of({ + { U("server_certificates"), value_of({ + value_of({ + { U("private_key_file"), U("key.pem") }, + { U("certificate_chain_file"), U("chain.pem") } + }) + }) } + }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } + { + // but explicitly setting it to the empty string is a sign of user confusion + const auto bad = value_of({ + { U("server_certificates"), value_of({ + value_of({ { U("key_algorithm"), U("") } }) + }) } + }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + // an entry without both private_key_file and certificate_chain_file is meaningless; + // the cert-loader logs warnings and pushes an empty certificate that fails at TLS time + const auto bad = value_of({ + { U("server_certificates"), value_of({ + value_of({ { U("key_algorithm"), U("ECDSA") } }) + }) } + }); + BST_CHECK_THROW(nmos::validate_node_settings(bad), web::json::json_exception); + } + { + // empty server_certificates array is the right way to say "no server certs" + const auto ok = value_of({ { U("server_certificates"), web::json::value::array() } }); + BST_CHECK_NO_THROW(nmos::validate_node_settings(ok)); + } +} + +BST_TEST_CASE(testValidateSettingsNonObject) +{ + BST_CHECK_THROW(nmos::validate_node_settings(web::json::value::string(U("hi"))), web::json::json_exception); + BST_CHECK_THROW(nmos::validate_registry_settings(web::json::value::array()), web::json::json_exception); +}