ores.cpp.http-route.route_tests

Table of Contents

The test file for one entity'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 derived verb appears in the router's route list, which is the only public view of what the unit registered. The count matches the derivation, so a route that stopped being registered fails rather than passing as a smaller set. Each route carries the authentication flag its own operation states, so a route that needs a session cannot be registered without one and a route that needs none is not made unreachable. Each route also carries the declaration itself, so a route the builder would have refused cannot pass by being assembled another way.

1. Mustache incantations

The expectations are built from {{http_route.routes}}, one entry per derived route, each carrying the method, the path and the auth flag the projection decided. The path is the pattern the route was registered under, so the case compares registration against the same source the registration read.

Every case name starts with {{entity_singular}}_routes. A component's test files link into one binary, so without the prefix two entities 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_tests.cpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.http-route.route_tests.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#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 derivation 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_derived_route", "[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_operation_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 operation's own stated requirement. A route that needs
    // a session cannot be registered without one, and one that does not is
    // not made unreachable by a flag the model 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).";
}
{{/domain_entity}}

3. See also

Emacs 29.3 (Org mode 9.6.15)