Skip to content

Commit 226c1f3

Browse files
locus313Copilot
andauthored
feat: add bats unit tests for lib/github-common.sh (#41)
* feat: add bats unit tests for lib/github-common.sh 15 tests covering the shared library's pure-logic functions and gh_api sentinel returns — all testable without a real GitHub token. - validate_slug: valid (alphanumeric/hyphen/underscore) and invalid (space/slash/dot/metachar) inputs - require_env_var: unset, empty, and set variable cases - require_command: found and not-found cases - gh_api: __404__, __422__ sentinels and 200 body pass-through (curl is mocked per-test via a temporary PATH-prepended binary) Also adds a 'test' job to ci.yml that installs bats and runs the suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: expand test suite to 91 tests covering all scripts and lib functions test_common.bats (29 tests): - Add err, configure_gh_auth, validate_token, validate_github_token URL warning, get_repo_page_count (with/without Link header), gh_api_paginate (404, 422, single-page) - Upgrade _mock_curl to use universal mock_curl.sh (handles both gh_api stdout mode and gh_api_paginate/validate_token -o/-D file mode) tests/mock_curl.sh: - Universal drop-in curl mock; response data via env vars (MOCK_CURL_CODE, MOCK_CURL_BODY, MOCK_CURL_LINK) — no quoting issues with JSON bodies test_script_validation.bats (62 tests): - Every script: missing required env vars exit 1 in the correct order - Arg parsing: --help exits 0, unknown args exit 1, recognised flags (--dry-run, --type) don't trigger Unknown argument errors - Invalid enum values: --type, DEPENDABOT_REASON, SECRET_SCANNING_RESOLUTION - github-import-repo: non-GitHub GIT_URL_PREFIX exits 1 (security guard) - github-repo-permissions-report: missing -r exits 1 ci.yml: run bats tests/ (all bats files) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: mandate bats tests for all new scripts in AGENTS.md and copilot-instructions.md - Replaced the placeholder Testing section in AGENTS.md with full bats suite documentation: test files table, what to test per script, and the mock_curl.sh pattern with a code example - Added step 7 (write tests) to the Adding a New Script checklist in AGENTS.md; renumbered subsequent steps - Updated CI/CD bullet to mention the bats test job - Updated Maintenance Matrix 'Add a new script' row to include tests/test_script_validation.bats - Mirrored all changes in .github/copilot-instructions.md: expanded Adding New Scripts checklist with mandatory test step, replaced single-line Testing Approach with table + requirement prose Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add bats test suite documentation to README.md - Add Unit Tests subsection after Pre-commit Hooks: install instructions for macOS/Ubuntu, bats tests/ run command, test-file table, note that CI runs bats on every PR - Update Contributing checklist: add 'add test section to test_script_validation.bats' to step 3, add step 6 'bats tests/' before the test-on-non-prod-org step Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c30f123 commit 226c1f3

7 files changed

Lines changed: 950 additions & 14 deletions

File tree

.github/copilot-instructions.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,33 @@ Uses API version `2026-03-10` and the new usage-metrics NDJSON endpoints (signed
227227
3. Source `lib/github-common.sh` for validation and output helpers
228228
4. Start with the standard boilerplate (see Script Anatomy above)
229229
5. Create `action.yml` in the same directory — expose every env var as a named input (required inputs without defaults, optional inputs with sensible defaults); map CLI flags such as `--dry-run` and `--type` to boolean/string inputs and build an `ARGS` array in the `run:` step. Mirror the pattern of any existing `action.yml`.
230-
6. Document in README.md following existing format:
230+
6. **Add tests to `tests/test_script_validation.bats`** — mandatory. Add a labelled section with tests for every required env var missing (exit 1), unknown CLI args (exit 1), `--help` exits 0, and script-specific validation guards. See existing sections for the pattern.
231+
7. Document in README.md following existing format:
231232
- Use case description
232233
- Required variables table
233234
- Usage example with exports
234235
- Output format (if applicable)
235236
- Add a row to the Available Actions table in the "Using Scripts in GitHub Actions" section
236237

237238
### Testing Approach
238-
- **Always test on a test organization first**
239+
240+
The project has a bats unit-test suite in `tests/`. Run the full suite with:
241+
242+
```bash
243+
bats tests/
244+
```
245+
246+
| File | What it covers |
247+
|------|----------------|
248+
| `tests/test_common.bats` | `lib/github-common.sh` pure-logic functions and API helpers |
249+
| `tests/test_script_validation.bats` | Every script — missing env vars, invalid args, `--help`, script-specific guards |
250+
| `tests/mock_curl.sh` | Universal curl mock used by both test files |
251+
252+
Every new script **must** include a test section in `tests/test_script_validation.bats` before it is merged. At minimum test: each required env var missing exits 1, unknown CLI args exit 1, and any enum or allowlist validation specific to the script.
253+
254+
Additional testing approaches:
255+
- **Always test on a test organization first** before running against production
256+
- **Dry-run flags** — several scripts support `--dry-run` to preview changes without applying them
239257

240258
### Commit Messages — Conventional Commits (required)
241259

@@ -285,7 +303,7 @@ When you change one of these files, you must also update the files in the "Also
285303
| `README.md` — script documentation | Verify the script's `# ===` header comment still matches (env vars, options, requirements) |
286304
| `.githooks/pre-commit` | `install-hooks.sh` if hook path or installation instructions change; README.md Best Practices section |
287305
| `install-hooks.sh` | README.md Installation section |
288-
| Add a new script | `action.yml` in the same directory; `README.md` (add use case, env var table, usage example, Available Actions table row) |
306+
| Add a new script | `tests/test_script_validation.bats` (add a test section for the new script); `action.yml` in the same directory; `README.md` (add use case, env var table, usage example, Available Actions table row) |
289307
| Add a new domain folder | `README.md` top-level structure description; `AGENTS.md` Repository Structure section |
290308
| `.github/workflows/ci.yml` — shellcheck flags | `.githooks/pre-commit` shellcheck invocation (keep them in sync) |
291309
| `.github/workflows/copilot-setup-steps.yml` — tool versions | `AGENTS.md` Tech Stack table |

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,18 @@ jobs:
3333
--severity=warning \
3434
--exclude=SC2034,SC1091 \
3535
--shell=bash
36+
37+
test:
38+
name: Unit Tests
39+
runs-on: ubuntu-latest
40+
permissions:
41+
contents: read
42+
steps:
43+
- name: Checkout code
44+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
45+
46+
- name: Install bats
47+
run: sudo apt-get update -qq && sudo apt-get install -y bats
48+
49+
- name: Run unit tests
50+
run: bats tests/

AGENTS.md

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,58 @@ find . -name "*.sh" | xargs shellcheck --severity=warning --exclude=SC2034,SC109
7979

8080
## Testing
8181

82-
There is no automated test suite. The validation approach is:
82+
The project has a bats unit-test suite in `tests/`. **Every new script must ship with tests.**
8383

84-
1. **Pre-commit hook** — shellcheck on every staged `.sh` file; gitleaks secret scan
85-
2. **Dry-run flags** — several scripts support `--dry-run` to preview changes without applying them:
86-
- `github-close-archived-repo-security-alerts`
87-
- `github-enable-issues`
88-
- `github-organize-stars`
89-
3. **Test org first** — always run against a non-production GitHub org before production
84+
```bash
85+
# Run all tests
86+
bats tests/
87+
88+
# Run a single file
89+
bats tests/test_common.bats
90+
```
91+
92+
### Test files
93+
94+
| File | What it covers |
95+
|------|----------------|
96+
| `tests/test_common.bats` | `lib/github-common.sh` — pure-logic functions (`validate_slug`, `require_env_var`, `require_command`, `err`, `configure_gh_auth`, `validate_token`, `get_repo_page_count`) and API helpers (`gh_api` sentinels, `gh_api_paginate`) |
97+
| `tests/test_script_validation.bats` | Every script — missing required env vars exit 1, invalid CLI args exit 1, `--help` exits 0, script-specific enum/allowlist validation |
98+
| `tests/mock_curl.sh` | Universal drop-in curl mock (used by both test files); response data via env vars `MOCK_CURL_CODE`, `MOCK_CURL_BODY`, `MOCK_CURL_LINK` |
99+
100+
### What to test for every new script
101+
102+
1. **Missing required env vars** — one `@test` per required variable, in the order the script checks them. Each test asserts `status -eq 1`.
103+
2. **Invalid CLI args** — unknown flag exits 1 with an "Unknown" message.
104+
3. **`--help` flag** — exits 0 (for scripts that implement it).
105+
4. **Recognised flags**`--dry-run` and other known flags do not trigger the unknown-arg error (test by asserting output does *not* contain "Unknown").
106+
5. **Script-specific guards** — enum validation (`--type`, `DEPENDABOT_REASON`), URL allowlists (`GIT_URL_PREFIX`), required positional args.
107+
108+
### Mocking pattern
109+
110+
Tests shadow real binaries by prepending a `MOCK_BIN` directory to `PATH`:
111+
112+
```bash
113+
setup() {
114+
MOCK_BIN="$(mktemp -d)"
115+
# Fail gh auth so GITHUB_TOKEN is never auto-resolved from a session
116+
printf '#!/bin/sh\nexit 1\n' > "$MOCK_BIN/gh"
117+
chmod +x "$MOCK_BIN/gh"
118+
}
119+
120+
teardown() { rm -rf "$MOCK_BIN"; }
121+
122+
@test "my-script: exits 1 when GITHUB_TOKEN is not set" {
123+
run bash -c "export PATH='${MOCK_BIN}:${PATH}'; unset GITHUB_TOKEN; bash '${REPO_ROOT}/domain/my-script/my-script.sh'"
124+
[ "$status" -eq 1 ]
125+
}
126+
```
127+
128+
Use `_mock_curl_200` (defined in `test_script_validation.bats`) when a test must reach code that runs after `validate_github_token`.
129+
130+
### Dry-run flags and other testing approaches
131+
132+
- **`--dry-run`** — several scripts support it to preview changes without applying them.
133+
- **Test org first** — always run against a non-production GitHub org before production.
90134

91135
---
92136

@@ -185,8 +229,9 @@ done
185229
4. **Source the shared library** using `SCRIPT_DIR`
186230
5. **Validate all inputs** before any API calls
187231
6. **Create `action.yml`** in the same directory — expose every env var as an input (required inputs first, optional inputs with defaults); map CLI flags (`--dry-run`, `--type`, etc.) to boolean/string inputs and construct the `ARGS` array in the `run:` step. See existing `action.yml` files for the pattern.
188-
7. **Add to README.md** — follow the existing format: use case, env var table, usage example, output format; add a row to the Available Actions table in the "Using Scripts in GitHub Actions" section
189-
8. Place in the correct domain:
232+
7. **Add tests to `tests/test_script_validation.bats`** — add a labelled section (`# ═══ github-<name> ═══`) with tests for: every required env var missing (exit 1), unknown CLI args (exit 1), `--help` exits 0, and any script-specific validation (enum guards, URL allowlists, positional args). See existing sections for the pattern.
233+
8. **Add to README.md** — follow the existing format: use case, env var table, usage example, output format; add a row to the Available Actions table in the "Using Scripts in GitHub Actions" section
234+
9. Place in the correct domain:
190235
- `org-admin/` — organization-level operations (repos, teams, members)
191236
- `enterprise/` — enterprise-level operations (licenses, org enumeration)
192237
- `reporting/` — read-only reports and audits
@@ -199,7 +244,7 @@ done
199244
- **Pre-commit hook:** `.githooks/pre-commit` — runs gitleaks + shellcheck on staged `.sh` files
200245
- **Install:** `./install-hooks.sh` or `git config core.hooksPath .githooks`
201246
- **Bypass (emergency only):** `git commit --no-verify`
202-
- **CI:** shellcheck runs on all `.sh` files on every PR (`.github/workflows/ci.yml`)
247+
- **CI:** shellcheck runs on all `.sh` files on every PR (`.github/workflows/ci.yml`); bats unit tests run in a dedicated `test` job (`bats tests/`)
203248
- **Releases:** automated by Release Please (`.github/workflows/release.yml`) — pushes to `main` trigger a release PR; merging it publishes the GitHub Release and tag
204249

205250
## Commit Messages — Conventional Commits (required)

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,28 @@ brew install gitleaks shellcheck
112112
> [!TIP]
113113
> To bypass the hooks in an emergency: `git commit --no-verify`. Use sparingly — the hooks exist to prevent secrets from reaching the remote.
114114
115+
### Unit Tests
116+
117+
The repository includes a [bats](https://github.com/bats-core/bats-core) unit-test suite in `tests/`. Install bats and run the full suite:
118+
119+
```bash
120+
# macOS
121+
brew install bats-core
122+
123+
# Ubuntu / Debian
124+
sudo apt-get install -y bats
125+
126+
# Run all tests
127+
bats tests/
128+
```
129+
130+
| File | What it covers |
131+
|------|----------------|
132+
| `tests/test_common.bats` | `lib/github-common.sh` functions (`validate_slug`, `require_env_var`, `gh_api` sentinels, `gh_api_paginate`, etc.) |
133+
| `tests/test_script_validation.bats` | Every script — missing required env vars, invalid CLI args, `--help`, and script-specific guards |
134+
135+
CI runs `bats tests/` on every pull request alongside shellcheck.
136+
115137
## Scripts
116138

117139
Each script is a self-contained utility designed for a specific task. Navigate to the script's directory, set the required environment variables, and execute.
@@ -1159,8 +1181,10 @@ Contributions are welcome! Please follow these steps:
11591181
- Start with the `# ===` header and `set -euo pipefail`
11601182
- Source `lib/github-common.sh` and validate all inputs
11611183
- Create `action.yml` in the same directory (see existing actions for the pattern)
1184+
- Add a test section for the new script in `tests/test_script_validation.bats`
11621185
4. **Update README.md** with the env var table, usage example, and a row in the Available Actions table
11631186
5. **Run shellcheck:** `shellcheck --severity=warning --exclude=SC2034,SC1091 --shell=bash your-script.sh`
1164-
6. **Test on a non-production org** before submitting
1187+
6. **Run the test suite:** `bats tests/`
1188+
7. **Test on a non-production org** before submitting
11651189
7. **Commit using [Conventional Commits](https://www.conventionalcommits.org/)** — `CHANGELOG.md` is auto-generated from commit messages; do not edit it manually
11661190
8. **Open a PR** — the PR template will guide you through the checklist

tests/mock_curl.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/sh
2+
# =============================================================================
3+
# mock_curl.sh
4+
#
5+
# Universal drop-in curl mock for bats tests. Copy into a directory that is
6+
# prepended to PATH; the real curl is then shadowed for the duration of a test.
7+
#
8+
# Response data is read from environment variables so callers never embed
9+
# special characters in the script body:
10+
#
11+
# MOCK_CURL_CODE HTTP status code to return (default: 200)
12+
# MOCK_CURL_BODY Response body (default: empty)
13+
# MOCK_CURL_LINK Full URL for Link: next header — set to make the
14+
# response look like a paginated "non-last" page;
15+
# leave empty (default) to signal the final page
16+
#
17+
# Handles two calling conventions used in lib/github-common.sh:
18+
#
19+
# stdout mode (gh_api, get_repo_page_count)
20+
# curl ... (no -o flag)
21+
# Output: <body>\n<code> — gh_api splits on the last line
22+
#
23+
# file mode (gh_api_paginate, validate_token)
24+
# curl ... -o <body-file> [-D <headers-file>]
25+
# Output: <code> (body written to -o file; headers written to -D file)
26+
# =============================================================================
27+
28+
HFILE=""
29+
BFILE=""
30+
31+
# Parse only the flags we care about; everything else is ignored
32+
while [ $# -gt 0 ]; do
33+
case "$1" in
34+
-D) HFILE="$2"; shift 2 ;;
35+
-o) BFILE="$2"; shift 2 ;;
36+
*) shift ;;
37+
esac
38+
done
39+
40+
CODE="${MOCK_CURL_CODE:-200}"
41+
42+
if [ -n "$BFILE" ]; then
43+
# File mode: write body to -o target, write headers to -D target (if set)
44+
printf '%s' "${MOCK_CURL_BODY:-}" > "$BFILE"
45+
if [ -n "$HFILE" ]; then
46+
printf 'HTTP/1.1 %s\r\n' "$CODE" > "$HFILE"
47+
[ -n "${MOCK_CURL_LINK:-}" ] && \
48+
printf 'link: <%s>; rel="next"\r\n' "${MOCK_CURL_LINK:-}" >> "$HFILE"
49+
printf '\r\n' >> "$HFILE"
50+
fi
51+
printf '%s' "$CODE"
52+
else
53+
# Stdout mode: body on first line(s), status code on last line
54+
printf '%s\n%s' "${MOCK_CURL_BODY:-}" "$CODE"
55+
fi

0 commit comments

Comments
 (0)