Files
imateusdev 8b813675be fix(routes): canonicalize path parameters so client calls match handlers
Route QNs (__route__METHOD__/path) are the rendezvous key for joining a client
HTTP_CALLS to the server handler that serves it, but they were built from the raw
path. Frameworks write path parameters with different placeholder syntaxes
(":id" Express/clients, "{id}" Axum/Spring/OpenAPI, "<id>" Flask/Rocket), so a
client call and its handler produced different QNs and never joined. Static
routes joined; parameterized routes (most of a REST surface) did not.

Add cbm_route_canon_path() which collapses each parameter token (":name",
"{name}", "<name>", "${...}") to a single "{}" token, and apply it to the path
at every HTTP-route QN construction site (pass_route_nodes, pass_parallel,
pass_calls, pass_cross_repo). Broker/async/infra QNs are left untouched. Only the
QN (identity/dedup key) is canonicalized; the Route node display name keeps the
original path. Parameter names are intentionally discarded so the same logical
endpoint matches across services that name the variable differently.

Adds tests/test_route_canon.c (11 cases). Fixes #505.

Signed-off-by: imateusdev <imateusdev@gmail.com>
2026-06-23 13:52:08 -03:00

109 lines
3.4 KiB
C

/*
* test_route_canon.c — Unit tests for cbm_route_canon_path().
*
* Verifies that framework-specific route-parameter placeholder syntaxes
* (":id", "{id}", "<id>", "${id}") collapse to a single "{}" token so that a
* client call site and a server handler rendezvous on the same Route QN
* regardless of the language/framework that produced each side.
* See src/pipeline/pass_route_nodes.c.
*/
#include "test_framework.h"
#include "pipeline/pipeline_internal.h"
#include <string.h>
TEST(route_canon_static_unchanged) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/products/categories", b, sizeof(b)),
"/products/categories");
PASS();
}
TEST(route_canon_colon_param) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/players/:id", b, sizeof(b)), "/players/{}");
PASS();
}
TEST(route_canon_brace_param) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/players/{id}", b, sizeof(b)), "/players/{}");
PASS();
}
/* The core invariant: Axum "{id}" and a JS client ":id" must converge. */
TEST(route_canon_colon_and_brace_converge) {
char a[128];
char c[128];
cbm_route_canon_path("/clients/{id}/authorized-users", a, sizeof(a));
cbm_route_canon_path("/clients/:clientId/authorized-users", c, sizeof(c));
ASSERT_STR_EQ(a, c);
ASSERT_STR_EQ(a, "/clients/{}/authorized-users");
PASS();
}
/* Parameter names are intentionally discarded ("{id}" == ":requestId"). */
TEST(route_canon_param_name_agnostic) {
char a[128];
char c[128];
cbm_route_canon_path("/link-requests/{id}/status", a, sizeof(a));
cbm_route_canon_path("/link-requests/:requestId/status", c, sizeof(c));
ASSERT_STR_EQ(a, c);
PASS();
}
TEST(route_canon_angle_param) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/users/<int:id>", b, sizeof(b)), "/users/{}");
PASS();
}
TEST(route_canon_template_interpolation) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/players/${playerId}", b, sizeof(b)), "/players/{}");
PASS();
}
TEST(route_canon_multiple_params) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/orders/{id}/items/{itemIndex}", b, sizeof(b)),
"/orders/{}/items/{}");
PASS();
}
/* A ':' that is not at a segment start is literal, not a route parameter. */
TEST(route_canon_colon_mid_segment_is_literal) {
char b[128];
ASSERT_STR_EQ(cbm_route_canon_path("/a/b:c", b, sizeof(b)), "/a/b:c");
PASS();
}
TEST(route_canon_null_and_empty) {
char b[8];
ASSERT_STR_EQ(cbm_route_canon_path("", b, sizeof(b)), "");
ASSERT_STR_EQ(cbm_route_canon_path(NULL, b, sizeof(b)), "");
PASS();
}
/* A tight output buffer must still yield a bounded, NUL-terminated string. */
TEST(route_canon_truncation_safe) {
char b[6];
const char *r = cbm_route_canon_path("/players/:id", b, sizeof(b));
ASSERT(strlen(r) < sizeof(b));
PASS();
}
SUITE(route_canon) {
RUN_TEST(route_canon_static_unchanged);
RUN_TEST(route_canon_colon_param);
RUN_TEST(route_canon_brace_param);
RUN_TEST(route_canon_colon_and_brace_converge);
RUN_TEST(route_canon_param_name_agnostic);
RUN_TEST(route_canon_angle_param);
RUN_TEST(route_canon_template_interpolation);
RUN_TEST(route_canon_multiple_params);
RUN_TEST(route_canon_colon_mid_segment_is_literal);
RUN_TEST(route_canon_null_and_empty);
RUN_TEST(route_canon_truncation_safe);
}