|
|
@@ -0,0 +1,269 @@
|
|
|
+#!/bin/bash
|
|
|
+# test-mail.sh - Test harness for mail-ops
|
|
|
+# Outputs: number of passing test cases
|
|
|
+# Each test prints PASS/FAIL and we count PASSes at the end
|
|
|
+
|
|
|
+set -uo pipefail
|
|
|
+
|
|
|
+MAIL_DB="$HOME/.claude/mail.db"
|
|
|
+MAIL_SCRIPT="$(dirname "$0")/mail-db.sh"
|
|
|
+HOOK_SCRIPT="$(dirname "$0")/../../hooks/check-mail.sh"
|
|
|
+# Resolve relative to repo root if needed
|
|
|
+if [ ! -f "$HOOK_SCRIPT" ]; then
|
|
|
+ HOOK_SCRIPT="$(cd "$(dirname "$0")/../../.." && pwd)/hooks/check-mail.sh"
|
|
|
+fi
|
|
|
+
|
|
|
+PASS=0
|
|
|
+FAIL=0
|
|
|
+TOTAL=0
|
|
|
+
|
|
|
+assert() {
|
|
|
+ local name="$1"
|
|
|
+ local expected="$2"
|
|
|
+ local actual="$3"
|
|
|
+ TOTAL=$((TOTAL + 1))
|
|
|
+ if [ "$expected" = "$actual" ]; then
|
|
|
+ echo "PASS: $name"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+ else
|
|
|
+ echo "FAIL: $name (expected='$expected', actual='$actual')"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+assert_contains() {
|
|
|
+ local name="$1"
|
|
|
+ local needle="$2"
|
|
|
+ local haystack="$3"
|
|
|
+ TOTAL=$((TOTAL + 1))
|
|
|
+ if echo "$haystack" | grep -qF "$needle"; then
|
|
|
+ echo "PASS: $name"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+ else
|
|
|
+ echo "FAIL: $name (expected to contain '$needle')"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+assert_not_empty() {
|
|
|
+ local name="$1"
|
|
|
+ local value="$2"
|
|
|
+ TOTAL=$((TOTAL + 1))
|
|
|
+ if [ -n "$value" ]; then
|
|
|
+ echo "PASS: $name"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+ else
|
|
|
+ echo "FAIL: $name (was empty)"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+assert_empty() {
|
|
|
+ local name="$1"
|
|
|
+ local value="$2"
|
|
|
+ TOTAL=$((TOTAL + 1))
|
|
|
+ if [ -z "$value" ]; then
|
|
|
+ echo "PASS: $name"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+ else
|
|
|
+ echo "FAIL: $name (expected empty, got '$value')"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+assert_exit_code() {
|
|
|
+ local name="$1"
|
|
|
+ local expected="$2"
|
|
|
+ local actual="$3"
|
|
|
+ TOTAL=$((TOTAL + 1))
|
|
|
+ if [ "$expected" = "$actual" ]; then
|
|
|
+ echo "PASS: $name"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+ else
|
|
|
+ echo "FAIL: $name (exit code expected=$expected, actual=$actual)"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+# --- Setup: clean slate ---
|
|
|
+rm -f "$MAIL_DB"
|
|
|
+
|
|
|
+echo "=== Basic Operations ==="
|
|
|
+
|
|
|
+# T1: Init creates database
|
|
|
+bash "$MAIL_SCRIPT" init >/dev/null 2>&1
|
|
|
+assert "init creates database" "true" "$([ -f "$MAIL_DB" ] && echo true || echo false)"
|
|
|
+
|
|
|
+# T2: Count on empty inbox
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+assert "empty inbox count is 0" "0" "$result"
|
|
|
+
|
|
|
+# T3: Send a message
|
|
|
+result=$(bash "$MAIL_SCRIPT" send "test-project" "Hello" "World" 2>&1)
|
|
|
+assert_contains "send succeeds" "Sent to test-project" "$result"
|
|
|
+
|
|
|
+# T4: Count after send (we're in claude-mods, sent to test-project)
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+assert "count still 0 for sender project" "0" "$result"
|
|
|
+
|
|
|
+# T5: Send to self
|
|
|
+result=$(bash "$MAIL_SCRIPT" send "claude-mods" "Self mail" "Testing self-send" 2>&1)
|
|
|
+assert_contains "self-send succeeds" "Sent to claude-mods" "$result"
|
|
|
+
|
|
|
+# T6: Count after self-send
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+assert "count is 1 after self-send" "1" "$result"
|
|
|
+
|
|
|
+# T7: Unread shows message
|
|
|
+result=$(bash "$MAIL_SCRIPT" unread)
|
|
|
+assert_contains "unread shows subject" "Self mail" "$result"
|
|
|
+
|
|
|
+# T8: Read marks as read
|
|
|
+bash "$MAIL_SCRIPT" read >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+assert "count is 0 after read" "0" "$result"
|
|
|
+
|
|
|
+# T9: List shows read messages
|
|
|
+result=$(bash "$MAIL_SCRIPT" list)
|
|
|
+assert_contains "list shows read status" "read" "$result"
|
|
|
+
|
|
|
+# T10: Projects lists known projects
|
|
|
+result=$(bash "$MAIL_SCRIPT" projects)
|
|
|
+assert_contains "projects lists claude-mods" "claude-mods" "$result"
|
|
|
+assert_contains "projects lists test-project" "test-project" "$result"
|
|
|
+
|
|
|
+echo ""
|
|
|
+echo "=== Edge Cases ==="
|
|
|
+
|
|
|
+# T11: Empty body - should fail gracefully
|
|
|
+result=$(bash "$MAIL_SCRIPT" send "target" "subject" "" 2>&1)
|
|
|
+exit_code=$?
|
|
|
+# Empty body should either fail or send empty - document the behavior
|
|
|
+TOTAL=$((TOTAL + 1))
|
|
|
+if [ $exit_code -ne 0 ] || echo "$result" | grep -qiE "error|required|empty"; then
|
|
|
+ echo "PASS: empty body rejected or warned"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+else
|
|
|
+ echo "FAIL: empty body accepted silently"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+fi
|
|
|
+
|
|
|
+# T12: Missing arguments to send
|
|
|
+result=$(bash "$MAIL_SCRIPT" send 2>&1)
|
|
|
+exit_code=$?
|
|
|
+assert_exit_code "send with no args fails" "1" "$exit_code"
|
|
|
+
|
|
|
+# T13: SQL injection in subject
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "'; DROP TABLE messages; --" "injection test" >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+# If table still exists and count works, injection failed (good)
|
|
|
+TOTAL=$((TOTAL + 1))
|
|
|
+if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
|
|
|
+ echo "PASS: SQL injection in subject blocked"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+else
|
|
|
+ echo "FAIL: SQL injection may have succeeded"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+fi
|
|
|
+
|
|
|
+# T14: SQL injection in body
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "test" "'); DELETE FROM messages; --" >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+TOTAL=$((TOTAL + 1))
|
|
|
+if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
|
|
|
+ echo "PASS: SQL injection in body blocked"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+else
|
|
|
+ echo "FAIL: SQL injection in body may have succeeded"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+fi
|
|
|
+
|
|
|
+# T15: SQL injection in project name
|
|
|
+bash "$MAIL_SCRIPT" send "'; DROP TABLE messages; --" "test" "injection via project" >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+TOTAL=$((TOTAL + 1))
|
|
|
+if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
|
|
|
+ echo "PASS: SQL injection in project name blocked"
|
|
|
+ PASS=$((PASS + 1))
|
|
|
+else
|
|
|
+ echo "FAIL: SQL injection in project name may have succeeded"
|
|
|
+ FAIL=$((FAIL + 1))
|
|
|
+fi
|
|
|
+
|
|
|
+# T16: Special characters in body (newlines, quotes, backslashes)
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "special chars" 'Line1\nLine2 "quoted" and back\\slash' >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" read 2>&1)
|
|
|
+assert_contains "special chars preserved" "special chars" "$result"
|
|
|
+
|
|
|
+# T17: Very long message body (1000+ chars)
|
|
|
+long_body=$(python3 -c "print('x' * 2000)" 2>/dev/null || printf '%0.s.' $(seq 1 2000))
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "long msg" "$long_body" >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+assert "long message accepted" "1" "$result"
|
|
|
+bash "$MAIL_SCRIPT" read >/dev/null 2>&1
|
|
|
+
|
|
|
+# T18: Unicode in subject and body
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "Unicode test" "Hello from Tokyo" >/dev/null 2>&1
|
|
|
+result=$(bash "$MAIL_SCRIPT" read 2>&1)
|
|
|
+assert_contains "unicode in body" "Tokyo" "$result"
|
|
|
+
|
|
|
+# T19: Read by specific ID
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "ID test" "Read me by ID" >/dev/null 2>&1
|
|
|
+msg_id=$(sqlite3 "$MAIL_DB" "SELECT id FROM messages WHERE subject='ID test' AND read=0 LIMIT 1;")
|
|
|
+result=$(bash "$MAIL_SCRIPT" read "$msg_id" 2>&1)
|
|
|
+assert_contains "read by ID works" "Read me by ID" "$result"
|
|
|
+
|
|
|
+# T20: Read by invalid ID
|
|
|
+result=$(bash "$MAIL_SCRIPT" read 99999 2>&1)
|
|
|
+assert_empty "read invalid ID returns nothing" "$result"
|
|
|
+
|
|
|
+echo ""
|
|
|
+echo "=== Hook Tests ==="
|
|
|
+
|
|
|
+# T21: Hook silent on empty inbox
|
|
|
+bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # clear any unread
|
|
|
+result=$(bash "$HOOK_SCRIPT" 2>&1)
|
|
|
+assert_empty "hook silent when no mail" "$result"
|
|
|
+
|
|
|
+# T22: Hook shows notification
|
|
|
+bash "$MAIL_SCRIPT" send "claude-mods" "Hook test" "Should trigger hook" >/dev/null 2>&1
|
|
|
+result=$(bash "$HOOK_SCRIPT" 2>&1)
|
|
|
+assert_contains "hook shows MAIL notification" "MAIL" "$result"
|
|
|
+assert_contains "hook shows message count" "1 unread" "$result"
|
|
|
+
|
|
|
+# T23: Hook with missing database
|
|
|
+backup_db="${MAIL_DB}.testbak"
|
|
|
+mv "$MAIL_DB" "$backup_db"
|
|
|
+result=$(bash "$HOOK_SCRIPT" 2>&1)
|
|
|
+exit_code=$?
|
|
|
+assert_exit_code "hook exits 0 with missing db" "0" "$exit_code"
|
|
|
+assert_empty "hook silent with missing db" "$result"
|
|
|
+mv "$backup_db" "$MAIL_DB"
|
|
|
+
|
|
|
+echo ""
|
|
|
+echo "=== Cleanup ==="
|
|
|
+
|
|
|
+# T24: Clear old messages
|
|
|
+bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # mark all as read
|
|
|
+result=$(bash "$MAIL_SCRIPT" clear 0 2>&1)
|
|
|
+assert_contains "clear reports deleted count" "Cleared" "$result"
|
|
|
+
|
|
|
+# T25: Count after clear
|
|
|
+result=$(bash "$MAIL_SCRIPT" count)
|
|
|
+assert "count 0 after clear" "0" "$result"
|
|
|
+
|
|
|
+# T26: Help command
|
|
|
+result=$(bash "$MAIL_SCRIPT" help 2>&1)
|
|
|
+assert_contains "help shows usage" "Usage" "$result"
|
|
|
+
|
|
|
+# T27: Unknown command
|
|
|
+result=$(bash "$MAIL_SCRIPT" nonexistent 2>&1)
|
|
|
+exit_code=$?
|
|
|
+assert_exit_code "unknown command fails" "1" "$exit_code"
|
|
|
+
|
|
|
+echo ""
|
|
|
+echo "=== Results ==="
|
|
|
+echo "Passed: $PASS / $TOTAL"
|
|
|
+echo "Failed: $FAIL / $TOTAL"
|
|
|
+echo ""
|
|
|
+echo "$PASS"
|