fnox 1.23.0

A flexible secret management tool supporting multiple providers and encryption methods
Documentation
#!/usr/bin/env bats

setup() {
	load 'test_helper/common_setup'
	_common_setup
}

teardown() {
	_common_teardown
}

@test "config with no providers returns error" {
	# Create config with no providers
	cat >fnox.toml <<'EOF'
root = true

[secrets]
MY_SECRET = { value = "test" }
EOF

	# Any command that loads the config should fail
	run "$FNOX_BIN" get MY_SECRET
	assert_failure
	assert_output --partial "No providers configured"
}

@test "single provider is auto-selected as default" {
	# Skip if age not installed
	if ! command -v age-keygen >/dev/null 2>&1; then
		skip "age-keygen not installed"
	fi

	# Generate age key
	local keygen_output
	keygen_output=$(age-keygen -o key.txt 2>&1)
	local public_key
	public_key=$(echo "$keygen_output" | grep "^Public key:" | cut -d' ' -f3)

	# Create config with single provider
	cat >fnox.toml <<EOF
root = true

[providers.age]
type = "age"
recipients = ["$public_key"]

[secrets]
EOF

	# Set a secret without specifying provider - should use the only one available
	run "$FNOX_BIN" set MY_SECRET "secret-value"
	assert_success

	# Verify the secret was encrypted with the age provider
	assert_config_contains "MY_SECRET"
	assert_config_not_contains "secret-value"

	# Should be able to get it back
	run "$FNOX_BIN" get MY_SECRET --age-key-file key.txt
	assert_success
	assert_output "secret-value"
}

@test "explicit default_provider is used when set" {
	# Skip if age not installed
	if ! command -v age-keygen >/dev/null 2>&1; then
		skip "age-keygen not installed"
	fi

	# Generate age key
	local keygen_output
	keygen_output=$(age-keygen -o key.txt 2>&1)
	local public_key
	public_key=$(echo "$keygen_output" | grep "^Public key:" | cut -d' ' -f3)

	# Create config with multiple providers and explicit default
	cat >fnox.toml <<EOF
root = true
default_provider = "age"

[providers.age]
type = "age"
recipients = ["$public_key"]

[providers.vault]
type = "vault"
address = "https://vault.example.com"

[secrets]
EOF

	# Set a secret without specifying provider - should use default_provider
	run "$FNOX_BIN" set MY_SECRET "secret-value"
	assert_success

	# Verify the secret was encrypted with the age provider
	assert_config_contains "MY_SECRET"
	assert_config_not_contains "secret-value"

	# Should be able to get it back
	run "$FNOX_BIN" get MY_SECRET --age-key-file key.txt
	assert_success
	assert_output "secret-value"
}

@test "profile default_provider overrides global default_provider" {
	# Skip if age not installed
	if ! command -v age-keygen >/dev/null 2>&1; then
		skip "age-keygen not installed"
	fi

	# Generate two age keys
	local keygen_output1
	keygen_output1=$(age-keygen -o key1.txt 2>&1)
	local public_key1
	public_key1=$(echo "$keygen_output1" | grep "^Public key:" | cut -d' ' -f3)

	local keygen_output2
	keygen_output2=$(age-keygen -o key2.txt 2>&1)
	local public_key2
	public_key2=$(echo "$keygen_output2" | grep "^Public key:" | cut -d' ' -f3)

	# Create config with global default and profile override
	cat >fnox.toml <<EOF
root = true
default_provider = "age1"

[providers.age1]
type = "age"
recipients = ["$public_key1"]

[providers.age2]
type = "age"
recipients = ["$public_key2"]

[secrets]

[profiles.prod]
default_provider = "age2"

[profiles.prod.secrets]
EOF

	# Set a secret in default profile - should use age1
	run "$FNOX_BIN" set DEFAULT_SECRET "default-value"
	assert_success

	# Set a secret in prod profile - should use age2
	run "$FNOX_BIN" set --profile prod PROD_SECRET "prod-value"
	assert_success

	# Verify we can get them back with the right keys
	run "$FNOX_BIN" get DEFAULT_SECRET --age-key-file key1.txt
	assert_success
	assert_output "default-value"

	run "$FNOX_BIN" get --profile prod PROD_SECRET --age-key-file key2.txt
	assert_success
	assert_output "prod-value"
}

@test "multiple providers without default_provider requires explicit provider" {
	# Skip if age not installed
	if ! command -v age-keygen >/dev/null 2>&1; then
		skip "age-keygen not installed"
	fi

	# Generate age key
	local keygen_output
	keygen_output=$(age-keygen -o key.txt 2>&1)
	local public_key
	public_key=$(echo "$keygen_output" | grep "^Public key:" | cut -d' ' -f3)

	# Create config with multiple providers and NO default
	cat >fnox.toml <<EOF
root = true

[providers.age]
type = "age"
recipients = ["$public_key"]

[providers.vault]
type = "vault"
address = "https://vault.example.com"

[secrets]
EOF

	# Set a secret without specifying provider - should store as plaintext since no default
	run "$FNOX_BIN" set MY_SECRET "secret-value"
	assert_success

	# Verify the secret was stored as plaintext (not encrypted)
	assert_config_contains "secret-value"

	# Get should work
	run "$FNOX_BIN" get MY_SECRET
	assert_success
	assert_output "secret-value"
}

@test "invalid default_provider returns error" {
	# Create config with invalid default_provider
	cat >fnox.toml <<'EOF'
root = true
default_provider = "nonexistent"

[providers.age]
type = "age"
recipients = ["age1test"]

[secrets]
EOF

	# Should fail to validate
	run "$FNOX_BIN" get MY_SECRET 2>&1 || true
	assert_failure
	assert_output --partial "Default provider 'nonexistent' not found"
}

@test "profile with no providers (and no global providers) returns error" {
	# Create config with profile that has no providers
	# Set if_missing = "error" to require the secret
	cat >fnox.toml <<'EOF'
root = true

[secrets]

[profiles.test]

[profiles.test.secrets]
MY_SECRET = { description = "test secret", if_missing = "error" }
EOF

	# Should fail because profile has no providers and secret has no value
	run "$FNOX_BIN" get --profile test MY_SECRET
	assert_failure
}

@test "profile inherits global providers" {
	# Skip if age not installed
	if ! command -v age-keygen >/dev/null 2>&1; then
		skip "age-keygen not installed"
	fi

	# Generate age key
	local keygen_output
	keygen_output=$(age-keygen -o key.txt 2>&1)
	local public_key
	public_key=$(echo "$keygen_output" | grep "^Public key:" | cut -d' ' -f3)

	# Create config with global provider only
	cat >fnox.toml <<EOF
root = true

[providers.age]
type = "age"
recipients = ["$public_key"]

[secrets]

[profiles.test]

[profiles.test.secrets]
EOF

	# Set a secret in test profile - should use inherited provider
	run "$FNOX_BIN" set --profile test TEST_SECRET "test-value"
	assert_success

	# Verify it was encrypted
	assert_config_not_contains "test-value"

	# Should be able to get it back
	run "$FNOX_BIN" get --profile test TEST_SECRET --age-key-file key.txt
	assert_success
	assert_output "test-value"
}

@test "explicit provider overrides default_provider" {
	# Skip if age not installed
	if ! command -v age-keygen >/dev/null 2>&1; then
		skip "age-keygen not installed"
	fi

	# Generate two age keys
	local keygen_output1
	keygen_output1=$(age-keygen -o key1.txt 2>&1)
	local public_key1
	public_key1=$(echo "$keygen_output1" | grep "^Public key:" | cut -d' ' -f3)

	local keygen_output2
	keygen_output2=$(age-keygen -o key2.txt 2>&1)
	local public_key2
	public_key2=$(echo "$keygen_output2" | grep "^Public key:" | cut -d' ' -f3)

	# Create config with default_provider
	cat >fnox.toml <<EOF
root = true
default_provider = "age1"

[providers.age1]
type = "age"
recipients = ["$public_key1"]

[providers.age2]
type = "age"
recipients = ["$public_key2"]

[secrets]
EOF

	# Set a secret with explicit provider (should override default)
	run "$FNOX_BIN" set MY_SECRET "secret-value" --provider age2
	assert_success

	# Should need key2 to decrypt (not key1)
	run "$FNOX_BIN" get MY_SECRET --age-key-file key2.txt
	assert_success
	assert_output "secret-value"

	# Should fail with key1
	run "$FNOX_BIN" get MY_SECRET --age-key-file key1.txt
	assert_failure
}