]> git.giacomolli.uk - onechat/blob - whatsapp_test.go
Wait for the WhatsApp catch-up before a turn
[onechat] / whatsapp_test.go
1 package main
2
3 import (
4 "errors"
5 "net/http"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/require"
13 "go.mau.fi/whatsmeow"
14 "go.mau.fi/whatsmeow/proto/waE2E"
15 "go.mau.fi/whatsmeow/proto/waWeb"
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 require.Equal(t, []string{whatsappTestNumber}, fake.Phones())
141 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
142 require.Equal(t, "[connected to OneChat]\n", other.Output())
143 require.Zero(t, server.Calls())
144 require.Equal(t, []string{"WhatsApp is linked."}, whatsappTestLogTexts(t, memory))
145
146 info, err := os.Stat(filepath.Join(dir, whatsappDirName))
147 require.NoError(t, err)
148 require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
149
150 info, err = os.Stat(filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
151 require.NoError(t, err)
152 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
153 }
154
155 func TestWhatsAppPairingCodeIsATelegramMessageAlone(t *testing.T) {
156 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
157 telegram := newTelegramFake(t)
158 bot, _ := telegramStartTestBot(t, telegram)
159 daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
160
161 telegram.Add(TelegramUpdate{
162 UpdateID: 1,
163 Message: telegramOwnerMessage("/whatsapp pair 15550001111"),
164 })
165
166 items := <-fake.pairings
167 items <- whatsmeow.QRChannelItem{
168 Event: whatsmeow.QRChannelEventCode,
169 }
170
171 telegramWaitForSent(t, telegram, whatsappPairSteps, whatsappTestCode)
172 }
173
174 func TestWhatsAppPairingEnds(t *testing.T) {
175 testCases := []struct {
176 name string
177 end func(items chan whatsmeow.QRChannelItem)
178 answer string
179 }{
180 {
181 name: "the code expires",
182 end: func(items chan whatsmeow.QRChannelItem) {
183 items <- whatsmeow.QRChannelTimeout
184 },
185 answer: whatsappPairExpired,
186 },
187 {
188 name: "the library closes the pairing",
189 end: func(items chan whatsmeow.QRChannelItem) {
190 close(items)
191 },
192 answer: whatsappPairExpired,
193 },
194 {
195 name: "the pairing fails",
196 end: func(items chan whatsmeow.QRChannelItem) {
197 items <- whatsmeow.QRChannelItem{
198 Event: whatsmeow.QRChannelEventError,
199 Error: errors.New("bad handshake"),
200 }
201 },
202 answer: "The pairing failed: bad handshake. Run /whatsapp pair PHONE again.",
203 },
204 }
205
206 for _, tc := range testCases {
207 t.Run(tc.name, func(t *testing.T) {
208 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
209 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
210 client := terminalStartTestClient(t, socket)
211 client.Type("/whatsapp pair 15550001111")
212
213 items := <-fake.pairings
214 items <- whatsmeow.QRChannelItem{
215 Event: whatsmeow.QRChannelEventCode,
216 }
217
218 client.WaitFor(t, whatsappTestCode)
219 tc.end(items)
220 client.WaitFor(t, tc.answer)
221 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
222 })
223 }
224 }
225
226 func TestWhatsAppNewPairingEndsTheOneBefore(t *testing.T) {
227 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
228 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
229 client := terminalStartTestClient(t, socket)
230
231 client.Type("/whatsapp pair 15550001111")
232 <-fake.pairings
233 client.Type("/whatsapp pair 15550002222")
234 items := <-fake.pairings
235 items <- whatsmeow.QRChannelItem{
236 Event: whatsmeow.QRChannelEventCode,
237 }
238
239 client.WaitFor(t, whatsappTestCode)
240 require.Equal(t, []string{"15550002222"}, fake.Phones())
241 require.NotContains(t, client.Output(), "did not start the pairing")
242 }
243
244 func TestWhatsAppPairWhenLinked(t *testing.T) {
245 dir := t.TempDir()
246 whatsappTestSession(t, dir, true)
247 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
248 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
249 client := terminalStartTestClient(t, socket)
250
251 client.Type("/whatsapp pair 15550001111")
252 client.WaitFor(t, "WhatsApp is linked already. Run /whatsapp unlink first.")
253 }
254
255 func TestWhatsAppUnlink(t *testing.T) {
256 testCases := []struct {
257 name string
258 logoutErr error
259 answer string
260 }{
261 {
262 name: "with a logout",
263 answer: "WhatsApp is unlinked.",
264 },
265 {
266 name: "with no logout",
267 logoutErr: errors.New("not connected"),
268 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.",
269 },
270 }
271
272 for _, tc := range testCases {
273 t.Run(tc.name, func(t *testing.T) {
274 dir := t.TempDir()
275 whatsappTestSession(t, dir, true)
276 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
277 fake.logoutErr = tc.logoutErr
278 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
279 client := terminalStartTestClient(t, socket)
280 <-fake.connects
281
282 client.Type("/whatsapp unlink")
283 client.WaitFor(t, tc.answer)
284 require.Equal(t, 1, fake.Logouts())
285 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
286 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
287 require.Equal(t, []string{"WhatsApp is unlinked."}, whatsappTestLogTexts(t, memory))
288
289 client.Type("/whatsapp unlink")
290 client.WaitFor(t, "WhatsApp is not linked.\n")
291 })
292 }
293 }
294
295 func TestWhatsAppLogoutFromThePhone(t *testing.T) {
296 dir := t.TempDir()
297 whatsappTestSession(t, dir, true)
298 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
299 telegram := newTelegramFake(t)
300 bot, _ := telegramStartTestBot(t, telegram)
301 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
302 client := terminalStartTestClient(t, socket)
303 client.WaitFor(t, "[connected to OneChat]")
304 <-fake.connects
305
306 require.True(t, fake.Emit(&events.LoggedOut{}))
307 notice := "WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again."
308 client.WaitFor(t, notice)
309 telegramWaitForSent(t, telegram, notice)
310 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
311 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
312 require.Equal(t, []string{"WhatsApp was unlinked from the phone."}, whatsappTestLogTexts(t, memory))
313 }
314
315 func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) {
316 dir := t.TempDir()
317 whatsappTestSession(t, dir, true)
318 clock := newTestClock()
319 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
320 whatsapp.noticeDelay = 50 * time.Millisecond
321 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
322 client := terminalStartTestClient(t, socket)
323 client.WaitFor(t, "[connected to OneChat]")
324 <-fake.connects
325
326 require.True(t, fake.Emit(&events.Connected{}))
327 require.True(t, fake.Emit(&events.Disconnected{}))
328 require.True(t, fake.Emit(&events.Connected{}))
329 time.Sleep(100 * time.Millisecond)
330 require.Equal(t, "[connected to OneChat]\n", client.Output())
331
332 clock.now = clock.now.Add(time.Hour)
333 require.True(t, fake.Emit(&events.Disconnected{}))
334 require.True(t, fake.Emit(&events.Disconnected{}))
335 client.WaitFor(t, "WhatsApp is disconnected since 2026-01-01 13:00. OneChat tries again.\n")
336 require.True(t, fake.Emit(&events.Disconnected{}))
337 time.Sleep(100 * time.Millisecond)
338
339 require.True(t, fake.Emit(&events.Connected{}))
340 client.WaitFor(t, "WhatsApp is connected again.\n")
341 require.Equal(t, 1, strings.Count(client.Output(), "WhatsApp is disconnected since"))
342 }
343
344 func TestWhatsAppDisconnectionWithNoRetryGivesANoticeAtOnce(t *testing.T) {
345 dir := t.TempDir()
346 whatsappTestSession(t, dir, true)
347 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
348 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
349 client := terminalStartTestClient(t, socket)
350 client.WaitFor(t, "[connected to OneChat]")
351 <-fake.connects
352
353 require.True(t, fake.Emit(&events.StreamReplaced{}))
354 client.WaitFor(t, "WhatsApp is disconnected and does not connect again by itself: stream replaced. Restart the daemon to try again.")
355 }
356
357 func TestWhatsAppLogsNoOwnNumber(t *testing.T) {
358 dir := t.TempDir()
359 whatsappTestSession(t, dir, true)
360 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
361 fake.connectErr = errors.New("no route for " + whatsappTestNumber + "@s.whatsapp.net and " + whatsappTestLID + "@lid")
362 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
363 <-fake.connects
364
365 require.Eventually(t, func() bool {
366 return logs.String() != ""
367 }, 10*time.Second, time.Millisecond)
368 require.Equal(t, "connect: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String())
369 }
370
371 func TestWhatsAppPhone(t *testing.T) {
372 testCases := []struct {
373 name string
374 phone string
375 valid bool
376 }{
377 {
378 name: "a number with a plus and dashes",
379 phone: "+1-555-000-1111",
380 valid: true,
381 },
382 {
383 name: "a number with no country code",
384 phone: "0115550001111",
385 valid: false,
386 },
387 {
388 name: "a short number",
389 phone: "123456",
390 valid: false,
391 },
392 {
393 name: "a long number",
394 phone: "1234567890123456",
395 valid: false,
396 },
397 {
398 name: "a number with letters",
399 phone: "1555ABC1111",
400 valid: false,
401 },
402 }
403
404 for _, tc := range testCases {
405 t.Run(tc.name, func(t *testing.T) {
406 _, valid := whatsappPhone(tc.phone)
407 require.Equal(t, tc.valid, valid)
408 })
409 }
410 }
411
412 func TestWhatsAppCommandArgs(t *testing.T) {
413 testCases := []struct {
414 name string
415 line string
416 args string
417 found bool
418 }{
419 {
420 name: "a command with its arguments",
421 line: " /whatsapp pair +1 555 ",
422 args: "pair +1 555",
423 found: true,
424 },
425 {
426 name: "the command alone",
427 line: "/whatsapp",
428 args: "",
429 found: true,
430 },
431 {
432 name: "a word that starts with the command",
433 line: "/whatsappstatus",
434 args: "",
435 found: false,
436 },
437 {
438 name: "a message that names the command",
439 line: "what does /whatsapp status do?",
440 args: "",
441 found: false,
442 },
443 }
444
445 for _, tc := range testCases {
446 t.Run(tc.name, func(t *testing.T) {
447 args, found := whatsappCommandArgs(tc.line)
448 require.Equal(t, tc.args, args)
449 require.Equal(t, tc.found, found)
450 })
451 }
452 }
453
454 // whatsappTestLogTexts returns the texts of the messages in the log of memory,
455 // after the log holds at least one message.
456 func whatsappTestLogTexts(t *testing.T, memory *Memory) []string {
457 t.Helper()
458
459 require.Eventually(t, func() bool {
460 return memory.Len() > 0
461 }, 10*time.Second, time.Millisecond)
462
463 var texts []string
464 for id := range memory.Len() {
465 message, err := memory.Message(id)
466 require.NoError(t, err)
467
468 texts = append(texts, message.Text)
469 }
470
471 return texts
472 }
473
474 func TestWhatsAppPrivateBatchStartsATurnWithAnEmptyReply(t *testing.T) {
475 whatsapp, fake := whatsappStartTestLinked(t)
476 require.NoError(t, whatsapp.device.Contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
477
478 server := newModelFake(t)
479 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
480 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
481 }
482
483 telegram := newTelegramFake(t)
484 bot, _ := telegramStartTestBot(t, telegram)
485 socket, memory := daemonStartTestWithWhatsApp(t, server, bot, whatsapp)
486 client := terminalStartTestClient(t, socket)
487 client.WaitFor(t, "[connected to OneChat]")
488 whatsappTestConnect(t, fake)
489
490 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
491 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "Are you at home?", at)))
492 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestOwn, "m2", "Yes", at)))
493
494 text := "[WhatsApp] Ana Souza, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n12:00 Ana Souza: Are you at home?\n12:00 You: Yes\n" + whatsappBatchEmptyReply
495 client.WaitFor(t, text+"\n[working]\n[done]\n")
496 require.Equal(t, 1, server.Calls())
497 require.Contains(t, string(server.Call(0).raw), "Are you at home?")
498 require.Equal(t, []string{text}, whatsappTestLogTexts(t, memory))
499 require.Empty(t, telegram.Sent())
500 require.Eventually(t, func() bool {
501 return len(whatsapp.batches.Closed()) == 0
502 }, 10*time.Second, time.Millisecond)
503 }
504
505 func TestWhatsAppGroupTurns(t *testing.T) {
506 testCases := []struct {
507 name string
508 message func(event *events.Message)
509 line string
510 turn bool
511 }{
512 {
513 name: "plain",
514 message: func(event *events.Message) {
515 },
516 line: "12:00 Bruno Lima: Who brings the cake?",
517 },
518 {
519 name: "mention",
520 message: func(event *events.Message) {
521 event.Message = &waE2E.Message{
522 ExtendedTextMessage: &waE2E.ExtendedTextMessage{
523 Text: new("@" + whatsappTestLID + " can you bring the cake? Call " + whatsappTestNumber + "."),
524 ContextInfo: &waE2E.ContextInfo{
525 MentionedJID: []string{whatsappTestLID + "@lid"},
526 },
527 },
528 }
529 },
530 line: "12:00 Bruno Lima (mentions you): @you can you bring the cake? Call OWN.",
531 turn: true,
532 },
533 {
534 name: "reply",
535 message: func(event *events.Message) {
536 event.Message = &waE2E.Message{
537 ExtendedTextMessage: &waE2E.ExtendedTextMessage{
538 Text: new("Who brings the cake?"),
539 ContextInfo: &waE2E.ContextInfo{
540 Participant: new(whatsappTestOwn.String()),
541 },
542 },
543 }
544 },
545 line: "12:00 Bruno Lima (replies to you): Who brings the cake?",
546 turn: true,
547 },
548 }
549
550 for _, tc := range testCases {
551 t.Run(tc.name, func(t *testing.T) {
552 whatsapp, fake := whatsappStartTestLinked(t)
553 fake.groups = []*types.GroupInfo{
554 {
555 JID: whatsappTestFamily,
556 GroupName: types.GroupName{
557 Name: "Family",
558 },
559 },
560 }
561
562 server := newModelFake(t)
563 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
564 client := terminalStartTestClient(t, socket)
565 client.WaitFor(t, "[connected to OneChat]")
566 whatsappTestConnect(t, fake)
567
568 event := whatsappTestMessage(whatsappTestFamily, whatsappTestBruno, "m1", "Who brings the cake?", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
569 event.Info.PushName = "Bruno Lima"
570 tc.message(event)
571 require.True(t, fake.Emit(event))
572
573 text := "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n" + tc.line
574 if tc.turn {
575 text += "\n" + whatsappBatchEmptyReply
576 client.WaitFor(t, "[done]")
577 require.Equal(t, 1, server.Calls())
578 } else {
579 client.WaitFor(t, text)
580 time.Sleep(100 * time.Millisecond)
581 require.Zero(t, server.Calls())
582 }
583
584 require.Equal(t, text, whatsappTestLogTexts(t, memory)[0])
585 })
586 }
587 }
588
589 func TestWhatsAppTakesOnlyText(t *testing.T) {
590 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
591 testCases := []struct {
592 name string
593 change func(event *events.Message)
594 }{
595 {
596 name: "an edit",
597 change: func(event *events.Message) {
598 event.IsEdit = true
599 },
600 },
601 {
602 name: "old history",
603 change: func(event *events.Message) {
604 event.SourceWebMsg = &waWeb.WebMessageInfo{}
605 },
606 },
607 {
608 name: "a view-once message",
609 change: func(event *events.Message) {
610 event.IsViewOnce = true
611 },
612 },
613 {
614 name: "a disappearing message",
615 change: func(event *events.Message) {
616 event.IsEphemeral = true
617 },
618 },
619 {
620 name: "the chat of the user with itself",
621 change: func(event *events.Message) {
622 event.Info.Chat = whatsappTestOwn
623 },
624 },
625 {
626 name: "a status broadcast",
627 change: func(event *events.Message) {
628 event.Info.Chat = types.StatusBroadcastJID
629 },
630 },
631 {
632 name: "a photo with a caption",
633 change: func(event *events.Message) {
634 event.Message = &waE2E.Message{
635 ImageMessage: &waE2E.ImageMessage{
636 Caption: new("look"),
637 },
638 }
639 },
640 },
641 {
642 name: "a text of spaces",
643 change: func(event *events.Message) {
644 event.Message.Conversation = new(" ")
645 },
646 },
647 }
648
649 for _, tc := range testCases {
650 t.Run(tc.name, func(t *testing.T) {
651 whatsapp, _ := whatsappStartTestLinked(t)
652 event := whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "hi", at)
653 tc.change(event)
654 require.True(t, whatsapp.handle(event))
655 require.NoError(t, whatsapp.batches.CloseAll())
656 require.Empty(t, whatsapp.batches.Closed())
657 })
658 }
659 }
660
661 // whatsappStartTestLinked opens a WhatsApp with a linked device in a temporary
662 // directory, with a quiet time of 50 ms for its batches.
663 func whatsappStartTestLinked(t *testing.T) (*WhatsApp, *WhatsAppFake) {
664 t.Helper()
665
666 dir := t.TempDir()
667 whatsappTestSession(t, dir, true)
668 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
669 whatsapp.batches.quiet = 50 * time.Millisecond
670
671 return whatsapp, fake
672 }
673
674 func TestWhatsAppCatchupGivesOneTurn(t *testing.T) {
675 whatsapp, fake := whatsappStartTestLinked(t)
676 server := newModelFake(t)
677 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
678 client := terminalStartTestClient(t, socket)
679 client.WaitFor(t, "[connected to OneChat]")
680 <-fake.connects
681
682 require.True(t, fake.Emit(&events.Connected{}))
683 at := time.Date(2026, 1, 1, 8, 0, 0, 0, time.UTC)
684 for index, event := range []*events.Message{
685 whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "Good morning", at),
686 whatsappTestMessage(whatsappTestBruno, whatsappTestBruno, "m2", "Call me", at.Add(time.Hour)),
687 whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m3", "Are you there?", at.Add(2*time.Hour)),
688 } {
689 event.Info.PushName = []string{"Ana", "Bruno", "Ana"}[index]
690 require.True(t, fake.Emit(event))
691 }
692
693 time.Sleep(100 * time.Millisecond)
694 require.Zero(t, memory.Len())
695
696 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
697 client.WaitFor(t, "[done]")
698 require.Equal(t, []string{
699 "[WhatsApp] Ana, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n08:00 Ana: Good morning\n" + whatsappBatchEmptyReply,
700 "[WhatsApp] Bruno, private chat 15550003333@s.whatsapp.net, Thursday 2026-01-01\n09:00 Bruno: Call me\n" + whatsappBatchEmptyReply,
701 "[WhatsApp] Ana, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n10:00 Ana: Are you there?\n" + whatsappBatchEmptyReply,
702 }, whatsappTestLogTexts(t, memory)[:3])
703
704 time.Sleep(100 * time.Millisecond)
705 require.Equal(t, 1, server.Calls())
706 for _, text := range []string{"Good morning", "Call me", "Are you there?"} {
707 require.Contains(t, string(server.Call(0).raw), text)
708 }
709 }
710
711 func TestWhatsAppCatchupEndsAfterItsLimit(t *testing.T) {
712 whatsapp, fake := whatsappStartTestLinked(t)
713 whatsapp.catchupLimit = 100 * time.Millisecond
714 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
715 client := terminalStartTestClient(t, socket)
716 client.WaitFor(t, "[connected to OneChat]")
717 <-fake.connects
718
719 require.True(t, fake.Emit(&events.Connected{}))
720 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestOwn, "m1", "On my way.", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))))
721 client.WaitFor(t, "12:00 You: On my way.")
722 require.Len(t, whatsappTestLogTexts(t, memory), 1)
723 }
724
725 func TestWhatsAppBatchesOfTheStateFileWaitForTheCatchup(t *testing.T) {
726 dir := t.TempDir()
727 whatsappTestSession(t, dir, true)
728 before, _, _ := whatsappStartTest(t, dir, newTestClock())
729 _, err := before.batches.Add(whatsappTestChat, WhatsAppMessage{
730 ID: "m1",
731 Sender: "Ana Souza",
732 Time: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
733 Text: "Did you see my message?",
734 }, time.Now().Add(-time.Hour))
735 require.NoError(t, err)
736
737 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
738 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
739 client := terminalStartTestClient(t, socket)
740 client.WaitFor(t, "[connected to OneChat]")
741 <-fake.connects
742
743 time.Sleep(100 * time.Millisecond)
744 require.Zero(t, memory.Len())
745
746 require.True(t, fake.Emit(&events.Connected{}))
747 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
748 client.WaitFor(t, "12:00 Ana Souza: Did you see my message?")
749 client.WaitFor(t, "[done]")
750 }
751
752 // whatsappTestConnect waits for the connection of the daemon and gives it the
753 // events of a connection with an empty catch-up.
754 func whatsappTestConnect(t *testing.T, fake *WhatsAppFake) {
755 t.Helper()
756
757 <-fake.connects
758 require.True(t, fake.Emit(&events.Connected{}))
759 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
760 }