secrets/test/manifest.bats
Brian Majewski 884da0965c feat: manifest-aware push — generator auto-add, autoAdd toggle, --frozen/--dry-run (EGB-677 stage 1)
- push syncs FROM the manifest; v1 store layout unchanged (nested
  entries land at <project>/<relpath>.age, same shape -w always used)
- discovery (root globs + quiet package.json workspace re-scan when a
  manifest exists) feeds the manifest as a generator; new files auto-add
  with ==> notice + undo guidance
- options.autoAdd committed toggle (default ON when absent); explicit
  false warns on undeclared files instead of enrolling them
- push --frozen: declared-only for one invocation; push --dry-run:
  reports would-add/would-sync, touches nothing
- bootstrap ordering: manifest written only after >=1 blob encrypts
- declared-but-missing warns and continues; unsafe manifest path dies
- jq // falsy gotcha: explicit autoAdd:false compared directly
2026-06-07 08:32:17 -07:00

246 lines
7.7 KiB
Bash

#!/usr/bin/env bats
# EGB-677 stage 1: .secrets.json manifest — parse, rails, add, generators.
load test_helper
# ─── A: manifest core — secrets add + rails + canonical form ──────────
@test "add creates .secrets.json with version 2 and the dotenv entry" {
create_project_dir addproj
run "$SECRETS_BIN" add .env
[ "$status" -eq 0 ]
[ -f ".secrets.json" ]
run jq -r '.version' .secrets.json
[ "$output" = "2" ]
run jq -r '.dotenv[0]' .secrets.json
[ "$output" = ".env" ]
}
@test "add is idempotent — no duplicate entries" {
create_project_dir addproj
"$SECRETS_BIN" add .env >/dev/null
run "$SECRETS_BIN" add .env
[ "$status" -eq 0 ]
run jq -r '.dotenv | length' .secrets.json
[ "$output" = "1" ]
}
@test "add accepts nested workspace paths" {
create_project_dir addproj
mkdir -p packages/web
echo "K=v" > packages/web/.env.development
run "$SECRETS_BIN" add packages/web/.env.development
[ "$status" -eq 0 ]
run jq -r '.dotenv | index("packages/web/.env.development") != null' .secrets.json
[ "$output" = "true" ]
}
@test "add accepts npm-scoped workspace paths (@)" {
create_project_dir addproj
mkdir -p "packages/@acme/web"
echo "K=v" > "packages/@acme/web/.env"
run "$SECRETS_BIN" add "packages/@acme/web/.env"
[ "$status" -eq 0 ]
run jq -r '.dotenv | index("packages/@acme/web/.env") != null' .secrets.json
[ "$output" = "true" ]
}
@test "add rejects path traversal (..)" {
create_project_dir addproj
run "$SECRETS_BIN" add ../escape/.env
[ "$status" -eq 1 ]
[[ "$output" == *"project-relative"* ]] || false
[ ! -f ".secrets.json" ]
}
@test "add rejects absolute paths" {
create_project_dir addproj
run "$SECRETS_BIN" add /etc/passwd
[ "$status" -eq 1 ]
[[ "$output" == *"project-relative"* ]] || false
[ ! -f ".secrets.json" ]
}
@test "add rejects shell metacharacters in path" {
create_project_dir addproj
run "$SECRETS_BIN" add '.env;rm -rf ~'
[ "$status" -eq 1 ]
[ ! -f ".secrets.json" ]
}
@test "add requires the file to exist" {
create_project_dir addproj
run "$SECRETS_BIN" add .env.missing
[ "$status" -eq 1 ]
[[ "$output" == *"not found"* ]] || false
}
@test "manifest serialization is canonical — order of adds does not matter" {
create_project_dir addproj
echo "A=1" > .env.alpha
echo "B=2" > .env.beta
"$SECRETS_BIN" add .env.alpha >/dev/null
"$SECRETS_BIN" add .env.beta >/dev/null
cp .secrets.json "$TEST_TMPDIR/order1.json"
rm .secrets.json
"$SECRETS_BIN" add .env.beta >/dev/null
"$SECRETS_BIN" add .env.alpha >/dev/null
cmp -s .secrets.json "$TEST_TMPDIR/order1.json"
}
@test "which shows manifest summary when .secrets.json is present" {
create_project_dir addproj
"$SECRETS_BIN" add .env >/dev/null
run "$SECRETS_BIN" which
[ "$status" -eq 0 ]
[[ "$output" == *".secrets.json"* ]] || false
[[ "$output" == *".env"* ]] || false
}
@test "malformed .secrets.json dies with a directed error naming the file" {
create_project_dir addproj
echo '{ not json' > .secrets.json
run "$SECRETS_BIN" which
[ "$status" -eq 1 ]
[[ "$output" == *".secrets.json"* ]] || false
[[ "$output" == *"invalid"* ]] || false
}
@test "unsupported manifest version dies with a directed upgrade error" {
create_project_dir addproj
echo '{"version": 99, "dotenv": [".env"]}' > .secrets.json
run "$SECRETS_BIN" which
[ "$status" -eq 1 ]
[[ "$output" == *"version 99"* ]] || false
[[ "$output" == *"supports"* ]] || false
}
@test "symlinked .secrets.json is refused" {
create_project_dir addproj
echo '{"version":2,"dotenv":[".env"]}' > "$TEST_TMPDIR/real-manifest.json"
ln -s "$TEST_TMPDIR/real-manifest.json" .secrets.json
run "$SECRETS_BIN" which
[ "$status" -eq 1 ]
[[ "$output" == *"symlink"* ]] || false
}
# ─── B: push from manifest — generators, autoAdd, --frozen/--dry-run ───
@test "push with manifest syncs nested declared file into v1 store layout" {
init_with_remote
create_project_dir nestproj
mkdir -p packages/web
echo "K=v" > packages/web/.env.development
"$SECRETS_BIN" add packages/web/.env.development >/dev/null
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
[ -f "$SECRETS_DIR/nestproj/packages/web/.env.development.age" ]
}
@test "push auto-adds newly discovered root files to an existing manifest" {
init_with_remote
create_project_dir autoproj
"$SECRETS_BIN" add .env >/dev/null
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
[[ "$output" == *"Added"* ]] || false
run jq -r '.dotenv | index(".env.staging") != null' .secrets.json
[ "$output" = "true" ]
[ -f "$SECRETS_DIR/autoproj/.env.staging.age" ]
}
@test "bootstrap: plain push creates the manifest from discovered files" {
init_with_remote
create_project_dir bootproj
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
[ -f ".secrets.json" ]
run jq -r '.dotenv | length' .secrets.json
[ "$output" = "2" ]
}
@test "failed push leaves no bootstrap manifest behind" {
init_with_remote
mkdir -p "$WORK_DIR/emptyproj"
cd "$WORK_DIR/emptyproj"
run "$SECRETS_BIN" push
[ "$status" -eq 1 ]
[ ! -f ".secrets.json" ]
}
@test "autoAdd=false: undeclared discovered file is warned about, not added or synced" {
init_with_remote
create_project_dir noaddproj
printf '{"version":2,"options":{"autoAdd":false},"dotenv":[".env"]}\n' > .secrets.json
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
[[ "$output" == *"not declared"* ]] || false
run jq -r '.dotenv | index(".env.staging") != null' .secrets.json
[ "$output" = "false" ]
[ -f "$SECRETS_DIR/noaddproj/.env.age" ]
[ ! -f "$SECRETS_DIR/noaddproj/.env.staging.age" ]
}
@test "push --frozen skips auto-add even when autoAdd is on" {
init_with_remote
create_project_dir frozenproj
"$SECRETS_BIN" add .env >/dev/null
run "$SECRETS_BIN" push --frozen
[ "$status" -eq 0 ]
run jq -r '.dotenv | index(".env.staging") != null' .secrets.json
[ "$output" = "false" ]
[ ! -f "$SECRETS_DIR/frozenproj/.env.staging.age" ]
# declared entry still synced under the REAL project name
[ -f "$SECRETS_DIR/frozenproj/.env.age" ]
}
@test "push --dry-run reports would-add entries and changes nothing" {
init_with_remote
create_project_dir dryproj
"$SECRETS_BIN" add .env >/dev/null
cp .secrets.json "$TEST_TMPDIR/manifest-before.json"
run "$SECRETS_BIN" push --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *".env.staging"* ]] || false
cmp -s .secrets.json "$TEST_TMPDIR/manifest-before.json"
[ ! -f "$SECRETS_DIR/dryproj/.env.age" ]
# nothing committed to the store at all
[ "$(git -C "$SECRETS_DIR" rev-list --count HEAD)" -eq 1 ]
}
@test "plain push re-scans package.json workspaces when a manifest exists" {
init_with_remote
local mono="$WORK_DIR/wsproj"
mkdir -p "$mono/packages/api"
printf '{"workspaces": ["packages/*"]}\n' > "$mono/package.json"
echo "ROOT=1" > "$mono/.env"
echo "API=1" > "$mono/packages/api/.dev.vars"
git init "$mono" >/dev/null 2>&1
cd "$mono"
"$SECRETS_BIN" add .env >/dev/null
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
run jq -r '.dotenv | index("packages/api/.dev.vars") != null' .secrets.json
[ "$output" = "true" ]
[ -f "$SECRETS_DIR/wsproj/packages/api/.dev.vars.age" ]
}
@test "declared-but-missing file warns and push continues" {
init_with_remote
create_project_dir missproj
"$SECRETS_BIN" add .env >/dev/null
printf '{"version":2,"dotenv":[".env",".env.gone"]}\n' > .secrets.json
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
[[ "$output" == *".env.gone"* ]] || false
[ -f "$SECRETS_DIR/missproj/.env.age" ]
}
@test "unsafe dotenv entry in a committed manifest dies on push" {
init_with_remote
create_project_dir evilproj
printf '{"version":2,"dotenv":["../escape/.env"]}\n' > .secrets.json
run "$SECRETS_BIN" push
[ "$status" -eq 1 ]
[[ "$output" == *"project-relative"* ]] || false
}