nandi/frqpublic Fork 0
23846db48565a450ee8a6a689892db23663ad48e
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 · 214 lines · 8.9 KBNim Blame HistoryRaw
Ask for the backlog, and survive hanging up 6214c7e nandi 23h 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 22h ago12import std/[json, sequtils, strutils, tables, unittest]
Faces, fetched off the thread that draws them 60bbd35 nandi 22h ago13import frq/[cells, model, profile, reducer, rooms]
Ask for the backlog, and survive hanging up 6214c7e nandi 23h 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 22h 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 22h 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
A face is asked for when somebody speaks 07a23ef nandi 12h ago176 test "a handle-shaped nick is asked for as soon as it speaks":
177 # Before WHO has answered, the screen looks a face up under the handle —
178 # so that is the name it has to be asked for under. Asking only when a
179 # DID turned up is why a face appeared only after opening the profile by
180 # hand, which asks under the handle.
181 say(":nandi.uk!u@freeq/plc/ngokl2gn PRIVMSG #freeq :hello")
182 check entry("nandi.uk")[1]
183 check entry("nandi.uk")[0].status == psLoading
184
185 test "and once WHO has answered, under the DID the screen then uses":
186 say(":irc.freeq.at 352 alice #freeq ~u freeq/plc/ngokl2gn irc.freeq.at " &
187 "nandi.uk H :0 did:plc:ngokl2gnmpbvuvrfckja3g7p",
188 ":nandi.uk!u@freeq/plc/ngokl2gn PRIVMSG #freeq :hello")
189 check entry("did:plc:ngokl2gnmpbvuvrfckja3g7p")[1]
190
191 test "a guest nick is nobody to look up":
192 # `sleek5209` is not a handle and has no DID; there is no profile behind
193 # it and a request for one can only fail.
194 say(":sleek5209!u@freeq/guest PRIVMSG #freeq :hello")
195 check not entry("sleek5209")[1]
196
197 test "and a system line is not somebody speaking":
198 say(":alice!a@h JOIN #freeq")
199 check not entry("*")[1]
200
201 test "the face does not blink when WHO changes which name is the actor":
202 # The handle's profile is what is cached when the first message lands;
203 # the actor becomes the DID a moment later. Without the fallback there is
204 # a hole between the two.
205 say(":nandi.uk!u@freeq/plc/ngokl2gn PRIVMSG #freeq :hello")
206 setProfileForTest("nandi.uk", "https://cdn/face.png")
207 check avatarFor("did:plc:ngokl2gnmpbvuvrfckja3g7p", "nandi.uk") ==
208 "https://cdn/face.png"
209
Faces, fetched off the thread that draws them 60bbd35 nandi 22h ago210 test "an agent is never asked about":
211 # `did:key:` has no Bluesky profile, so a request for one can only 400.
212 say(":irc.freeq.at 352 alice #freeq ~u freeq/key/z6Mkp5we irc.freeq.at " &
213 "cartographer H :0 did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4sh")
214 check not entry("did:key:z6Mkp5wegrxZR62h54HwR329yz7TJ8Ccx4sh")[1]