11 "github.com/stretchr/testify/require"
13 "go.mau.fi/whatsmeow/types"
14 "go.mau.fi/whatsmeow/types/events"
17 func TestWhatsAppStatusFollowsTheConnection(t *testing.T) {
19 whatsappTestSession(t, dir, true)
20 clock := newTestClock()
21 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
22 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
25 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
27 require.True(t, fake.Emit(&events.Connected{}))
28 require.Equal(t, "WhatsApp is linked and connected since 2026-01-01 12:00.", whatsapp.Status())
30 clock.now = clock.now.Add(20 * time.Minute)
31 require.True(t, fake.Emit(&events.Disconnected{}))
32 clock.now = clock.now.Add(time.Minute)
33 require.True(t, fake.Emit(&events.Disconnected{}))
34 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())
37 func TestWhatsAppStatusOfAFirstConnectionThatFailed(t *testing.T) {
39 whatsappTestSession(t, dir, true)
40 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
41 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
44 require.True(t, fake.Emit(&events.Disconnected{}))
45 require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:00.", whatsapp.Status())
46 require.Equal(t, "new WhatsApp messages wait, because the first connection failed (the next try is automatic)\n", logs.String())
49 func TestWhatsAppLogsEachChangeOfTheConnectionOnce(t *testing.T) {
51 whatsappTestSession(t, dir, true)
52 clock := newTestClock()
53 whatsapp, fake, logs := whatsappStartTest(t, dir, clock)
54 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
57 require.True(t, fake.Emit(&events.Connected{}))
58 clock.now = clock.now.Add(time.Minute)
59 require.True(t, fake.Emit(&events.Disconnected{}))
60 clock.now = clock.now.Add(time.Minute)
61 require.True(t, fake.Emit(&events.Disconnected{}))
62 clock.now = clock.now.Add(25 * time.Second)
63 require.True(t, fake.Emit(&events.Connected{}))
65 require.Equal(t, []string{
66 "WhatsApp is connected, and new messages come in",
67 "new WhatsApp messages wait, because the connection is lost (the next try is automatic)",
68 "WhatsApp is connected again, after 1m25s",
69 }, strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n"))
70 require.NotContains(t, logs.String(), whatsappTestNumber)
71 require.NotContains(t, logs.String(), whatsappTestLID)
74 func TestWhatsAppIsNotLinkedWithoutALinkedDevice(t *testing.T) {
75 testCases := []struct {
77 session func(t *testing.T, dir string)
80 name: "no session file",
81 session: func(t *testing.T, dir string) {
85 name: "a session of a pairing that did not end",
86 session: func(t *testing.T, dir string) {
87 whatsappTestSession(t, dir, false)
92 for _, tc := range testCases {
93 t.Run(tc.name, func(t *testing.T) {
96 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
97 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
102 func TestWhatsAppRefusesASessionThatOthersCanRead(t *testing.T) {
104 whatsappTestSession(t, dir, true)
105 require.NoError(t, os.Chmod(filepath.Join(dir, whatsappDirName, whatsappSessionFileName), 0o644))
107 _, err := openWhatsApp(t.Context(), dir, time.Now, whatsappDiscardLog(), whatsappNewClient)
108 require.EqualError(t, err, "whatsapp/session.db holds the WhatsApp session, so its mode must be 0600")
111 func TestWhatsAppCommandsAnswerOnlyTheClientThatAsked(t *testing.T) {
112 server := newModelFake(t)
113 telegram := newTelegramFake(t)
114 bot, _ := telegramStartTestBot(t, telegram)
115 socket := daemonStartTestWithBot(t, server, bot)
116 asking := terminalStartTestClient(t, socket)
117 other := terminalStartTestClient(t, socket)
118 asking.WaitFor(t, "[connected to OneChat]")
119 other.WaitFor(t, "[connected to OneChat]")
121 asking.Type("/whatsapp status")
122 asking.WaitFor(t, "WhatsApp is not linked.")
123 asking.Type(" /whatsapp unknown ")
124 asking.WaitFor(t, whatsappUsage)
125 asking.Type("/whatsapp pair 0123")
126 asking.WaitFor(t, "PHONE must be the number of your WhatsApp account")
128 telegram.Add(TelegramUpdate{
130 Message: telegramOwnerMessage("/whatsapp status"),
133 telegramWaitForSent(t, telegram, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.")
134 require.Len(t, telegram.Sent(), 1)
135 require.Equal(t, "[connected to OneChat]\n", other.Output())
136 require.Zero(t, server.Calls())
139 func TestWhatsAppPairsWithACode(t *testing.T) {
141 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
142 server := newModelFake(t)
143 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
144 asking := terminalStartTestClient(t, socket)
145 other := terminalStartTestClient(t, socket)
146 asking.WaitFor(t, "[connected to OneChat]")
147 other.WaitFor(t, "[connected to OneChat]")
149 asking.Type("/whatsapp pair +1 555-000-1111")
150 items := <-fake.pairings
151 items <- whatsmeow.QRChannelItem{
152 Event: whatsmeow.QRChannelEventCode,
155 asking.WaitFor(t, whatsappPairSteps+"\n"+whatsappTestCode+"\n")
156 require.True(t, fake.Emit(&events.PairSuccess{
157 ID: types.NewADJID(whatsappTestNumber, 0, 1),
158 LID: types.NewADJID(whatsappTestLID, 0, 1),
160 items <- whatsmeow.QRChannelSuccess
161 asking.WaitFor(t, "WhatsApp is linked to +15550001111.")
163 // The library connects again after the link with the context of the
164 // connection, so the end of the pairing must not end it.
165 connections := fake.Connections()
166 require.Len(t, connections, 1)
167 require.Never(t, func() bool {
168 return connections[0].Err() != nil
169 }, 200*time.Millisecond, time.Millisecond)
171 require.Equal(t, []string{whatsappTestNumber}, fake.Phones())
172 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
173 require.Equal(t, "[connected to OneChat]\n", other.Output())
174 require.Zero(t, server.Calls())
175 require.Equal(t, []string{"WhatsApp is linked."}, whatsappTestLogTexts(t, memory))
177 info, err := os.Stat(filepath.Join(dir, whatsappDirName))
178 require.NoError(t, err)
179 require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
181 info, err = os.Stat(filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
182 require.NoError(t, err)
183 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
186 func TestWhatsAppPairingCodeIsATelegramMessageAlone(t *testing.T) {
187 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
188 telegram := newTelegramFake(t)
189 bot, _ := telegramStartTestBot(t, telegram)
190 daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
192 telegram.Add(TelegramUpdate{
194 Message: telegramOwnerMessage("/whatsapp pair 15550001111"),
197 items := <-fake.pairings
198 items <- whatsmeow.QRChannelItem{
199 Event: whatsmeow.QRChannelEventCode,
202 telegramWaitForSent(t, telegram, whatsappPairSteps, whatsappTestCode)
205 func TestWhatsAppPairingEnds(t *testing.T) {
206 testCases := []struct {
208 end func(items chan whatsmeow.QRChannelItem)
212 name: "the code expires",
213 end: func(items chan whatsmeow.QRChannelItem) {
214 items <- whatsmeow.QRChannelTimeout
216 answer: whatsappPairExpired,
219 name: "the library closes the pairing",
220 end: func(items chan whatsmeow.QRChannelItem) {
223 answer: whatsappPairExpired,
226 name: "the pairing fails",
227 end: func(items chan whatsmeow.QRChannelItem) {
228 items <- whatsmeow.QRChannelItem{
229 Event: whatsmeow.QRChannelEventError,
230 Error: errors.New("bad handshake"),
233 answer: "The pairing failed: bad handshake. Run /whatsapp pair PHONE again.",
237 for _, tc := range testCases {
238 t.Run(tc.name, func(t *testing.T) {
239 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
240 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
241 client := terminalStartTestClient(t, socket)
242 client.Type("/whatsapp pair 15550001111")
244 items := <-fake.pairings
245 items <- whatsmeow.QRChannelItem{
246 Event: whatsmeow.QRChannelEventCode,
249 client.WaitFor(t, whatsappTestCode)
251 client.WaitFor(t, tc.answer)
252 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
257 func TestWhatsAppNewPairingEndsTheOneBefore(t *testing.T) {
258 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
259 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
260 client := terminalStartTestClient(t, socket)
262 client.Type("/whatsapp pair 15550001111")
264 client.Type("/whatsapp pair 15550002222")
265 items := <-fake.pairings
266 items <- whatsmeow.QRChannelItem{
267 Event: whatsmeow.QRChannelEventCode,
270 client.WaitFor(t, whatsappTestCode)
271 require.Equal(t, []string{"15550002222"}, fake.Phones())
272 require.NotContains(t, client.Output(), "did not start the pairing")
275 func TestWhatsAppPairWhenLinked(t *testing.T) {
277 whatsappTestSession(t, dir, true)
278 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
279 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
280 client := terminalStartTestClient(t, socket)
282 client.Type("/whatsapp pair 15550001111")
283 client.WaitFor(t, "WhatsApp is linked already. Run /whatsapp unlink first.")
286 func TestWhatsAppUnlink(t *testing.T) {
287 testCases := []struct {
293 name: "with a logout",
294 answer: "WhatsApp is unlinked.",
297 name: "with no logout",
298 logoutErr: errors.New("not connected"),
299 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.",
303 for _, tc := range testCases {
304 t.Run(tc.name, func(t *testing.T) {
306 whatsappTestSession(t, dir, true)
307 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
308 fake.logoutErr = tc.logoutErr
309 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
310 client := terminalStartTestClient(t, socket)
313 client.Type("/whatsapp unlink")
314 client.WaitFor(t, tc.answer)
315 require.Equal(t, 1, fake.Logouts())
316 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
317 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
318 require.Equal(t, []string{"WhatsApp is unlinked."}, whatsappTestLogTexts(t, memory))
320 client.Type("/whatsapp unlink")
321 client.WaitFor(t, "WhatsApp is not linked.\n")
326 func TestWhatsAppLogoutFromThePhone(t *testing.T) {
328 whatsappTestSession(t, dir, true)
329 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
330 telegram := newTelegramFake(t)
331 bot, _ := telegramStartTestBot(t, telegram)
332 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
333 client := terminalStartTestClient(t, socket)
334 client.WaitFor(t, "[connected to OneChat]")
337 require.True(t, fake.Emit(&events.LoggedOut{}))
338 notice := "WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again."
339 client.WaitFor(t, notice)
340 telegramWaitForSent(t, telegram, notice)
341 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
342 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
343 require.Equal(t, []string{"WhatsApp was unlinked from the phone."}, whatsappTestLogTexts(t, memory))
346 func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) {
348 whatsappTestSession(t, dir, true)
349 clock := newTestClock()
350 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
351 whatsapp.noticeDelay = 300 * time.Millisecond
352 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
353 client := terminalStartTestClient(t, socket)
354 client.WaitFor(t, "[connected to OneChat]")
357 require.True(t, fake.Emit(&events.Connected{}))
358 require.True(t, fake.Emit(&events.Disconnected{}))
359 require.True(t, fake.Emit(&events.Connected{}))
360 time.Sleep(400 * time.Millisecond)
361 require.Equal(t, "[connected to OneChat]\n", client.Output())
363 clock.now = clock.now.Add(time.Hour)
364 require.True(t, fake.Emit(&events.Disconnected{}))
365 require.True(t, fake.Emit(&events.Disconnected{}))
366 client.WaitFor(t, "WhatsApp is disconnected since 2026-01-01 13:00. OneChat tries again.\n")
367 require.True(t, fake.Emit(&events.Disconnected{}))
368 time.Sleep(400 * time.Millisecond)
370 require.True(t, fake.Emit(&events.Connected{}))
371 client.WaitFor(t, "WhatsApp is connected again.\n")
372 require.Equal(t, 1, strings.Count(client.Output(), "WhatsApp is disconnected since"))
375 func TestWhatsAppDisconnectionWithNoRetryGivesANoticeAtOnce(t *testing.T) {
377 whatsappTestSession(t, dir, true)
378 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
379 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
380 client := terminalStartTestClient(t, socket)
381 client.WaitFor(t, "[connected to OneChat]")
384 require.True(t, fake.Emit(&events.StreamReplaced{}))
385 client.WaitFor(t, "WhatsApp is disconnected and does not connect again by itself: stream replaced. Restart the daemon to try again.")
388 func TestWhatsAppLogsNoOwnNumber(t *testing.T) {
390 whatsappTestSession(t, dir, true)
391 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
392 fake.connectErr = errors.New("no route for " + whatsappTestNumber + "@s.whatsapp.net and " + whatsappTestLID + "@lid")
393 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
396 require.Eventually(t, func() bool {
397 return logs.String() != ""
398 }, 10*time.Second, time.Millisecond)
399 require.Equal(t, "WhatsApp messages do not come, because the connection failed, and it is tried again only at a restart of the daemon: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String())
402 func TestWhatsAppPhone(t *testing.T) {
403 testCases := []struct {
409 name: "a number with a plus and dashes",
410 phone: "+1-555-000-1111",
414 name: "a number with no country code",
415 phone: "0115550001111",
419 name: "a short number",
424 name: "a long number",
425 phone: "1234567890123456",
429 name: "a number with letters",
430 phone: "1555ABC1111",
435 for _, tc := range testCases {
436 t.Run(tc.name, func(t *testing.T) {
437 _, valid := whatsappPhone(tc.phone)
438 require.Equal(t, tc.valid, valid)
443 func TestWhatsAppCommandArgs(t *testing.T) {
444 testCases := []struct {
451 name: "a command with its arguments",
452 line: " /whatsapp pair +1 555 ",
457 name: "the command alone",
463 name: "a word that starts with the command",
464 line: "/whatsappstatus",
469 name: "a message that names the command",
470 line: "what does /whatsapp status do?",
476 for _, tc := range testCases {
477 t.Run(tc.name, func(t *testing.T) {
478 args, found := whatsappCommandArgs(tc.line)
479 require.Equal(t, tc.args, args)
480 require.Equal(t, tc.found, found)
485 func TestSubagentHasNoWhatsAppTools(t *testing.T) {
487 require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`))
488 require.Equal(t, `error: unknown tool "whatsapp_send"`, box.run(t.Context(), "whatsapp_send", `{"id":"a","chat":"15550002222@s.whatsapp.net","text":"hi"}`))