Files
slocker-lite/tests/unit/test_port_forward.cpp
ceamac 42f9d36edf Add [unit] tests: port_forward, env_spec, network_subnet, cli_args
One file per source area, exercising the pure/isolated parsing and CIDR-
arithmetic functions already exposed via headers with no side effects --
parse_port_forward_spec() (protocol suffix parsing/validation, network
resolution left to add_port_forward()), resolve_env_specs() (literal and
--env-file parsing, ordering, error cases -- a small local RAII ScratchFile
helper writes the --env-file fixtures under /tmp), network_subnet.h's CIDR
validation/overlap/allocation/address-arithmetic functions, and parse_args()
itself against synthetic argv's.

Two real bugs found running parse_args() repeatedly in one process (never
possible before -- a real invocation only ever calls it once), not
assumed:
1. getopt_long's scanning position (`optind`) is process-global and never
   reset, so a second parse_args() call would silently resume scanning
   wherever the first one left off. Fixing this alone (optind = 1) wasn't
   enough on its own, either --
2. -h/-V return out of the getopt_long loop early (their own `return 0`
   case), before a call ever completes its scan and lets getopt_long null
   out its own private `nextchar` pointer -- the *next* parse_args() call
   then resumed scanning through that stale pointer into the *previous*
   call's already-destroyed argv strings, misparsing its own fresh argv.
   glibc documents `optind = 0` (not 1) as the "fully reinitialize private
   state before rescanning a new argv" signal; switching to it fixed this
   for good, confirmed by 3 repeated runs each in both random and
   deterministic (--order lex) Catch2 ordering with zero flakiness either
   way.

Neither bug could ever have surfaced in real usage (parse_args() is only
ever called once per process from main()) -- purely a testability gap the
new unit tests exposed, now fixed at the source rather than worked around
in the test file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
2026-09-04 11:23:24 +00:00

87 lines
3.4 KiB
C++

// Copyright (C) 2026 Viorel Munteanu
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
// [unit] tests for parse_port_forward_spec() (port_forward.h) -- pure
// syntax/range parsing, no networks/processes involved.
#include <catch2/catch_test_macros.hpp>
#include "port_forward.h"
TEST_CASE("parse_port_forward_spec: bare host:container defaults to tcp, no network", "[unit]") {
auto spec = parse_port_forward_spec("8080:80");
REQUIRE(spec.has_value());
CHECK_FALSE(spec->network.has_value());
CHECK(spec->host_port == 8080);
CHECK(spec->container_port == 80);
CHECK(spec->protocol == PortForwardProtocol::tcp);
}
TEST_CASE("parse_port_forward_spec: network:host:container", "[unit]") {
auto spec = parse_port_forward_spec("mynet:8080:80");
REQUIRE(spec.has_value());
REQUIRE(spec->network.has_value());
CHECK(*spec->network == "mynet");
CHECK(spec->host_port == 8080);
CHECK(spec->container_port == 80);
}
TEST_CASE("parse_port_forward_spec: explicit /tcp suffix", "[unit]") {
auto spec = parse_port_forward_spec("8080:80/tcp");
REQUIRE(spec.has_value());
CHECK(spec->container_port == 80);
CHECK(spec->protocol == PortForwardProtocol::tcp);
}
TEST_CASE("parse_port_forward_spec: /udp suffix", "[unit]") {
auto spec = parse_port_forward_spec("8080:80/udp");
REQUIRE(spec.has_value());
CHECK(spec->host_port == 8080);
CHECK(spec->container_port == 80);
CHECK(spec->protocol == PortForwardProtocol::udp);
}
TEST_CASE("parse_port_forward_spec: network + /udp suffix combined", "[unit]") {
auto spec = parse_port_forward_spec("mynet:8080:80/udp");
REQUIRE(spec.has_value());
REQUIRE(spec->network.has_value());
CHECK(*spec->network == "mynet");
CHECK(spec->protocol == PortForwardProtocol::udp);
}
TEST_CASE("parse_port_forward_spec: rejects an unrecognized protocol suffix", "[unit]") {
CHECK_FALSE(parse_port_forward_spec("8080:80/xyz").has_value());
}
TEST_CASE("parse_port_forward_spec: rejects out-of-range ports", "[unit]") {
CHECK_FALSE(parse_port_forward_spec("0:80").has_value());
CHECK_FALSE(parse_port_forward_spec("8080:0").has_value());
CHECK_FALSE(parse_port_forward_spec("65536:80").has_value());
CHECK_FALSE(parse_port_forward_spec("8080:65536").has_value());
}
TEST_CASE("parse_port_forward_spec: rejects non-numeric ports", "[unit]") {
CHECK_FALSE(parse_port_forward_spec("abc:80").has_value());
CHECK_FALSE(parse_port_forward_spec("8080:abc").has_value());
CHECK_FALSE(parse_port_forward_spec("80.5:80").has_value());
}
TEST_CASE("parse_port_forward_spec: rejects malformed field counts", "[unit]") {
CHECK_FALSE(parse_port_forward_spec("8080").has_value());
CHECK_FALSE(parse_port_forward_spec("").has_value());
CHECK_FALSE(parse_port_forward_spec("a:b:c:d").has_value());
}