Back to Repositories

Testing AWS Secrets Manager Integration in Kamal

This test suite validates the AWS Secrets Manager adapter implementation in Kamal, ensuring secure secret management and retrieval functionality. It covers error handling, secret fetching, and CLI integration verification.

Test Coverage Overview

The test suite provides comprehensive coverage of AWS Secrets Manager operations:

  • Error handling for non-existent secrets
  • Batch secret value retrieval
  • Support for JSON and string secret formats
  • Secret name prefixing and path handling
  • AWS CLI availability verification

Implementation Analysis

The testing approach uses Minitest framework with stubbed AWS CLI commands to simulate various scenarios. It implements systematic verification of the adapter’s core functionalities through isolated unit tests, ensuring reliable secret management operations.

The tests utilize JSON response mocking and command execution validation to verify the adapter’s behavior under different conditions.

Technical Details

  • Testing Framework: Minitest
  • Mocking Utility: stub_ticks
  • Test Data Format: JSON
  • Command Line Interface: Kamal::Cli::Secrets
  • Configuration: YAML-based test fixtures

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear assertions
  • Comprehensive error scenario coverage
  • Structured test data organization
  • Clean setup and teardown patterns
  • Effective command-line interface testing

basecamp/kamal

test/secrets/aws_secrets_manager_adapter_test.rb

            
require "test_helper"

class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
  test "fails when errors are present" do
    stub_ticks.with("aws --version 2> /dev/null")
    stub_ticks
      .with("aws secretsmanager batch-get-secret-value --secret-id-list unknown1 unknown2 --profile default")
      .returns(<<~JSON)
        {
          "SecretValues": [],
          "Errors": [
            {
                "SecretId": "unknown1",
                "ErrorCode": "ResourceNotFoundException",
                "Message": "Secrets Manager can't find the specified secret."
            },
            {
                "SecretId": "unknown2",
                "ErrorCode": "ResourceNotFoundException",
                "Message": "Secrets Manager can't find the specified secret."
            }
          ]
        }
      JSON

    error = assert_raises RuntimeError do
      JSON.parse(shellunescape(run_command("fetch", "unknown1", "unknown2")))
    end

    assert_equal [ "unknown1: Secrets Manager can't find the specified secret.", "unknown2: Secrets Manager can't find the specified secret." ].join(" "), error.message
  end

  test "fetch" do
    stub_ticks.with("aws --version 2> /dev/null")
    stub_ticks
      .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 secret2/KEY3 --profile default")
      .returns(<<~JSON)
        {
          "SecretValues": [
            {
              "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
              "Name": "secret",
              "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
              "SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}",
              "VersionStages": [
                  "AWSCURRENT"
              ],
              "CreatedDate": "2024-01-01T00:00:00.000000"
            },
            {
              "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
              "Name": "secret2",
              "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
              "SecretString": "{\\"KEY3\\":\\"VALUE3\\"}",
              "VersionStages": [
                  "AWSCURRENT"
              ],
              "CreatedDate": "2024-01-01T00:00:00.000000"
            }
          ],
          "Errors": []
        }
      JSON

    json = JSON.parse(shellunescape(run_command("fetch", "secret/KEY1", "secret/KEY2", "secret2/KEY3")))

    expected_json = {
      "secret/KEY1"=>"VALUE1",
      "secret/KEY2"=>"VALUE2",
      "secret2/KEY3"=>"VALUE3"
    }

    assert_equal expected_json, json
  end

  test "fetch with string value" do
    stub_ticks.with("aws --version 2> /dev/null")
    stub_ticks
      .with("aws secretsmanager batch-get-secret-value --secret-id-list secret secret2/KEY1 --profile default")
      .returns(<<~JSON)
        {
          "SecretValues": [
            {
              "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
              "Name": "secret",
              "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
              "SecretString": "a-string-secret",
              "VersionStages": [
                  "AWSCURRENT"
              ],
              "CreatedDate": "2024-01-01T00:00:00.000000"
            },
            {
              "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
              "Name": "secret2",
              "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
              "SecretString": "{\\"KEY2\\":\\"VALUE2\\"}",
              "VersionStages": [
                  "AWSCURRENT"
              ],
              "CreatedDate": "2024-01-01T00:00:00.000000"
            }
          ],
          "Errors": []
        }
      JSON

    json = JSON.parse(shellunescape(run_command("fetch", "secret", "secret2/KEY1")))

    expected_json = {
      "secret"=>"a-string-secret",
      "secret2/KEY2"=>"VALUE2"
    }

    assert_equal expected_json, json
  end

  test "fetch with secret names" do
    stub_ticks.with("aws --version 2> /dev/null")
    stub_ticks
      .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 --profile default")
      .returns(<<~JSON)
        {
          "SecretValues": [
            {
              "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
              "Name": "secret",
              "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
              "SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}",
              "VersionStages": [
                  "AWSCURRENT"
              ],
              "CreatedDate": "2024-01-01T00:00:00.000000"
            }
          ],
          "Errors": []
        }
      JSON

    json = JSON.parse(shellunescape(run_command("fetch", "--from", "secret", "KEY1", "KEY2")))

    expected_json = {
      "secret/KEY1"=>"VALUE1",
      "secret/KEY2"=>"VALUE2"
    }

    assert_equal expected_json, json
  end

  test "fetch without CLI installed" do
    stub_ticks_with("aws --version 2> /dev/null", succeed: false)

    error = assert_raises RuntimeError do
      JSON.parse(shellunescape(run_command("fetch", "SECRET1")))
    end
    assert_equal "AWS CLI is not installed", error.message
  end

  private
    def run_command(*command)
      stdouted do
        Kamal::Cli::Secrets.start \
          [ *command,
            "-c", "test/fixtures/deploy_with_accessories.yml",
            "--adapter", "aws_secrets_manager",
            "--account", "default" ]
      end
    end
end