test: coverage for external-file workspace/multi-entry/warning paths (EGB-531)

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.
This commit is contained in:
Brian Majewski 2026-05-26 14:26:41 -07:00
parent 110ac514cc
commit 31f2741c44
4 changed files with 118 additions and 3 deletions

View file

@ -28,7 +28,7 @@ and this project adheres to a four-digit MAJOR.MINOR.PATCH.MICRO version scheme.
### Tests ### 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 ## [0.1.1.0] - 2026-05-09

View file

@ -37,7 +37,7 @@ Single bash script (`secrets`) with subcommands: init, push, pull, list, rm, rek
secrets # CLI script (~600 lines bash) secrets # CLI script (~600 lines bash)
hooks/pre-commit # Pre-commit hook template hooks/pre-commit # Pre-commit hook template
test/ test/
secrets.bats # bats-core test suite (104 tests) secrets.bats # bats-core test suite (112 tests)
test_helper.bash # Shared setup/teardown test_helper.bash # Shared setup/teardown
README.md # User-facing documentation README.md # User-facing documentation
CLAUDE.md # This file CLAUDE.md # This file

View file

@ -432,7 +432,7 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/`
## Development ## Development
```bash ```bash
# Run the test suite (104 tests) # Run the test suite (112 tests)
brew install bats-core brew install bats-core
bats test/secrets.bats bats test/secrets.bats
``` ```

View file

@ -1349,3 +1349,118 @@ gradle_project() {
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
[[ "$output" == *"Plaintext"* ]] [[ "$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"
}