diff options
| author | EuAndreh <eu@euandre.org> | 2026-04-21 22:16:16 -0300 |
|---|---|---|
| committer | EuAndreh <eu@euandre.org> | 2026-04-21 22:16:16 -0300 |
| commit | 64dbf930f11a1d1a70f970627e23ab82ab188528 (patch) | |
| tree | d74a1de15d9c8245b33557403034f40eb3023154 /tests | |
| parent | m (diff) | |
| download | papod-64dbf930f11a1d1a70f970627e23ab82ab188528.tar.gz papod-64dbf930f11a1d1a70f970627e23ab82ab188528.tar.xz | |
m
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/unit.clj | 772 |
1 files changed, 770 insertions, 2 deletions
diff --git a/tests/unit.clj b/tests/unit.clj index e92b389..af13931 100644 --- a/tests/unit.clj +++ b/tests/unit.clj @@ -1,5 +1,9 @@ (ns unit (:require [clojure.test :as t :refer [are deftest is testing]] + [clojure.string :as string] + [cracha] + [datomic.api :as d] + [fiinha] [papod]) (:gen-class)) @@ -38,10 +42,774 @@ [{:prefix nil :command "CMD" :params []} - nil])))) + nil]))) + (testing "prefix with nick only" + (is (= (parse-message ":nick CMD") + [{:prefix {:nick "nick" :user nil :host nil} + :command "CMD" + :params []} + nil]))) + (testing "prefix with nick and user" + (is (= (parse-message ":nick!user CMD") + [{:prefix {:nick "nick" :user "user" :host nil} + :command "CMD" + :params []} + nil]))) + (testing "prefix with nick and host" + (is (= (parse-message ":nick@host CMD") + [{:prefix {:nick "nick" :user nil :host "host"} + :command "CMD" + :params []} + nil]))) + (testing "prefix with nick, user and host" + (is (= (parse-message ":nick!user@host CMD") + [{:prefix {:nick "nick" :user "user" :host "host"} + :command "CMD" + :params []} + nil]))) + (testing "prefix errors from parse-prefix" + (is (= :bad-empty-prefix-and-message + (-> (parse-message ":") second :type))) + (is (= :bad-empty-prefix + (-> (parse-message ": CMD") second :type))) + (is (= :no-prefix-separator + (-> (parse-message ":nospaceatall") second :type)))) + (testing "prefix errors from parse-prefix-components" + (is (= :bad-prefix-format + (-> (parse-message ":a@b!c CMD") second :type))) + (is (= :bad-empty-nick + (-> (parse-message ":@host CMD") second :type))) + (is (= :bad-empty-user + (-> (parse-message ":nick!@host CMD") second :type))) + (is (= :bad-empty-host + (-> (parse-message ":nick!user@ CMD") second :type))) + (is (= :bad-empty-nick + (-> (parse-message ":!user CMD") second :type))))) + +(def replies-for! @#'papod/replies-for!) +(def clean-params @#'papod/clean-params) +(defn client [] (atom {:nick nil :user nil :pass nil :registered? false})) +(def no-conn {}) + +(deftest test_clean-params + (testing "removes leading empty strings" + (is (= (clean-params ["" "a" "b"]) ["a" "b"])) + (is (= (clean-params ["a" "b"]) ["a" "b"])) + (is (= (clean-params []) [])))) + +(deftest test_replies-for! + (testing "PASS stores password" + (let [c (client)] + (is (= (replies-for! {:command "PASS" :params ["" "secret"]} + c no-conn) + [])) + (is (= (:pass @c) "secret")))) + (testing "PASS with no params" + (let [c (client)] + (is (string/includes? + (first (replies-for! {:command "PASS" :params []} + c no-conn)) + "461")))) + (testing "NICK stores nickname" + (let [c (client)] + (is (= (replies-for! {:command "NICK" :params ["" "joe"]} + c no-conn) + [])) + (is (= (:nick @c) "joe")))) + (testing "NICK with no params" + (let [c (client)] + (is (string/includes? + (first (replies-for! {:command "NICK" :params []} + c no-conn)) + "431")))) + (testing "registration completes after NICK and USER" + (let [c (client)] + (replies-for! {:command "NICK" :params ["" "joe"]} c no-conn) + (let [replies (replies-for! + {:command "USER" + :params ["" "joe" "0" "x" ":Joe"]} + c no-conn)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "001")) + (is (:registered? @c))))) + (testing "USER before NICK does not register" + (let [c (client)] + (let [replies (replies-for! + {:command "USER" + :params ["" "joe" "0" "x" ":Joe"]} + c no-conn)] + (is (empty? replies)) + (is (not (:registered? @c)))))) + (testing "USER after registration" + (let [c (client)] + (replies-for! {:command "NICK" :params ["" "joe"]} c no-conn) + (replies-for! {:command "USER" :params ["" "joe" "0" "x" ":Joe"]} + c no-conn) + (is (string/includes? + (first (replies-for! + {:command "USER" :params ["" "joe" "0" "x" ":Joe"]} + c no-conn)) + "462")))) + (testing "PASS after registration" + (let [c (client)] + (replies-for! {:command "NICK" :params ["" "joe"]} c no-conn) + (replies-for! {:command "USER" :params ["" "joe" "0" "x" ":Joe"]} + c no-conn) + (is (string/includes? + (first (replies-for! {:command "PASS" :params ["" "pw"]} + c no-conn)) + "462")))) + (testing "unregistered client gets 451 for other commands" + (let [c (client)] + (is (string/includes? + (first (replies-for! {:command "JOIN" :params ["" "#chan"]} + c no-conn)) + "451")))) + (testing "PING responds with PONG" + (let [c (client)] + (let [replies (replies-for! + {:command "PING" :params ["" "token"]} + c no-conn)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "PONG")) + (is (string/includes? (first replies) "token"))))) + (testing "PING with no params" + (let [c (client)] + (is (string/includes? + (first (replies-for! {:command "PING" :params []} + c no-conn)) + "409"))))) + +(defn test-components + [] + (let [fiinha-state (fiinha/initdb! + (str "datomic:mem://fiinha-" (java.util.UUID/randomUUID))) + cracha-state (cracha/init! + (str "datomic:mem://cracha-" (java.util.UUID/randomUUID)) + fiinha-state) + papod-uri (str "datomic:mem://papod-" (java.util.UUID/randomUUID)) + _ (d/create-database papod-uri) + conn (d/connect papod-uri)] + @(d/transact conn @#'papod/schema) + {:conn conn :cracha cracha-state + :clients (atom {}) :channels (atom {})})) + +(defn test-network! + [conn] + (let [net-id (java.util.UUID/randomUUID)] + @(d/transact conn + [{:db/ensure :papod.network/attrs + :papod.network/id net-id + :papod.network/name (str "test-" net-id) + :papod.network/description "" + :papod.network/type "public" + :papod.network/created-at (java.util.Date.)}]) + net-id)) + +(defn test-components-with-network + [] + (let [components (test-components) + net-id (test-network! (:conn components))] + (assoc components :test-network-id net-id))) + +(defn registered-client + ([nick w] + (atom {:nick nick :user {:username nick} :pass nil :registered? true :w w})) + ([nick w net-id] + (atom {:nick nick :user {:username nick} :pass nil :registered? true + :w w :network-id net-id}))) + +(def handle-privmsg @#'papod/handle-privmsg) +(def handle-join @#'papod/handle-join) +(def resolve-channel @#'papod/resolve-channel) +(def handle-cap @#'papod/handle-cap) +(def handle-authenticate @#'papod/handle-authenticate) + +(deftest test_persist-first + (testing "DM persists before delivery" + (let [sender-out (java.io.ByteArrayOutputStream.) + target-out (java.io.ByteArrayOutputStream.) + sender (registered-client "alice" sender-out) + components (assoc (test-components) + :clients (atom {"alice" {:w sender-out} + "bob" {:w target-out}}) + :channels (atom {})) + conn (:conn components)] + (handle-privmsg ["bob" ":hello world"] sender components) + ;; Verify event persisted with target-nick + (let [db (d/db conn) + events (d/q '{:find [?nick ?target ?payload] + :where [[?e :papod.event/source-nick ?nick] + [?e :papod.event/target-nick ?target] + [?e :papod.event/payload ?payload]]} + db)] + (is (= 1 (count events))) + (is (= ["alice" "bob" ":hello world"] + (vec (first events))))) + ;; Delivered to target, not sender + (is (string/includes? (.toString target-out "UTF-8") + "PRIVMSG bob :hello world")) + (is (= "" (.toString sender-out "UTF-8"))))) + (testing "channel PRIVMSG persists with channel ref" + (let [alice-out (java.io.ByteArrayOutputStream.) + bob-out (java.io.ByteArrayOutputStream.) + {:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {"alice" {:w alice-out} + "bob" {:w bob-out}}) + :channels (atom {"#test" #{"alice" "bob"}})) + sender (registered-client "alice" alice-out test-network-id) + conn (:conn components)] + ;; Create the channel in DB first (JOIN would do this normally) + @(d/transact conn + [{:papod.channel/id (java.util.UUID/randomUUID) + :papod.channel/network [:papod.network/id test-network-id] + :papod.channel/name "#test" + :papod.channel/type "public" + :papod.channel/description "" + :papod.channel/created-at (java.util.Date.)}]) + (handle-privmsg ["#test" ":hi everyone"] sender components) + ;; Verify event references the channel entity + (let [db (d/db conn) + events (d/q '{:find [?type ?payload ?chan-name] + :where [[?e :papod.event/channel ?c] + [?e :papod.event/type ?type] + [?e :papod.event/payload ?payload] + [?c :papod.channel/name ?chan-name]]} + db)] + (is (= 1 (count events))) + (is (= ["user-message" ":hi everyone" "#test"] + (vec (first events))))) + ;; Delivered to bob, not alice + (is (string/includes? (.toString bob-out "UTF-8") + "PRIVMSG #test :hi everyone")) + (is (= "" (.toString alice-out "UTF-8"))))) + (testing "JOIN creates channel + membership + event atomically" + (let [alice-out (java.io.ByteArrayOutputStream.) + {:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {"alice" {:w alice-out}}) + :channels (atom {})) + sender (registered-client "alice" alice-out test-network-id) + conn (:conn components)] + (handle-join ["#test"] sender components) + (let [db (d/db conn)] + ;; Channel entity created + (is (some? (resolve-channel db "#test"))) + ;; Membership recorded + (let [memberships (d/q '{:find [?nick ?chan-name] + :where [[?m :papod.membership/nick ?nick] + [?m :papod.membership/channel ?c] + [?c :papod.channel/name ?chan-name]]} + db)] + (is (= #{["alice" "#test"]} memberships))) + ;; Event recorded + (let [events (d/q '{:find [?type ?nick ?chan-name] + :where [[?e :papod.event/type ?type] + [?e :papod.event/source-nick ?nick] + [?e :papod.event/channel ?c] + [?c :papod.channel/name ?chan-name]]} + db)] + (is (= #{["user-join" "alice" "#test"]} events)))) + ;; In-memory updated + (is (= #{"alice"} (get @(:channels components) "#test"))) + ;; Notification delivered + (is (string/includes? (.toString alice-out "UTF-8") "JOIN #test")))) + (testing "second JOIN reuses existing channel" + (let [alice-out (java.io.ByteArrayOutputStream.) + bob-out (java.io.ByteArrayOutputStream.) + {:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {"alice" {:w alice-out} + "bob" {:w bob-out}}) + :channels (atom {})) + conn (:conn components)] + (handle-join ["#test"] (registered-client "alice" alice-out test-network-id) components) + (handle-join ["#test"] (registered-client "bob" bob-out test-network-id) components) + ;; Still one channel entity + (let [db (d/db conn) + channels (d/q '{:find [?name] + :where [[?c :papod.channel/name ?name]]} + db)] + (is (= 1 (count channels)))) + ;; Two memberships + (let [db (d/db conn) + memberships (d/q '{:find [?nick] + :where [[?m :papod.membership/nick ?nick]]} + db)] + (is (= #{["alice"] ["bob"]} memberships))))) + (testing "PRIVMSG error cases" + (let [c (registered-client "alice" (java.io.ByteArrayOutputStream.))] + (is (string/includes? + (first (handle-privmsg [] c no-conn)) + "411")) + (is (string/includes? + (first (handle-privmsg ["bob"] c no-conn)) + "412"))))) + +(deftest test_nickserv + (testing "REGISTER creates user in cracha" + (let [out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out) + components (test-components) + conn (:conn components) + replies (handle-privmsg ["NickServ" ":REGISTER mypass"] c components)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "registered successfully")) + (let [cc (:conn (:cracha components)) + user-id (cracha/user-by-email cc "alice")] + (is (some? user-id)) + (is (cracha/user-confirmed? cc user-id))))) + (testing "REGISTER rejects duplicate nick" + (let [out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out) + components (test-components)] + (handle-privmsg ["NickServ" ":REGISTER pass1"] c components) + (let [replies (handle-privmsg ["NickServ" ":REGISTER pass2"] c components)] + (is (string/includes? (first replies) "already registered"))))) + (testing "IDENTIFY succeeds with correct password" + (let [out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out) + components (test-components)] + (handle-privmsg ["NickServ" ":REGISTER mypass"] c components) + (let [replies (handle-privmsg ["NickServ" ":IDENTIFY mypass"] c components)] + (is (string/includes? (first replies) "now identified")) + (is (:identified? @c))))) + (testing "IDENTIFY fails with wrong password" + (let [out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out) + components (test-components)] + (handle-privmsg ["NickServ" ":REGISTER mypass"] c components) + (let [replies (handle-privmsg ["NickServ" ":IDENTIFY wrong"] c components)] + (is (string/includes? (first replies) "Invalid password")) + (is (not (:identified? @c)))))) + (testing "IDENTIFY fails for unregistered nick" + (let [out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out) + components (test-components) + replies (handle-privmsg ["NickServ" ":IDENTIFY mypass"] c components)] + (is (string/includes? (first replies) "not registered")))) + (testing "NickServ messages are NOT persisted" + (let [out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out) + components (test-components) + conn (:conn components)] + (handle-privmsg ["NickServ" ":REGISTER mypass"] c components) + (let [db (d/db conn) + evts (d/q '{:find [?e] + :where [[?e :papod.event/id _]]} + db)] + (is (zero? (count evts))))))) + +(defn b64 + [s] + (.encodeToString (java.util.Base64/getEncoder) (.getBytes s "UTF-8"))) + +(deftest test_sasl + (testing "full CAP + SASL PLAIN flow" + (let [out (java.io.ByteArrayOutputStream.) + c (client) + components (test-components)] + ;; Register user in cracha first + (swap! c assoc :nick "alice" :registered? true :w out) + (handle-privmsg ["NickServ" ":REGISTER mypass"] c components) + ;; Reset client for fresh connection + (let [c2 (atom {:nick nil :user nil :pass nil :registered? false + :w out})] + ;; CAP LS suspends registration + (let [replies (handle-cap ["LS" "302"] c2 components)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "sasl=PLAIN")) + (is (:cap-negotiating? @c2))) + ;; CAP REQ sasl + (let [replies (handle-cap ["REQ" ":sasl"] c2 components)] + (is (string/includes? (first replies) "ACK")) + (is (contains? (:caps @c2) "sasl"))) + ;; NICK + USER during negotiation do not trigger registration + (swap! c2 assoc :nick "alice") + (let [replies (replies-for! {:command "USER" :params ["" "alice" "0" "x" ":A"]} + c2 components)] + (is (empty? replies)) + (is (not (:registered? @c2)))) + ;; AUTHENTICATE PLAIN + (let [replies (handle-authenticate ["PLAIN"] c2 components)] + (is (= ["AUTHENTICATE +"] replies))) + ;; Send credentials + (let [creds (b64 "\u0000alice\u0000mypass") + replies (handle-authenticate [creds] c2 components)] + (is (= 2 (count replies))) + (is (string/includes? (first replies) "900")) + (is (string/includes? (second replies) "903")) + (is (:authenticated? @c2))) + ;; CAP END completes registration + (let [replies (handle-cap ["END"] c2 components)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "001")) + (is (:registered? @c2)))))) + (testing "SASL with wrong password" + (let [out (java.io.ByteArrayOutputStream.) + c (client) + components (test-components)] + ;; Register user + (swap! c assoc :nick "bob" :registered? true :w out) + (handle-privmsg ["NickServ" ":REGISTER secret"] c components) + ;; Fresh connection + (let [c2 (atom {:nick "bob" :user nil :pass nil :registered? false + :w out :caps #{"sasl"}})] + (handle-authenticate ["PLAIN"] c2 components) + (let [creds (b64 "\u0000bob\u0000wrong") + replies (handle-authenticate [creds] c2 components)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "904")) + (is (not (:authenticated? @c2))))))) + (testing "AUTHENTICATE without CAP sasl" + (let [c (atom {:nick "x" :caps #{}})] + (is (string/includes? + (first (handle-authenticate ["PLAIN"] c no-conn)) + "904")))) + (testing "AUTHENTICATE abort" + (let [c (atom {:nick "x" :caps #{"sasl"} :sasl-state :authenticating})] + (is (string/includes? + (first (handle-authenticate ["*"] c no-conn)) + "906")))) + (testing "AUTHENTICATE unsupported mechanism" + (let [c (atom {:nick "x" :caps #{"sasl"}})] + (let [replies (handle-authenticate ["SCRAM-SHA-1"] c no-conn)] + (is (string/includes? (first replies) "908")) + (is (string/includes? (second replies) "904"))))) + (testing "CAP REQ unsupported capability" + (let [c (atom {:nick "x" :cap-negotiating? true})] + (is (string/includes? + (first (handle-cap ["REQ" ":multi-prefix"] c no-conn)) + "NAK")))) + (testing "already authenticated" + (let [c (atom {:nick "x" :caps #{"sasl"} :authenticated? true})] + (is (string/includes? + (first (handle-authenticate ["PLAIN"] c no-conn)) + "907"))))) + +(deftest test_chanserv + (testing "REGISTER + INFO + SET" + (let [{:keys [test-network-id] :as components} (test-components-with-network) + out (java.io.ByteArrayOutputStream.) + c (registered-client "alice" out test-network-id)] + (handle-join ["#test"] c + (assoc components + :clients (atom {"alice" {:w out}}) + :channels (atom {}))) + (let [replies (handle-privmsg ["ChanServ" ":REGISTER #test"] + c components)] + (is (string/includes? (first replies) "has been registered"))) + (let [replies (handle-privmsg ["ChanServ" ":INFO #test"] + c components)] + (is (some #(string/includes? % "alice") replies))) + (let [replies (handle-privmsg ["ChanServ" ":SET #test TOPIC Hello world"] + c components)] + (is (string/includes? (first replies) "Topic"))) + (let [replies (handle-privmsg ["ChanServ" ":INFO #test"] + c components)] + (is (some #(string/includes? % "Hello world") replies))))) + (testing "OP and DEOP" + (let [{:keys [test-network-id] :as components} (test-components-with-network) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id)] + (handle-join ["#test"] alice + (assoc components + :clients (atom {"alice" {:w out}}) + :channels (atom {}))) + (handle-privmsg ["ChanServ" ":REGISTER #test"] alice components) + (let [replies (handle-privmsg ["ChanServ" ":OP #test bob"] + alice components)] + (is (string/includes? (first replies) "now an operator"))) + (let [replies (handle-privmsg ["ChanServ" ":DEOP #test bob"] + alice components)] + (is (string/includes? (first replies) "no longer an operator"))))) + (testing "KICK and BAN" + (let [alice-out (java.io.ByteArrayOutputStream.) + bob-out (java.io.ByteArrayOutputStream.) + {:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {"alice" {:w alice-out} + "bob" {:w bob-out}}) + :channels (atom {"#test" #{"alice" "bob"}})) + alice (registered-client "alice" alice-out test-network-id) + bob (registered-client "bob" bob-out test-network-id)] + (handle-join ["#test"] alice components) + (handle-privmsg ["ChanServ" ":REGISTER #test"] alice components) + (let [replies (handle-privmsg ["ChanServ" ":KICK #test bob bad behavior"] + alice components)] + (is (empty? replies)) + (is (not (contains? (get @(:channels components) "#test") "bob"))) + (is (string/includes? (.toString bob-out "UTF-8") "KICK"))) + (let [replies (handle-privmsg ["ChanServ" ":BAN #test bob"] + alice components)] + (is (string/includes? (first replies) "banned"))) + (let [replies (handle-join ["#test"] bob components)] + (is (string/includes? (first replies) "474"))))) + (testing "permission denied for non-op" + (let [{:keys [test-network-id] :as components} (test-components-with-network) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id) + bob (registered-client "bob" out test-network-id)] + (handle-join ["#test"] alice + (assoc components + :clients (atom {"alice" {:w out}}) + :channels (atom {}))) + (handle-privmsg ["ChanServ" ":REGISTER #test"] alice components) + (let [replies (handle-privmsg ["ChanServ" ":SET #test TOPIC nope"] + bob components)] + (is (string/includes? (first replies) "Permission denied")))))) + +(deftest test_memoserv + (testing "SEND + LIST + READ + DELETE" + (let [components (test-components) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out) + bob (registered-client "bob" out)] + ;; Send memo + (let [replies (handle-privmsg ["MemoServ" ":SEND bob Hello Bob!"] + alice components)] + (is (string/includes? (first replies) "Memo sent"))) + ;; List as bob + (let [replies (handle-privmsg ["MemoServ" ":LIST"] bob components)] + (is (= 1 (count replies))) + (is (string/includes? (first replies) "alice")) + (is (string/includes? (first replies) "unread")) + ;; Extract memo ID for READ/DELETE + (let [memo-id (last (string/split (first replies) #" "))] + ;; Read + (let [replies (handle-privmsg + ["MemoServ" (str ":READ " memo-id)] + bob components)] + (is (string/includes? (first replies) "Hello Bob!"))) + ;; Now listed as read + (let [replies (handle-privmsg ["MemoServ" ":LIST"] bob components)] + (is (string/includes? (first replies) "read"))) + ;; Delete + (let [replies (handle-privmsg + ["MemoServ" (str ":DELETE " memo-id)] + bob components)] + (is (string/includes? (first replies) "deleted"))) + ;; List empty + (let [replies (handle-privmsg ["MemoServ" ":LIST"] bob components)] + (is (string/includes? (first replies) "No memos"))))))) + (testing "auto-delivery on registration" + (let [components (dissoc (test-components) :cracha) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out)] + ;; Send memo to bob while offline + (handle-privmsg ["MemoServ" ":SEND bob Hey!"] alice components) + ;; Bob registers (no cracha = auth skipped) + (let [c (atom {:nick nil :user nil :pass nil :registered? false + :w out})] + (swap! c assoc :nick "bob") + (let [replies (@#'papod/replies-for! + {:command "USER" :params ["" "bob" "0" "x" ":B"]} + c components)] + (is (= 2 (count replies))) + (is (string/includes? (first replies) "001")) + (is (string/includes? (second replies) "MemoServ"))))))) + +(deftest test_schema-invariants + (testing "events get monotonic sequence numbers per channel" + (let [alice-out (java.io.ByteArrayOutputStream.) + bob-out (java.io.ByteArrayOutputStream.) + {:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {"alice" {:w alice-out} + "bob" {:w bob-out}}) + :channels (atom {})) + conn (:conn components) + alice (registered-client "alice" alice-out test-network-id)] + (handle-join ["#test"] alice components) + (swap! (:channels components) update "#test" conj "bob") + (handle-privmsg ["#test" ":first"] alice components) + (handle-privmsg ["#test" ":second"] alice components) + (let [db (d/db conn) + seqs (sort (map first + (d/q '{:find [?seq] + :in [$ ?chan-name] + :where [[?e :papod.event/seq ?seq] + [?e :papod.event/channel ?c] + [?c :papod.channel/name ?chan-name]]} + db "#test")))] + (is (= [1 2 3] seqs))))) + (testing "duplicate access entry is rejected" + (let [{:keys [test-network-id] :as components} (test-components-with-network) + conn (:conn components) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id)] + (handle-join ["#test"] alice + (assoc components + :clients (atom {"alice" {:w out}}) + :channels (atom {}))) + (handle-privmsg ["ChanServ" ":REGISTER #test"] alice components) + (handle-privmsg ["ChanServ" ":OP #test bob"] alice components) + ;; Second OP for same nick is caught by has-access? check + (let [replies (handle-privmsg ["ChanServ" ":OP #test bob"] + alice components)] + (is (string/includes? (first replies) "already an operator"))))) + (testing "non-empty predicates reject empty strings" + (let [components (test-components) + conn (:conn components)] + (is (thrown? java.util.concurrent.ExecutionException + @(d/transact conn + [{:db/ensure :papod.memo/attrs + :papod.memo/id (java.util.UUID/randomUUID) + :papod.memo/from "" + :papod.memo/to "bob" + :papod.memo/content "hi" + :papod.memo/created-at (java.util.Date.) + :papod.memo/read? false}]))))) + (testing "entity attrs enforce required fields" + (let [components (test-components) + conn (:conn components)] + (is (thrown? java.util.concurrent.ExecutionException + @(d/transact conn + [{:db/ensure :papod.memo/attrs + :papod.memo/id (java.util.UUID/randomUUID) + :papod.memo/from "alice"}])))))) -; (parse-message " CMD") +(deftest test_private-channels + (testing "private channel created via ChanServ is joinable by & handle" + (let [alice-out (java.io.ByteArrayOutputStream.) + bob-out (java.io.ByteArrayOutputStream.) + {:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {"alice" {:w alice-out} + "bob" {:w bob-out}}) + :channels (atom {})) + alice (registered-client "alice" alice-out test-network-id) + bob (registered-client "bob" bob-out test-network-id) + conn (:conn components) + chan-id (java.util.UUID/randomUUID) + _ @(d/transact conn + [{:db/ensure :papod.channel/attrs + :db/id "new-channel" + :papod.channel/id chan-id + :papod.channel/network [:papod.network/id test-network-id] + :papod.channel/type "private" + :papod.channel/label "secret-project" + :papod.channel/description "" + :papod.channel/created-at (java.util.Date.)} + {:db/ensure :papod.access/attrs + :papod.access/id (java.util.UUID/randomUUID) + :papod.access/channel "new-channel" + :papod.access/nick "alice" + :papod.access/level "owner"}]) + handle (str "&" chan-id)] + ;; Alice (owner) can join + (let [replies (handle-join [handle] alice components)] + (is (empty? replies)) + (is (contains? (get @(:channels components) handle) "alice"))) + ;; Bob (no access) cannot join — gets "No such channel" + (let [replies (handle-join [handle] bob components)] + (is (string/includes? (first replies) "403"))))) + (testing "probing #private-name returns no such channel" + (let [{:keys [test-network-id] :as components} (test-components-with-network) + conn (:conn components) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id) + _ @(d/transact conn + [{:db/ensure :papod.channel/attrs + :papod.channel/id (java.util.UUID/randomUUID) + :papod.channel/network [:papod.network/id test-network-id] + :papod.channel/type "private" + :papod.channel/label "bob-firing-wg" + :papod.channel/description "" + :papod.channel/created-at (java.util.Date.)}])] + ;; Probing by name fails — no public name exists + (let [replies (handle-join ["#bob-firing-wg"] alice + (assoc components :channels (atom {})))] + ;; Creates a NEW public channel named #bob-firing-wg + ;; (doesn't find the private one — that's the point) + (is (empty? replies))))) + (testing "#-prefixed UUID name works as a normal public channel" + (let [{:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {}) + :channels (atom {})) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id) + uuid-name (str "#" (java.util.UUID/randomUUID))] + ;; JOIN with a UUID-shaped name — treated as a public channel name + (let [c (assoc components + :clients (atom {"alice" {:w out}}))] + (let [replies (handle-join [uuid-name] alice c)] + (is (empty? replies)) + (is (contains? (get @(:channels c) uuid-name) "alice")))))) + (testing "&non-uuid is rejected" + (let [{:keys [test-network-id] :as components} + (assoc (test-components-with-network) :channels (atom {})) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id)] + (let [replies (handle-join ["¬-a-uuid"] alice components)] + (is (string/includes? (first replies) "403")))))) +(deftest test_networks-and-members + (testing "registration creates member in connection's network" + (let [{:keys [test-network-id] :as components} + (dissoc (test-components-with-network) :cracha) + conn (:conn components) + out (java.io.ByteArrayOutputStream.) + c (atom {:nick nil :user nil :pass nil :registered? false + :w out :network-id test-network-id})] + (swap! c assoc :nick "alice") + (@#'papod/replies-for! + {:command "USER" :params ["" "alice" "0" "x" ":A"]} + c components) + (is (:registered? @c)) + (let [db (d/db conn) + members (d/q '{:find [?nick ?status] + :in [$ ?net] + :where [[?m :papod.member/network ?net] + [?m :papod.member/nick ?nick] + [?m :papod.member/status ?status]]} + db [:papod.network/id test-network-id])] + (is (= #{["alice" "active"]} members))))) + (testing "no member created without NETWORK command" + (let [components (dissoc (test-components-with-network) :cracha) + conn (:conn components) + out (java.io.ByteArrayOutputStream.) + c (atom {:nick nil :user nil :pass nil :registered? false + :w out})] + (swap! c assoc :nick "bob") + (@#'papod/replies-for! + {:command "USER" :params ["" "bob" "0" "x" ":B"]} + c components) + (is (:registered? @c)) + (let [db (d/db conn) + members (d/q '{:find [?nick] + :where [[?m :papod.member/nick ?nick]]} + db)] + (is (empty? members))))) + (testing "channels are scoped to network" + (let [{:keys [test-network-id] :as components} + (assoc (test-components-with-network) + :clients (atom {}) + :channels (atom {})) + conn (:conn components) + out (java.io.ByteArrayOutputStream.) + alice (registered-client "alice" out test-network-id)] + (handle-join ["#general"] alice + (assoc components :clients (atom {"alice" {:w out}}))) + (let [db (d/db conn) + chans (d/q '{:find [?name ?net-id] + :in [$ ?net] + :where [[?c :papod.channel/name ?name] + [?c :papod.channel/network ?net] + [?net :papod.network/id ?net-id]]} + db [:papod.network/id test-network-id])] + (is (= 1 (count chans))) + (is (= "#general" (ffirst chans))) + (is (= test-network-id (second (first chans))))))) + (testing "network has required attrs" + (let [conn (:conn (test-components))] + (is (thrown? java.util.concurrent.ExecutionException + @(d/transact conn + [{:db/ensure :papod.network/attrs + :papod.network/id (java.util.UUID/randomUUID) + :papod.network/name "incomplete"}])))))) (defn -main [& _args] |
