diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7facdf8835..edf585c9a1e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres Fto [Semantic Versioning](http://semver.org/spec/v2.0.0 [6.0.0-dev11]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev11 +### Added + +- `GET /gov/service/javascript-app` now takes an optional `?case=original` query argument. When passed, the response will contain the raw original `snake_case` field names, for direct comparison, rather than the API-standard `camelCase` projections. + ### Deprecated - The function `ccf::get_js_plugins()` and associated FFI plugin system for JS is deprecated. Similar functionality should now be implemented through a `js::Extension` returned from `DynamicJSEndpointRegistry::get_extensions()`. diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index c941e14f960a..eabd6d65ea3a 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -283,11 +283,35 @@ namespace ccf::gov::endpoints { auto endpoints = nlohmann::json::object(); + bool original_case = false; + { + const auto parsed_query = + ccf::http::parse_query(ctx.rpc_ctx->get_request_query()); + std::string error_reason; + const auto case_opt = ccf::http::get_query_value_opt( + parsed_query, "case", error_reason); + + if (case_opt.has_value()) + { + if (case_opt.value() != "original") + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidQueryParameterValue, + "Accepted values for the 'case' query parameter are: " + "original"); + return; + } + + original_case = true; + } + } + auto js_endpoints_handle = ctx.tx.template ro( ccf::endpoints::Tables::ENDPOINTS); js_endpoints_handle->foreach( - [&endpoints]( + [&endpoints, original_case]( const ccf::endpoints::EndpointKey& key, const ccf::endpoints::EndpointProperties& properties) { auto ib = @@ -296,20 +320,29 @@ namespace ccf::gov::endpoints auto operation = nlohmann::json::object(); - operation["jsModule"] = properties.js_module; - operation["jsFunction"] = properties.js_function; - operation["forwardingRequired"] = - properties.forwarding_required; - - auto policies = nlohmann::json::array(); - for (const auto& policy : properties.authn_policies) + if (original_case) { - policies.push_back(policy); + operation = properties; + } + else + { + operation["jsModule"] = properties.js_module; + operation["jsFunction"] = properties.js_function; + operation["forwardingRequired"] = + properties.forwarding_required; + operation["redirectionStrategy"] = + properties.redirection_strategy; + + auto policies = nlohmann::json::array(); + for (const auto& policy : properties.authn_policies) + { + policies.push_back(policy); + } + operation["authnPolicies"] = policies; + + operation["mode"] = properties.mode; + operation["openApi"] = properties.openapi; } - operation["authnPolicies"] = policies; - - operation["mode"] = properties.mode; - operation["openApi"] = properties.openapi; operations[key.verb.c_str()] = operation; @@ -330,6 +363,7 @@ namespace ccf::gov::endpoints HTTP_GET, api_version_adapter(get_javascript_app, ApiVersion::v1), no_auth_required) + .add_query_parameter("case") .set_openapi_hidden(true) .install(); diff --git a/tests/js-modules/modules.py b/tests/js-modules/modules.py index d838fbf8f198..d9b5f39a9379 100644 --- a/tests/js-modules/modules.py +++ b/tests/js-modules/modules.py @@ -39,6 +39,36 @@ def test_module_import(network, args): return network +def compare_app_metadata(expected, actual, api_key_renames, route=[]): + path = ".".join(route) + assert isinstance( + actual, type(actual) + ), f"Expected same type of values at {path}, found {type(expected)} vs {type(actual)}" + + if isinstance(expected, dict): + for orig_k, v_expected in expected.items(): + k = orig_k + if k in api_key_renames: + k = api_key_renames[k] + + assert ( + k in actual + ), f"Expected key {k} (normalised from {orig_k}) at {path}, found: {actual}" + v_actual = actual[k] + + compare_app_metadata(v_expected, v_actual, api_key_renames, route + [k]) + else: + if not isinstance(expected, list) and expected in api_key_renames: + k = api_key_renames[expected] + assert ( + k == actual + ), f"Mismatch at {path}, expected {k} (normalised from {expected}) and found {actual}" + else: + assert ( + expected == actual + ), f"Mismatch at {path}, expected {expected} and found {actual}" + + @reqs.description("Test module access") def test_module_access(network, args): primary, _ = network.find_nodes() @@ -48,8 +78,49 @@ def test_module_access(network, args): network.consortium.set_js_app_from_bundle(primary, bundle) expected_modules = bundle["modules"] + expected_metadata = bundle["metadata"] + + http_methods_renamed = { + method: method.upper() for method in ("post", "get", "put", "delete") + } + module_names_prefixed = { + module["name"]: f"/{module['name']}" + for module in expected_modules + if not module["name"].startswith("/") + } + endpoint_def_camelcased = { + "js_module": "jsModule", + "js_function": "jsFunction", + "forwarding_required": "forwardingRequired", + "redirection_strategy": "redirectionStrategy", + "authn_policies": "authnPolicies", + "openapi": "openApi", + } with primary.api_versioned_client(api_version=args.gov_api_version) as c: + r = c.get("/gov/service/javascript-app?case=original") + assert r.status_code == http.HTTPStatus.OK, r.status_code + compare_app_metadata( + expected_metadata, + r.body.json(), + { + **http_methods_renamed, + **module_names_prefixed, + }, + ) + + r = c.get("/gov/service/javascript-app") + assert r.status_code == http.HTTPStatus.OK, r.status_code + compare_app_metadata( + expected_metadata, + r.body.json(), + { + **http_methods_renamed, + **module_names_prefixed, + **endpoint_def_camelcased, + }, + ) + r = c.get("/gov/service/javascript-modules") assert r.status_code == http.HTTPStatus.OK, r.status_code