ores.cpp.shell-command.command_tests

Table of Contents

The test file for one entity's shell command unit. Every case stops at a guard or at the transport boundary, so the file needs no server 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 menu's completion list, which is the only public view of the menu's children. The add verb refuses a signed-out session before it reads a token. It reports the arity it expects and the count it was given. It names the field when a token will not convert. It refuses a session whose account has no default party when a column reads the acting party. The delete and history verbs each refuse a signed-out session. A valid token vector converts and reaches the transport.

The shape-two units carry one more: the history verb rejects --version with no --diff. That check runs before the login guard, so the case needs no session.

A current_state entity has no history verb at all, so it carries none of the history cases.

See the Template variable reference for the complete list of available variables and their semantics.

1. Mustache incantations

The valid token vector comes from {{sample_token}}, one per user-supplied column, injected by core.py from the column's own type. It satisfies the unit's parse, not the server's validation, because the case stops at the transport boundary.

The malformed-token case reads {{malformed_token_column}}, which is absent when every column accepts any token. It carries the positional index to overwrite, the token to write there, and the message the unit must print. The {{#has_session_party_columns}} section emits the default-party case only when a column reads the acting party.

Every case name starts with {{entity_singular}}_commands. 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_shell_command_tests.cpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.shell-command.command_tests.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#include "ores.logging/make_logger.hpp"
#include "ores.nats/service/nats_client.hpp"
#include "ores.shell/app/command_feedback.hpp"
#include "ores.shell/app/commands/{{component}}/{{entity_singular}}_commands.hpp"
#include <catch2/catch_test_macros.hpp>
#include <cli/cli.h>
#include <algorithm>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>

using ores::nats::service::nats_client;
using ores::shell::app::command_feedback;
using ores::shell::app::commands::{{entity_singular}}_commands;
using namespace ores::logging;

namespace {

const std::string_view test_suite("ores.shell.{{component}}.tests");
const std::string tags("[commands]");

const std::string valid_party_id("3f2504e0-4f89-11d3-9a0c-0305e82c3301");

void log_in(nats_client& session) {
    nats_client::login_info info;
    info.username = "tester";
    info.jwt = "token";
    info.default_party_id = valid_party_id;
    session.set_auth(std::move(info));
}

// One token per positional the command takes. A value the command parses is
// not a value the guard reaches, so any token does to prove the guard.
std::vector<std::string> tokens(const std::size_t count) {
    return std::vector<std::string>(count, std::string{"sample"});
}

}

TEST_CASE("{{entity_singular}}_commands_registers_every_derived_verb", tags) {
    auto lg(make_logger(test_suite));

    cli::Menu root_menu("root");
    nats_client session;

    {{entity_singular}}_commands::register_commands(root_menu, session);

    // The menu's completion list is the only public view of its children, so
    // a verb that is missing from it was never registered.
    const auto completions = root_menu.GetCompletions("{{entity_plural}} ");
    for (const auto& verb : {
{{#shell.commands}}
             std::string{"{{entity_plural}} {{command}}"},
{{/shell.commands}}
         })
        CHECK(std::find(completions.begin(), completions.end(), verb) !=
              completions.end());

    BOOST_LOG_SEV(lg, debug) << "Registered {{shell.command_count}} command(s).";
}
{{#shell.commands}}

TEST_CASE("{{entity_singular}}_commands_process_{{identifier}}_requires_a_session", tags) {
    auto lg(make_logger(test_suite));

    nats_client session;
    std::ostringstream out;

    command_feedback::reset();
    {{entity_singular}}_commands::process_{{identifier}}(out, session,
                                                        tokens({{positional_count}}));

    BOOST_LOG_SEV(lg, debug) << "Output for a signed-out session: " << out.str();
    CHECK(out.str().find("You must be logged in") != std::string::npos);
    CHECK(command_feedback::failed());
}
{{#positional_count}}
{{#exact_count}}

TEST_CASE("{{entity_singular}}_commands_process_{{identifier}}_reports_the_expected_count",
          tags) {
    auto lg(make_logger(test_suite));

    nats_client session;
    log_in(session);
    std::ostringstream out;

    command_feedback::reset();
    {{entity_singular}}_commands::process_{{identifier}}(out, session, {});

    BOOST_LOG_SEV(lg, debug) << "Output for an empty argument list: " << out.str();
    // The arity guard names both the count it expects and the count it
    // received. The expected count is shape-specific in the command (a write
    // also reads its intent), so the case pins the wording and the received
    // count rather than restating that arithmetic.
    CHECK(out.str().find("Expected ") != std::string::npos);
    CHECK(out.str().find("got 0.") != std::string::npos);
    CHECK(command_feedback::failed());
}
{{/exact_count}}
{{/positional_count}}
{{/shell.commands}}
{{/domain_entity}}

3. See also

Emacs 29.3 (Org mode 9.6.15)