17 "github.com/stretchr/testify/require"
18 "go.mau.fi/whatsmeow/types"
19 "go.mau.fi/whatsmeow/types/events"
22 func TestWhatsAppSendSendsOnceForEachID(t *testing.T) {
23 whatsapp, fake := whatsappStartTestLinked(t)
24 whatsappTestContacts(t, whatsapp)
25 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
26 whatsappTestConnect(t, fake)
28 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.", "")
29 require.NoError(t, err)
30 require.Equal(t, "Sent to Ana Souza at 12:05.", answer)
32 answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.", "")
33 require.NoError(t, err)
34 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)
36 _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.", "")
37 require.EqualError(t, err, "the id keys belongs to another message: give a new id for each message")
39 require.Equal(t, []WhatsAppFakeSend{
41 to: whatsappTestAna.String(),
42 text: "The keys are under the mat.",
47 again, err := openWhatsAppState(filepath.Join(whatsapp.dir, whatsappStateFileName))
48 require.NoError(t, err)
50 record, found := again.Send("keys")
51 require.True(t, found)
52 require.Equal(t, whatsappSent, record.State)
53 require.Equal(t, "MSG1", record.MessageID)
56 func TestWhatsAppSendToAGroup(t *testing.T) {
57 whatsapp, fake := whatsappStartTestLinked(t)
58 fake.groups = []*types.GroupInfo{
60 JID: whatsappTestFamily,
61 GroupName: types.GroupName{
67 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
68 whatsappTestConnect(t, fake)
70 answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.", "")
71 require.NoError(t, err)
72 require.Equal(t, "Sent to Family at 12:05.", answer)
75 func TestWhatsAppSendThatFails(t *testing.T) {
76 whatsapp, fake := whatsappStartTestLinked(t)
77 fake.sendErr = errors.New("server error for " + whatsappTestNumber)
79 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
80 whatsappTestConnect(t, fake)
83 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
84 require.NoError(t, err)
85 require.True(t, strings.HasPrefix(answer, "The send failed: server error for OWN."), answer)
88 require.Len(t, fake.Sent(), 1)
91 func TestWhatsAppSendLogsEachSendWithoutTextOrNumbers(t *testing.T) {
93 whatsappTestSession(t, dir, true)
95 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
96 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
97 whatsappTestConnect(t, fake)
99 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 4, 3))
100 require.NoError(t, err)
102 _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.", "")
103 require.NoError(t, err)
105 _, err = whatsapp.Send(t.Context(), "plan", whatsappTestAna.String(), "The plan.", name)
106 require.NoError(t, err)
108 fake.sendErr = errors.New("server error for " + whatsappTestNumber)
109 _, err = whatsapp.Send(t.Context(), "late", whatsappTestAna.String(), "Late.", "")
110 require.NoError(t, err)
112 chat := whatsappChatHash(whatsappTestAna)
113 require.Contains(t, logs.String(), "<6>message sent chat="+chat+" image=false\n")
114 require.Contains(t, logs.String(), "<6>message sent chat="+chat+" image=true\n")
115 require.Contains(t, logs.String(), "<4>message send failed chat="+chat+" image=false: server error for OWN\n")
117 for _, secret := range []string{"keys", "plan", "Late", whatsappTestNumber, whatsappTestAna.User} {
118 require.NotContains(t, logs.String(), secret)
122 func TestWhatsAppSendCutByAStop(t *testing.T) {
123 whatsapp, fake := whatsappStartTestLinked(t)
124 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
125 whatsappTestConnect(t, fake)
127 digest := sha256.Sum256([]byte("hi"))
128 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
130 Chat: whatsappTestAna.String(),
131 Digest: hex.EncodeToString(digest[:]),
132 State: whatsappSending,
135 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
136 require.NoError(t, err)
137 require.Contains(t, answer, "its result is unknown")
138 require.Empty(t, fake.Sent())
141 func TestWhatsAppSendRefuses(t *testing.T) {
142 testCases := []struct {
152 chat: whatsappTestAna.String(),
154 err: "the id must have 1 to 100 bytes",
159 chat: whatsappTestAna.String(),
161 err: "the text must have 1 to 65536 bytes",
164 name: "a name for a chat",
168 err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`,
171 name: "a status broadcast",
173 chat: types.StatusBroadcastJID.String(),
175 err: `"status@broadcast" is not a private chat or a group`,
178 name: "the chat of the user",
180 chat: whatsappTestOwn.String(),
182 err: "this is the chat of the user with itself: send to another chat",
186 whatsapp, fake := whatsappStartTestLinked(t)
187 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
188 whatsappTestConnect(t, fake)
190 for _, tc := range testCases {
191 t.Run(tc.name, func(t *testing.T) {
192 _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text, "")
193 require.EqualError(t, err, tc.err)
197 require.Empty(t, fake.Sent())
200 func TestWhatsAppSendWhileDisconnected(t *testing.T) {
201 whatsapp, fake := whatsappStartTestLinked(t)
202 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
203 whatsappTestConnect(t, fake)
204 require.True(t, fake.Emit(&events.Disconnected{}))
206 _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
207 require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later")
209 require.True(t, fake.Emit(&events.Connected{}))
211 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
212 require.NoError(t, err)
213 require.Equal(t, "Sent to +15550002222 at 12:05.", answer)
216 func TestWhatsAppSendWithoutALink(t *testing.T) {
217 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
219 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
220 require.NoError(t, err)
221 require.Equal(t, whatsappNotLinked, answer)
224 func TestAgentSendsAWhatsAppMessage(t *testing.T) {
225 whatsapp, fake := whatsappStartTestLinked(t)
226 whatsappTestContacts(t, whatsapp)
228 server := newModelFake(t)
229 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
230 if call.number == 0 {
231 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))
236 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5))
239 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
240 client := terminalStartTestClient(t, socket)
241 whatsappTestConnect(t, fake)
243 client.Type("Tell Ana that the keys are under the mat.")
244 client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]")
245 require.Len(t, fake.Sent(), 1)
248 func TestWhatsAppImageSendUploadsOnceAndKeepsCaption(t *testing.T) {
249 whatsapp, fake := whatsappStartTestLinked(t)
250 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
251 whatsappTestConnect(t, fake)
253 picture := imageTestPNG(t, 4, 3)
254 name, err := whatsapp.inbox.images.Save(picture)
255 require.NoError(t, err)
257 box := &Toolbox{whatsapp: whatsapp}
258 arguments := `{"id":"photo","chat":"15550002222@s.whatsapp.net","text":"The plan.","image":"` + name + `"}`
259 answer := box.run(t.Context(), "whatsapp_send", arguments)
260 require.Contains(t, answer, "Sent to")
261 require.Contains(t, box.run(t.Context(), "whatsapp_send", arguments), "nothing was sent again")
262 require.Equal(t, [][]byte{picture}, fake.Uploads())
263 require.Len(t, fake.Sent(), 1)
264 require.Equal(t, "MSG1", fake.Sent()[0].id)
265 require.Empty(t, fake.Sent()[0].text)
267 messages := fake.ImageSends()
268 require.Len(t, messages, 1)
270 message := messages[0]
271 require.Equal(t, "The plan.", message.GetCaption())
272 require.Equal(t, "image/png", message.GetMimetype())
273 require.Equal(t, "https://example.com/image", message.GetURL())
274 require.Equal(t, "/image", message.GetDirectPath())
275 require.Equal(t, []byte("made-up key"), message.GetMediaKey())
276 require.Equal(t, []byte("made-up hash"), message.GetFileSHA256())
277 require.Equal(t, []byte("made-up encrypted hash"), message.GetFileEncSHA256())
278 require.Equal(t, uint64(len(picture)), message.GetFileLength())
280 state, err := openWhatsAppState(whatsapp.state.path)
281 require.NoError(t, err)
283 record, found := state.Send("photo")
284 require.True(t, found)
285 require.Equal(t, name, record.Image)
286 require.Equal(t, whatsappSent, record.State)
288 again, err := whatsappSend(t.Context(), whatsapp.link(), whatsapp.groups, state, whatsapp.inbox.images, discardLog(), whatsapp.clock, "photo", whatsappTestAna.String(), "The plan.", name)
289 require.NoError(t, err)
290 require.Contains(t, again, "nothing was sent again")
291 require.Len(t, fake.Uploads(), 1)
292 require.Len(t, fake.Sent(), 1)
294 other, err := whatsapp.inbox.images.Save(imageTestPNG(t, 1, 1))
295 require.NoError(t, err)
297 _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", other)
298 require.ErrorContains(t, err, "belongs to another message")
300 _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", "")
301 require.ErrorContains(t, err, "belongs to another message")
302 require.Len(t, fake.Uploads(), 1)
305 func TestWhatsAppImageSendAllowsEmptyCaptionAndRefusesUnknownImage(t *testing.T) {
306 whatsapp, fake := whatsappStartTestLinked(t)
307 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
308 whatsappTestConnect(t, fake)
310 _, err := whatsapp.Send(t.Context(), "unknown", whatsappTestAna.String(), "A caption.", "../image.png")
311 require.ErrorContains(t, err, "invalid image name")
313 _, found := whatsapp.state.Send("unknown")
314 require.False(t, found)
315 require.Empty(t, fake.Uploads())
316 require.Empty(t, fake.Sent())
318 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 3, 2))
319 require.NoError(t, err)
321 _, err = whatsapp.Send(t.Context(), "no-caption", whatsappTestAna.String(), "", name)
322 require.NoError(t, err)
323 require.Len(t, fake.ImageSends(), 1)
324 require.Empty(t, fake.ImageSends()[0].GetCaption())
327 func TestWhatsAppImageFailureKeepsNoSecretsAndDoesNotRetry(t *testing.T) {
328 testCases := []struct {
333 name: "upload failure stores a safe cause",
336 {name: "send failure stores a safe cause"},
338 for _, tc := range testCases {
339 t.Run(tc.name, func(t *testing.T) {
340 whatsapp, fake := whatsappStartTestLinked(t)
341 failure := errors.New("https://example.com/media?token=secret /private-media/key phone=" + whatsappTestNumber)
343 fake.uploadErr = failure
345 fake.sendErr = failure
348 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
349 whatsappTestConnect(t, fake)
351 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 1))
352 require.NoError(t, err)
355 answer, err := whatsapp.Send(t.Context(), "failed-photo", whatsappTestAna.String(), "Private caption.", name)
356 require.NoError(t, err)
357 require.Contains(t, answer, "media send failed")
358 require.NotContains(t, answer, "secret")
359 require.NotContains(t, answer, "/private-media")
360 require.NotContains(t, answer, whatsappTestNumber)
361 require.NotContains(t, answer, "Private caption")
364 require.Len(t, fake.Uploads(), 1)
367 require.Empty(t, fake.Sent())
369 require.Len(t, fake.Sent(), 1)
372 record, found := whatsapp.state.Send("failed-photo")
373 require.True(t, found)
374 require.Equal(t, whatsappFailed, record.State)
375 require.NotContains(t, record.Answer, "secret")
380 func TestWhatsAppImageInProgressRecordDoesNotUploadAgain(t *testing.T) {
381 whatsapp, fake := whatsappStartTestLinked(t)
382 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
383 whatsappTestConnect(t, fake)
385 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 2))
386 require.NoError(t, err)
388 digest := sha256.Sum256([]byte("The plan."))
389 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
390 ID: "interrupted-image",
391 Chat: whatsappTestAna.String(),
392 Digest: hex.EncodeToString(digest[:]),
394 State: whatsappSending,
397 answer, err := whatsapp.Send(t.Context(), "interrupted-image", whatsappTestAna.String(), "The plan.", name)
398 require.NoError(t, err)
399 require.Contains(t, answer, "its result is unknown")
400 require.Empty(t, fake.Uploads())
401 require.Empty(t, fake.Sent())
404 func TestTelegramReceivedPhotoCanBeShownAndSentToWhatsApp(t *testing.T) {
405 whatsapp, fake := whatsappStartTestLinked(t)
406 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
407 whatsappTestConnect(t, fake)
409 telegram := newTelegramFake(t)
410 picture := imageTestPNG(t, 5, 5)
411 telegram.Image("received", picture)
413 api := newTelegramAPI(telegram.URL, telegramTestToken)
414 text, err := telegramImageText(t.Context(), api, whatsapp.inbox.images, &TelegramMessage{
415 Caption: "The new plan.",
416 Photo: []TelegramPhoto{{
422 require.NoError(t, err)
424 names := imageFiles(text)
425 require.Len(t, names, 1)
427 bot, logs := telegramStartTestBot(t, telegram)
430 imageShow: &ImageShow{
431 store: whatsapp.inbox.images,
435 require.NotContains(t, box.run(t.Context(), "image_show", `{"image":"`+names[0]+`","caption":"The plan."}`), "error:")
436 require.Contains(t, box.run(t.Context(), "whatsapp_send", `{"id":"forward","chat":"15550002222@s.whatsapp.net","text":"The plan.","image":"`+names[0]+`"}`), "Sent to")
437 require.Equal(t, [][]byte{picture}, fake.Uploads())
438 require.Equal(t, "The plan.", fake.ImageSends()[0].GetCaption())
440 calls := telegram.Calls()
441 require.Equal(t, "sendPhoto", calls[len(calls)-1].method)
442 require.Equal(t, picture, calls[len(calls)-1].params["photo"])
443 require.Equal(t, "<6>image sent\n", logs.String())
446 func TestWhatsAppImageUploadEndsWithItsCallerOrTimeout(t *testing.T) {
447 synctest.Test(t, func(t *testing.T) {
448 client := &WhatsAppUploadWaitFake{WhatsAppFake: &WhatsAppFake{}}
450 _, err := whatsappImageMessage(t.Context(), client, imageTestPNG(t, 1, 1), "")
451 require.ErrorIs(t, err, context.DeadlineExceeded)
452 require.Equal(t, 2*time.Minute, time.Since(start))
454 ctx, cancel := context.WithCancel(t.Context())
460 _, err = whatsappImageMessage(ctx, client, imageTestPNG(t, 1, 1), "")
461 require.ErrorIs(t, err, context.Canceled)
462 require.Zero(t, time.Since(start))
466 type WhatsAppUploadWaitFake struct {
470 func (f *WhatsAppUploadWaitFake) Upload(ctx context.Context, _ []byte, _ whatsmeow.MediaType) (whatsmeow.UploadResponse, error) {
473 return whatsmeow.UploadResponse{}, ctx.Err()