From 31f2741c44ca73805646178d8c9e497db66d9486 Mon Sep 17 00:00:00 2001 From: Brian Majewski Date: Tue, 26 May 2026 14:26:41 -0700 Subject: [PATCH] test: coverage for external-file workspace/multi-entry/warning paths (EGB-531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the high-value gaps surfaced by the ship coverage audit (71% → ~92%): push -w / pull -w external sync, multi-entry manifests, partial-key push warnings, missing-blob pull warnings, unsafe-key-charset rejection, symlinked parent dir refusal, and source-side comment/continuation skipping. Tests: 104 -> 112. Docs updated to match. --- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- README.md | 2 +- test/secrets.bats | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 557254b..b96af01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ and this project adheres to a four-digit MAJOR.MINOR.PATCH.MICRO version scheme. ### Tests -- 80 → 104 (+24). New coverage: manifest parse/read-back, key extraction across `=`/`:`/space separators, merge (preserve unrelated/comments/order, substring-key isolation, sed-metachar value round-trip, duplicate-key collapse, continuation-line safety, idempotency), path validation (wrong basename, outside `$HOME`, symlink), first-create mode `600`, manifest injection/symlink rejection, rekey round-trip of external blobs, glob isolation (blob not leaked to cwd), `list` surfacing, pre-commit blocking plaintext `gradle.properties`, and backward compatibility. +- 80 → 112 (+32). New coverage: manifest parse/read-back, key extraction across `=`/`:`/space separators, merge (preserve unrelated/comments/order, substring-key isolation, sed-metachar value round-trip, duplicate-key collapse, continuation-line safety, idempotency), path validation (wrong basename, outside `$HOME`, symlinked target, symlinked parent dir), first-create mode `600`, manifest injection/symlink/unsafe-key rejection, rekey round-trip of external blobs, glob isolation (blob not leaked to cwd), `list` surfacing, pre-commit blocking plaintext `gradle.properties`, workspace (`push -w`/`pull -w`) external sync, multi-entry manifests, partial-key push warnings, missing-blob pull warnings, source-side comment/continuation skipping, and backward compatibility. ## [0.1.1.0] - 2026-05-09 diff --git a/CLAUDE.md b/CLAUDE.md index 8ada649..c3e001c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ Single bash script (`secrets`) with subcommands: init, push, pull, list, rm, rek secrets # CLI script (~600 lines bash) hooks/pre-commit # Pre-commit hook template test/ - secrets.bats # bats-core test suite (104 tests) + secrets.bats # bats-core test suite (112 tests) test_helper.bash # Shared setup/teardown README.md # User-facing documentation CLAUDE.md # This file diff --git a/README.md b/README.md index 026d375..ded1889 100644 --- a/README.md +++ b/README.md @@ -432,7 +432,7 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/` ## Development ```bash -# Run the test suite (104 tests) +# Run the test suite (112 tests) brew install bats-core bats test/secrets.bats ``` diff --git a/test/secrets.bats b/test/secrets.bats index 9bd5f0b..bd95488 100644 --- a/test/secrets.bats +++ b/test/secrets.bats @@ -1349,3 +1349,118 @@ gradle_project() { [ "$status" -eq 1 ] [[ "$output" == *"Plaintext"* ]] } + +# ── EGB-531: coverage for warning/error branches, workspaces, multi-entry ── + +@test "EGB-531: push -w pushes external blob once at monorepo root" { + init_with_remote + gradle_src $'beaconClerkPkTest=pk_w\n' + local mono="$WORK_DIR/mono" + mkdir -p "$mono/apps/web" + printf '{"workspaces":["apps/*"]}\n' > "$mono/package.json" + echo "ROOT=1" > "$mono/.env" + echo "WEB=1" > "$mono/apps/web/.env" + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > "$mono/.secrets-files" + git init "$mono" >/dev/null 2>&1 + cd "$mono" + run "$SECRETS_BIN" push -w + [ "$status" -eq 0 ] + # External blob pushed exactly once (not once per workspace) + run bash -c "ls $SECRETS_DIR/mono/external/*.gradle-properties.age 2>/dev/null | wc -l | tr -d ' '" + [ "$output" = "1" ] +} + +@test "EGB-531: pull -w merges external keys at monorepo root" { + init_with_remote + gradle_src $'beaconClerkPkTest=pk_w\n' + local mono="$WORK_DIR/mono" + mkdir -p "$mono/apps/web" + printf '{"workspaces":["apps/*"]}\n' > "$mono/package.json" + echo "ROOT=1" > "$mono/.env" + echo "WEB=1" > "$mono/apps/web/.env" + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > "$mono/.secrets-files" + git init "$mono" >/dev/null 2>&1 + cd "$mono" + "$SECRETS_BIN" push -w >/dev/null 2>&1 + gradle_src $'unrelated=keep\n' + run "$SECRETS_BIN" pull -w + [ "$status" -eq 0 ] + grep -q '^beaconClerkPkTest=pk_w$' "$HOME/.gradle/gradle.properties" + grep -q '^unrelated=keep$' "$HOME/.gradle/gradle.properties" +} + +@test "EGB-531: push warns for missing key but still syncs present ones" { + init_with_remote + gradle_src $'beaconClerkPkTest=present\n' + gradle_project gproj + run "$SECRETS_BIN" push gproj + [ "$status" -eq 0 ] + [[ "$output" == *"beaconClerkPkLive"* ]] + [[ "$output" == *"not found"* ]] + [[ "$output" == *"Extracted 1 key"* ]] +} + +@test "EGB-531: pull warns when manifest entry has no blob in store" { + init_with_remote + create_project_dir gproj + "$SECRETS_BIN" push gproj >/dev/null 2>&1 + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > "$WORK_DIR/gproj/.secrets-files" + cd "$WORK_DIR/gproj" + run "$SECRETS_BIN" pull gproj + [ "$status" -eq 0 ] + [[ "$output" == *"no encrypted data exists"* ]] +} + +@test "EGB-531: multi-entry manifest syncs each target" { + init_with_remote + mkdir -p "$HOME/.gradle" "$HOME/.gradle-b" + printf 'beaconClerkPkTest=a\n' > "$HOME/.gradle/gradle.properties" + printf 'beaconClerkPkLive=b\n' > "$HOME/.gradle-b/gradle.properties" + mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj" + printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\ngradle-properties ~/.gradle-b/gradle.properties beaconClerkPkLive\n' > .secrets-files + run "$SECRETS_BIN" push gproj + [ "$status" -eq 0 ] + run bash -c "ls $SECRETS_DIR/gproj/external/*.age 2>/dev/null | wc -l | tr -d ' '" + [ "$output" = "2" ] + printf 'x=1\n' > "$HOME/.gradle/gradle.properties" + printf 'y=1\n' > "$HOME/.gradle-b/gradle.properties" + run "$SECRETS_BIN" pull gproj + [ "$status" -eq 0 ] + grep -q '^beaconClerkPkTest=a$' "$HOME/.gradle/gradle.properties" + grep -q '^beaconClerkPkLive=b$' "$HOME/.gradle-b/gradle.properties" +} + +@test "EGB-531: manifest with unsafe key chars is skipped with warning" { + init_with_remote + mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj" + printf 'gradle-properties ~/.gradle/gradle.properties bad=key\n' > .secrets-files + run "$SECRETS_BIN" which + [ "$status" -eq 0 ] + [[ "$output" == *"WARNING"* ]] + [[ "$output" != *"bad=key"* ]] +} + +@test "EGB-531: symlinked parent dir of target is refused" { + init_with_remote + mkdir -p "$HOME/realdir" + printf 'beaconClerkPkTest=x\n' > "$HOME/realdir/gradle.properties" + ln -s "$HOME/realdir" "$HOME/.gradle" + gradle_project gproj beaconClerkPkTest + run "$SECRETS_BIN" push gproj + [ "$status" -eq 1 ] + [[ "$output" == *"symlink"* ]] +} + +@test "EGB-531: push skips comment and continuation lines in source" { + init_with_remote + mkdir -p "$HOME/.gradle" + printf '%s' $'! bang comment\nunrelated=foo\\\nbeaconClerkPkTest=is_a_continuation\nbeaconClerkPkTest=realkey\n' > "$HOME/.gradle/gradle.properties" + gradle_project gproj beaconClerkPkTest + "$SECRETS_BIN" push gproj >/dev/null 2>&1 + gradle_src $'z=1\n' + run "$SECRETS_BIN" pull gproj + [ "$status" -eq 0 ] + # The continuation line that looks like the key must NOT win; the real + # definition must, and the '!' comment must be ignored. + grep -q '^beaconClerkPkTest=realkey$' "$HOME/.gradle/gradle.properties" +}