fnox 1.25.1

A flexible secret management tool supporting multiple providers and encryption methods
Documentation
#!/usr/bin/env bats
#
# AWS Parameter Store Provider Tests
#
# These tests verify the AWS Systems Manager Parameter Store provider integration with fnox
# using LocalStack for mock AWS services.
#
# Prerequisites:
#   1. Start LocalStack: docker run -d -p 4566:4566 -e SERVICES=ssm localstack/localstack
#   2. Set LOCALSTACK_ENDPOINT=http://localhost:4566
#   3. Run tests: mise run test:bats -- test/aws_parameter_store.bats
#
# Note: Tests will automatically skip if LOCALSTACK_ENDPOINT is not set.
#

setup_file() {
	if [ -z "$LOCALSTACK_ENDPOINT" ]; then
		export SKIP_AWS_PS_TESTS="LOCALSTACK_ENDPOINT not set. Start LocalStack and set LOCALSTACK_ENDPOINT=http://localhost:4566"
		return
	fi

	# Wait for LocalStack to be ready
	local retries=10
	while ! curl -sf "$LOCALSTACK_ENDPOINT/_localstack/health" >/dev/null 2>&1; do
		retries=$((retries - 1))
		if [ "$retries" -le 0 ]; then
			export SKIP_AWS_PS_TESTS="LocalStack not ready"
			return
		fi
		sleep 1
	done
}

setup() {
	load 'test_helper/common_setup'
	_common_setup

	if [ -n "$SKIP_AWS_PS_TESTS" ]; then
		skip "$SKIP_AWS_PS_TESTS"
	fi

	# Set dummy AWS credentials for LocalStack
	export AWS_ACCESS_KEY_ID="test"
	export AWS_SECRET_ACCESS_KEY="test"
	export AWS_DEFAULT_REGION="us-east-1"

	# Check if aws CLI is installed
	if ! command -v aws >/dev/null 2>&1; then
		skip "AWS CLI not installed. Install with: brew install awscli"
	fi

	# Set the region
	export PS_REGION="us-east-1"
}

teardown() {
	# Clean up any test parameters created during tests
	if [ -n "$TEST_PARAM_NAME" ] && [ -n "$LOCALSTACK_ENDPOINT" ]; then
		aws --endpoint-url "$LOCALSTACK_ENDPOINT" ssm delete-parameter \
			--name "$TEST_PARAM_NAME" \
			--region "$PS_REGION" >/dev/null 2>&1 || true
	fi

	_common_teardown
}

# Helper function to create an AWS Parameter Store test config
create_ps_config() {
	local region="${1:-us-east-1}"
	local prefix="${2}"
	if [ -z "$prefix" ] && [ "$#" -lt 2 ]; then
		prefix="/fnox-test/"
	fi

	if [ -z "$prefix" ]; then
		# Omit prefix line entirely when empty
		cat >"${FNOX_CONFIG_FILE:-fnox.toml}" <<EOF
root = true

[providers.ps]
type = "aws-ps"
region = "$region"
endpoint = "$LOCALSTACK_ENDPOINT"

[secrets]
EOF
	else
		cat >"${FNOX_CONFIG_FILE:-fnox.toml}" <<EOF
root = true

[providers.ps]
type = "aws-ps"
region = "$region"
prefix = "$prefix"
endpoint = "$LOCALSTACK_ENDPOINT"

[secrets]
EOF
	fi
}

# Helper function to create a test parameter in LocalStack
create_test_parameter() {
	local param_name="$1"
	local param_value="$2"

	aws --endpoint-url "$LOCALSTACK_ENDPOINT" ssm put-parameter \
		--name "$param_name" \
		--value "$param_value" \
		--type "SecureString" \
		--overwrite \
		--region "$PS_REGION"

	export TEST_PARAM_NAME="$param_name"
}

@test "fnox get retrieves parameter from AWS Parameter Store" {
	create_ps_config

	# Create a test parameter
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/fnox-test/test-param-${timestamp}"
	local param_value="my-test-param-value"
	create_test_parameter "$param_name" "$param_value"

	# Add parameter reference to config (using just the name without prefix)
	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.PS_TEST]
provider = "ps"
value = "test-param-${timestamp}"
EOF

	# Get the parameter
	run "$FNOX_BIN" get PS_TEST
	assert_success
	assert_output "$param_value"
}

@test "fnox get with prefix prepends prefix to parameter name" {
	create_ps_config "us-east-1" "/fnox-test/"

	# Create a test parameter with full path
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/fnox-test/prefixed-${timestamp}"
	local param_value="value-with-prefix"
	create_test_parameter "$param_name" "$param_value"

	# Add parameter reference using just the suffix
	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.PREFIXED_PARAM]
provider = "ps"
value = "prefixed-${timestamp}"
EOF

	# Get the parameter
	run "$FNOX_BIN" get PREFIXED_PARAM
	assert_success
	assert_output "$param_value"
}

@test "fnox get without prefix uses full parameter name" {
	create_ps_config "us-east-1" ""

	# Create a test parameter without prefix
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/fnox-full-name-${timestamp}"
	local param_value="value-no-prefix"
	create_test_parameter "$param_name" "$param_value"

	# Add parameter reference
	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.FULL_NAME_PARAM]
provider = "ps"
value = "$param_name"
EOF

	# Get the parameter
	run "$FNOX_BIN" get FULL_NAME_PARAM
	assert_success
	assert_output "$param_value"
}

@test "fnox get fails with non-existent parameter" {
	create_ps_config

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.NONEXISTENT]
provider = "ps"
value = "does-not-exist-$(date +%s)"
EOF

	# Try to get non-existent parameter
	run "$FNOX_BIN" get NONEXISTENT
	assert_failure
	assert_output --partial "secret_not_found"
}

@test "fnox get with multiline parameter" {
	create_ps_config

	# Create a multiline parameter
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/fnox-test/multiline-${timestamp}"
	local param_value="line1
line2
line3"
	create_test_parameter "$param_name" "$param_value"

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.MULTILINE_PARAM]
provider = "ps"
value = "multiline-${timestamp}"
EOF

	# Get the parameter
	run "$FNOX_BIN" get MULTILINE_PARAM
	assert_success
	assert_output "$param_value"
}

@test "fnox list shows Parameter Store parameters" {
	create_ps_config

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.PS_PARAM_1]
description = "First Parameter Store parameter"
provider = "ps"
value = "param1"

[secrets.PS_PARAM_2]
description = "Second Parameter Store parameter"
provider = "ps"
value = "param2"
EOF

	run "$FNOX_BIN" list
	assert_success
	assert_output --partial "PS_PARAM_1"
	assert_output --partial "PS_PARAM_2"
	assert_output --partial "First Parameter Store parameter"
}

@test "fnox get respects region configuration" {
	create_ps_config "us-east-1"

	# Create a parameter in the specified region
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/fnox-test/regional-${timestamp}"
	local param_value="region-specific-value"
	create_test_parameter "$param_name" "$param_value"

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.REGIONAL_PARAM]
provider = "ps"
value = "regional-${timestamp}"
EOF

	# Get the parameter
	run "$FNOX_BIN" get REGIONAL_PARAM
	assert_success
	assert_output "$param_value"
}

@test "fnox get with special characters in parameter value" {
	create_ps_config

	# Create a parameter with special characters
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/fnox-test/special-${timestamp}"
	local param_value='p@ssw0rd!#$%^&*()_+-={}[]|\:";'\''<>?,./~`'
	create_test_parameter "$param_name" "$param_value"

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.SPECIAL_CHARS]
provider = "ps"
value = "special-${timestamp}"
EOF

	# Get the parameter
	run "$FNOX_BIN" get SPECIAL_CHARS
	assert_success
	assert_output "$param_value"
}

@test "fnox get with hierarchical path" {
	create_ps_config "us-east-1" "/myapp/prod/"

	# Create a parameter with hierarchical path
	local timestamp
	timestamp="$(date +%s)-$$-${BATS_TEST_NUMBER:-0}"
	local param_name="/myapp/prod/database/url-${timestamp}"
	local param_value="postgres://localhost/mydb"
	create_test_parameter "$param_name" "$param_value"

	# Update TEST_PARAM_NAME for cleanup
	export TEST_PARAM_NAME="$param_name"

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.DATABASE_URL]
provider = "ps"
value = "database/url-${timestamp}"
EOF

	# Get the parameter
	run "$FNOX_BIN" get DATABASE_URL
	assert_success
	assert_output "$param_value"
}

@test "fnox get with description" {
	create_ps_config

	cat >>"${FNOX_CONFIG_FILE}" <<EOF

[secrets.DESCRIBED_PARAM]
description = "A parameter with a description"
provider = "ps"
value = "some-param"
EOF

	# List to verify description
	run "$FNOX_BIN" list
	assert_success
	assert_output --partial "DESCRIBED_PARAM"
	assert_output --partial "A parameter with a description"
}