diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5adc518 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/ptyunit"] + path = tests/ptyunit + url = https://github.com/fissible/ptyunit diff --git a/tests/integration/test-tui.sh b/tests/integration/test-tui.sh new file mode 100644 index 0000000..292347c --- /dev/null +++ b/tests/integration/test-tui.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +source tests/ptyunit/assert.sh + +ptyunit_require_bash 4 3 # HISTORY[-1] requires bash 4.3+ + +PTY_INIT=0.4 # give the TUI time to render before sending keys +PTY_TIMEOUT=8 + +PASSGEN="./passgen" +PTY="python3 tests/ptyunit/pty_run.py" + +# ── Basic rendering ──────────────────────────────────────────────────────────── + +describe "TUI rendering" + test_that "TUI launches and renders password label" + out=$($PTY "$PASSGEN" q) + assert_contains "$out" "Password" + + test_that "TUI renders Base64 line" + out=$($PTY "$PASSGEN" q) + assert_contains "$out" "Base64:" + + test_that "TUI renders SHA-256 line" + out=$($PTY "$PASSGEN" q) + assert_contains "$out" "SHA-256:" + + test_that "TUI renders strength indicator" + out=$($PTY "$PASSGEN" q) + assert_contains "$out" "Strength:" + + test_that "TUI renders hotkey hints" + out=$($PTY "$PASSGEN" q) + assert_contains "$out" "new" + assert_contains "$out" "quit" +end_describe + +# ── Key handling ─────────────────────────────────────────────────────────────── + +describe "key handling" + test_that "r key regenerates password" + out=$($PTY "$PASSGEN" r q) + assert_eq "0" "$?" + assert_contains "$out" "Password" + + test_that "Enter regenerates password" + out=$($PTY "$PASSGEN" ENTER q) + assert_eq "0" "$?" + assert_contains "$out" "Password" + + test_that "Space regenerates password" + out=$($PTY "$PASSGEN" SPACE q) + assert_eq "0" "$?" + assert_contains "$out" "Password" + + test_that "+ key increases displayed length" + out=$($PTY "$PASSGEN" + q) + assert_contains "$out" "28" # default 24 + 4 + + test_that "++ increases length by 8" + out=$($PTY "$PASSGEN" + + q) + assert_contains "$out" "32" # 24 + 4 + 4 + + test_that "- key decreases displayed length" + out=$($PTY "$PASSGEN" - q) + assert_contains "$out" "20" # 24 - 4 + + test_that "1 preset sets length to 4" + out=$($PTY "$PASSGEN" 1 q) + assert_contains "$out" "Length: 4" + + test_that "8 preset sets length to 64" + out=$($PTY "$PASSGEN" 8 q) + assert_contains "$out" "Length: 64" +end_describe + +# ── DB-safe mode ─────────────────────────────────────────────────────────────── + +describe "DB-safe mode" + test_that "R key shows DB-safe label" + out=$($PTY "$PASSGEN" R q) + assert_contains "$out" "DB-safe" + + test_that "s key after R clears DB-safe mode" + out=$($PTY "$PASSGEN" R s q) + # DB-safe label should no longer be in the hotkey/status area after toggle + # (it may still appear in history display; just confirm the mode changed) + assert_eq "0" "$?" +end_describe + +# ── History navigation ───────────────────────────────────────────────────────── + +describe "history navigation" + test_that "three generations creates history of 3" + out=$($PTY "$PASSGEN" r r r q) + assert_contains "$out" "3/3" + + test_that "< key navigates back in history" + out=$($PTY "$PASSGEN" r r r '<' q) + assert_contains "$out" "2/3" + + test_that "< < navigates back two steps" + out=$($PTY "$PASSGEN" r r r '<' '<' q) + assert_contains "$out" "1/3" + + test_that "> key navigates forward after going back" + out=$($PTY "$PASSGEN" r r r '<' '>' q) + assert_contains "$out" "3/3" +end_describe + +# ── Help screen ──────────────────────────────────────────────────────────────── + +describe "help screen" + test_that "? key opens help screen" + out=$($PTY "$PASSGEN" '?' q) + assert_contains "$out" "Controls:" + + test_that "help screen lists key bindings" + out=$($PTY "$PASSGEN" '?' q) + assert_contains "$out" "Generate new password" + + test_that "any key dismisses help and returns to main UI" + out=$($PTY "$PASSGEN" '?' SPACE q) + assert_contains "$out" "Password" + assert_contains "$out" "Strength:" +end_describe + +ptyunit_test_summary diff --git a/tests/ptyunit b/tests/ptyunit new file mode 160000 index 0000000..072c3b2 --- /dev/null +++ b/tests/ptyunit @@ -0,0 +1 @@ +Subproject commit 072c3b2a99e205a8f8744d0157ef7142ac1d9b28 diff --git a/tests/run.sh b/tests/run.sh new file mode 100644 index 0000000..0babc99 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run the passgen test suite via ptyunit. +# Usage: bash tests/run.sh [ptyunit run.sh flags] +# e.g. bash tests/run.sh --unit +# bash tests/run.sh --filter cli +exec bash tests/ptyunit/run.sh "$@" diff --git a/tests/unit/test-cli.sh b/tests/unit/test-cli.sh new file mode 100644 index 0000000..bac4351 --- /dev/null +++ b/tests/unit/test-cli.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +source tests/ptyunit/assert.sh + +PASSGEN="./passgen" + +# ── Password output ──────────────────────────────────────────────────────────── + +describe "password output" + test_that "quiet mode outputs exactly one line" + run "$PASSGEN" -q + assert_eq "0" "$status" + assert_eq "1" "$(echo "$output" | wc -l | tr -d ' ')" + + test_that "generated password matches requested length" + run "$PASSGEN" -l 32 -q + assert_eq "0" "$status" + assert_eq "32" "${#output}" + + test_that "default length is 24" + run "$PASSGEN" -q + assert_eq "24" "${#output}" + + test_that "minimum length (4) is respected" + run "$PASSGEN" -l 4 -q + assert_eq "0" "$status" + assert_eq "4" "${#output}" + + test_that "maximum length (128) is respected" + run "$PASSGEN" -l 128 -q + assert_eq "0" "$status" + assert_eq "128" "${#output}" + + test_that "-n flag suppresses trailing newline" + result=$(./passgen -q -n | wc -c | tr -d ' ') + assert_eq "24" "$result" +end_describe + +# ── Character sets ───────────────────────────────────────────────────────────── + +describe "character sets" + test_that "default password contains mixed character classes" + run "$PASSGEN" -l 32 -q + assert_match '[a-z]' "$output" + assert_match '[A-Z]' "$output" + assert_match '[0-9]' "$output" + + test_that "simple mode produces alphanumeric-only output" + run "$PASSGEN" -s -l 32 -q + assert_eq "0" "$status" + assert_false echo "$output" | grep -q '[^A-Za-z0-9]' + + test_that "DB-safe password contains only DB-safe special chars" + run "$PASSGEN" -d -l 32 -q + assert_eq "0" "$status" + assert_false echo "$output" | grep -q '[^A-Za-z0-9_.\-]' + + test_that "DB-safe password never starts with - or ." + for i in 1 2 3 4 5; do + run "$PASSGEN" -d -l 24 -q + first="${output:0:1}" + assert_not_eq "-" "$first" "password should not start with -" + assert_not_eq "." "$first" "password should not start with ." + done + + test_that "password never starts with - or . in default mode" + for i in 1 2 3 4 5; do + run "$PASSGEN" -l 24 -q + first="${output:0:1}" + assert_not_eq "-" "$first" + assert_not_eq "." "$first" + done +end_describe + +# ── Flag conflict: -d vs -s ──────────────────────────────────────────────────── +# Bug: flag ordering determines winner. -d should always win over -s since +# DB-safe implies a specific charset requirement. + +describe "flag conflicts" + test_that "-d -s: DB-safe flag wins, password contains DB-safe special chars" + run "$PASSGEN" -d -s -l 32 -q + assert_eq "0" "$status" + assert_match '[_.\-]' "$output" + + test_that "-s -d: DB-safe flag wins regardless of order" + run "$PASSGEN" -s -d -l 32 -q + assert_eq "0" "$status" + assert_match '[_.\-]' "$output" +end_describe + +# ── Error handling ───────────────────────────────────────────────────────────── + +describe "error handling" + test_that "-l without a value exits non-zero" + run "$PASSGEN" -l + assert_not_eq "0" "$status" + + test_that "length below minimum exits non-zero" + run "$PASSGEN" -l 3 -q + assert_not_eq "0" "$status" + + test_that "length above maximum exits non-zero" + run "$PASSGEN" -l 129 -q + assert_not_eq "0" "$status" + + test_that "unknown flag exits non-zero" + run "$PASSGEN" --bogus + assert_not_eq "0" "$status" +end_describe + +# ── Non-quiet output structure ───────────────────────────────────────────────── + +describe "verbose output" + test_that "non-quiet output includes Password label" + run "$PASSGEN" -l 16 + assert_eq "0" "$status" + assert_contains "$output" "Password" + + test_that "non-quiet output includes Base64 line" + run "$PASSGEN" -l 16 + assert_contains "$output" "Base64:" + + test_that "non-quiet output includes SHA-256 line" + run "$PASSGEN" -l 16 + assert_contains "$output" "SHA-256:" + + test_that "DB-safe non-quiet output includes DB-safe label" + run "$PASSGEN" -d -l 16 + assert_contains "$output" "DB-safe" +end_describe + +ptyunit_test_summary