Back to Repositories

Testing Docker Container Management Commands in Kamal Framework

This test suite validates Docker container management and deployment commands in the Kamal framework, focusing on container lifecycle operations, logging, and asset handling. The tests ensure proper command generation for running, stopping, deploying, and monitoring Docker containers with various configuration options.

Test Coverage Overview

Comprehensive test coverage for Docker container operations including:
  • Container lifecycle management (run, start, stop, deploy)
  • Logging and monitoring capabilities
  • Asset handling and volume management
  • Container configuration and environment variables
  • Network and proxy settings

Implementation Analysis

The test suite implements a thorough validation approach using Minitest framework with ActiveSupport::TestCase. It employs setup/teardown patterns for test isolation and uses assertion-based testing to verify command string generation. The implementation focuses on command construction for Docker operations with various configuration parameters.

Technical Details

Testing tools and configuration:
  • Framework: Minitest with ActiveSupport
  • Test Setup: Configuration objects and command builders
  • Command Verification: String matching and equality assertions
  • Environment: Docker-specific command testing

Best Practices Demonstrated

The test suite exhibits several testing best practices:
  • Isolated test cases with proper setup/teardown
  • Comprehensive edge case coverage
  • Clear test naming conventions
  • Modular test organization
  • Thorough validation of command structure

basecamp/kamal

test/commands/app_test.rb

            
require "test_helper"

class CommandsAppTest < ActiveSupport::TestCase
  setup do
    setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456")

    @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: { "web" => [ "1.1.1.1" ], "workers" => [ "1.1.1.2" ] }, env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } }
  end

  teardown do
    teardown_test_secrets
  end

  test "run" do
    assert_equal \
      "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
      new_command.run.join(" ")
  end

  test "run with hostname" do
    assert_equal \
      "docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
      new_command.run(hostname: "myhost").join(" ")
  end

  test "run with volumes" do
    @config[:volumes] = [ "/local/path:/container/path" ]

    assert_equal \
      "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
      new_command.run.join(" ")
  end

  test "run with custom options" do
    @config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } }
    assert_equal \
      "docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
      new_command(role: "jobs", host: "1.1.1.2").run.join(" ")
  end

  test "run with logging config" do
    @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }

    assert_equal \
      "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
      new_command.run.join(" ")
  end

  test "run with role logging config" do
    @config[:logging] = { "driver" => "local", "options" => { "max-size" => "10m", "max-file" => "3" } }
    @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } }

    assert_equal \
      "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
      new_command.run.join(" ")
  end

  test "run with tags" do
    @config[:servers] = [ { "1.1.1.1" => "tag1" } ]
    @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }

    assert_equal \
      "docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
      new_command.run.join(" ")
  end

  test "start" do
    assert_equal \
      "docker start app-web-999",
      new_command.start.join(" ")
  end

  test "start with destination" do
    @destination = "staging"
    assert_equal \
      "docker start app-web-staging-999",
      new_command.start.join(" ")
  end

  test "stop" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
      new_command.stop.join(" ")
  end

  test "stop with custom drain timeout" do
    @config[:drain_timeout] = 20
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
      new_command.stop.join(" ")

    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20",
      new_command(role: "workers").stop.join(" ")
  end

  test "stop with version" do
    assert_equal \
      "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop",
      new_command.stop(version: "123").join(" ")
  end

  test "info" do
    assert_equal \
      "docker ps --filter label=service=app --filter label=destination= --filter label=role=web",
      new_command.info.join(" ")
  end

  test "info with destination" do
    @destination = "staging"
    assert_equal \
      "docker ps --filter label=service=app --filter label=destination=staging --filter label=role=web",
      new_command.info.join(" ")
  end

  test "deploy" do
    assert_equal \
      "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
      new_command.deploy(target: "172.1.0.2").join(" ")
  end

  test "deploy with SSL" do
    @config[:proxy] = { "ssl" => true, "host" => "example.com" }

    assert_equal \
      "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
      new_command.deploy(target: "172.1.0.2").join(" ")
  end

  test "deploy with SSL targeting multiple hosts" do
    @config[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }

    assert_equal \
      "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --host=\"anotherexample.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
      new_command.deploy(target: "172.1.0.2").join(" ")
  end

  test "deploy with SSL false" do
    @config[:proxy] = { "ssl" => false }

    assert_equal \
      "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
      new_command.deploy(target: "172.1.0.2").join(" ")
  end

  test "remove" do
    assert_equal \
      "docker exec kamal-proxy kamal-proxy remove app-web",
      new_command.remove.join(" ")
  end



  test "logs" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
      new_command.logs.join(" ")
  end

  test "logs with container_id" do
    assert_equal \
      "echo C137 | xargs docker logs --timestamps 2>&1",
      new_command.logs(container_id: "C137").join(" ")
  end

  test "logs with since" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
      new_command.logs(since: "5m").join(" ")
  end

  test "logs with lines" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1",
      new_command.logs(lines: "100").join(" ")
  end

  test "logs with since and lines" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1",
      new_command.logs(since: "5m", lines: "100").join(" ")
  end

  test "logs with grep" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'",
      new_command.logs(grep: "my-id").join(" ")
  end

  test "logs with grep and grep options" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2",
      new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ")
  end

  test "logs with since, grep and grep options" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2",
      new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ")
  end

  test "logs with since and grep" do
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'",
      new_command.logs(since: "5m", grep: "my-id").join(" ")
  end

  test "follow logs" do
    assert_equal \
      "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1'",
      new_command.follow_logs(host: "app-1")

    assert_equal \
      "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'",
      new_command.follow_logs(host: "app-1", grep: "Completed")

    assert_equal \
      "ssh -t root@app-1 -p 22 'echo ID321 | xargs docker logs --timestamps --follow 2>&1'",
      new_command.follow_logs(host: "app-1", container_id: "ID321")

    assert_equal \
      "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'",
      new_command.follow_logs(host: "app-1", lines: 123)

    assert_equal \
      "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
      new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")

    assert_equal \
      "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'",
      new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed")
  end


  test "execute in new container" do
    assert_equal \
      "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
      new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
  end

  test "execute in new container with logging" do
    @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }

    assert_equal \
      "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" dhh/app:999 bin/rails db:setup",
      new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
  end

  test "execute in new container with env" do
    assert_equal \
      "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
      new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
  end

  test "execute in new detached container" do
    assert_equal \
      "docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
      new_command.execute_in_new_container("bin/rails", "db:setup", detach: true, env: {}).join(" ")
  end

  test "execute in new container with tags" do
    @config[:servers] = [ { "1.1.1.1" => "tag1" } ]
    @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }

    assert_equal \
      "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
      new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
  end

  test "execute in new container with custom options" do
    @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
    assert_equal \
      "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
      new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
  end

  test "execute in existing container" do
    assert_equal \
      "docker exec app-web-999 bin/rails db:setup",
      new_command.execute_in_existing_container("bin/rails", "db:setup", env: {}).join(" ")
  end

  test "execute in existing container with env" do
    assert_equal \
      "docker exec --env foo=\"bar\" app-web-999 bin/rails db:setup",
      new_command.execute_in_existing_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
  end

  test "execute in new container over ssh" do
    assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:999 bin/rails c},
      new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
  end

  test "execute in new container over ssh with tags" do
    @config[:servers] = [ { "1.1.1.1" => "tag1" } ]
    @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }

    assert_equal "ssh -t [email protected] -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails c'",
      new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
  end

  test "execute in new container with custom options over ssh" do
    @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
    assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
      new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
  end

  test "execute in existing container over ssh" do
    assert_match %r{docker exec -it app-web-999 bin/rails c},
      new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {})
  end

  test "run over ssh" do
    assert_equal "ssh -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with custom user" do
    @config[:ssh] = { "user" => "app" }
    assert_equal "ssh -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with custom port" do
    @config[:ssh] = { "port" => "2222" }
    assert_equal "ssh -t [email protected] -p 2222 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with proxy" do
    @config[:ssh] = { "proxy" => "2.2.2.2" }
    assert_equal "ssh -J [email protected] -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with proxy user" do
    @config[:ssh] = { "proxy" => "[email protected]" }
    assert_equal "ssh -J [email protected] -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with custom user with proxy" do
    @config[:ssh] = { "user" => "app", "proxy" => "2.2.2.2" }
    assert_equal "ssh -J [email protected] -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with keys config" do
    @config[:ssh] = { "keys" => [ "path_to_key.pem" ] }
    assert_equal "ssh -i path_to_key.pem -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with keys config with keys_only" do
    @config[:ssh] = { "keys" => [ "path_to_key.pem" ], "keys_only" => true }
    assert_equal "ssh -i path_to_key.pem -o IdentitiesOnly=yes -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "run over ssh with proxy_command" do
    @config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" }
    assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t [email protected] -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
  end

  test "current_running_container_id" do
    assert_equal \
    "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1",
      new_command.current_running_container_id.join(" ")
  end

  test "current_running_container_id with destination" do
    @destination = "staging"
    assert_equal \
      "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest-staging --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting' | head -1",
      new_command.current_running_container_id.join(" ")
  end

  test "container_id_for" do
    assert_equal \
      "docker container ls --all --filter name=^app-999$ --quiet",
      new_command.container_id_for(container_name: "app-999").join(" ")
  end

  test "current_running_version" do
    assert_equal \
      "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done",
      new_command.current_running_version.join(" ")
  end

  test "list_versions" do
    assert_equal \
      "docker ps --filter label=service=app --filter label=destination= --filter label=role=web --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
      new_command.list_versions.join(" ")

    assert_equal \
      "docker ps --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
      new_command.list_versions("--latest", statuses: [ :running, :restarting ]).join(" ")
  end

  test "list_containers" do
    assert_equal \
      "docker container ls --all --filter label=service=app --filter label=destination= --filter label=role=web",
      new_command.list_containers.join(" ")
  end

  test "list_containers with destination" do
    @destination = "staging"
    assert_equal \
      "docker container ls --all --filter label=service=app --filter label=destination=staging --filter label=role=web",
      new_command.list_containers.join(" ")
  end

  test "list_container_names" do
    assert_equal \
      "docker container ls --all --filter label=service=app --filter label=destination= --filter label=role=web --format '{{ .Names }}'",
      new_command.list_container_names.join(" ")
  end

  test "remove_container" do
    assert_equal \
      "docker container ls --all --filter name=^app-web-999$ --quiet | xargs docker container rm",
      new_command.remove_container(version: "999").join(" ")
  end

  test "remove_container with destination" do
    @destination = "staging"
    assert_equal \
      "docker container ls --all --filter name=^app-web-staging-999$ --quiet | xargs docker container rm",
      new_command.remove_container(version: "999").join(" ")
  end

  test "remove_containers" do
    assert_equal \
      "docker container prune --force --filter label=service=app --filter label=destination= --filter label=role=web",
      new_command.remove_containers.join(" ")
  end

  test "remove_containers with destination" do
    @destination = "staging"
    assert_equal \
      "docker container prune --force --filter label=service=app --filter label=destination=staging --filter label=role=web",
      new_command.remove_containers.join(" ")
  end

  test "list_images" do
    assert_equal \
      "docker image ls dhh/app",
      new_command.list_images.join(" ")
  end

  test "remove_images" do
    assert_equal \
      "docker image prune --all --force --filter label=service=app",
      new_command.remove_images.join(" ")
  end

  test "remove_images with destination" do
    @destination = "staging"
    assert_equal \
      "docker image prune --all --force --filter label=service=app",
      new_command.remove_images.join(" ")
  end

  test "tag_latest_image" do
    assert_equal \
      "docker tag dhh/app:999 dhh/app:latest",
      new_command.tag_latest_image.join(" ")
  end

  test "tag_latest_image with destination" do
    @destination = "staging"
    assert_equal \
      "docker tag dhh/app:999 dhh/app:latest-staging",
      new_command.tag_latest_image.join(" ")
  end

  test "extract assets" do
    assert_equal [
      :mkdir, "-p", ".kamal/apps/app/assets/extracted/web-999", "&&",
      :docker, :stop, "-t 1", "app-web-assets", "2> /dev/null", "|| true", "&&",
      :docker, :run, "--name", "app-web-assets", "--detach", "--rm", "--entrypoint", "sleep", "dhh/app:999", "1000000", "&&",
      :docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/apps/app/assets/extracted/web-999", "&&",
      :docker, :stop, "-t 1", "app-web-assets"
    ], new_command(asset_path: "/public/assets").extract_assets
  end

  test "sync asset volumes" do
    assert_equal [
      :mkdir, "-p", ".kamal/apps/app/assets/volumes/web-999", ";",
      :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-999"
    ], new_command(asset_path: "/public/assets").sync_asset_volumes

    assert_equal [
      :mkdir, "-p", ".kamal/apps/app/assets/volumes/web-999", ";",
      :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-999", ";",
      :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-999", ".kamal/apps/app/assets/volumes/web-998", "|| true", ";",
      :cp, "-rnT", ".kamal/apps/app/assets/extracted/web-998", ".kamal/apps/app/assets/volumes/web-999", "|| true"
    ], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998)
  end

  test "clean up assets" do
    assert_equal [
      :find, ".kamal/apps/app/assets/extracted", "-maxdepth 1", "-name", "'web-*'", "!", "-name", "web-999", "-exec rm -rf \"{}\" +", ";",
      :find, ".kamal/apps/app/assets/volumes", "-maxdepth 1", "-name", "'web-*'", "!", "-name", "web-999", "-exec rm -rf \"{}\" +"
    ], new_command(asset_path: "/public/assets").clean_up_assets
  end

  private
    def new_command(role: "web", host: "1.1.1.1", **additional_config)
      config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999")
      Kamal::Commands::App.new(config, role: config.role(role), host: host)
    end
end