]> git.giacomolli.uk - onechat/blob - whatsapp_test.go
Move whatsapp_find to its own file
[onechat] / whatsapp_test.go
1 package main
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "errors"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/stretchr/testify/require"
15 "go.mau.fi/whatsmeow"
16 "go.mau.fi/whatsmeow/types"
17 "go.mau.fi/whatsmeow/types/events"
18 )
19
20 func TestWhatsAppStatusFollowsTheConnection(t *testing.T) {
21 dir := t.TempDir()
22 whatsappTestSession(t, dir, true)
23 clock := newTestClock()
24 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
25 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
26
27 <-fake.connects
28 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
29
30 require.True(t, fake.Emit(&events.Connected{}))
31 require.Equal(t, "WhatsApp is linked and connected since 2026-01-01 12:00.", whatsapp.Status())
32
33 clock.now = clock.now.Add(20 * time.Minute)
34 require.True(t, fake.Emit(&events.Disconnected{}))
35 clock.now = clock.now.Add(time.Minute)
36 require.True(t, fake.Emit(&events.Disconnected{}))
37 require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:20. The last connection started at 2026-01-01 12:00.", whatsapp.Status())
38 }
39
40 func TestWhatsAppStatusOfAFirstConnectionThatFailed(t *testing.T) {
41 dir := t.TempDir()
42 whatsappTestSession(t, dir, true)
43 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
44 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
45
46 <-fake.connects
47 require.True(t, fake.Emit(&events.Disconnected{}))
48 require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:00.", whatsapp.Status())
49 }
50
51 func TestWhatsAppIsNotLinkedWithoutALinkedDevice(t *testing.T) {
52 testCases := []struct {
53 name string
54 session func(t *testing.T, dir string)
55 }{
56 {
57 name: "no session file",
58 session: func(t *testing.T, dir string) {
59 },
60 },
61 {
62 name: "a session of a pairing that did not end",
63 session: func(t *testing.T, dir string) {
64 whatsappTestSession(t, dir, false)
65 },
66 },
67 }
68
69 for _, tc := range testCases {
70 t.Run(tc.name, func(t *testing.T) {
71 dir := t.TempDir()
72 tc.session(t, dir)
73 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
74 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
75 })
76 }
77 }
78
79 func TestWhatsAppRefusesASessionThatOthersCanRead(t *testing.T) {
80 dir := t.TempDir()
81 whatsappTestSession(t, dir, true)
82 require.NoError(t, os.Chmod(filepath.Join(dir, whatsappDirName, whatsappSessionFileName), 0o644))
83
84 _, err := openWhatsApp(t.Context(), dir, time.Now, whatsappDiscardLog(), whatsappNewClient)
85 require.EqualError(t, err, "whatsapp/session.db holds the WhatsApp session, so its mode must be 0600")
86 }
87
88 func TestWhatsAppCommandsAnswerOnlyTheClientThatAsked(t *testing.T) {
89 server := newModelFake(t)
90 telegram := newTelegramFake(t)
91 bot, _ := telegramStartTestBot(t, telegram)
92 socket := daemonStartTestWithBot(t, server, bot)
93 asking := terminalStartTestClient(t, socket)
94 other := terminalStartTestClient(t, socket)
95 asking.WaitFor(t, "[connected to OneChat]")
96 other.WaitFor(t, "[connected to OneChat]")
97
98 asking.Type("/whatsapp status")
99 asking.WaitFor(t, "WhatsApp is not linked.")
100 asking.Type(" /whatsapp unknown ")
101 asking.WaitFor(t, whatsappUsage)
102 asking.Type("/whatsapp pair 0123")
103 asking.WaitFor(t, "PHONE must be the number of your WhatsApp account")
104
105 telegram.Add(TelegramUpdate{
106 UpdateID: 1,
107 Message: telegramOwnerMessage("/whatsapp status"),
108 })
109
110 telegramWaitForSent(t, telegram, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.")
111 require.Len(t, telegram.Sent(), 1)
112 require.Equal(t, "[connected to OneChat]\n", other.Output())
113 require.Zero(t, server.Calls())
114 }
115
116 func TestWhatsAppPairsWithACode(t *testing.T) {
117 dir := t.TempDir()
118 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
119 server := newModelFake(t)
120 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
121 asking := terminalStartTestClient(t, socket)
122 other := terminalStartTestClient(t, socket)
123 asking.WaitFor(t, "[connected to OneChat]")
124 other.WaitFor(t, "[connected to OneChat]")
125
126 asking.Type("/whatsapp pair +1 555-000-1111")
127 items := <-fake.pairings
128 items <- whatsmeow.QRChannelItem{
129 Event: whatsmeow.QRChannelEventCode,
130 }
131
132 asking.WaitFor(t, whatsappPairSteps+"\n"+whatsappTestCode+"\n")
133 require.True(t, fake.Emit(&events.PairSuccess{
134 ID: types.NewADJID(whatsappTestNumber, 0, 1),
135 LID: types.NewADJID(whatsappTestLID, 0, 1),
136 }))
137 items <- whatsmeow.QRChannelSuccess
138 asking.WaitFor(t, "WhatsApp is linked to +15550001111.")
139
140 // The library connects again after the link with the context of the
141 // connection, so the end of the pairing must not end it.
142 connections := fake.Connections()
143 require.Len(t, connections, 1)
144 require.Never(t, func() bool {
145 return connections[0].Err() != nil
146 }, 200*time.Millisecond, time.Millisecond)
147
148 require.Equal(t, []string{whatsappTestNumber}, fake.Phones())
149 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
150 require.Equal(t, "[connected to OneChat]\n", other.Output())
151 require.Zero(t, server.Calls())
152 require.Equal(t, []string{"WhatsApp is linked."}, whatsappTestLogTexts(t, memory))
153
154 info, err := os.Stat(filepath.Join(dir, whatsappDirName))
155 require.NoError(t, err)
156 require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
157
158 info, err = os.Stat(filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
159 require.NoError(t, err)
160 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
161 }
162
163 func TestWhatsAppPairingCodeIsATelegramMessageAlone(t *testing.T) {
164 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
165 telegram := newTelegramFake(t)
166 bot, _ := telegramStartTestBot(t, telegram)
167 daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
168
169 telegram.Add(TelegramUpdate{
170 UpdateID: 1,
171 Message: telegramOwnerMessage("/whatsapp pair 15550001111"),
172 })
173
174 items := <-fake.pairings
175 items <- whatsmeow.QRChannelItem{
176 Event: whatsmeow.QRChannelEventCode,
177 }
178
179 telegramWaitForSent(t, telegram, whatsappPairSteps, whatsappTestCode)
180 }
181
182 func TestWhatsAppPairingEnds(t *testing.T) {
183 testCases := []struct {
184 name string
185 end func(items chan whatsmeow.QRChannelItem)
186 answer string
187 }{
188 {
189 name: "the code expires",
190 end: func(items chan whatsmeow.QRChannelItem) {
191 items <- whatsmeow.QRChannelTimeout
192 },
193 answer: whatsappPairExpired,
194 },
195 {
196 name: "the library closes the pairing",
197 end: func(items chan whatsmeow.QRChannelItem) {
198 close(items)
199 },
200 answer: whatsappPairExpired,
201 },
202 {
203 name: "the pairing fails",
204 end: func(items chan whatsmeow.QRChannelItem) {
205 items <- whatsmeow.QRChannelItem{
206 Event: whatsmeow.QRChannelEventError,
207 Error: errors.New("bad handshake"),
208 }
209 },
210 answer: "The pairing failed: bad handshake. Run /whatsapp pair PHONE again.",
211 },
212 }
213
214 for _, tc := range testCases {
215 t.Run(tc.name, func(t *testing.T) {
216 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
217 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
218 client := terminalStartTestClient(t, socket)
219 client.Type("/whatsapp pair 15550001111")
220
221 items := <-fake.pairings
222 items <- whatsmeow.QRChannelItem{
223 Event: whatsmeow.QRChannelEventCode,
224 }
225
226 client.WaitFor(t, whatsappTestCode)
227 tc.end(items)
228 client.WaitFor(t, tc.answer)
229 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
230 })
231 }
232 }
233
234 func TestWhatsAppNewPairingEndsTheOneBefore(t *testing.T) {
235 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
236 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
237 client := terminalStartTestClient(t, socket)
238
239 client.Type("/whatsapp pair 15550001111")
240 <-fake.pairings
241 client.Type("/whatsapp pair 15550002222")
242 items := <-fake.pairings
243 items <- whatsmeow.QRChannelItem{
244 Event: whatsmeow.QRChannelEventCode,
245 }
246
247 client.WaitFor(t, whatsappTestCode)
248 require.Equal(t, []string{"15550002222"}, fake.Phones())
249 require.NotContains(t, client.Output(), "did not start the pairing")
250 }
251
252 func TestWhatsAppPairWhenLinked(t *testing.T) {
253 dir := t.TempDir()
254 whatsappTestSession(t, dir, true)
255 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
256 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
257 client := terminalStartTestClient(t, socket)
258
259 client.Type("/whatsapp pair 15550001111")
260 client.WaitFor(t, "WhatsApp is linked already. Run /whatsapp unlink first.")
261 }
262
263 func TestWhatsAppUnlink(t *testing.T) {
264 testCases := []struct {
265 name string
266 logoutErr error
267 answer string
268 }{
269 {
270 name: "with a logout",
271 answer: "WhatsApp is unlinked.",
272 },
273 {
274 name: "with no logout",
275 logoutErr: errors.New("not connected"),
276 answer: "WhatsApp is unlinked here, but the phone did not get the logout. In WhatsApp on your phone, open Linked devices and remove the device.",
277 },
278 }
279
280 for _, tc := range testCases {
281 t.Run(tc.name, func(t *testing.T) {
282 dir := t.TempDir()
283 whatsappTestSession(t, dir, true)
284 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
285 fake.logoutErr = tc.logoutErr
286 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
287 client := terminalStartTestClient(t, socket)
288 <-fake.connects
289
290 client.Type("/whatsapp unlink")
291 client.WaitFor(t, tc.answer)
292 require.Equal(t, 1, fake.Logouts())
293 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
294 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
295 require.Equal(t, []string{"WhatsApp is unlinked."}, whatsappTestLogTexts(t, memory))
296
297 client.Type("/whatsapp unlink")
298 client.WaitFor(t, "WhatsApp is not linked.\n")
299 })
300 }
301 }
302
303 func TestWhatsAppLogoutFromThePhone(t *testing.T) {
304 dir := t.TempDir()
305 whatsappTestSession(t, dir, true)
306 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
307 telegram := newTelegramFake(t)
308 bot, _ := telegramStartTestBot(t, telegram)
309 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
310 client := terminalStartTestClient(t, socket)
311 client.WaitFor(t, "[connected to OneChat]")
312 <-fake.connects
313
314 require.True(t, fake.Emit(&events.LoggedOut{}))
315 notice := "WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again."
316 client.WaitFor(t, notice)
317 telegramWaitForSent(t, telegram, notice)
318 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
319 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
320 require.Equal(t, []string{"WhatsApp was unlinked from the phone."}, whatsappTestLogTexts(t, memory))
321 }
322
323 func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) {
324 dir := t.TempDir()
325 whatsappTestSession(t, dir, true)
326 clock := newTestClock()
327 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
328 whatsapp.noticeDelay = 300 * time.Millisecond
329 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
330 client := terminalStartTestClient(t, socket)
331 client.WaitFor(t, "[connected to OneChat]")
332 <-fake.connects
333
334 require.True(t, fake.Emit(&events.Connected{}))
335 require.True(t, fake.Emit(&events.Disconnected{}))
336 require.True(t, fake.Emit(&events.Connected{}))
337 time.Sleep(400 * time.Millisecond)
338 require.Equal(t, "[connected to OneChat]\n", client.Output())
339
340 clock.now = clock.now.Add(time.Hour)
341 require.True(t, fake.Emit(&events.Disconnected{}))
342 require.True(t, fake.Emit(&events.Disconnected{}))
343 client.WaitFor(t, "WhatsApp is disconnected since 2026-01-01 13:00. OneChat tries again.\n")
344 require.True(t, fake.Emit(&events.Disconnected{}))
345 time.Sleep(400 * time.Millisecond)
346
347 require.True(t, fake.Emit(&events.Connected{}))
348 client.WaitFor(t, "WhatsApp is connected again.\n")
349 require.Equal(t, 1, strings.Count(client.Output(), "WhatsApp is disconnected since"))
350 }
351
352 func TestWhatsAppDisconnectionWithNoRetryGivesANoticeAtOnce(t *testing.T) {
353 dir := t.TempDir()
354 whatsappTestSession(t, dir, true)
355 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
356 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
357 client := terminalStartTestClient(t, socket)
358 client.WaitFor(t, "[connected to OneChat]")
359 <-fake.connects
360
361 require.True(t, fake.Emit(&events.StreamReplaced{}))
362 client.WaitFor(t, "WhatsApp is disconnected and does not connect again by itself: stream replaced. Restart the daemon to try again.")
363 }
364
365 func TestWhatsAppLogsNoOwnNumber(t *testing.T) {
366 dir := t.TempDir()
367 whatsappTestSession(t, dir, true)
368 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
369 fake.connectErr = errors.New("no route for " + whatsappTestNumber + "@s.whatsapp.net and " + whatsappTestLID + "@lid")
370 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
371 <-fake.connects
372
373 require.Eventually(t, func() bool {
374 return logs.String() != ""
375 }, 10*time.Second, time.Millisecond)
376 require.Equal(t, "connect: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String())
377 }
378
379 func TestWhatsAppPhone(t *testing.T) {
380 testCases := []struct {
381 name string
382 phone string
383 valid bool
384 }{
385 {
386 name: "a number with a plus and dashes",
387 phone: "+1-555-000-1111",
388 valid: true,
389 },
390 {
391 name: "a number with no country code",
392 phone: "0115550001111",
393 valid: false,
394 },
395 {
396 name: "a short number",
397 phone: "123456",
398 valid: false,
399 },
400 {
401 name: "a long number",
402 phone: "1234567890123456",
403 valid: false,
404 },
405 {
406 name: "a number with letters",
407 phone: "1555ABC1111",
408 valid: false,
409 },
410 }
411
412 for _, tc := range testCases {
413 t.Run(tc.name, func(t *testing.T) {
414 _, valid := whatsappPhone(tc.phone)
415 require.Equal(t, tc.valid, valid)
416 })
417 }
418 }
419
420 func TestWhatsAppCommandArgs(t *testing.T) {
421 testCases := []struct {
422 name string
423 line string
424 args string
425 found bool
426 }{
427 {
428 name: "a command with its arguments",
429 line: " /whatsapp pair +1 555 ",
430 args: "pair +1 555",
431 found: true,
432 },
433 {
434 name: "the command alone",
435 line: "/whatsapp",
436 args: "",
437 found: true,
438 },
439 {
440 name: "a word that starts with the command",
441 line: "/whatsappstatus",
442 args: "",
443 found: false,
444 },
445 {
446 name: "a message that names the command",
447 line: "what does /whatsapp status do?",
448 args: "",
449 found: false,
450 },
451 }
452
453 for _, tc := range testCases {
454 t.Run(tc.name, func(t *testing.T) {
455 args, found := whatsappCommandArgs(tc.line)
456 require.Equal(t, tc.args, args)
457 require.Equal(t, tc.found, found)
458 })
459 }
460 }
461
462 func TestSubagentHasNoWhatsAppTools(t *testing.T) {
463 box := &Toolbox{}
464 require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`))
465 require.Equal(t, `error: unknown tool "whatsapp_send"`, box.run(t.Context(), "whatsapp_send", `{"id":"a","chat":"15550002222@s.whatsapp.net","text":"hi"}`))
466 }
467
468 func TestWhatsAppSendSendsOnceForEachID(t *testing.T) {
469 whatsapp, fake := whatsappStartTestLinked(t)
470 whatsappTestContacts(t, whatsapp)
471 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
472 whatsappTestConnect(t, fake)
473
474 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.")
475 require.NoError(t, err)
476 require.Equal(t, "Sent to Ana Souza at 12:05.", answer)
477
478 answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.")
479 require.NoError(t, err)
480 require.Equal(t, "Sent to Ana Souza at 12:05. This is the answer of the first call with this id: nothing was sent again.", answer)
481
482 _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.")
483 require.EqualError(t, err, "the id keys belongs to another message: give a new id for each message")
484
485 require.Equal(t, []WhatsAppFakeSend{
486 {
487 to: whatsappTestAna.String(),
488 text: "The keys are under the mat.",
489 id: "MSG1",
490 },
491 }, fake.Sent())
492
493 again, err := openWhatsAppState(filepath.Join(whatsapp.dir, whatsappStateFileName), whatsappBatchQuiet, whatsappBatchWindow)
494 require.NoError(t, err)
495
496 record, found := again.Send("keys")
497 require.True(t, found)
498 require.Equal(t, whatsappSent, record.State)
499 require.Equal(t, "MSG1", record.MessageID)
500 }
501
502 func TestWhatsAppSendToAGroup(t *testing.T) {
503 whatsapp, fake := whatsappStartTestLinked(t)
504 fake.groups = []*types.GroupInfo{
505 {
506 JID: whatsappTestFamily,
507 GroupName: types.GroupName{
508 Name: "Family",
509 },
510 },
511 }
512
513 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
514 whatsappTestConnect(t, fake)
515
516 answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.")
517 require.NoError(t, err)
518 require.Equal(t, "Sent to Family at 12:05.", answer)
519 }
520
521 func TestWhatsAppSendThatFails(t *testing.T) {
522 whatsapp, fake := whatsappStartTestLinked(t)
523 fake.sendErr = errors.New("server error for " + whatsappTestNumber)
524 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
525 whatsappTestConnect(t, fake)
526
527 for range 2 {
528 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
529 require.NoError(t, err)
530 require.True(t, strings.HasPrefix(answer, "The send failed: server error for OWN."), answer)
531 }
532
533 require.Len(t, fake.Sent(), 1)
534 }
535
536 func TestWhatsAppSendCutByAStop(t *testing.T) {
537 whatsapp, fake := whatsappStartTestLinked(t)
538 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
539 whatsappTestConnect(t, fake)
540
541 digest := sha256.Sum256([]byte("hi"))
542 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
543 ID: "keys",
544 Chat: whatsappTestAna.String(),
545 Digest: hex.EncodeToString(digest[:]),
546 State: whatsappSending,
547 }))
548
549 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
550 require.NoError(t, err)
551 require.Contains(t, answer, "its result is unknown")
552 require.Empty(t, fake.Sent())
553 }
554
555 func TestWhatsAppSendRefuses(t *testing.T) {
556 testCases := []struct {
557 name string
558 id string
559 chat string
560 text string
561 err string
562 }{
563 {
564 name: "no id",
565 id: " ",
566 chat: whatsappTestAna.String(),
567 text: "hi",
568 err: "the id must have 1 to 100 bytes",
569 },
570 {
571 name: "no text",
572 id: "a",
573 chat: whatsappTestAna.String(),
574 text: "",
575 err: "the text must have 1 to 65536 bytes",
576 },
577 {
578 name: "a name for a chat",
579 id: "a",
580 chat: "Ana Souza",
581 text: "hi",
582 err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`,
583 },
584 {
585 name: "a status broadcast",
586 id: "a",
587 chat: types.StatusBroadcastJID.String(),
588 text: "hi",
589 err: `"status@broadcast" is not a private chat or a group`,
590 },
591 {
592 name: "the chat of the user",
593 id: "a",
594 chat: whatsappTestOwn.String(),
595 text: "hi",
596 err: "this is the chat of the user with itself: send to another chat",
597 },
598 }
599
600 whatsapp, fake := whatsappStartTestLinked(t)
601 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
602 whatsappTestConnect(t, fake)
603
604 for _, tc := range testCases {
605 t.Run(tc.name, func(t *testing.T) {
606 _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text)
607 require.EqualError(t, err, tc.err)
608 })
609 }
610
611 require.Empty(t, fake.Sent())
612 }
613
614 func TestWhatsAppSendWhileDisconnected(t *testing.T) {
615 whatsapp, fake := whatsappStartTestLinked(t)
616 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
617 whatsappTestConnect(t, fake)
618 require.True(t, fake.Emit(&events.Disconnected{}))
619
620 _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
621 require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later")
622
623 require.True(t, fake.Emit(&events.Connected{}))
624 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
625 require.NoError(t, err)
626 require.Equal(t, "Sent to +15550002222 at 12:05.", answer)
627 }
628
629 func TestWhatsAppSendWithoutALink(t *testing.T) {
630 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
631
632 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
633 require.NoError(t, err)
634 require.Equal(t, whatsappNotLinked, answer)
635 }
636
637 func TestAgentSendsAWhatsAppMessage(t *testing.T) {
638 whatsapp, fake := whatsappStartTestLinked(t)
639 whatsappTestContacts(t, whatsapp)
640 server := newModelFake(t)
641 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
642 if call.number == 0 {
643 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "whatsapp_send", `{"id":"keys","chat":"15550002222@s.whatsapp.net","text":"The keys are under the mat."}`)}, 100, 0, 5))
644 return
645 }
646
647 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5))
648 }
649
650 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
651 client := terminalStartTestClient(t, socket)
652 whatsappTestConnect(t, fake)
653
654 client.Type("Tell Ana that the keys are under the mat.")
655 client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]")
656 require.Len(t, fake.Sent(), 1)
657 }