diff options
Diffstat (limited to 'languages/typescript/tests')
| -rw-r--r-- | languages/typescript/tests/pre-commit.bats | 150 | ||||
| -rw-r--r-- | languages/typescript/tests/validate-typescript.bats | 125 |
2 files changed, 275 insertions, 0 deletions
diff --git a/languages/typescript/tests/pre-commit.bats b/languages/typescript/tests/pre-commit.bats new file mode 100644 index 0000000..5519baa --- /dev/null +++ b/languages/typescript/tests/pre-commit.bats @@ -0,0 +1,150 @@ +#!/usr/bin/env bats +# +# Tests for languages/typescript/githooks/pre-commit — the secret scan plus +# syntax/lint gate that runs on staged TS/JS files. +# +# The secret scan is the security-critical half and is language-independent, so +# it gets the same coverage here as in the bash bundle: a real key blocks, a +# clean diff passes, and the case-sensitivity split that keeps base64 blobs from +# false-positiving is exercised directly. +# +# Each test builds a throwaway git repo, stages content, and runs the hook from +# inside it — the hook reads `git diff --cached`, so a real index is required. + +HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/githooks/pre-commit" + +setup() { + TEST_DIR="$(mktemp -d -t pre-commit-ts-bats.XXXXXX)" + cd "$TEST_DIR" || exit 1 + git init -q . + git config user.email t@example.com + git config user.name Test + # A base commit so `git diff --cached` has a parent to diff against. + echo "seed" > seed.txt + git add seed.txt + git commit -qm seed +} + +teardown() { + cd / || true + rm -rf "$TEST_DIR" +} + +# ---- Normal ---------------------------------------------------------- + +@test "pre-commit(ts): a clean staged JS file passes (exit 0)" { + printf 'export const f = (x) => x + 1;\n' > ok.js + git add ok.js + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(ts): an empty staging area passes (exit 0)" { + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +# ---- Error: the secret scan ------------------------------------------ + +@test "pre-commit(ts): an AWS key in a staged file blocks (exit 1)" { + printf 'const KEY = "AKIAIOSFODNN7EXAMPLE";\n' > conf.ts + git add conf.ts + run bash "$HOOK" + [ "$status" -eq 1 ] + [[ "$output" == *"potential secret"* ]] +} + +@test "pre-commit(ts): an sk- style token blocks (exit 1)" { + printf 'const TOKEN = "sk-abcdefghijklmnopqrstuvwxyz0123";\n' > conf.ts + git add conf.ts + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +@test "pre-commit(ts): a quoted api_key assignment blocks (exit 1)" { + printf 'const api_key = "abcdefghijklmnopqrstuvwxyz";\n' > conf.ts + git add conf.ts + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +@test "pre-commit(ts): a private-key header blocks (exit 1)" { + printf 'const PEM = "-----BEGIN RSA PRIVATE KEY-----";\n' > conf.ts + git add conf.ts + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +# ---- Boundary: the case-sensitivity split ---------------------------- + +@test "pre-commit(ts): a mixed-case base64 blob does NOT false-positive" { + # The AWS pattern is uppercase-only by design. Under -i it would match any + # 20-char mixed-case run, which random base64 hits often enough to block + # real commits. This is the regression test for that split. + printf 'const BLOB = "AKIAbcdefGHIJklmnOPqr0123456789abcdefGHIJ";\n' > data.ts + git add data.ts + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(ts): a short quoted password value does NOT block" { + # The keyword patterns require 16+ chars, so a placeholder stays quiet. + printf 'const password = "short";\n' > conf.ts + git add conf.ts + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(ts): a secret only in a REMOVED line does not block" { + printf 'const KEY = "AKIAIOSFODNN7EXAMPLE";\n' > conf.ts + git add conf.ts + git commit -qm "add key" + rm conf.ts + git add -A + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +# ---- Error: the syntax gate ------------------------------------------ + +@test "pre-commit(ts): a staged JS syntax error blocks (exit 1)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'const x = ;\n' > bad.js + git add bad.js + run bash "$HOOK" + [ "$status" -eq 1 ] + [[ "$output" == *"syntax"* ]] +} + +@test "pre-commit(ts): a staged TS syntax error blocks (exit 1)" { + command -v tsc >/dev/null 2>&1 || skip "tsc not installed" + printf 'export function f( {\n return 1;\n}\n' > bad.ts + git add bad.ts + run bash "$HOOK" + [ "$status" -eq 1 ] +} + +@test "pre-commit(ts): valid TS-only syntax is NOT read as broken JS" { + command -v tsc >/dev/null 2>&1 || skip "tsc not installed" + # The regression guard for the node --check trap: `node --check` rejects + # valid TypeScript, so using it on .ts would block every real commit. + printf 'interface P { a: string }\nexport const p: P = { a: "x" };\n' > types.ts + git add types.ts + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(ts): a TYPE error that parses does not block (out of scope)" { + command -v tsc >/dev/null 2>&1 || skip "tsc not installed" + printf 'const n: number = "nope";\nexport { n };\n' > typeerr.ts + git add typeerr.ts + run bash "$HOOK" + [ "$status" -eq 0 ] +} + +@test "pre-commit(ts): a broken NON-TS/JS file does not trip the syntax gate" { + printf 'this is (((not javascript\n' > notes.txt + git add notes.txt + run bash "$HOOK" + [ "$status" -eq 0 ] +} diff --git a/languages/typescript/tests/validate-typescript.bats b/languages/typescript/tests/validate-typescript.bats new file mode 100644 index 0000000..c5da5d4 --- /dev/null +++ b/languages/typescript/tests/validate-typescript.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats +# +# Tests for languages/typescript/claude/hooks/validate-typescript.sh — the +# PostToolUse hook that syntax-checks edited TS/JS files and blocks on a +# violation. +# +# The hook reads tool-call JSON on stdin and extracts the file path, so each +# test pipes a JSON payload naming a real file it wrote into a temp dir. +# +# The syntax gate needs node, so those tests skip when node is absent. Full +# type checking is deliberately out of scope for the hook (it needs the whole +# project graph), so a type error that is syntactically valid must pass. + +HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/claude/hooks/validate-typescript.sh" + +setup() { + TEST_DIR="$(mktemp -d -t validate-ts-bats.XXXXXX)" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +payload() { + printf '{"tool_input": {"file_path": "%s"}}' "$1" +} + +# ---- Normal ---------------------------------------------------------- + +@test "validate-typescript: a clean .ts file passes silently (exit 0)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export function f(x: number): number {\n return x + 1;\n}\n' > "$TEST_DIR/clean.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.ts")" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "validate-typescript: a clean .js file passes silently (exit 0)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export function f(x) {\n return x + 1;\n}\n' > "$TEST_DIR/clean.js" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.js")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a .tsx file is validated too" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export const A = 1;\n' > "$TEST_DIR/clean.tsx" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/clean.tsx")" + [ "$status" -eq 0 ] +} + +# ---- Error ----------------------------------------------------------- + +@test "validate-typescript: a syntax error blocks (exit 2, names the failure)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'export function f( {\n return 1;\n}\n' > "$TEST_DIR/bad.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.ts")" + [ "$status" -eq 2 ] + [[ "$output" == *"SYNTAX"* ]] +} + +@test "validate-typescript: the block payload is valid JSON carrying the context" { + command -v node >/dev/null 2>&1 || skip "node not installed" + printf 'const x = ;\n' > "$TEST_DIR/bad.js" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/bad.js")" + [ "$status" -eq 2 ] + echo "$output" | head -1 | jq -e '.hookSpecificOutput.hookEventName == "PostToolUse"' +} + +# ---- Boundary -------------------------------------------------------- + +@test "validate-typescript: a type error that parses is NOT blocked (out of scope)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + # Assigning a string to a number is a type error, not a syntax error. The + # hook checks parseability only; tsc over the project graph owns this. + printf 'const n: number = "not a number";\nexport { n };\n' > "$TEST_DIR/typeerr.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/typeerr.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: TS-only syntax in a .ts file parses (not read as JS)" { + command -v node >/dev/null 2>&1 || skip "node not installed" + # Interfaces and type annotations are invalid JS. Stripping types must happen + # before the parse, or every real .ts file would be reported as broken. + printf 'interface P { a: string }\nexport const p: P = { a: "x" };\n' > "$TEST_DIR/types.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/types.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a non-TS/JS file is ignored (exit 0)" { + printf 'not javascript at all (((\n' > "$TEST_DIR/notes.txt" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/notes.txt")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a .json file is ignored (exit 0)" { + printf '{"a": 1}\n' > "$TEST_DIR/data.json" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/data.json")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: empty file_path is a no-op (exit 0)" { + run bash "$HOOK" <<< '{"tool_input": {}}' + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a missing file is a no-op (exit 0)" { + run bash "$HOOK" <<< "$(payload "$TEST_DIR/does-not-exist.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: an empty .ts file passes" { + command -v node >/dev/null 2>&1 || skip "node not installed" + : > "$TEST_DIR/empty.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/empty.ts")" + [ "$status" -eq 0 ] +} + +@test "validate-typescript: a file in a dotted parent dir is still matched" { + command -v node >/dev/null 2>&1 || skip "node not installed" + mkdir -p "$TEST_DIR/my.project" + printf 'const x = ;\n' > "$TEST_DIR/my.project/bad.ts" + run bash "$HOOK" <<< "$(payload "$TEST_DIR/my.project/bad.ts")" + [ "$status" -eq 2 ] +} |
