nandi/frqpublic Fork 0
4925e54c9cbf2c2fa55214ac147d458c00384a20
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

tsession.nim · 180 lines · 7.3 KBNim Blame HistoryRaw
Ask for the backlog, and survive hanging up 6214c7e nandi 21h ago1## What this client says back to a server, given what a server says to it.
2##
3## The reducer's answer to an incoming line was the largest untested thing in
4## the program: reachable only through a real socket, so nothing checked it.
5## That is where the missing CHATHISTORY request sat through the whole port —
6## every room came back with no history and the only comment about it was in
7## another module, describing behaviour that had never been ported.
8##
9## `conn.feed` puts a line in as though the server had sent it; `tryOutbound`
10## reads what went out. No socket at either end.
11
Find out who somebody is 4b72ad1 nandi 21h ago12import std/[json, sequtils, strutils, tables, unittest]
Faces, fetched off the thread that draws them 60bbd35 nandi 21h ago13import frq/[cells, model, profile, reducer, rooms]
Ask for the backlog, and survive hanging up 6214c7e nandi 21h ago14import frq/conn as tr
15
16proc sent(): seq[string] =
17 ## Everything queued to go out, drained.
18 while true:
19 let (ok, line) = tr.tryOutbound()
20 if not ok: break
21 result.add line
22
23proc say(lines: varargs[string]) =
24 for l in lines: tr.feed(l)
25 drain()
26
27proc reset() =
28 app = initState()
29 app.formNick = "alice"
30 discard sent()
31
32proc joined(room: string) =
33 ## The server putting us in a room, which is how every one of them starts —
34 ## a JOIN we sent, or one freeq made for a signed-in account at
35 ## registration. A 366 for a room this client has never heard of is not
36 ## ours and is left alone.
37 say(":alice!a@h JOIN " & room)
38 discard sent()
39
40suite "asking for the backlog":
41 setup: reset()
42
43 test "end of NAMES in an empty room asks for history":
44 joined("#freeq")
45 # freeq re-joins an authenticated user's channels at registration and
46 # leaves the backlog for the client to ask for. A room that arrives this
47 # way has no history coming unless we ask, which is why a signed-in
48 # connection showed new lines and nothing else.
49 say(":server 366 alice #freeq :End of /NAMES list")
50 check app.rooms.hasKey("#freeq")
51 check sent().anyIt(it.startsWith("CHATHISTORY LATEST #freeq * "))
52
53 test "and asks for a hundred lines of it":
54 joined("#freeq")
55 say(":server 366 alice #freeq :End of /NAMES list")
56 check "CHATHISTORY LATEST #freeq * 100" in sent()
57
58 test "a room that already has lines is not asked twice":
59 # The replay comes back as ordinary PRIVMSGs; asking again is a second
60 # copy of the same history crossing the wire to be thrown away.
61 joined("#freeq")
62 say(":bob!b@h PRIVMSG #freeq :already here")
63 discard sent()
64 say(":server 366 alice #freeq :End of /NAMES list")
65 check not sent().anyIt(it.startsWith("CHATHISTORY"))
66
67 test "a room this client was never put in is not asked about":
68 # Not a room of ours: 366 for it arrives before any JOIN, and answering
69 # it would ask a server for the history of somewhere we are not.
70 say(":server 366 alice #nowhere :End of /NAMES list")
71 check not app.rooms.hasKey("#nowhere")
72 check not sent().anyIt(it.startsWith("CHATHISTORY"))
73
74suite "the rest of the conversation":
75 setup: reset()
76
77 test "a PING is answered with its own token":
78 say("PING :abc123")
79 check "PONG :abc123" in sent()
80
81 test "our own JOIN marks the room joined; somebody else's adds a name":
82 say(":alice!a@h JOIN #freeq")
83 check app.rooms["#freeq"].joined
84 say(":bob!b@h JOIN #freeq")
85 check app.rooms["#freeq"].users.hasKey("bob")
86
87 test "NAMES arrives over several lines and lands in one go":
88 joined("#freeq")
89 say(":server 353 alice = #freeq :alice @bob",
90 ":server 353 alice = #freeq :carol")
91 # Still pending: replacing the list per line empties the panel and
92 # refills it a name at a time.
93 check app.rooms["#freeq"].users.len == 0
94 say(":server 366 alice #freeq :End of /NAMES list")
95 check app.rooms["#freeq"].users.len == 3
96
97 test "a replayed line lands in the room it names":
98 joined("#freeq")
99 say(":server 366 alice #freeq :End of /NAMES list",
100 ":bob!b@h PRIVMSG #freeq :an old line")
101 check app.rooms["#freeq"].messages.anyIt(it.text == "an old line")
102
103 test "a message to us is filed under whoever sent it":
104 say(":bob!b@h PRIVMSG alice :a direct word")
105 check app.rooms.hasKey("bob")
106 check app.rooms["bob"].messages[^1].text == "a direct word"
Find out who somebody is 4b72ad1 nandi 21h ago107
108suite "who is who":
109 setup: reset()
110
111 test "WHO reports the whole DID where the hostmask has eight characters":
112 # `freeq/plc/ngokl2gn` is enough to tell two people apart and not enough
113 # to look either of them up. The realname field carries all of it.
114 say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/ngokl2gn irc.freeq.at " &
115 "nandi.uk H :0 did:plc:ngokl2gnmpbvuvrfckja3g7p")
116 check app.dids["nandi.uk"] == "did:plc:ngokl2gnmpbvuvrfckja3g7p"
117
118 test "an agent signs with a key, and that is an identity too":
119 say(":irc.freeq.at 352 alice #freeq ~u freeq/key/z6Mkp5we irc.freeq.at " &
120 "cartographer H :0 did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4shCpSB")
121 check app.dids["cartographer"].startsWith("did:key:")
122
123 test "a guest has none, and is not recorded as having one":
124 # freeq puts the literal "IRC User" there for an unauthenticated
125 # connection, which is not a DID and must not be stored as one.
126 say(":irc.freeq.at 352 alice #freeq ~u freeq/guest irc.freeq.at " &
127 "adam12 H :0 IRC User")
128 check not app.dids.hasKey("adam12")
129
130 test "WHOIS answers for one nick the same way":
131 say(":irc.freeq.at 330 alice zapnap did:plc:k2n3e2vsabcdefghijklmnop " &
132 ":is authenticated as")
133 check app.dids["zapnap"] == "did:plc:k2n3e2vsabcdefghijklmnop"
134
135 test "and the room is asked who is in it once it has arrived":
136 joined("#freeq")
137 say(":server 366 alice #freeq :End of /NAMES list")
138 check "WHO #freeq" in sent()
139
140suite "opening a profile":
141 setup: reset()
142
143 test "uses what WHO reported for a nick that is not a handle":
144 say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/k2n3e2vs irc.freeq.at " &
145 "zapnap H :0 did:plc:k2n3e2vsabcdefghijklmnop")
146 dispatch(%*{"id": "profile.open:zapnap:"})
147 check app.profileViewing.actor == "did:plc:k2n3e2vsabcdefghijklmnop"
148
149 test "asks the server about somebody it has never seen":
150 dispatch(%*{"id": "profile.open:stranger:"})
151 check "WHOIS stranger" in sent()
152
153 test "and what the message itself knew still wins":
154 dispatch(%*{"id": "profile.open:bob:did:plc:fromtheaccounttag"})
155 check app.profileViewing.actor == "did:plc:fromtheaccounttag"
Faces, fetched off the thread that draws them 60bbd35 nandi 21h ago156
157suite "faces":
158 setup:
159 reset()
160 forgetProfiles()
161
162 test "learning a DID starts the face on its way, without waiting for it":
163 # `want` is not `fetch`: a room of twelve is twelve HTTPS round trips,
164 # and this is the thread that answers every keystroke.
165 say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/ngokl2gn irc.freeq.at " &
166 "nandi.uk H :0 did:plc:ngokl2gnmpbvuvrfckja3g7p")
167 let (p, known) = entry("did:plc:ngokl2gnmpbvuvrfckja3g7p")
168 check known
169 check p.status == psLoading
170
171 test "and nothing is painted for one that has not arrived":
172 say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/ngokl2gn irc.freeq.at " &
173 "nandi.uk H :0 did:plc:ngokl2gnmpbvuvrfckja3g7p")
174 check avatarFor("did:plc:ngokl2gnmpbvuvrfckja3g7p") == ""
175
176 test "an agent is never asked about":
177 # `did:key:` has no Bluesky profile, so a request for one can only 400.
178 say(":irc.freeq.at 352 alice #freeq ~u freeq/key/z6Mkp5we irc.freeq.at " &
179 "cartographer H :0 did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4sh")
180 check not entry("did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4sh")[1]