diff --git a/lib/mrsk.rb b/lib/mrsk.rb index 19b86051..70cbad40 100644 --- a/lib/mrsk.rb +++ b/lib/mrsk.rb @@ -1,6 +1,7 @@ module Mrsk end +require "active_support" require "zeitwerk" loader = Zeitwerk::Loader.for_gem diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 19bf4187..fc6c25fb 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -1,42 +1,133 @@ require_relative "cli_test_case" class CliAccessoryTest < CliTestCase + test "boot" do + Mrsk::Cli::Accessory.any_instance.expects(:directories).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:upload).with("mysql") + + run_command("boot", "mysql").tap do |output| + assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + end + end + + test "boot all" do + Mrsk::Cli::Accessory.any_instance.expects(:directories).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:upload).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:directories).with("redis") + Mrsk::Cli::Accessory.any_instance.expects(:upload).with("redis") + + run_command("boot", "all").tap do |output| + assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=10m --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.4", output + end + end + test "upload" do - assert_match "test/fixtures/files/my.cnf app-mysql/etc/mysql/my.cnf", run_command("upload", "mysql") + run_command("upload", "mysql").tap do |output| + assert_match "mkdir -p app-mysql/etc/mysql", output + assert_match "test/fixtures/files/my.cnf app-mysql/etc/mysql/my.cnf", output + assert_match "chmod 755 app-mysql/etc/mysql/my.cnf", output + end end test "directories" do assert_match "mkdir -p $PWD/app-mysql/data", run_command("directories", "mysql") end - test "remove service direcotry" do - assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql") + test "reboot" do + Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:boot).with("mysql") + + run_command("reboot", "mysql") end - test "boot" do - assert_match "Running docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", run_command("boot", "mysql") + test "start" do + assert_match "docker container start app-mysql", run_command("start", "mysql") + end + + test "stop" do + assert_match "docker container stop app-mysql", run_command("stop", "mysql") + end + + test "restart" do + Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:start).with("mysql") + + run_command("restart", "mysql") + end + + test "details" do + assert_match "docker ps --filter label=service=app-mysql", run_command("details", "mysql") + end + + test "details with all" do + run_command("details", "all").tap do |output| + assert_match "docker ps --filter label=service=app-mysql", output + assert_match "docker ps --filter label=service=app-redis", output + end end test "exec" do run_command("exec", "mysql", "mysql -v").tap do |output| - assert_match /Launching command from new container/, output - assert_match /mysql -v/, output + assert_match "Launching command from new container", output + assert_match "mysql -v", output end end test "exec with reuse" do run_command("exec", "mysql", "--reuse", "mysql -v").tap do |output| - assert_match /Launching command from existing container/, output - assert_match %r[docker exec app-mysql mysql -v], output + assert_match "Launching command from existing container", output + assert_match "docker exec app-mysql mysql -v", output end end + test "logs" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.3 'docker logs app-mysql --timestamps --tail 10 2>&1'") + + assert_match "docker logs app-mysql --tail 100 --timestamps 2>&1", run_command("logs", "mysql") + end + + test "logs with follow" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.3 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'") + + assert_match "docker logs app-mysql --timestamps --tail 10 --follow 2>&1", run_command("logs", "mysql", "--follow") + end + test "remove with confirmation" do - run_command("remove", "mysql", "-y").tap do |output| - assert_match /docker container stop app-mysql/, output - assert_match /docker image prune --all --force --filter label=service=app-mysql/, output - assert_match /rm -rf app-mysql/, output - end + Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_image).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_service_directory).with("mysql") + + run_command("remove", "mysql", "-y") + end + + test "remove all with confirmation" do + Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_image).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:remove_service_directory).with("mysql") + Mrsk::Cli::Accessory.any_instance.expects(:stop).with("redis") + Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("redis") + Mrsk::Cli::Accessory.any_instance.expects(:remove_image).with("redis") + Mrsk::Cli::Accessory.any_instance.expects(:remove_service_directory).with("redis") + + run_command("remove", "all", "-y") + end + + test "remove_container" do + assert_match "docker container prune --force --filter label=service=app-mysql", run_command("remove_container", "mysql") + end + + test "remove_image" do + assert_match "docker image prune --all --force --filter label=service=app-mysql", run_command("remove_image", "mysql") + end + + test "remove_service_directory" do + assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql") end private diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 4cc2ae08..badc3a0d 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -9,8 +9,8 @@ class CliAppTest < CliTestCase .returns("123") # old version run_command("boot").tap do |output| - assert_match /#{Regexp.escape("docker run --detach --restart unless-stopped")}/, output - assert_match /#{Regexp.escape("docker container ls --all --filter name=app-web-123 --quiet | xargs docker stop")}/, output + assert_match "docker run --detach --restart unless-stopped", output + assert_match "docker container ls --all --filter name=app-web-123 --quiet | xargs docker stop", output end end @@ -26,10 +26,10 @@ class CliAppTest < CliTestCase .returns([ :docker, :run ]) run_command("boot").tap do |output| - assert_match /#{Regexp.escape("Rebooting container with same version 999 already deployed")}/, output # Can't start what's already running - assert_match /#{Regexp.escape("docker container ls --all --filter name=app-web-999 --quiet | xargs docker stop")}/, output # Stop old running - assert_match /#{Regexp.escape("docker container ls --all --filter name=app-web-999 --quiet | xargs docker container rm")}/, output # Remove old container - assert_match /#{Regexp.escape("docker run on 1.1.1.1")}/, output # Start new container + assert_match "Rebooting container with same version 999 already deployed", output # Can't start what's already running + assert_match "docker container ls --all --filter name=app-web-999 --quiet | xargs docker container rm", output # Stop old running + assert_match "docker container ls --all --filter name=app-web-999 --quiet | xargs docker container rm", output # Remove old container + assert_match "docker run", output # Start new container end ensure Thread.report_on_exception = true @@ -37,19 +37,19 @@ class CliAppTest < CliTestCase test "start" do run_command("start").tap do |output| - assert_match /#{Regexp.escape("docker start app-web-999")}/, output + assert_match "docker start app-web-999", output end end test "stop" do run_command("stop").tap do |output| - assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop")}/, output + assert_match "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop", output end end test "details" do run_command("details").tap do |output| - assert_match /#{Regexp.escape("docker ps --filter label=service=app --filter label=role=web")}/, output + assert_match "docker ps --filter label=service=app --filter label=role=web", output end end @@ -69,14 +69,72 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match /#{Regexp.escape("docker run --rm dhh/app:999 ruby -v")}/, output + assert_match "docker run --rm dhh/app:999 ruby -v", output end end test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| - assert_match /#{Regexp.escape("docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1")}/, output - assert_match /#{Regexp.escape("docker exec app-web-999 ruby -v")}/, output + assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output # Get current version + assert_match "docker exec app-web-999 ruby -v", output + end + end + + test "containers" do + run_command("containers").tap do |output| + assert_match "docker container ls --all --filter label=service=app", output + end + end + + test "images" do + run_command("images").tap do |output| + assert_match "docker image ls dhh/app", output + end + end + + test "logs" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 2>&1'") + + assert_match "docker ps --quiet --filter label=service=app | xargs docker logs --tail 100 2>&1", run_command("logs") + end + + test "logs with follow" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1'") + + assert_match "docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow") + end + + test "remove" do + Mrsk::Cli::App.any_instance.expects(:stop) + Mrsk::Cli::App.any_instance.expects(:remove_containers) + Mrsk::Cli::App.any_instance.expects(:remove_images) + + run_command("remove") + end + + test "remove_container" do + run_command("remove_container", "1234567").tap do |output| + assert_match "docker container ls --all --filter name=app-1234567 --quiet | xargs docker container rm", output + end + end + + test "remove_containers" do + run_command("remove_containers").tap do |output| + assert_match "docker container prune --force --filter label=service=app", output + end + end + + test "remove_images" do + run_command("remove_images").tap do |output| + assert_match "docker image prune --all --force --filter label=service=app", output + end + end + + test "version" do + run_command("version").tap do |output| + assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output end end diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 3d5f4431..44802549 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -1,6 +1,31 @@ require_relative "cli_test_case" class CliBuildTest < CliTestCase + test "deliver" do + Mrsk::Cli::Build.any_instance.expects(:push) + Mrsk::Cli::Build.any_instance.expects(:pull) + + run_command("deliver") + end + + test "push" do + run_command("push").tap do |output| + assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder mrsk-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output + end + end + + test "push without builder" do + Mrsk::Cli::Build.any_instance.stubs(:create).returns(true) + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .raises(SSHKit::Command::Failed.new("no builder")) + .then + .returns(true) + + run_command("push").tap do |output| + assert_match /Missing compatible builder, so creating a new one first/, output + end + end + test "pull" do run_command("pull").tap do |output| assert_match /docker image rm --force dhh\/app:999/, output @@ -8,6 +33,38 @@ class CliBuildTest < CliTestCase end end + test "create" do + run_command("create").tap do |output| + assert_match /docker buildx create --use --name mrsk-app-multiarch/, output + end + end + + test "create with error" do + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .raises(SSHKit::Command::Failed.new("stderr=error")) + + run_command("create").tap do |output| + assert_match /Couldn't create remote builder: error/, output + end + end + + test "remove" do + run_command("remove").tap do |output| + assert_match /docker buildx rm mrsk-app-multiarch/, output + end + end + + test "details" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture) + .with(:docker, :context, :ls, "&&", :docker, :buildx, :ls) + .returns("docker builder info") + + run_command("details").tap do |output| + assert_match /Builder: multiarch/, output + assert_match /docker builder info/, output + end + end + private def run_command(*command) stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } diff --git a/test/cli/healthcheck_test.rb b/test/cli/healthcheck_test.rb new file mode 100644 index 00000000..55d1c9ec --- /dev/null +++ b/test/cli/healthcheck_test.rb @@ -0,0 +1,70 @@ +require_relative "cli_test_case" + +class CliHealthcheckTest < CliTestCase + test "perform" do + # Prevent expected failures from outputting to terminal + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.stubs(:sleep) # No sleeping when retrying + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, :container, :ls, "--all", "--filter", "name=healthcheck-app", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "MRSK_CONTAINER_NAME=\"healthcheck-app\"", "dhh/app:999") + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, :container, :ls, "--all", "--filter", "name=healthcheck-app", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false) + + # Fail twice to test retry logic + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with(:curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", "http://localhost:3999/up") + .raises(SSHKit::Command::Failed) + .then + .raises(SSHKit::Command::Failed) + .then + .returns("200") + + run_command("perform").tap do |output| + assert_match "Health check against /up failed to respond, retrying in 1s...", output + assert_match "Health check against /up failed to respond, retrying in 2s...", output + assert_match "Health check against /up succeeded with 200 OK!", output + end + end + + test "perform failing because of curl" do + # Prevent expected failures from outputting to terminal + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) # No need to execute anything here + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with(:curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", "http://localhost:3999/up") + .returns("curl: command not found") + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=healthcheck-app", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1") + + exception = assert_raises SSHKit::Runner::ExecuteError do + run_command("perform") + end + assert_match "Health check against /up failed to return 200 OK!", exception.message + end + + test "perform failing for unknown reason" do + # Prevent expected failures from outputting to terminal + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) # No need to execute anything here + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with(:curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", "http://localhost:3999/up") + .returns("500") + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with(:docker, :container, :ls, "--all", "--filter", "name=healthcheck-app", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1") + + exception = assert_raises do + run_command("perform") + end + assert_match "Health check against /up failed with status 500", exception.message + end + + private + def run_command(*command) + stdouted { Mrsk::Cli::Healthcheck.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + end +end diff --git a/test/cli/prune_test.rb b/test/cli/prune_test.rb new file mode 100644 index 00000000..15e54575 --- /dev/null +++ b/test/cli/prune_test.rb @@ -0,0 +1,29 @@ +require_relative "cli_test_case" + +class CliPruneTest < CliTestCase + test "all" do + Mrsk::Cli::Prune.any_instance.expects(:containers) + Mrsk::Cli::Prune.any_instance.expects(:images) + + run_command("all") + end + + test "images" do + run_command("images").tap do |output| + assert_match "docker image prune --all --force --filter label=service=app --filter until=168h on 1.1.1.1", output + assert_match "docker image prune --all --force --filter label=service=app --filter until=168h on 1.1.1.2", output + end + end + + test "containers" do + run_command("containers").tap do |output| + assert_match "docker container prune --force --filter label=service=app --filter until=72h on 1.1.1.1", output + assert_match "docker container prune --force --filter label=service=app --filter until=72h on 1.1.1.2", output + end + end + + private + def run_command(*command) + stdouted { Mrsk::Cli::Prune.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + end +end diff --git a/test/cli/registry_test.rb b/test/cli/registry_test.rb new file mode 100644 index 00000000..d100402c --- /dev/null +++ b/test/cli/registry_test.rb @@ -0,0 +1,23 @@ +require_relative "cli_test_case" + +class CliRegistryTest < CliTestCase + test "login" do + run_command("login").tap do |output| + assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output + assert_match "docker login -u [REDACTED] -p [REDACTED] on 1.1.1.1", output + assert_match "docker login -u [REDACTED] -p [REDACTED] on 1.1.1.2", output + end + end + + test "logout" do + run_command("logout").tap do |output| + assert_match "docker logout on 1.1.1.1", output + assert_match "docker logout on 1.1.1.2", output + end + end + + private + def run_command(*command) + stdouted { Mrsk::Cli::Registry.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + end +end diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb new file mode 100644 index 00000000..7eda6019 --- /dev/null +++ b/test/cli/traefik_test.rb @@ -0,0 +1,85 @@ +require_relative "cli_test_case" + +class CliTraefikTest < CliTestCase + test "boot" do + run_command("boot").tap do |output| + assert_match "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG", output + end + end + + test "reboot" do + Mrsk::Cli::Traefik.any_instance.expects(:stop) + Mrsk::Cli::Traefik.any_instance.expects(:remove_container) + Mrsk::Cli::Traefik.any_instance.expects(:boot) + + run_command("reboot") + end + + test "start" do + run_command("start").tap do |output| + assert_match "docker container start traefik", output + end + end + + test "stop" do + run_command("stop").tap do |output| + assert_match "docker container stop traefik", output + end + end + + test "restart" do + Mrsk::Cli::Traefik.any_instance.expects(:stop) + Mrsk::Cli::Traefik.any_instance.expects(:start) + + run_command("restart") + end + + test "details" do + run_command("details").tap do |output| + assert_match "docker ps --filter name=traefik", output + end + end + + test "logs" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture) + .with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1") + .returns("Log entry") + + run_command("logs").tap do |output| + assert_match "Traefik Host: 1.1.1.1", output + assert_match "Log entry", output + end + end + + test "logs with follow" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1'") + + assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow") + end + + test "remove" do + Mrsk::Cli::Traefik.any_instance.expects(:stop) + Mrsk::Cli::Traefik.any_instance.expects(:remove_container) + Mrsk::Cli::Traefik.any_instance.expects(:remove_image) + + run_command("remove") + end + + test "remove_container" do + run_command("remove_container").tap do |output| + assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output + end + end + + test "remove_image" do + run_command("remove_image").tap do |output| + assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output + end + end + + private + def run_command(*command) + stdouted { Mrsk::Cli::Traefik.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + end +end diff --git a/test/utils_test.rb b/test/utils_test.rb new file mode 100644 index 00000000..f2af428b --- /dev/null +++ b/test/utils_test.rb @@ -0,0 +1,39 @@ +require "test_helper" + +class UtilsTest < ActiveSupport::TestCase + test "argumentize" do + assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux ], \ + Mrsk::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil }) + end + + test "argumentize with redacted" do + assert_kind_of SSHKit::Redaction, \ + Mrsk::Utils.argumentize("--label", { foo: "bar" }, redacted: true).last + end + + test "argumentize_env_with_secrets" do + ENV.expects(:fetch).with("FOO").returns("secret") + assert_equal [ "-e", "FOO=\"secret\"", "-e", "BAZ=\"qux\"" ], \ + Mrsk::Utils.argumentize_env_with_secrets({ "secret" => [ "FOO" ], "clear" => { BAZ: "qux" } }) + end + + test "optionize" do + assert_equal [ "--foo", "\"bar\"", "--baz", "\"qux\"", "--quux" ], \ + Mrsk::Utils.optionize({ foo: "bar", baz: "qux", quux: true }) + end + + test "optionize with" do + assert_equal [ "--foo=\"bar\"", "--baz=\"qux\"", "--quux" ], \ + Mrsk::Utils.optionize({ foo: "bar", baz: "qux", quux: true }, with: "=") + end + + test "redact" do + assert_kind_of SSHKit::Redaction, Mrsk::Utils.redact("secret") + assert_equal "secret", Mrsk::Utils.redact("secret") + end + + test "escape_shell_value" do + assert_equal "\"foo\"", Mrsk::Utils.escape_shell_value("foo") + assert_equal "\"\\`foo\\`\"", Mrsk::Utils.escape_shell_value("`foo`") + end +end