// 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 resolve_env_specs() (env_spec.h). The --env-file case // needs a real file on disk -- a tiny local RAII helper below writes one // to a unique path under /tmp and removes it in its destructor, so a // REQUIRE-triggered early return from within a TEST_CASE still cleans up // (unlike the real host-state tests under tests/integration/, a stray // leftover file here is harmless but there's no reason to leave one). #include #include #include #include #include #include #include "env_spec.h" namespace { class ScratchFile { public: explicit ScratchFile(const std::string& content) : path_(fmt::format("/tmp/slocker-lite-test-env-{}", getpid())) { std::ofstream out(path_); out << content; } ~ScratchFile() { std::remove(path_.c_str()); } ScratchFile(const ScratchFile&) = delete; ScratchFile& operator=(const ScratchFile&) = delete; const std::string& path() const { return path_; } private: std::string path_; }; } // namespace TEST_CASE("resolve_env_specs: a single literal --env", "[unit]") { auto resolved = resolve_env_specs({{false, "FOO=bar"}}); REQUIRE(resolved.has_value()); REQUIRE(resolved->size() == 1); CHECK((*resolved)[0].first == "FOO"); CHECK((*resolved)[0].second == "bar"); } TEST_CASE("resolve_env_specs: splits at the first '=' only, value may contain '='", "[unit]") { auto resolved = resolve_env_specs({{false, "FOO=bar=baz"}}); REQUIRE(resolved.has_value()); REQUIRE(resolved->size() == 1); CHECK((*resolved)[0].first == "FOO"); CHECK((*resolved)[0].second == "bar=baz"); } TEST_CASE("resolve_env_specs: later --env for the same key is kept (append-order, last wins by convention)", "[unit]") { auto resolved = resolve_env_specs({{false, "FOO=first"}, {false, "FOO=second"}}); REQUIRE(resolved.has_value()); REQUIRE(resolved->size() == 2); CHECK((*resolved)[0].second == "first"); CHECK((*resolved)[1].second == "second"); } TEST_CASE("resolve_env_specs: rejects a literal with no '='", "[unit]") { CHECK_FALSE(resolve_env_specs({{false, "NOEQUALS"}}).has_value()); } TEST_CASE("resolve_env_specs: rejects an empty key", "[unit]") { CHECK_FALSE(resolve_env_specs({{false, "=value"}}).has_value()); } TEST_CASE("resolve_env_specs: rejects a missing --env-file", "[unit]") { CHECK_FALSE(resolve_env_specs({{true, "/nonexistent/path/slocker-lite-test-env-file"}}).has_value()); } TEST_CASE("resolve_env_specs: --env-file skips blank lines and #-comments", "[unit]") { ScratchFile file( "# a comment\n" "\n" " \n" "FOO=bar\n" " # indented comment\n" "BAZ=qux\n"); auto resolved = resolve_env_specs({{true, file.path()}}); REQUIRE(resolved.has_value()); REQUIRE(resolved->size() == 2); CHECK((*resolved)[0].first == "FOO"); CHECK((*resolved)[0].second == "bar"); CHECK((*resolved)[1].first == "BAZ"); CHECK((*resolved)[1].second == "qux"); } TEST_CASE("resolve_env_specs: --env-file strips a trailing '\\r' (CRLF files)", "[unit]") { ScratchFile file("FOO=bar\r\n"); auto resolved = resolve_env_specs({{true, file.path()}}); REQUIRE(resolved.has_value()); REQUIRE(resolved->size() == 1); CHECK((*resolved)[0].second == "bar"); } TEST_CASE("resolve_env_specs: --env-file with a malformed line fails the whole file, not skip-and-warn", "[unit]") { ScratchFile file( "GOOD=1\n" "NOEQUALS\n"); CHECK_FALSE(resolve_env_specs({{true, file.path()}}).has_value()); } TEST_CASE("resolve_env_specs: --env and --env-file interleave in exact command-line order", "[unit]") { ScratchFile file("FOO=from-file\n"); auto resolved = resolve_env_specs({{false, "FOO=from-cli-1"}, {true, file.path()}, {false, "FOO=from-cli-2"}}); REQUIRE(resolved.has_value()); REQUIRE(resolved->size() == 3); CHECK((*resolved)[0].second == "from-cli-1"); CHECK((*resolved)[1].second == "from-file"); CHECK((*resolved)[2].second == "from-cli-2"); } TEST_CASE("resolve_env_specs: empty spec list resolves to an empty list", "[unit]") { auto resolved = resolve_env_specs({}); REQUIRE(resolved.has_value()); CHECK(resolved->empty()); }