ores.cpp.http-route.operation_tests
Table of Contents
The test file for one operation model's HTTP route unit. Every case stops at the router, so the file needs no server, no NATS connection and no database. It links against Catch2 and the part's own library.
Each case pins one thing the unit promises. Every declared message the model
exposes – one that carries a subject, a response and an :http_route: true –
appears in the router's route list, which is the only public view of what the
unit registered. The count matches the projection, so a route that stopped
being registered fails rather than passing as a smaller set, and a message the
model never exposed is absent by the same count. Each route carries the
authentication flag its own message states, so a route that needs a session
cannot be registered without one and a route the model declares public is not
made unreachable.
1. Mustache incantations
The expectations are built from {{http_route.routes}}, one entry per declared
route, each carrying the method, the path and the auth flag the projection
decided.
Every case name starts with {{entity_singular}}_routes. A component's test
files link into one binary, so without the prefix two models in the same
component declare each case name twice. Catch2 rejects the duplicate, and
ctest does not always surface the rejection, so the prefix is what keeps the
component's suite both startable and honest.
2. Template
The full template source. Edit here and re-tangle with
compass build --direct tangle_codegen_templates to regenerate
library/templates/cpp_http_route_operation_tests.cpp.mustache.
{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.http-route.operation_tests.org. Edit the org source. }}
{{{cpp_license}}}
{{#operation}}
#include "ores.http/routes/{{component}}/{{entity_singular}}_routes.hpp"
#include "ores.http.api/net/router.hpp"
#include "ores.http.api/openapi/endpoint_registry.hpp"
#include "ores.logging/make_logger.hpp"
#include "ores.nats/service/nats_client.hpp"
#include "test_suite.hpp"
#include <algorithm>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <string_view>
using namespace ores::logging;
using ores::http::domain::http_method;
using ores::http::net::router;
using ores::http::openapi::endpoint_registry;
using ores::http::routes::{{component}}::{{entity_singular}}_routes;
using ores::nats::service::nats_client;
namespace {
/// One route the unit must register, as the projection states it.
struct {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_expected_route {
http_method method;
std::string pattern;
bool requires_auth;
};
/// A router holding the unit's routes, registered against a bare client.
///
/// Registration touches no transport: the client is only what each route
/// forwards on, so an unconnected one is enough to read the route table.
std::shared_ptr<router> {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_registered_routes(nats_client& session) {
auto table = std::make_shared<router>();
auto registry = std::make_shared<endpoint_registry>();
{{entity_singular}}_routes::register_routes(table, registry, session);
return table;
}
}
TEST_CASE("{{entity_singular}}_routes_registers_every_declared_operation", "[routes]") {
auto lg(make_logger(test_suite));
nats_client session;
const auto table = {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_registered_routes(session);
const auto& routes = table->routes();
REQUIRE(routes.size() == {{http_route.route_count}});
// The router's own list is the only public view of what was registered,
// so a route that is missing from it was never registered.
for (const auto& expected : {
{{#http_route.routes}}
{{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_expected_route{http_method::{{method}}, std::string{"{{pattern}}"},
{{#requires_session}}
true},
{{/requires_session}}
{{^requires_session}}
false},
{{/requires_session}}
{{/http_route.routes}}
}) {
const auto found = std::find_if(routes.begin(), routes.end(),
[&](const auto& route) {
return route.method == expected.method &&
route.pattern == expected.pattern;
});
CHECK(found != routes.end());
}
BOOST_LOG_SEV(lg, debug) << "Registered {{http_route.route_count}} route(s).";
}
TEST_CASE("{{entity_singular}}_routes_requires_a_session_where_the_message_does", "[routes]") {
auto lg(make_logger(test_suite));
nats_client session;
const auto table = {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_registered_routes(session);
const auto& routes = table->routes();
// The flag is the message's own stated requirement. A route that needs a
// session cannot be registered without one, and one the model declares
// public is not made unreachable by a flag it never asked for.
for (const auto& expected : {
{{#http_route.routes}}
{{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_expected_route{http_method::{{method}}, std::string{"{{pattern}}"},
{{#requires_session}}
true},
{{/requires_session}}
{{^requires_session}}
false},
{{/requires_session}}
{{/http_route.routes}}
}) {
const auto found = std::find_if(routes.begin(), routes.end(),
[&](const auto& route) {
return route.method == expected.method &&
route.pattern == expected.pattern;
});
REQUIRE(found != routes.end());
CHECK(found->requires_auth == expected.requires_auth);
// The builder refuses a route that states no position, so every
// registered route must carry the declaration as well as the value.
CHECK(found->auth_declared);
}
BOOST_LOG_SEV(lg, debug) << "Checked the auth flag of {{http_route.route_count}} route(s).";
}
{{/operation}}
3. See also
- Parent facet: ores.cpp.http-route.
- Sibling archetypes: ores.cpp.http-route.operation_header and ores.cpp.http-route.operation_implementation.
- ores.cpp.shell-command — the facet whose operation archetypes these mirror.