diff --git a/plugins/os-backlog/bin/os-backlog b/plugins/os-backlog/bin/os-backlog index cc975d2..6b5f8e3 100755 --- a/plugins/os-backlog/bin/os-backlog +++ b/plugins/os-backlog/bin/os-backlog @@ -40,6 +40,18 @@ def fail_soft(message) exit 1 end +# Guards board-ensure/activate/archive's positional NAME argument against a +# mistaken flag-style invocation (e.g. `board-ensure --board apprise-api`, +# where `--board` is swallowed as the name and `apprise-api` is dropped +# silently). Called immediately after each command shifts its name arg, +# before build_client, so a bad name never reaches Planka. +def validate_board_name!(name, command) + return unless name.start_with?("--") + + fail_soft("#{command}: board name #{name.inspect} must not start with \"--\" " \ + "(looks like a flag was passed positionally — check your quoting/flags)") +end + def build_client require "planka_api" client = Planka::Client.new @@ -190,6 +202,7 @@ case command when "board-ensure" name = rest.shift fail_soft("board-ensure requires a board name") unless name + validate_board_name!(name, "board-ensure") project = parse_project_flag(rest) client = build_client result = Backlog::BoardEnsurer.new(client: client).ensure(name, project_name: project) @@ -199,12 +212,14 @@ when "board-ensure" when "activate" name = rest.shift fail_soft("activate requires a board name") unless name + validate_board_name!(name, "activate") client = build_client board = Backlog::BoardEnsurer.new(client: client).activate(name) puts "activated: #{board.name}" when "archive" name = rest.shift fail_soft("archive requires a board name") unless name + validate_board_name!(name, "archive") fail_soft("archive requires explicit --yes (human go-ahead only)") unless rest.include?("--yes") client = build_client diff --git a/plugins/os-backlog/tests/board_name_validation_test.rb b/plugins/os-backlog/tests/board_name_validation_test.rb new file mode 100644 index 0000000..4ab051a --- /dev/null +++ b/plugins/os-backlog/tests/board_name_validation_test.rb @@ -0,0 +1,50 @@ +require_relative "test_helper" + +# Card #1818501985909868001: a mistaken flag-style invocation like +# `os-backlog board-ensure --board apprise-api` must fail loudly with a +# usage error instead of silently creating a Planka board literally named +# "--board". These are CLI-contract tests (shell out to bin/os-backlog) +# because the bug is about what the positional NAME arg parsing accepts +# BEFORE any Planka client/network call is made — no PLANKA_* env vars are +# set, so if validation didn't happen first, these would instead fail on +# network auth with a different message. +class BoardNameValidationCliTest < Minitest::Test + BIN = File.expand_path("../bin/os-backlog", __dir__) + + # Strip any live Planka credentials from the child's env: these tests + # assert that flag-like names are rejected BEFORE build_client ever runs, + # so no test here may be allowed to reach the network even accidentally. + def run_cli(*args) + env = { "PLANKA_BASE_URL" => "", "PLANKA_USERNAME" => "", "PLANKA_PASSWORD" => "" } + out = IO.popen(env, [RbConfig.ruby, BIN, *args], err: [:child, :out], &:read) + [out, $?.exitstatus] + end + + def test_board_ensure_rejects_flag_like_name + out, status = run_cli("board-ensure", "--board", "apprise-api") + + refute_equal 0, status + assert_match(/board name.*must not start with/i, out) + end + + def test_activate_rejects_flag_like_name + out, status = run_cli("activate", "--board") + + refute_equal 0, status + assert_match(/board name.*must not start with/i, out) + end + + def test_archive_rejects_flag_like_name + out, status = run_cli("archive", "--board", "--yes") + + refute_equal 0, status + assert_match(/board name.*must not start with/i, out) + end + + def test_board_ensure_accepts_normal_name_and_fails_later_on_network + out, status = run_cli("board-ensure", "apprise-api") + + refute_equal 0, status + refute_match(/board name.*must not start with/i, out) + end +end