- Updated file tracking to include `.dev.vars` alongside `.env` and `.env.*`. - Improved pre-commit hook to block plaintext secret files, including `.dev.vars`. - Added `clear` command to remove plaintext secret files from the current directory and workspaces. - Enhanced tests to cover new functionality for `.dev.vars` and the `clear` command. - Updated documentation to reflect changes in tracked files and command usage.
521 lines
14 KiB
Bash
521 lines
14 KiB
Bash
#!/usr/bin/env bats
|
|
|
|
load test_helper
|
|
|
|
# ─── init ──────────────────────────────────────────────────────────────
|
|
|
|
@test "init creates repo with key and gitignore" {
|
|
run "$SECRETS_BIN" init
|
|
[ "$status" -eq 0 ]
|
|
[ -d "$SECRETS_DIR/.git" ]
|
|
[ -f "$SECRETS_DIR/key.txt" ]
|
|
[ -f "$SECRETS_DIR/.gitignore" ]
|
|
grep -q "key.txt" "$SECRETS_DIR/.gitignore"
|
|
grep -qF '!**/.env.*.age' "$SECRETS_DIR/.gitignore"
|
|
}
|
|
|
|
@test "init installs pre-commit hook" {
|
|
run "$SECRETS_BIN" init
|
|
[ "$status" -eq 0 ]
|
|
[ -x "$SECRETS_DIR/.git/hooks/pre-commit" ]
|
|
}
|
|
|
|
@test "init warns if already initialized" {
|
|
"$SECRETS_BIN" init >/dev/null 2>&1
|
|
local key_before
|
|
key_before=$(cat "$SECRETS_DIR/key.txt")
|
|
|
|
run "$SECRETS_BIN" init
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"Already initialized"* ]]
|
|
|
|
# Key must not be overwritten
|
|
local key_after
|
|
key_after=$(cat "$SECRETS_DIR/key.txt")
|
|
[ "$key_before" = "$key_after" ]
|
|
}
|
|
|
|
@test "init fails without age" {
|
|
# Create a temp PATH without age
|
|
local fake_path="$TEST_TMPDIR/fake-bin"
|
|
mkdir -p "$fake_path"
|
|
ln -s "$(which git)" "$fake_path/git"
|
|
ln -s "$(which bash)" "$fake_path/bash"
|
|
ln -s "$(which mkdir)" "$fake_path/mkdir"
|
|
ln -s "$(which cat)" "$fake_path/cat"
|
|
ln -s "$(which chmod)" "$fake_path/chmod"
|
|
ln -s "$(which cp)" "$fake_path/cp"
|
|
ln -s "$(which basename)" "$fake_path/basename"
|
|
ln -s "$(which dirname)" "$fake_path/dirname"
|
|
ln -s "$(which cd)" "$fake_path/cd" 2>/dev/null || true
|
|
|
|
run env PATH="$fake_path" "$SECRETS_BIN" init
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"age"* ]]
|
|
}
|
|
|
|
# ─── push ──────────────────────────────────────────────────────────────
|
|
|
|
@test "push encrypts .env files" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
|
|
run "$SECRETS_BIN" push testproj
|
|
[ "$status" -eq 0 ]
|
|
[ -f "$SECRETS_DIR/testproj/.env.age" ]
|
|
[ -f "$SECRETS_DIR/testproj/.env.staging.age" ]
|
|
}
|
|
|
|
@test "push errors with no .env files" {
|
|
init_with_remote
|
|
mkdir -p "$WORK_DIR/empty"
|
|
cd "$WORK_DIR/empty"
|
|
|
|
run "$SECRETS_BIN" push testproj
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"No secret files"* ]]
|
|
}
|
|
|
|
@test "push errors with missing key" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
rm "$SECRETS_DIR/key.txt"
|
|
|
|
run "$SECRETS_BIN" push testproj
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"Key file"* ]]
|
|
}
|
|
|
|
@test "push derives project name from dirname" {
|
|
init_with_remote
|
|
create_project_dir myproject
|
|
# Don't pass explicit project name
|
|
run "$SECRETS_BIN" push
|
|
[ "$status" -eq 0 ]
|
|
[ -d "$SECRETS_DIR/myproject" ]
|
|
}
|
|
|
|
@test "push succeeds on repeated push (age is non-deterministic)" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
# Push again — age produces different ciphertext each time, so this creates a new commit
|
|
run "$SECRETS_BIN" push testproj
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "push encrypts .dev.vars files" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
echo "CF_SECRET=wrangler123" > "$WORK_DIR/testproj/.dev.vars"
|
|
|
|
run "$SECRETS_BIN" push testproj
|
|
[ "$status" -eq 0 ]
|
|
[ -f "$SECRETS_DIR/testproj/.env.age" ]
|
|
[ -f "$SECRETS_DIR/testproj/.dev.vars.age" ]
|
|
}
|
|
|
|
@test "pull decrypts .dev.vars files" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
echo "CF_SECRET=wrangler123" > "$WORK_DIR/testproj/.dev.vars"
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
|
|
local pull_dir="$WORK_DIR/pull-devvars"
|
|
mkdir -p "$pull_dir"
|
|
cd "$pull_dir"
|
|
|
|
run "$SECRETS_BIN" pull testproj
|
|
[ "$status" -eq 0 ]
|
|
[ "$(cat "$pull_dir/.dev.vars")" = "CF_SECRET=wrangler123" ]
|
|
}
|
|
|
|
@test "pre-commit blocks plaintext .dev.vars files" {
|
|
init_with_remote
|
|
cd "$SECRETS_DIR"
|
|
|
|
echo "LEAKED=true" > .dev.vars
|
|
git add -f .dev.vars
|
|
|
|
run git commit -m "should fail"
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"Plaintext"* ]]
|
|
}
|
|
|
|
# ─── pull ──────────────────────────────────────────────────────────────
|
|
|
|
@test "pull decrypts files correctly" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
|
|
# Pull into a different directory
|
|
local pull_dir="$WORK_DIR/pull-target"
|
|
mkdir -p "$pull_dir"
|
|
cd "$pull_dir"
|
|
|
|
run "$SECRETS_BIN" pull testproj
|
|
[ "$status" -eq 0 ]
|
|
[ -f "$pull_dir/.env" ]
|
|
[ -f "$pull_dir/.env.staging" ]
|
|
[ "$(cat "$pull_dir/.env")" = "SECRET_KEY=abc123" ]
|
|
[ "$(cat "$pull_dir/.env.staging")" = "DB_HOST=staging.db.example.com" ]
|
|
}
|
|
|
|
@test "pull errors for nonexistent project" {
|
|
init_with_remote
|
|
|
|
run "$SECRETS_BIN" pull nonexistent
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"not found"* ]]
|
|
}
|
|
|
|
@test "pull errors with missing key" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
rm "$SECRETS_DIR/key.txt"
|
|
|
|
local pull_dir="$WORK_DIR/pull-target"
|
|
mkdir -p "$pull_dir"
|
|
cd "$pull_dir"
|
|
|
|
run "$SECRETS_BIN" pull testproj
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"Key file"* ]]
|
|
}
|
|
|
|
@test "pull overwrites existing files" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
|
|
local pull_dir="$WORK_DIR/pull-target"
|
|
mkdir -p "$pull_dir"
|
|
echo "OLD_VALUE=stale" > "$pull_dir/.env"
|
|
cd "$pull_dir"
|
|
|
|
run "$SECRETS_BIN" pull testproj
|
|
[ "$status" -eq 0 ]
|
|
[ "$(cat "$pull_dir/.env")" = "SECRET_KEY=abc123" ]
|
|
}
|
|
|
|
@test "pull reinstalls missing pre-commit hook" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
|
|
# Remove the hook
|
|
rm -f "$SECRETS_DIR/.git/hooks/pre-commit"
|
|
[ ! -f "$SECRETS_DIR/.git/hooks/pre-commit" ]
|
|
|
|
local pull_dir="$WORK_DIR/pull-target"
|
|
mkdir -p "$pull_dir"
|
|
cd "$pull_dir"
|
|
|
|
run "$SECRETS_BIN" pull testproj
|
|
[ "$status" -eq 0 ]
|
|
[ -x "$SECRETS_DIR/.git/hooks/pre-commit" ]
|
|
[[ "$output" == *"Reinstalled"* ]]
|
|
}
|
|
|
|
# ─── list ──────────────────────────────────────────────────────────────
|
|
|
|
@test "list shows projects and files" {
|
|
init_with_remote
|
|
create_project_dir projA
|
|
"$SECRETS_BIN" push projA >/dev/null 2>&1
|
|
create_project_dir projB
|
|
"$SECRETS_BIN" push projB >/dev/null 2>&1
|
|
|
|
run "$SECRETS_BIN" list
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"projA"* ]]
|
|
[[ "$output" == *"projB"* ]]
|
|
}
|
|
|
|
@test "list shows empty message" {
|
|
"$SECRETS_BIN" init >/dev/null 2>&1
|
|
|
|
run "$SECRETS_BIN" list
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"No projects"* ]]
|
|
}
|
|
|
|
# ─── rm ────────────────────────────────────────────────────────────────
|
|
|
|
@test "rm removes project from repo" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
[ -d "$SECRETS_DIR/testproj" ]
|
|
|
|
run "$SECRETS_BIN" rm testproj
|
|
[ "$status" -eq 0 ]
|
|
[ ! -d "$SECRETS_DIR/testproj" ]
|
|
}
|
|
|
|
@test "rm errors for nonexistent project" {
|
|
init_with_remote
|
|
|
|
run "$SECRETS_BIN" rm nonexistent
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"not found"* ]]
|
|
}
|
|
|
|
# ─── pre-commit hook ──────────────────────────────────────────────────
|
|
|
|
@test "pre-commit blocks plaintext env files" {
|
|
init_with_remote
|
|
cd "$SECRETS_DIR"
|
|
|
|
echo "LEAKED=true" > .env.test
|
|
git add -f .env.test
|
|
|
|
run git commit -m "should fail"
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"Plaintext"* ]]
|
|
}
|
|
|
|
@test "pre-commit allows .age files" {
|
|
init_with_remote
|
|
cd "$SECRETS_DIR"
|
|
|
|
mkdir -p testproj
|
|
echo "encrypted-blob" > testproj/.env.test.age
|
|
git add testproj/.env.test.age
|
|
|
|
run git commit -m "should succeed"
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
# ─── clear ─────────────────────────────────────────────────────────────
|
|
|
|
@test "clear removes plaintext secret files" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
echo "CF_SECRET=wrangler123" > "$WORK_DIR/testproj/.dev.vars"
|
|
|
|
# Verify files exist
|
|
[ -f "$WORK_DIR/testproj/.env" ]
|
|
[ -f "$WORK_DIR/testproj/.env.staging" ]
|
|
[ -f "$WORK_DIR/testproj/.dev.vars" ]
|
|
|
|
run "$SECRETS_BIN" clear
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"Cleared 3"* ]]
|
|
|
|
# Files should be gone
|
|
[ ! -f "$WORK_DIR/testproj/.env" ]
|
|
[ ! -f "$WORK_DIR/testproj/.env.staging" ]
|
|
[ ! -f "$WORK_DIR/testproj/.dev.vars" ]
|
|
}
|
|
|
|
@test "clear does nothing when no secret files exist" {
|
|
mkdir -p "$WORK_DIR/empty"
|
|
cd "$WORK_DIR/empty"
|
|
|
|
run "$SECRETS_BIN" clear
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"No secret files"* ]]
|
|
}
|
|
|
|
@test "clear does not remove non-secret files" {
|
|
mkdir -p "$WORK_DIR/mixed"
|
|
cd "$WORK_DIR/mixed"
|
|
echo "SECRET=yes" > .env
|
|
echo "config" > .envrc
|
|
echo "other" > app.js
|
|
|
|
run "$SECRETS_BIN" clear
|
|
[ "$status" -eq 0 ]
|
|
[ ! -f "$WORK_DIR/mixed/.env" ]
|
|
[ -f "$WORK_DIR/mixed/.envrc" ]
|
|
[ -f "$WORK_DIR/mixed/app.js" ]
|
|
}
|
|
|
|
@test "clear --workspaces removes secrets from all workspaces" {
|
|
local mono
|
|
mono=$(create_monorepo)
|
|
cd "$mono"
|
|
|
|
# Verify files exist
|
|
[ -f "$mono/.env" ]
|
|
[ -f "$mono/apps/web/.env.staging" ]
|
|
[ -f "$mono/apps/api/.env" ]
|
|
|
|
run "$SECRETS_BIN" clear --workspaces
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"Cleared"* ]]
|
|
|
|
# All should be gone
|
|
[ ! -f "$mono/.env" ]
|
|
[ ! -f "$mono/apps/web/.env.staging" ]
|
|
[ ! -f "$mono/apps/api/.env" ]
|
|
}
|
|
|
|
# ─── run ───────────────────────────────────────────────────────────────
|
|
|
|
@test "run pulls secrets, runs command, then clears" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
|
|
# Remove plaintext files
|
|
rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging"
|
|
|
|
# Run a command that reads the secret
|
|
run "$SECRETS_BIN" run cat .env
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"SECRET_KEY=abc123"* ]]
|
|
|
|
# After run completes, plaintext files should be cleared
|
|
[ ! -f "$WORK_DIR/testproj/.env" ]
|
|
[ ! -f "$WORK_DIR/testproj/.env.staging" ]
|
|
}
|
|
|
|
@test "run clears secrets even if command fails" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
|
|
# Remove plaintext files
|
|
rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging"
|
|
|
|
# Run a command that will fail (set +e so bats captures it)
|
|
run "$SECRETS_BIN" run false
|
|
[ "$status" -ne 0 ]
|
|
|
|
# Secrets should still be cleared
|
|
[ ! -f "$WORK_DIR/testproj/.env" ]
|
|
[ ! -f "$WORK_DIR/testproj/.env.staging" ]
|
|
}
|
|
|
|
@test "run errors with no command" {
|
|
run "$SECRETS_BIN" run
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"Usage"* ]]
|
|
}
|
|
|
|
@test "run passes arguments through to command" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging"
|
|
|
|
# Run with multiple args
|
|
run "$SECRETS_BIN" run ls -la .env
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *".env"* ]]
|
|
}
|
|
|
|
@test "run supports -- separator" {
|
|
init_with_remote
|
|
create_project_dir testproj
|
|
"$SECRETS_BIN" push testproj >/dev/null 2>&1
|
|
rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging"
|
|
|
|
run "$SECRETS_BIN" run -- cat .env
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"SECRET_KEY=abc123"* ]]
|
|
}
|
|
|
|
# ─── workspaces ────────────────────────────────────────────────────────
|
|
|
|
# Helper: create a monorepo with package.json workspaces
|
|
create_monorepo() {
|
|
local dir="$WORK_DIR/myapp"
|
|
mkdir -p "$dir/apps/web" "$dir/apps/api" "$dir/packages/auth"
|
|
|
|
cat > "$dir/package.json" << 'PKGJSON'
|
|
{
|
|
"name": "myapp",
|
|
"workspaces": ["apps/*", "packages/*"]
|
|
}
|
|
PKGJSON
|
|
|
|
# Root env
|
|
echo "ROOT_SECRET=top" > "$dir/.env"
|
|
# Workspace envs
|
|
echo "WEB_DB=webdb" > "$dir/apps/web/.env.staging"
|
|
echo "API_KEY=abc" > "$dir/apps/api/.env"
|
|
# packages/auth has no .env — should be skipped silently
|
|
|
|
# Init a git repo so derive_project_name can use dirname
|
|
git init "$dir" >/dev/null 2>&1
|
|
echo "$dir"
|
|
}
|
|
|
|
@test "push --workspaces encrypts root and workspace env files" {
|
|
init_with_remote
|
|
local mono
|
|
mono=$(create_monorepo)
|
|
cd "$mono"
|
|
|
|
run "$SECRETS_BIN" push --workspaces
|
|
[ "$status" -eq 0 ]
|
|
|
|
# Root env
|
|
[ -f "$SECRETS_DIR/myapp/.env.age" ]
|
|
# Workspace envs
|
|
[ -f "$SECRETS_DIR/myapp/apps/web/.env.staging.age" ]
|
|
[ -f "$SECRETS_DIR/myapp/apps/api/.env.age" ]
|
|
# packages/auth should NOT have a dir (no .env files)
|
|
[ ! -d "$SECRETS_DIR/myapp/packages/auth" ]
|
|
}
|
|
|
|
@test "pull --workspaces decrypts into correct directories" {
|
|
init_with_remote
|
|
local mono
|
|
mono=$(create_monorepo)
|
|
cd "$mono"
|
|
"$SECRETS_BIN" push --workspaces >/dev/null 2>&1
|
|
|
|
# Remove the original env files
|
|
rm "$mono/.env" "$mono/apps/web/.env.staging" "$mono/apps/api/.env"
|
|
|
|
run "$SECRETS_BIN" pull --workspaces
|
|
[ "$status" -eq 0 ]
|
|
|
|
# Verify decrypted into correct locations
|
|
[ "$(cat "$mono/.env")" = "ROOT_SECRET=top" ]
|
|
[ "$(cat "$mono/apps/web/.env.staging")" = "WEB_DB=webdb" ]
|
|
[ "$(cat "$mono/apps/api/.env")" = "API_KEY=abc" ]
|
|
}
|
|
|
|
@test "push --workspaces errors without package.json" {
|
|
init_with_remote
|
|
mkdir -p "$WORK_DIR/nopkg"
|
|
cd "$WORK_DIR/nopkg"
|
|
|
|
run "$SECRETS_BIN" push --workspaces
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"No package.json"* ]]
|
|
}
|
|
|
|
@test "push --workspaces errors without workspaces field" {
|
|
init_with_remote
|
|
mkdir -p "$WORK_DIR/nows"
|
|
echo '{"name": "nows"}' > "$WORK_DIR/nows/package.json"
|
|
cd "$WORK_DIR/nows"
|
|
|
|
run "$SECRETS_BIN" push --workspaces
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"No workspaces"* ]]
|
|
}
|
|
|
|
@test "push --workspaces errors when no env files anywhere" {
|
|
init_with_remote
|
|
local dir="$WORK_DIR/empty-mono"
|
|
mkdir -p "$dir/apps/web" "$dir/packages/lib"
|
|
cat > "$dir/package.json" << 'EOF'
|
|
{"workspaces": ["apps/*", "packages/*"]}
|
|
EOF
|
|
git init "$dir" >/dev/null 2>&1
|
|
cd "$dir"
|
|
|
|
run "$SECRETS_BIN" push --workspaces
|
|
[ "$status" -eq 1 ]
|
|
[[ "$output" == *"No secret files"* ]]
|
|
}
|