zscripts-token-savers/tests/bash/args.bats
kellymichels 37a703ce19
test: bats suite for the bash port, + fix underscore-key guard (phase 5 of #26) (#32)
* test: add bats suite for the bash port + fix underscore-key guard (phase 5)

Part of #26. Closes #30.

The bash port reimplements the exclude lists, config accessors and argument
parsing, so it can drift from PowerShell independently. 50 bats tests mirror
the Pester suites assertion-for-assertion where the two are meant to agree:
z_archive_excludes (per-kind lists and the deploy-vs-backup gating), config
accessors, z_path Windows->WSL translation, json_build_label, and argument
handling (bare invocation, unknown key, 'all' expansion, --port override).

Fixes #30 along the way, because the alternative was a test enshrining the bug:
zproj_require accepted underscore comment keys. It only checked the key was
non-null, and a comment is a non-null JSON string, so 'zkill _note' sailed
through and exited 0 having done nothing - the silent-success failure mode.
Now rejects any _-prefixed key and requires the value to be a JSON object.
Both checks earn their place: the type check catches string comments, the
prefix rule catches an object-valued _template key that PowerShell refuses and
the type check alone would allow.

Documents #31 rather than fixing it: a leading dash on a project key works in
every PowerShell script but only in bash/zdeploy - the others reject -myapp as
an unknown option. Stripping it everywhere would make a mistyped flag resolve
as a project key, so the tests pin current behaviour and bash/README.md now
states the difference instead of the README's blanket claim.

Verified by mutation testing: all 9 mutations turn the suite red - removing the
python and vite backup gates, reintroducing #23 in bash, unfiltering underscore
keys in zproj_keys and zproj_require, breaking zremote_compose_dir fallback and
z_path translation, and removing zkill's all-expansion and no-args guard. Both
mutated files confirmed restored byte-for-byte.

An early run also caught a bug in the tests themselves: the membership helper
used 'grep -qx' (regex), so the needle '.env' matched 'venv' and several
'excludes .env' assertions were false passes. Now uses -qxF.

* fix(gitattributes): keep .bats files LF so 'bats tests/bash' works on a Windows checkout

The LF rule was scoped to 'bash/**', which does not match tests/bash/. With
core.autocrlf a Windows working copy got CRLF .bats files, and bats fails on
them - so the command the README documents would not run on the machine the
suite was written on without stripping \r first.

Adds tests/bash/** and *.bats to the same eol=lf rule and renormalises.
Verified by running 'bats tests/bash' with no sed preprocessing: 50/50.
2026-07-26 13:02:23 -05:00

233 lines
8.2 KiB
Bash

#!/usr/bin/env bats
# Evomedia.net Token Savers - https://github.com/kellymichels/zscripts-token-savers
# Created by Kelly Michels - dev@evomedia.net
# Licensed under the MIT License. See LICENSE.
#
# args.bats - how the bash scripts read their target arguments.
#
# bats tests/bash
#
# Mirrors tests/ArgumentParsing.Tests.ps1. Same safety rule as the PowerShell
# side: these run the real scripts, so they stay on code paths that exit BEFORE
# doing any work (no args, unknown key). Only zkill runs with a valid target,
# and only because the fixture's dev ports are deliberately unused and its
# localRoots do not exist - it finds no listeners and kills nothing. zdeploy,
# zbackup_ec2, zec2, zec2online, zrepair, zstop, zstart and zbackup are never
# invoked with a real target; that needs a disposable server.
setup() {
REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
BASHDIR="$REPO/bash"
export ZCONFIG="$BATS_TEST_TMPDIR/zconfig.json"
cat > "$ZCONFIG" <<'JSON'
{
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/fixtures/test.pem", "stackRoot": "/home/testuser/stack" },
"paths": { "temp": "/fixtures/temp", "backupsLocal": "/fixtures/backups", "backupsEc2": "/fixtures/backups-ec2", "scriptsRoot": "/fixtures/zscripts", "oneDriveBackups": "" },
"projects": {
"_note": "comment key - must never be treated as a project",
"pyapp": {
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp",
"startModule": "pyapp.main", "ports": { "dev": 59990 },
"remote": { "path": "/home/testuser/stack/pyapp" }
},
"viteapp": {
"label": "Fixture Vite Site", "kind": "vite", "localRoot": "/fixtures/nonexistent-viteapp",
"ports": { "dev": 59991 }, "remote": { "path": "/home/testuser/stack/viteapp" }
},
"edgeproxy": {
"label": "Fixture Edge", "kind": "edge", "localRoot": "/fixtures/nonexistent-edge",
"remote": { "path": "/home/testuser/stack/edge" }
}
}
}
JSON
}
# ---- no arguments -----------------------------------------------------------
# Running bare must never mean "do it to everything".
@test "zkill with no args shows usage and exits 1" {
run "$BASHDIR/zkill"
[ "$status" -eq 1 ]
[[ "$output" == *"Usage"* ]]
}
@test "zstart with no args shows usage and exits non-zero" {
run "$BASHDIR/zstart"
[ "$status" -ne 0 ]
[[ "$output" == *"Usage"* ]]
}
@test "zdeploy with no args shows usage and exits non-zero" {
run "$BASHDIR/zdeploy"
[ "$status" -ne 0 ]
[[ "$output" == *"Usage"* ]]
}
@test "zbackup with no args shows usage and exits non-zero" {
run "$BASHDIR/zbackup"
[ "$status" -ne 0 ]
[[ "$output" == *"Usage"* ]]
}
@test "usage lists the configured project keys" {
run "$BASHDIR/zkill"
[[ "$output" == *"pyapp"* ]]
[[ "$output" == *"viteapp"* ]]
}
@test "usage never advertises the underscore comment key" {
run "$BASHDIR/zkill"
[[ "$output" != *"_note"* ]]
}
# ---- unknown key ------------------------------------------------------------
# Exiting 0 having selected nothing is the dangerous outcome.
@test "zkill rejects an unknown project key" {
run "$BASHDIR/zkill" definitelynotaproject
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown project key"* ]]
}
@test "zstart rejects an unknown project key" {
run "$BASHDIR/zstart" definitelynotaproject
[ "$status" -ne 0 ]
}
@test "zdeploy rejects an unknown project key" {
run "$BASHDIR/zdeploy" definitelynotaproject
[ "$status" -ne 0 ]
}
@test "the unknown-key error names both the bad key and the valid ones" {
run "$BASHDIR/zkill" definitelynotaproject
[[ "$output" == *"definitelynotaproject"* ]]
[[ "$output" == *"pyapp"* ]]
}
@test "the underscore comment key is not addressable as a project" {
run "$BASHDIR/zkill" _note
[ "$status" -ne 0 ]
}
# The convention is that ANY "_"-prefixed key is a comment, not a project -
# PowerShell's Get-ZProject refuses them outright. A string-valued comment is
# also caught by the "is it an object?" check, so this uses an OBJECT-valued
# underscore key (e.g. someone keeping a "_template" block to copy from) to
# prove the prefix rule itself is enforced.
@test "an object-valued underscore key is still not addressable" {
cat > "$BATS_TEST_TMPDIR/objkey.json" <<'JSON'
{
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/f/t.pem", "stackRoot": "/s" },
"paths": { "temp": "/f/t", "backupsLocal": "/f/b", "backupsEc2": "/f/be", "scriptsRoot": "/f/z", "oneDriveBackups": "" },
"projects": {
"_template": {
"label": "Template To Copy", "kind": "python", "localRoot": "/fixtures/none",
"ports": { "dev": 59993 }, "remote": { "path": "/s/t" }
},
"pyapp": {
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp",
"ports": { "dev": 59990 }, "remote": { "path": "/s/py" }
}
}
}
JSON
ZCONFIG="$BATS_TEST_TMPDIR/objkey.json" run "$BASHDIR/zkill" _template
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown project key"* ]]
}
@test "an object-valued underscore key is excluded from 'all'" {
cat > "$BATS_TEST_TMPDIR/objkey2.json" <<'JSON'
{
"ec2": { "ip": "203.0.113.10", "user": "testuser", "pemKey": "/f/t.pem", "stackRoot": "/s" },
"paths": { "temp": "/f/t", "backupsLocal": "/f/b", "backupsEc2": "/f/be", "scriptsRoot": "/f/z", "oneDriveBackups": "" },
"projects": {
"_template": {
"label": "Template To Copy", "kind": "python", "localRoot": "/fixtures/none",
"ports": { "dev": 59993 }, "remote": { "path": "/s/t" }
},
"pyapp": {
"label": "Fixture Python App", "kind": "python", "localRoot": "/fixtures/nonexistent-pyapp",
"ports": { "dev": 59990 }, "remote": { "path": "/s/py" }
}
}
}
JSON
ZCONFIG="$BATS_TEST_TMPDIR/objkey2.json" run "$BASHDIR/zkill" all
[ "$status" -eq 0 ]
[[ "$output" == *"Fixture Python App"* ]]
[[ "$output" != *"Template To Copy"* ]]
}
# ---- leading dash: CURRENT bash behaviour ------------------------------------
# NOTE: this is a known divergence from the PowerShell port, pinned here as it
# actually behaves rather than as it ideally would. PowerShell tolerates a
# leading dash on any project key (zdeploy -myapp == zdeploy myapp). In bash,
# only zdeploy strips it ("${1#-}"); every other script treats -myapp as an
# unknown option and exits 1. These tests document today's behaviour so a fix
# is a deliberate change with a failing test to update, not a silent surprise.
@test "bash zdeploy strips a leading dash from a project key" {
# Unknown key keeps this on the no-work path; the error must name the key
# without its dash, proving the strip happened.
run "$BASHDIR/zdeploy" -definitelynotaproject
[ "$status" -ne 0 ]
[[ "$output" == *"definitelynotaproject"* ]]
[[ "$output" != *"-definitelynotaproject"* ]]
}
@test "bash zkill rejects a dashed key as an unknown option (diverges from PowerShell)" {
run "$BASHDIR/zkill" -pyapp
[ "$status" -eq 1 ]
[[ "$output" == *"Unknown option"* ]]
}
@test "bash zstart rejects a dashed key as an unknown option (diverges from PowerShell)" {
run "$BASHDIR/zstart" -pyapp
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown option"* ]]
}
# ---- zkill target resolution (safe: fixture ports are unused) ----------------
@test "zkill accepts a bare project key" {
run "$BASHDIR/zkill" pyapp
[ "$status" -eq 0 ]
[[ "$output" == *"Fixture Python App"* ]]
[[ "$output" == *"59990"* ]]
}
@test "zkill accepts several keys at once" {
run "$BASHDIR/zkill" pyapp viteapp
[ "$status" -eq 0 ]
[[ "$output" == *"Fixture Python App"* ]]
[[ "$output" == *"Fixture Vite Site"* ]]
}
@test "zkill 'all' expands to every project with a dev port" {
run "$BASHDIR/zkill" all
[ "$status" -eq 0 ]
[[ "$output" == *"Fixture Python App"* ]]
[[ "$output" == *"Fixture Vite Site"* ]]
}
@test "zkill 'all' skips projects with no dev port (edge/docker stacks)" {
run "$BASHDIR/zkill" all
[[ "$output" != *"Fixture Edge"* ]]
}
@test "zkill --port overrides the configured dev port" {
run "$BASHDIR/zkill" pyapp --port 59999
[ "$status" -eq 0 ]
[[ "$output" == *"59999"* ]]
[[ "$output" != *"59990"* ]]
}
@test "zkill announces that --kill-all is not implemented rather than silently ignoring it" {
run "$BASHDIR/zkill" pyapp --kill-all
[ "$status" -eq 0 ]
[[ "$output" == *"kill-all"* ]]
}