diff options
| author | EuAndreh <eu@euandre.org> | 2026-04-24 14:45:13 -0300 |
|---|---|---|
| committer | EuAndreh <eu@euandre.org> | 2026-04-24 14:45:13 -0300 |
| commit | 9b4dd184568819330cad09d26d526cdde6fb73d2 (patch) | |
| tree | c9f18fce9cd309652720a02db28cb7f06493b86e /tests | |
| parent | m (diff) | |
| download | papod-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-x | tests/acceptance.sh | 47 | ||||
| -rw-r--r-- | tests/integration.clj | 45 | ||||
| -rw-r--r-- | tests/unit.clj | 102 |
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" |
