summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2026-04-24 14:45:13 -0300
committerEuAndreh <eu@euandre.org>2026-04-24 14:45:13 -0300
commit9b4dd184568819330cad09d26d526cdde6fb73d2 (patch)
treec9f18fce9cd309652720a02db28cb7f06493b86e /tests
parentm (diff)
downloadpapod-9b4dd184568819330cad09d26d526cdde6fb73d2.tar.gz
papod-9b4dd184568819330cad09d26d526cdde6fb73d2.tar.xz
Implement core IRC commands: QUIT, AWAY, NOTICE, WHOIS, USERHOST
- QUIT: notify channel members, clean up client/channel state, close connection properly - AWAY: set/clear away status with RPL_NOWAWAY/RPL_UNAWAY, send RPL_AWAY on PRIVMSG to away users - NOTICE: deliver to channels and users without error replies (per RFC) - WHOIS: return RPL_WHOISUSER + RPL_AWAY + RPL_ENDOFWHOIS - USERHOST: return RPL_USERHOST with away indicator - PRIVMSG: return ERR_NOSUCHNICK (401) for nonexistent targets, ERR_NOTEXTTOSEND (412) for empty messages - USER: reject empty realname with ERR_NEEDMOREPARAMS (461) - PING: handle empty token (PING :) as ERR_NOORIGIN (409) - Add TCP listen support via PAPOD_TCP_PORT for direct irctest compatibility (bypasses binder proxy) - Fix member creation to handle reconnection with same nick (upsert) - Fix client-loop! to check :quit? after process-input! returns - Move AWAY, NOTICE, WHOIS, USERHOST, MODE, WHO before network check (they don't require network context) - Store client-atom reference in clients map for cross-client state access Tests: 207 unit assertions (15 tests), 21 integration assertions (10 tests), 122 irctest acceptance tests pass.
Diffstat (limited to 'tests')
-rwxr-xr-xtests/acceptance.sh47
-rw-r--r--tests/integration.clj45
-rw-r--r--tests/unit.clj102
3 files changed, 155 insertions, 39 deletions
diff --git a/tests/acceptance.sh b/tests/acceptance.sh
index 9f775ef..725bd65 100755
--- a/tests/acceptance.sh
+++ b/tests/acceptance.sh
@@ -1,59 +1,40 @@
#!/bin/sh
set -eu
-# Acceptance tests: run irctest against papod via binder (TCP→Unix proxy)
+# Acceptance tests: run irctest against papod (direct TCP)
-SOCKET_PATH="${TMPDIR:-/tmp}/papod-acceptance-$$.socket"
PORT="${IRCTEST_PORT:-16667}"
-BINDER="${BINDER:-/home/andreh/src/binder/binder.bin}"
IRCTEST_DIR="${IRCTEST_DIR:-$(dirname "$0")/../STUFF/irctest}"
JARDEPS="cracha fiinha jasm labareda peer dtmc base clojure"
CLASSPATH="papod.jar$(printf ':/home/andreh/.usr/var/mkg/share/java/%s.jar' $JARDEPS)"
PAPOD_PID=""
-BINDER_PID=""
cleanup() {
- [ -n "$PAPOD_PID" ] && kill "$PAPOD_PID" 2>/dev/null || true
- [ -n "$BINDER_PID" ] && kill "$BINDER_PID" 2>/dev/null || true
- wait "$PAPOD_PID" "$BINDER_PID" 2>/dev/null || true
- rm -f "$SOCKET_PATH"
+ [ -n "$PAPOD_PID" ] && kill "$PAPOD_PID" 2>/dev/null || true
+ wait "$PAPOD_PID" 2>/dev/null || true
}
trap cleanup EXIT
-echo "=== Starting papod on $SOCKET_PATH ==="
-PAPOD_SOCKET="$SOCKET_PATH" java -client -cp "$CLASSPATH" papod &
+PAPOD_TCP_PORT="$PORT" \
+PAPOD_SERVER_NAME=My.Little.Server \
+ java -client -cp "$CLASSPATH" papod 2>/dev/null &
PAPOD_PID=$!
-# Wait for socket to appear
for i in $(seq 1 50); do
- [ -S "$SOCKET_PATH" ] && break
+ nc -z 127.0.0.1 "$PORT" 2>/dev/null && break
sleep 0.1
done
-if [ ! -S "$SOCKET_PATH" ]; then
- echo "ERROR: papod socket did not appear at $SOCKET_PATH"
+if ! nc -z 127.0.0.1 "$PORT" 2>/dev/null; then
+ echo "ERROR: papod TCP port $PORT not listening" >&2
exit 1
fi
-echo "papod started (PID $PAPOD_PID)"
-echo "=== Starting binder on 127.0.0.1:$PORT → $SOCKET_PATH ==="
-"$BINDER" "127.0.0.1:$PORT" "$SOCKET_PATH" &
-BINDER_PID=$!
-sleep 0.2
-echo "binder started (PID $BINDER_PID)"
-
-echo "=== Running irctest ==="
cd "$IRCTEST_DIR"
-IRCTEST_SERVER_HOSTNAME=127.0.0.1 \
-IRCTEST_SERVER_PORT=$PORT \
-pytest --controller irctest.controllers.external_server \
- -k 'not deprecated' \
- -x -v \
- "$@" || {
- rc=$?
- echo "=== irctest exited with code $rc ==="
- exit $rc
-}
+export IRCTEST_SERVER_HOSTNAME=127.0.0.1
+export IRCTEST_SERVER_PORT=$PORT
-echo "=== All irctest tests passed ==="
+exec pytest \
+ --controller irctest.controllers.external_server \
+ "$@"
diff --git a/tests/integration.clj b/tests/integration.clj
index 2ae2d92..b3076ee 100644
--- a/tests/integration.clj
+++ b/tests/integration.clj
@@ -295,6 +295,51 @@
((:close! bob))))))
+(deftest test_quit-notifies-channel
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (send! alice "JOIN #qtest")
+ (wait-for alice "JOIN" 2000)
+ (send! bob "JOIN #qtest")
+ (wait-for bob "JOIN" 2000)
+ ;; Bob quits
+ (.reset (:client-out alice))
+ (send! bob "QUIT :bye!")
+ (let [alice-out (wait-for alice "QUIT" 2000)]
+ (is (string/includes? alice-out "QUIT"))
+ (is (string/includes? alice-out "bye!")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
+(deftest test_notice-channel
+ (let [components (test-components)
+ alice (make-client components)
+ bob (make-client components)]
+ (try
+ (register! alice "alice" (:net-id components))
+ (wait-for alice "001" 2000)
+ (register! bob "bob" (:net-id components))
+ (wait-for bob "001" 2000)
+ (send! alice "JOIN #ntest")
+ (wait-for alice "JOIN" 2000)
+ (send! bob "JOIN #ntest")
+ (wait-for bob "JOIN" 2000)
+ (.reset (:client-out bob))
+ (send! alice "NOTICE #ntest :hello notice")
+ (let [bob-out (wait-for bob "hello notice" 2000)]
+ (is (string/includes? bob-out "NOTICE #ntest"))
+ (is (string/includes? bob-out "hello notice")))
+ (finally
+ ((:close! alice))
+ ((:close! bob))))))
+
(defn -main
[& _args]
(binding [*out* *err*]
diff --git a/tests/unit.clj b/tests/unit.clj
index c764844..71513f2 100644
--- a/tests/unit.clj
+++ b/tests/unit.clj
@@ -177,7 +177,7 @@
{:command "USER"
:params ["" "joe" "0" "x" ":Joe"]}
c no-conn)]
- (is (= 1 (count replies)))
+ (is (= 6 (count replies)))
(is (string/includes? (first replies) "001"))
(is (:registered? @c)))))
(testing "USER before NICK does not register"
@@ -226,7 +226,66 @@
(is (string/includes?
(first (replies-for! {:command "PING" :params []}
c no-conn))
- "409")))))
+ "409"))))
+ (testing "USER with empty realname"
+ (let [c (client)]
+ (replies-for! {:command "NICK" :params ["" "joe"]}
+ c no-conn)
+ (let [replies (replies-for!
+ {:command "USER"
+ :params ["" "joe" "0" "*" ":"]}
+ c no-conn)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "461"))
+ (is (not (:registered? @c))))))
+ (testing "unknown command returns 421"
+ (let [{:keys [test-network-id] :as components}
+ (test-components-with-network)
+ c (client)]
+ (replies-for! {:command "PASS"
+ :params ["" (str test-network-id)]}
+ c components)
+ (replies-for! {:command "NICK" :params ["" "joe"]}
+ c components)
+ (replies-for! {:command "USER"
+ :params ["" "joe" "0" "*" ":Joe"]}
+ c components)
+ (is (string/includes?
+ (first (replies-for!
+ {:command "FOOBAR" :params []}
+ c components))
+ "421"))))
+ (testing "AWAY sets and clears away status"
+ (let [c (client)]
+ (replies-for! {:command "NICK" :params ["" "joe"]}
+ c no-conn)
+ (replies-for! {:command "USER"
+ :params ["" "joe" "0" "*" ":Joe"]}
+ c no-conn)
+ ;; Set away
+ (let [replies (replies-for!
+ {:command "AWAY"
+ :params ["" ":I'm not here"]}
+ c no-conn)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "306"))
+ (is (= "I'm not here" (:away @c))))
+ ;; Clear away
+ (let [replies (replies-for!
+ {:command "AWAY" :params []}
+ c no-conn)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "305"))
+ (is (nil? (:away @c))))))
+ (testing "QUIT sets :quit? and returns ERROR"
+ (let [c (client)]
+ (let [replies (replies-for!
+ {:command "QUIT"
+ :params [":Goodbye"]}
+ c no-conn)]
+ (is (= 1 (count replies)))
+ (is (string/starts-with? (first replies) "ERROR"))
+ (is (:quit? @c))))))
(defn registered-client
([nick w]
@@ -361,7 +420,38 @@
"411"))
(is (string/includes?
(first (handle-privmsg ["bob"] nil c no-conn))
- "412")))))
+ "412"))))
+ (testing "PRIVMSG to nonexistent user returns 401"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {"alice" {:w (java.io.ByteArrayOutputStream.)}})
+ :channels (atom {}))]
+ (let [replies (handle-privmsg ["nobody" ":hello"]
+ nil c components)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "401")))))
+ (testing "NOTICE delivers to channel without error replies"
+ (let [alice-out (java.io.ByteArrayOutputStream.)
+ bob-out (java.io.ByteArrayOutputStream.)
+ components
+ (assoc (test-components)
+ :clients (atom {"alice" {:w alice-out}
+ "bob" {:w bob-out}})
+ :channels (atom {"#test" #{"alice" "bob"}}))
+ alice (registered-client "alice" alice-out)]
+ (@#'papod/handle-notice ["#test" ":hi"] alice components)
+ (is (string/includes? (.toString bob-out "UTF-8")
+ "NOTICE #test :hi"))
+ (is (= "" (.toString alice-out "UTF-8")))))
+ (testing "NOTICE to nonexistent channel generates no error"
+ (let [c (registered-client "alice"
+ (java.io.ByteArrayOutputStream.))
+ components (assoc (test-components)
+ :clients (atom {})
+ :channels (atom {}))]
+ (is (empty? (@#'papod/handle-notice
+ ["#nope" ":hi"] c components))))))
(deftest test_nickserv
(testing "REGISTER creates user in cracha"
@@ -460,7 +550,7 @@
(is (:authenticated? @c2)))
;; CAP END completes registration
(let [replies (handle-cap ["END"] c2 components)]
- (is (= 1 (count replies)))
+ (is (= 6 (count replies)))
(is (string/includes? (first replies) "001"))
(is (:registered? @c2))))))
(testing "SASL with wrong password"
@@ -623,9 +713,9 @@
(let [replies (@#'papod/replies-for!
{:command "USER" :params ["" "bob" "0" "x" ":B"]}
c components)]
- (is (= 2 (count replies)))
+ (is (= 7 (count replies)))
(is (string/includes? (first replies) "001"))
- (is (string/includes? (second replies) "MemoServ")))))))
+ (is (string/includes? (last replies) "MemoServ")))))))
(deftest test_schema-invariants
(testing "events get monotonic sequence numbers per channel"