fnox 1.25.1

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
}

# Helper function to setup age provider
setup_age_provider() {
	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 age provider
	cat >fnox.toml <<EOF
root = true

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

[secrets]
EOF
}

@test "fnox import requires --provider flag" {
	setup_age_provider

	# Create a .env file
	cat >.env <<EOF
TEST_SECRET=value123
EOF

	# Import without --provider should fail
	assert_fnox_failure import -i .env --force
	assert_output --partial "required arguments were not provided"
	assert_output --partial "--provider"
}

@test "fnox import reads from .env file with -i flag" {
	setup_age_provider

	# Create a .env file with test secrets
	cat >.env <<EOF
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=secret-key-123
DEBUG_MODE=true
EOF

	# Import from .env file with age provider
	assert_fnox_success import -i .env --provider age --force

	# Verify secrets were imported and encrypted (not plaintext)
	assert_config_not_contains "postgresql://localhost:5432/mydb"
	assert_config_not_contains "secret-key-123"

	# Verify secrets can be retrieved with age key
	assert_fnox_success get DATABASE_URL --age-key-file key.txt
	assert_output "postgresql://localhost:5432/mydb"

	assert_fnox_success get API_KEY --age-key-file key.txt
	assert_output "secret-key-123"

	assert_fnox_success get DEBUG_MODE --age-key-file key.txt
	assert_output "true"
}

@test "fnox import handles quoted values in .env file" {
	setup_age_provider

	# Create a .env file with quoted values
	cat >.env <<EOF
SINGLE_QUOTED='value with spaces'
DOUBLE_QUOTED="another value with spaces"
UNQUOTED=no_spaces
EOF

	assert_fnox_success import -i .env --provider age --force

	assert_fnox_success get SINGLE_QUOTED --age-key-file key.txt
	assert_output "value with spaces"

	assert_fnox_success get DOUBLE_QUOTED --age-key-file key.txt
	assert_output "another value with spaces"

	assert_fnox_success get UNQUOTED --age-key-file key.txt
	assert_output "no_spaces"
}

@test "fnox import handles export statements in .env file" {
	setup_age_provider

	# Create a .env file with export statements
	cat >.env <<EOF
export DATABASE_URL=postgresql://localhost:5432/mydb
export API_KEY=secret-key-456
REGULAR_VAR=regular-value
EOF

	assert_fnox_success import -i .env --provider age --force

	assert_fnox_success get DATABASE_URL --age-key-file key.txt
	assert_output "postgresql://localhost:5432/mydb"

	assert_fnox_success get API_KEY --age-key-file key.txt
	assert_output "secret-key-456"

	assert_fnox_success get REGULAR_VAR --age-key-file key.txt
	assert_output "regular-value"
}

@test "fnox import skips comments and empty lines" {
	setup_age_provider

	# Create a .env file with comments and empty lines
	cat >.env <<EOF
# This is a comment
DATABASE_URL=postgresql://localhost:5432/mydb

# Another comment
API_KEY=secret-key-789

EOF

	assert_fnox_success import -i .env --provider age --force

	# Should only import the two actual variables
	assert_fnox_success list
	assert_output --partial "DATABASE_URL"
	assert_output --partial "API_KEY"
	refute_output --partial "#"
}

@test "fnox import with --filter flag filters secrets by regex" {
	setup_age_provider

	# Create a .env file
	cat >.env <<EOF
DATABASE_URL=postgresql://localhost:5432/mydb
DATABASE_PASSWORD=secret123
API_KEY=secret-key-abc
API_SECRET=secret-abc-456
DEBUG_MODE=true
EOF

	# Import only DATABASE_* secrets
	assert_fnox_success import -i .env --filter "^DATABASE_" --provider age --force

	# Should have DATABASE_* secrets
	assert_fnox_success get DATABASE_URL --age-key-file key.txt
	assert_output "postgresql://localhost:5432/mydb"

	assert_fnox_success get DATABASE_PASSWORD --age-key-file key.txt
	assert_output "secret123"

	# Should not have API_* or DEBUG_MODE
	assert_fnox_failure get API_KEY
	assert_fnox_failure get DEBUG_MODE
}

@test "fnox import with --prefix flag adds prefix to secret names" {
	setup_age_provider

	# Create a .env file
	cat >.env <<EOF
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=secret-key-xyz
EOF

	# Import with prefix
	assert_fnox_success import -i .env --prefix "MYAPP_" --provider age --force

	# Should be accessible with prefix
	assert_fnox_success get MYAPP_DATABASE_URL --age-key-file key.txt
	assert_output "postgresql://localhost:5432/mydb"

	assert_fnox_success get MYAPP_API_KEY --age-key-file key.txt
	assert_output "secret-key-xyz"

	# Should not be accessible without prefix
	assert_fnox_failure get DATABASE_URL
	assert_fnox_failure get API_KEY
}

@test "fnox import requires confirmation by default" {
	setup_age_provider

	# Create a .env file
	cat >.env <<EOF
DATABASE_URL=postgresql://localhost:5432/mydb
EOF

	# Import without --force should prompt for confirmation
	run bash -c "echo 'n' | $FNOX_BIN import -i .env --provider age"
	assert_output --partial "Continue? [y/N]"
	assert_output --partial "Import cancelled"

	# Secret should not have been imported
	assert_fnox_failure get DATABASE_URL
}

@test "fnox import reads from stdin when -i is not specified" {
	setup_age_provider

	# Import from stdin
	run bash -c "echo -e 'DATABASE_URL=postgresql://localhost:5432/mydb\nAPI_KEY=secret-key' | $FNOX_BIN import --provider age --force"
	assert_success

	# Verify secrets were imported
	assert_fnox_success get DATABASE_URL --age-key-file key.txt
	assert_output "postgresql://localhost:5432/mydb"

	assert_fnox_success get API_KEY --age-key-file key.txt
	assert_output "secret-key"
}

@test "fnox import from stdin requires --force flag" {
	setup_age_provider

	# FIXED: stdin imports now require --force to avoid double-stdin consumption bug
	# Without --force, importing from stdin would consume stdin twice:
	#   1. First to read import data (read_input)
	#   2. Then to read confirmation (stdin.read_line)
	# This would cause the import to fail because stdin is at EOF after reading data

	# First test: import without --provider should fail with missing provider error
	run bash -c "echo -e 'TEST_VAR=test123' | $FNOX_BIN import"
	assert_failure
	assert_output --partial "required arguments were not provided"
	assert_output --partial "--provider"

	# Second test: import with --provider but without --force should fail with stdin consumption error
	run bash -c "echo -e 'TEST_VAR=test456' | $FNOX_BIN import --provider age"
	assert_failure
	assert_output --partial "--force"
	assert_output --partial "Stdin is consumed during import"

	# Verify secrets were NOT imported
	assert_fnox_failure get TEST_VAR

	# Now try with both --provider and --force - should succeed
	run bash -c "echo -e 'TEST_VAR=test123' | $FNOX_BIN import --provider age --force"
	assert_success

	# Verify secret WAS imported
	assert_fnox_success get TEST_VAR --age-key-file key.txt
	assert_output "test123"
}

@test "fnox import supports json format" {
	setup_age_provider

	# Create a JSON file
	cat >secrets.json <<EOF
{
  "DATABASE_URL": "postgresql://localhost:5432/mydb",
  "API_KEY": "secret-key-json"
}
EOF

	assert_fnox_success import -i secrets.json json --provider age --force

	assert_fnox_success get DATABASE_URL --age-key-file key.txt
	assert_output "postgresql://localhost:5432/mydb"

	assert_fnox_success get API_KEY --age-key-file key.txt
	assert_output "secret-key-json"
}

@test "fnox import shows helpful error when file does not exist" {
	setup_age_provider

	# Try to import from non-existent file
	assert_fnox_failure import -i nonexistent.env --provider age --force
	assert_output --partial "Failed to read import source"
}

@test "fnox import fails with non-existent provider" {
	setup_age_provider

	# Create a .env file
	cat >.env <<EOF
SECRET=value
EOF

	# Try to import with non-existent provider
	assert_fnox_failure import -i .env --provider nonexistent --force
	assert_output --partial "Provider 'nonexistent' not configured"
}