load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
setup() {
TEST_DIR="$(mktemp -d)"
cd "$TEST_DIR" || exit 1
AGE_KEY_FILE="$TEST_DIR/age-key.txt"
age-keygen -o "$AGE_KEY_FILE" 2>/dev/null
FNOX_AGE_KEY="$(grep 'AGE-SECRET-KEY' "$AGE_KEY_FILE")"
export FNOX_AGE_KEY
cat >fnox.toml <<EOF
[providers.age]
type = "age"
recipients = ["$(grep 'public key:' "$AGE_KEY_FILE" | cut -d: -f2- | xargs)"]
[secrets]
EOF
echo "secret123" | fnox set TEST_SECRET --provider age
echo "password456" | fnox set TEST_PASSWORD --provider age
}
teardown() {
if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then
rm -rf "$TEST_DIR"
fi
}
@test "edit command with non-interactive editor (modify secret)" {
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
import re
with open(sys.argv[1], 'r') as f:
content = f.read()
content = re.sub(
r'TEST_SECRET= \{ provider = "age", value = "[^"]*" \}',
r'TEST_SECRET= { provider = "age", value = "newsecret789" }',
content
)
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run fnox get TEST_SECRET
assert_success
assert_output "newsecret789"
}
@test "edit command preserves unchanged secrets" {
skip "Debugging - need to check why edit breaks the config"
original_secret=$(fnox get TEST_SECRET)
original_password=$(fnox get TEST_PASSWORD)
echo "Original TEST_SECRET: $original_secret" >&3
echo "Original TEST_PASSWORD: $original_password" >&3
echo "Config before edit:" >&3
cat fnox.toml >&3
cat >"$TEST_DIR/test-editor.sh" <<'EDITOR_SCRIPT'
echo "Editor called with file: $1" >&2
cat "$1" >&2
exit 0
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.sh"
export EDITOR="$TEST_DIR/test-editor.sh"
run fnox edit
echo "Edit command output: $output" >&3
assert_success
echo "Config after edit:" >&3
cat fnox.toml >&3
run fnox get TEST_SECRET
echo "TEST_SECRET after edit: $output" >&3
assert_success
assert_output "$original_secret"
run fnox get TEST_PASSWORD
echo "TEST_PASSWORD after edit: $output" >&3
assert_success
assert_output "$original_password"
}
@test "edit command decrypts secrets in temporary file" {
cat >"$TEST_DIR/test-editor.sh" <<EDITOR_SCRIPT
cp "\$1" "$TEST_DIR/decrypted-content.txt"
exit 0
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.sh"
export EDITOR="$TEST_DIR/test-editor.sh"
run fnox edit
assert_success
assert [ -f "$TEST_DIR/decrypted-content.txt" ]
run grep -q "secret123" "$TEST_DIR/decrypted-content.txt"
assert_success
run grep -q "password456" "$TEST_DIR/decrypted-content.txt"
assert_success
}
@test "edit command handles editor failure" {
cat >"$TEST_DIR/test-editor.sh" <<'EDITOR_SCRIPT'
exit 1
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.sh"
export EDITOR="$TEST_DIR/test-editor.sh"
run fnox edit
assert_failure
}
@test "edit command works with multiple secrets" {
echo "value1" | fnox set SECRET1 --provider age
echo "value2" | fnox set SECRET2 --provider age
echo "value3" | fnox set SECRET3 --provider age
cat >"$TEST_DIR/test-editor.sh" <<'EDITOR_SCRIPT'
sed -i.bak 's/SECRET1= { provider = "age", value = "[^"]*" }/SECRET1= { provider = "age", value = "modified1" }/' "$1"
sed -i.bak 's/SECRET2= { provider = "age", value = "[^"]*" }/SECRET2= { provider = "age", value = "modified2" }/' "$1"
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.sh"
export EDITOR="$TEST_DIR/test-editor.sh"
run fnox edit
assert_success
run fnox get SECRET1
assert_success
assert_output "modified1"
run fnox get SECRET2
assert_success
assert_output "modified2"
run fnox get SECRET3
assert_success
assert_output "value3"
}
@test "edit command: create, edit, and remove encrypted secrets" {
echo "original-value" | fnox set EXISTING_SECRET --provider age
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
import re
with open(sys.argv[1], 'r') as f:
content = f.read()
content = re.sub(
r'EXISTING_SECRET= \{ provider = "age", value = "[^"]*" \}',
r'EXISTING_SECRET= { provider = "age", value = "edited-value" }',
content
)
if '[secrets]' in content:
content = re.sub(
r'(\[secrets\]\n)',
r'\1NEW_SECRET= { provider = "age", value = "new-secret-value" }\n',
content
)
content = re.sub(
r'TEST_SECRET= \{[^}]*\}\n',
'',
content
)
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run fnox get NEW_SECRET
assert_success
assert_output "new-secret-value"
run fnox get EXISTING_SECRET
assert_success
assert_output "edited-value"
run fnox get TEST_SECRET
assert_failure
}
@test "edit command: create, edit, and remove keychain secrets" {
if ! command -v security &>/dev/null && ! command -v secret-tool &>/dev/null; then
skip "Keychain/secret-tool not available"
fi
cat >>fnox.toml <<EOF
[providers.keychain]
type = "keychain"
service = "fnox-test"
prefix = "test-$$/"
EOF
echo "kc-original" | fnox set KC_EXISTING --provider keychain
echo "kc-to-delete" | fnox set KC_DELETE --provider keychain
run fnox get KC_EXISTING
if [ "$status" -ne 0 ]; then
skip "Keychain not accessible in this environment"
fi
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
import re
with open(sys.argv[1], 'r') as f:
content = f.read()
content = re.sub(
r'KC_EXISTING= \{ provider = "keychain", value = "[^"]*" \}',
r'KC_EXISTING= { provider = "keychain", value = "kc-edited" }',
content
)
if '[secrets]' in content:
content = re.sub(
r'(\[secrets\]\n)',
r'\1KC_NEW= { provider = "keychain", value = "kc-new-value" }\n',
content
)
content = re.sub(
r'KC_DELETE= \{[^}]*\}\n',
'',
content
)
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run fnox get KC_NEW
assert_success
assert_output "kc-new-value"
run fnox get KC_EXISTING
assert_success
assert_output "kc-edited"
run fnox get KC_DELETE
assert_failure
if command -v security &>/dev/null; then
security delete-generic-password -s "fnox-test" -a "test-$$/KC_NEW" 2>/dev/null || true
security delete-generic-password -s "fnox-test" -a "test-$$/KC_EXISTING" 2>/dev/null || true
elif command -v secret-tool &>/dev/null; then
secret-tool clear service "fnox-test" account "test-$$/KC_NEW" 2>/dev/null || true
secret-tool clear service "fnox-test" account "test-$$/KC_EXISTING" 2>/dev/null || true
fi
}
@test "edit command persists default_provider and provider changes (issue #118)" {
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
with open(sys.argv[1], 'r') as f:
content = f.read()
if 'default_provider' not in content:
content = content.replace('[providers.age]', 'default_provider = "age"\n\n[providers.age]')
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run grep 'default_provider = "age"' fnox.toml
assert_success
}
@test "edit command preserves comments added during edit" {
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
with open(sys.argv[1], 'r') as f:
content = f.read()
content = content.replace('[providers.age]', '# My custom comment\n[providers.age]')
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run grep '# My custom comment' fnox.toml
assert_success
}
@test "edit command respects provider removal from existing secret" {
cat >fnox.toml <<EOF
default_provider = "age"
[providers.age]
type = "age"
recipients = ["$(grep 'public key:' "$AGE_KEY_FILE" | cut -d: -f2- | xargs)"]
[secrets]
EOF
echo "mysecret" | fnox set MY_SECRET --provider age
run grep 'provider = "age"' fnox.toml
assert_success
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
import re
with open(sys.argv[1], 'r') as f:
content = f.read()
content = re.sub(
r'MY_SECRET= \{ provider = "age", value = "([^"]*)" \}',
r'MY_SECRET= { value = "\1" }',
content
)
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run fnox get MY_SECRET
assert_success
assert_output "mysecret"
run grep 'MY_SECRET.*provider = "age"' fnox.toml
assert_failure "Provider field should have been removed"
}
@test "edit command recognizes new providers added during edit (issue #118)" {
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
import re
with open(sys.argv[1], 'r') as f:
content = f.read()
content = content.replace(
'[secrets]',
'[providers.plain]\ntype = "plain"\n\n[secrets]'
)
content = re.sub(
r'(\[secrets\]\n)',
r'\1PLAIN_SECRET= { provider = "plain", value = "my-plain-value" }\n',
content
)
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
assert_success
run fnox get PLAIN_SECRET
assert_success
assert_output "my-plain-value"
run grep '\[providers.plain\]' fnox.toml
assert_success
}
@test "edit command: move secret to new profile section (issue #105)" {
echo "my-secret-value" | fnox set MY_SECRET --provider age
run fnox get MY_SECRET
assert_success
assert_output "my-secret-value"
echo "Initial config:" >&3
cat fnox.toml >&3
cat >"$TEST_DIR/test-editor.py" <<'EDITOR_SCRIPT'
import sys
import re
with open(sys.argv[1], 'r') as f:
content = f.read()
secret_match = re.search(r'MY_SECRET= \{ provider = "age", value = "([^"]*)" \}', content)
if secret_match:
secret_value = secret_match.group(1)
content = re.sub(
r'MY_SECRET= \{[^}]*\}\n',
'',
content
)
profile_section = f'''
[profiles.production]
[profiles.production.secrets]
MY_SECRET= {{ provider = "age", value = "{secret_value}" }}
'''
content = content.rstrip() + profile_section + '\n'
with open(sys.argv[1], 'w') as f:
f.write(content)
EDITOR_SCRIPT
chmod +x "$TEST_DIR/test-editor.py"
export EDITOR="$TEST_DIR/test-editor.py"
run fnox edit
echo "Edit output: $output" >&3
assert_success
echo "Config after edit:" >&3
cat fnox.toml >&3
run fnox get MY_SECRET
echo "Getting MY_SECRET from default profile: status=$status output=$output" >&3
assert_failure
run fnox get MY_SECRET --profile production
echo "Getting MY_SECRET from production profile: status=$status output=$output" >&3
assert_success
assert_output "my-secret-value"
run grep -q '\[profiles.production\]' fnox.toml
assert_success "Config should contain [profiles.production] section"
run grep -q '\[profiles.production.secrets\]' fnox.toml
assert_success "Config should contain [profiles.production.secrets] section"
}