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
}

@test "fnox.local.toml overrides fnox.toml secrets" {
	# Create main config
	cat >fnox.toml <<EOF
root = true

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

[secrets]
SHARED_SECRET = { description = "Main config secret", default = "main-value" }
MAIN_ONLY_SECRET = { description = "Main only secret", default = "main-only-value" }
EOF

	# Create local config that overrides SHARED_SECRET
	cat >fnox.local.toml <<EOF
[secrets]
SHARED_SECRET = { description = "Local override", default = "local-value" }
LOCAL_ONLY_SECRET = { description = "Local only secret", default = "local-only-value" }
EOF

	# Test that local config overrides shared secret
	run "$FNOX_BIN" get SHARED_SECRET
	assert_success
	assert_output --partial "local-value"

	# Test that main-only secret is still accessible
	run "$FNOX_BIN" get MAIN_ONLY_SECRET
	assert_success
	assert_output --partial "main-only-value"

	# Test that local-only secret is accessible
	run "$FNOX_BIN" get LOCAL_ONLY_SECRET
	assert_success
	assert_output --partial "local-only-value"
}

@test "fnox.local.toml can add new providers" {
	# Create main config with one provider
	cat >fnox.toml <<EOF
root = true

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

[secrets]
MAIN_SECRET = { provider = "main_provider", default = "main-value" }
EOF

	# Create local config with additional provider
	cat >fnox.local.toml <<EOF
[providers.local_provider]
type = "age"
recipients = ["age1localtest"]

[secrets]
LOCAL_SECRET = { provider = "local_provider", default = "local-value" }
EOF

	# Test that both secrets work
	run "$FNOX_BIN" get MAIN_SECRET
	assert_success
	assert_output --partial "main-value"

	run "$FNOX_BIN" get LOCAL_SECRET
	assert_success
	assert_output --partial "local-value"
}

@test "fnox.local.toml works with profile-specific secrets" {
	# Create main config
	cat >fnox.toml <<EOF
root = true

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

[profiles.production.secrets]
PROD_SECRET = { description = "Production secret", default = "prod-value" }
EOF

	# Create local config with production profile override
	cat >fnox.local.toml <<EOF
[profiles.production.secrets]
PROD_SECRET = { description = "Local prod override", default = "local-prod-value" }
LOCAL_PROD_SECRET = { description = "Local prod secret", default = "local-prod-only" }
EOF

	# Test that local overrides production secret
	run "$FNOX_BIN" get PROD_SECRET --profile production
	assert_success
	assert_output --partial "local-prod-value"

	# Test that local-only production secret is accessible
	run "$FNOX_BIN" get LOCAL_PROD_SECRET --profile production
	assert_success
	assert_output --partial "local-prod-only"
}

@test "fnox.local.toml works with config recursion" {
	# Create directory structure
	mkdir -p parent/child

	# Create parent config
	cat >parent/fnox.toml <<EOF
[providers.test]
type = "age"
recipients = ["age1test"]

[secrets]
PARENT_SECRET = { description = "Parent secret", default = "parent-value" }
EOF

	# Create parent local config
	cat >parent/fnox.local.toml <<EOF
[secrets]
PARENT_SECRET = { description = "Parent local override", default = "parent-local-value" }
PARENT_LOCAL_SECRET = { description = "Parent local secret", default = "parent-local-only" }
EOF

	# Create child config
	cat >parent/child/fnox.toml <<EOF
[secrets]
CHILD_SECRET = { description = "Child secret", default = "child-value" }
EOF

	# Create child local config
	cat >parent/child/fnox.local.toml <<EOF
[secrets]
CHILD_SECRET = { description = "Child local override", default = "child-local-value" }
CHILD_LOCAL_SECRET = { description = "Child local secret", default = "child-local-only" }
EOF

	# Change to child directory
	cd parent/child

	# Test that child local overrides child config
	run "$FNOX_BIN" get CHILD_SECRET
	assert_success
	assert_output --partial "child-local-value"

	# Test that child local secrets are accessible
	run "$FNOX_BIN" get CHILD_LOCAL_SECRET
	assert_success
	assert_output --partial "child-local-only"

	# Test that parent local overrides parent config
	run "$FNOX_BIN" get PARENT_SECRET
	assert_success
	assert_output --partial "parent-local-value"

	# Test that parent local secrets are accessible
	run "$FNOX_BIN" get PARENT_LOCAL_SECRET
	assert_success
	assert_output --partial "parent-local-only"
}

@test "fnox.local.toml alone without fnox.toml works" {
	# Create only local config (no main config)
	cat >fnox.local.toml <<EOF
root = true

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

[secrets]
LOCAL_ONLY = { description = "Local only secret", default = "local-only-value" }
EOF

	# Test that local config works on its own
	run "$FNOX_BIN" get LOCAL_ONLY
	assert_success
	assert_output --partial "local-only-value"
}

@test "fnox.local.toml can override default_provider" {
	# Create main config with default provider (use plain to avoid encryption)
	cat >fnox.toml <<EOF
root = true
default_provider = "main_provider"

[providers.main_provider]
type = "plain"

[providers.alt_provider]
type = "plain"
EOF

	# Create local config that changes default provider
	cat >fnox.local.toml <<EOF
default_provider = "alt_provider"
EOF

	# Set a secret without specifying provider (should use default)
	run "$FNOX_BIN" set TEST_SECRET "test-value"
	assert_success

	# Check that it used the local default provider
	# The config should show the secret was set with alt_provider
	run cat fnox.toml
	assert_success
	assert_output --partial 'TEST_SECRET'
}

@test "fnox.local.toml can set if_missing default" {
	# Create main config
	cat >fnox.toml <<EOF
root = true

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

[secrets]
NO_VALUE_SECRET = { description = "Secret without value" }
EOF

	# Create local config with if_missing = ignore
	cat >fnox.local.toml <<EOF
if_missing = "ignore"
EOF

	# Test that missing secret is ignored (no error)
	run "$FNOX_BIN" exec -- env
	assert_success
	# Should not fail even though NO_VALUE_SECRET has no value
}

@test "fnox list shows secrets from both fnox.toml and fnox.local.toml" {
	# Create main config
	cat >fnox.toml <<EOF
root = true

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

[secrets]
MAIN_SECRET = { description = "Main config secret", default = "main-value" }
SHARED_SECRET = { description = "Shared secret", default = "main-value" }
EOF

	# Create local config
	cat >fnox.local.toml <<EOF
[secrets]
LOCAL_SECRET = { description = "Local config secret", default = "local-value" }
SHARED_SECRET = { description = "Local override", default = "local-value" }
EOF

	# Test that list shows all secrets
	run "$FNOX_BIN" list --complete
	assert_success

	# Should show MAIN_SECRET, LOCAL_SECRET, and SHARED_SECRET (merged)
	assert_output --partial "MAIN_SECRET"
	assert_output --partial "LOCAL_SECRET"
	assert_output --partial "SHARED_SECRET"
}

@test "fnox.local.toml respects root flag" {
	# Create parent config
	cat >fnox.toml <<EOF
[providers.test]
type = "age"
recipients = ["age1test"]

[secrets]
PARENT_SECRET = { description = "Parent secret", default = "parent-value" }
EOF

	# Create local config with root=true to stop recursion
	cat >fnox.local.toml <<EOF
root = true

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

[secrets]
LOCAL_SECRET = { description = "Local secret", default = "local-value" }
EOF

	# Test that local config's root flag is respected
	run "$FNOX_BIN" get LOCAL_SECRET
	assert_success
	assert_output --partial "local-value"

	# Parent secret should still be accessible because it's in the same directory
	# (Both fnox.toml and fnox.local.toml are loaded before checking root)
	run "$FNOX_BIN" get PARENT_SECRET
	assert_success
	assert_output --partial "parent-value"
}

@test "explicit config path ignores fnox.local.toml" {
	# Create main config
	cat >fnox.toml <<EOF
root = true

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

[secrets]
MAIN_SECRET = { description = "Main secret", default = "main-value" }
EOF

	# Create local config
	cat >fnox.local.toml <<EOF
[secrets]
LOCAL_SECRET = { description = "Local secret", default = "local-value" }
MAIN_SECRET = { description = "Local override", default = "local-override" }
EOF

	# Using explicit path with ./ prefix should only load that specific file
	# (bypasses recursion and local config merging)
	run "$FNOX_BIN" -c ./fnox.toml get MAIN_SECRET
	assert_success
	assert_output --partial "main-value"

	# Local secret should not be accessible with explicit path
	run "$FNOX_BIN" -c ./fnox.toml get LOCAL_SECRET
	assert_failure
	assert_output --partial "not found"
}

@test "fnox.local.toml can have imports" {
	# Create imported config
	cat >shared.toml <<EOF
root = true

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

[secrets]
SHARED_IMPORT = { description = "Shared import secret", default = "shared-value" }
EOF

	# Create main config
	cat >fnox.toml <<EOF
root = true

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

[secrets]
MAIN_SECRET = { description = "Main secret", default = "main-value" }
EOF

	# Create local config with import
	cat >fnox.local.toml <<EOF
import = ["./shared.toml"]

[secrets]
LOCAL_SECRET = { description = "Local secret", default = "local-value" }
EOF

	# Test that imported secret is accessible
	run "$FNOX_BIN" get SHARED_IMPORT
	assert_success
	assert_output --partial "shared-value"

	# Test that main and local secrets are still accessible
	run "$FNOX_BIN" get MAIN_SECRET
	assert_success
	assert_output --partial "main-value"

	run "$FNOX_BIN" get LOCAL_SECRET
	assert_success
	assert_output --partial "local-value"
}