test-mail.sh 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/bin/bash
  2. # test-mail.sh - Test harness for mail-ops
  3. # Outputs: number of passing test cases
  4. # Each test prints PASS/FAIL and we count PASSes at the end
  5. set -uo pipefail
  6. MAIL_DB="$HOME/.claude/mail.db"
  7. MAIL_SCRIPT="$(dirname "$0")/mail-db.sh"
  8. HOOK_SCRIPT="$(dirname "$0")/../../hooks/check-mail.sh"
  9. # Resolve relative to repo root if needed
  10. if [ ! -f "$HOOK_SCRIPT" ]; then
  11. HOOK_SCRIPT="$(cd "$(dirname "$0")/../../.." && pwd)/hooks/check-mail.sh"
  12. fi
  13. PASS=0
  14. FAIL=0
  15. TOTAL=0
  16. assert() {
  17. local name="$1"
  18. local expected="$2"
  19. local actual="$3"
  20. TOTAL=$((TOTAL + 1))
  21. if [ "$expected" = "$actual" ]; then
  22. echo "PASS: $name"
  23. PASS=$((PASS + 1))
  24. else
  25. echo "FAIL: $name (expected='$expected', actual='$actual')"
  26. FAIL=$((FAIL + 1))
  27. fi
  28. }
  29. assert_contains() {
  30. local name="$1"
  31. local needle="$2"
  32. local haystack="$3"
  33. TOTAL=$((TOTAL + 1))
  34. if echo "$haystack" | grep -qF "$needle"; then
  35. echo "PASS: $name"
  36. PASS=$((PASS + 1))
  37. else
  38. echo "FAIL: $name (expected to contain '$needle')"
  39. FAIL=$((FAIL + 1))
  40. fi
  41. }
  42. assert_not_empty() {
  43. local name="$1"
  44. local value="$2"
  45. TOTAL=$((TOTAL + 1))
  46. if [ -n "$value" ]; then
  47. echo "PASS: $name"
  48. PASS=$((PASS + 1))
  49. else
  50. echo "FAIL: $name (was empty)"
  51. FAIL=$((FAIL + 1))
  52. fi
  53. }
  54. assert_empty() {
  55. local name="$1"
  56. local value="$2"
  57. TOTAL=$((TOTAL + 1))
  58. if [ -z "$value" ]; then
  59. echo "PASS: $name"
  60. PASS=$((PASS + 1))
  61. else
  62. echo "FAIL: $name (expected empty, got '$value')"
  63. FAIL=$((FAIL + 1))
  64. fi
  65. }
  66. assert_exit_code() {
  67. local name="$1"
  68. local expected="$2"
  69. local actual="$3"
  70. TOTAL=$((TOTAL + 1))
  71. if [ "$expected" = "$actual" ]; then
  72. echo "PASS: $name"
  73. PASS=$((PASS + 1))
  74. else
  75. echo "FAIL: $name (exit code expected=$expected, actual=$actual)"
  76. FAIL=$((FAIL + 1))
  77. fi
  78. }
  79. # --- Setup: clean slate ---
  80. rm -f "$MAIL_DB"
  81. echo "=== Basic Operations ==="
  82. # T1: Init creates database
  83. bash "$MAIL_SCRIPT" init >/dev/null 2>&1
  84. assert "init creates database" "true" "$([ -f "$MAIL_DB" ] && echo true || echo false)"
  85. # T2: Count on empty inbox
  86. result=$(bash "$MAIL_SCRIPT" count)
  87. assert "empty inbox count is 0" "0" "$result"
  88. # T3: Send a message
  89. result=$(bash "$MAIL_SCRIPT" send "test-project" "Hello" "World" 2>&1)
  90. assert_contains "send succeeds" "Sent to test-project" "$result"
  91. # T4: Count after send (we're in claude-mods, sent to test-project)
  92. result=$(bash "$MAIL_SCRIPT" count)
  93. assert "count still 0 for sender project" "0" "$result"
  94. # T5: Send to self
  95. result=$(bash "$MAIL_SCRIPT" send "claude-mods" "Self mail" "Testing self-send" 2>&1)
  96. assert_contains "self-send succeeds" "Sent to claude-mods" "$result"
  97. # T6: Count after self-send
  98. result=$(bash "$MAIL_SCRIPT" count)
  99. assert "count is 1 after self-send" "1" "$result"
  100. # T7: Unread shows message
  101. result=$(bash "$MAIL_SCRIPT" unread)
  102. assert_contains "unread shows subject" "Self mail" "$result"
  103. # T8: Read marks as read
  104. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  105. result=$(bash "$MAIL_SCRIPT" count)
  106. assert "count is 0 after read" "0" "$result"
  107. # T9: List shows read messages
  108. result=$(bash "$MAIL_SCRIPT" list)
  109. assert_contains "list shows read status" "read" "$result"
  110. # T10: Projects lists known projects
  111. result=$(bash "$MAIL_SCRIPT" projects)
  112. assert_contains "projects lists claude-mods" "claude-mods" "$result"
  113. assert_contains "projects lists test-project" "test-project" "$result"
  114. echo ""
  115. echo "=== Edge Cases ==="
  116. # T11: Empty body - should fail gracefully
  117. result=$(bash "$MAIL_SCRIPT" send "target" "subject" "" 2>&1)
  118. exit_code=$?
  119. # Empty body should either fail or send empty - document the behavior
  120. TOTAL=$((TOTAL + 1))
  121. if [ $exit_code -ne 0 ] || echo "$result" | grep -qiE "error|required|empty"; then
  122. echo "PASS: empty body rejected or warned"
  123. PASS=$((PASS + 1))
  124. else
  125. echo "FAIL: empty body accepted silently"
  126. FAIL=$((FAIL + 1))
  127. fi
  128. # T12: Missing arguments to send
  129. result=$(bash "$MAIL_SCRIPT" send 2>&1)
  130. exit_code=$?
  131. assert_exit_code "send with no args fails" "1" "$exit_code"
  132. # T13: SQL injection in subject
  133. bash "$MAIL_SCRIPT" send "claude-mods" "'; DROP TABLE messages; --" "injection test" >/dev/null 2>&1
  134. result=$(bash "$MAIL_SCRIPT" count)
  135. # If table still exists and count works, injection failed (good)
  136. TOTAL=$((TOTAL + 1))
  137. if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
  138. echo "PASS: SQL injection in subject blocked"
  139. PASS=$((PASS + 1))
  140. else
  141. echo "FAIL: SQL injection may have succeeded"
  142. FAIL=$((FAIL + 1))
  143. fi
  144. # T14: SQL injection in body
  145. bash "$MAIL_SCRIPT" send "claude-mods" "test" "'); DELETE FROM messages; --" >/dev/null 2>&1
  146. result=$(bash "$MAIL_SCRIPT" count)
  147. TOTAL=$((TOTAL + 1))
  148. if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
  149. echo "PASS: SQL injection in body blocked"
  150. PASS=$((PASS + 1))
  151. else
  152. echo "FAIL: SQL injection in body may have succeeded"
  153. FAIL=$((FAIL + 1))
  154. fi
  155. # T15: SQL injection in project name
  156. bash "$MAIL_SCRIPT" send "'; DROP TABLE messages; --" "test" "injection via project" >/dev/null 2>&1
  157. result=$(bash "$MAIL_SCRIPT" count)
  158. TOTAL=$((TOTAL + 1))
  159. if [ -n "$result" ] && [ "$result" -ge 0 ] 2>/dev/null; then
  160. echo "PASS: SQL injection in project name blocked"
  161. PASS=$((PASS + 1))
  162. else
  163. echo "FAIL: SQL injection in project name may have succeeded"
  164. FAIL=$((FAIL + 1))
  165. fi
  166. # T16: Special characters in body (newlines, quotes, backslashes)
  167. bash "$MAIL_SCRIPT" send "claude-mods" "special chars" 'Line1\nLine2 "quoted" and back\\slash' >/dev/null 2>&1
  168. result=$(bash "$MAIL_SCRIPT" read 2>&1)
  169. assert_contains "special chars preserved" "special chars" "$result"
  170. # T17: Very long message body (1000+ chars)
  171. long_body=$(python3 -c "print('x' * 2000)" 2>/dev/null || printf '%0.s.' $(seq 1 2000))
  172. bash "$MAIL_SCRIPT" send "claude-mods" "long msg" "$long_body" >/dev/null 2>&1
  173. result=$(bash "$MAIL_SCRIPT" count)
  174. assert "long message accepted" "1" "$result"
  175. bash "$MAIL_SCRIPT" read >/dev/null 2>&1
  176. # T18: Unicode in subject and body
  177. bash "$MAIL_SCRIPT" send "claude-mods" "Unicode test" "Hello from Tokyo" >/dev/null 2>&1
  178. result=$(bash "$MAIL_SCRIPT" read 2>&1)
  179. assert_contains "unicode in body" "Tokyo" "$result"
  180. # T19: Read by specific ID
  181. bash "$MAIL_SCRIPT" send "claude-mods" "ID test" "Read me by ID" >/dev/null 2>&1
  182. msg_id=$(sqlite3 "$MAIL_DB" "SELECT id FROM messages WHERE subject='ID test' AND read=0 LIMIT 1;")
  183. result=$(bash "$MAIL_SCRIPT" read "$msg_id" 2>&1)
  184. assert_contains "read by ID works" "Read me by ID" "$result"
  185. # T20: Read by invalid ID
  186. result=$(bash "$MAIL_SCRIPT" read 99999 2>&1)
  187. assert_empty "read invalid ID returns nothing" "$result"
  188. echo ""
  189. echo "=== Hook Tests ==="
  190. # T21: Hook silent on empty inbox
  191. bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # clear any unread
  192. result=$(bash "$HOOK_SCRIPT" 2>&1)
  193. assert_empty "hook silent when no mail" "$result"
  194. # T22: Hook shows notification
  195. bash "$MAIL_SCRIPT" send "claude-mods" "Hook test" "Should trigger hook" >/dev/null 2>&1
  196. result=$(bash "$HOOK_SCRIPT" 2>&1)
  197. assert_contains "hook shows MAIL notification" "MAIL" "$result"
  198. assert_contains "hook shows message count" "1 unread" "$result"
  199. # T23: Hook with missing database
  200. backup_db="${MAIL_DB}.testbak"
  201. mv "$MAIL_DB" "$backup_db"
  202. result=$(bash "$HOOK_SCRIPT" 2>&1)
  203. exit_code=$?
  204. assert_exit_code "hook exits 0 with missing db" "0" "$exit_code"
  205. assert_empty "hook silent with missing db" "$result"
  206. mv "$backup_db" "$MAIL_DB"
  207. echo ""
  208. echo "=== Cleanup ==="
  209. # T24: Clear old messages
  210. bash "$MAIL_SCRIPT" read >/dev/null 2>&1 # mark all as read
  211. result=$(bash "$MAIL_SCRIPT" clear 0 2>&1)
  212. assert_contains "clear reports deleted count" "Cleared" "$result"
  213. # T25: Count after clear
  214. result=$(bash "$MAIL_SCRIPT" count)
  215. assert "count 0 after clear" "0" "$result"
  216. # T26: Help command
  217. result=$(bash "$MAIL_SCRIPT" help 2>&1)
  218. assert_contains "help shows usage" "Usage" "$result"
  219. # T27: Unknown command
  220. result=$(bash "$MAIL_SCRIPT" nonexistent 2>&1)
  221. exit_code=$?
  222. assert_exit_code "unknown command fails" "1" "$exit_code"
  223. echo ""
  224. echo "=== Results ==="
  225. echo "Passed: $PASS / $TOTAL"
  226. echo "Failed: $FAIL / $TOTAL"
  227. echo ""
  228. echo "$PASS"