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 TestWhatsAppSendCutByAStop(t *testing.T) {
92 whatsapp, fake := whatsappStartTestLinked(t)
93 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
94 whatsappTestConnect(t, fake)
96 digest := sha256.Sum256([]byte("hi"))
97 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
99 Chat: whatsappTestAna.String(),
100 Digest: hex.EncodeToString(digest[:]),
101 State: whatsappSending,
104 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
105 require.NoError(t, err)
106 require.Contains(t, answer, "its result is unknown")
107 require.Empty(t, fake.Sent())
110 func TestWhatsAppSendRefuses(t *testing.T) {
111 testCases := []struct {
121 chat: whatsappTestAna.String(),
123 err: "the id must have 1 to 100 bytes",
128 chat: whatsappTestAna.String(),
130 err: "the text must have 1 to 65536 bytes",
133 name: "a name for a chat",
137 err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`,
140 name: "a status broadcast",
142 chat: types.StatusBroadcastJID.String(),
144 err: `"status@broadcast" is not a private chat or a group`,
147 name: "the chat of the user",
149 chat: whatsappTestOwn.String(),
151 err: "this is the chat of the user with itself: send to another chat",
155 whatsapp, fake := whatsappStartTestLinked(t)
156 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
157 whatsappTestConnect(t, fake)
159 for _, tc := range testCases {
160 t.Run(tc.name, func(t *testing.T) {
161 _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text, "")
162 require.EqualError(t, err, tc.err)
166 require.Empty(t, fake.Sent())
169 func TestWhatsAppSendWhileDisconnected(t *testing.T) {
170 whatsapp, fake := whatsappStartTestLinked(t)
171 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
172 whatsappTestConnect(t, fake)
173 require.True(t, fake.Emit(&events.Disconnected{}))
175 _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
176 require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later")
178 require.True(t, fake.Emit(&events.Connected{}))
180 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
181 require.NoError(t, err)
182 require.Equal(t, "Sent to +15550002222 at 12:05.", answer)
185 func TestWhatsAppSendWithoutALink(t *testing.T) {
186 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
188 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
189 require.NoError(t, err)
190 require.Equal(t, whatsappNotLinked, answer)
193 func TestAgentSendsAWhatsAppMessage(t *testing.T) {
194 whatsapp, fake := whatsappStartTestLinked(t)
195 whatsappTestContacts(t, whatsapp)
197 server := newModelFake(t)
198 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
199 if call.number == 0 {
200 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))
205 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5))
208 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
209 client := terminalStartTestClient(t, socket)
210 whatsappTestConnect(t, fake)
212 client.Type("Tell Ana that the keys are under the mat.")
213 client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]")
214 require.Len(t, fake.Sent(), 1)
217 func TestWhatsAppImageSendUploadsOnceAndKeepsCaption(t *testing.T) {
218 whatsapp, fake := whatsappStartTestLinked(t)
219 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
220 whatsappTestConnect(t, fake)
222 picture := imageTestPNG(t, 4, 3)
223 name, err := whatsapp.inbox.images.Save(picture)
224 require.NoError(t, err)
226 box := &Toolbox{whatsapp: whatsapp}
227 arguments := `{"id":"photo","chat":"15550002222@s.whatsapp.net","text":"The plan.","image":"` + name + `"}`
228 answer := box.run(t.Context(), "whatsapp_send", arguments)
229 require.Contains(t, answer, "Sent to")
230 require.Contains(t, box.run(t.Context(), "whatsapp_send", arguments), "nothing was sent again")
231 require.Equal(t, [][]byte{picture}, fake.Uploads())
232 require.Len(t, fake.Sent(), 1)
233 require.Equal(t, "MSG1", fake.Sent()[0].id)
234 require.Empty(t, fake.Sent()[0].text)
236 messages := fake.ImageSends()
237 require.Len(t, messages, 1)
239 message := messages[0]
240 require.Equal(t, "The plan.", message.GetCaption())
241 require.Equal(t, "image/png", message.GetMimetype())
242 require.Equal(t, "https://example.com/image", message.GetURL())
243 require.Equal(t, "/image", message.GetDirectPath())
244 require.Equal(t, []byte("made-up key"), message.GetMediaKey())
245 require.Equal(t, []byte("made-up hash"), message.GetFileSHA256())
246 require.Equal(t, []byte("made-up encrypted hash"), message.GetFileEncSHA256())
247 require.Equal(t, uint64(len(picture)), message.GetFileLength())
249 state, err := openWhatsAppState(whatsapp.state.path)
250 require.NoError(t, err)
252 record, found := state.Send("photo")
253 require.True(t, found)
254 require.Equal(t, name, record.Image)
255 require.Equal(t, whatsappSent, record.State)
257 again, err := whatsappSend(t.Context(), whatsapp.link(), whatsapp.groups, state, whatsapp.inbox.images, whatsapp.clock, "photo", whatsappTestAna.String(), "The plan.", name)
258 require.NoError(t, err)
259 require.Contains(t, again, "nothing was sent again")
260 require.Len(t, fake.Uploads(), 1)
261 require.Len(t, fake.Sent(), 1)
263 other, err := whatsapp.inbox.images.Save(imageTestPNG(t, 1, 1))
264 require.NoError(t, err)
266 _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", other)
267 require.ErrorContains(t, err, "belongs to another message")
269 _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", "")
270 require.ErrorContains(t, err, "belongs to another message")
271 require.Len(t, fake.Uploads(), 1)
274 func TestWhatsAppImageSendAllowsEmptyCaptionAndRefusesUnknownImage(t *testing.T) {
275 whatsapp, fake := whatsappStartTestLinked(t)
276 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
277 whatsappTestConnect(t, fake)
279 _, err := whatsapp.Send(t.Context(), "unknown", whatsappTestAna.String(), "A caption.", "../image.png")
280 require.ErrorContains(t, err, "invalid image name")
282 _, found := whatsapp.state.Send("unknown")
283 require.False(t, found)
284 require.Empty(t, fake.Uploads())
285 require.Empty(t, fake.Sent())
287 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 3, 2))
288 require.NoError(t, err)
290 _, err = whatsapp.Send(t.Context(), "no-caption", whatsappTestAna.String(), "", name)
291 require.NoError(t, err)
292 require.Len(t, fake.ImageSends(), 1)
293 require.Empty(t, fake.ImageSends()[0].GetCaption())
296 func TestWhatsAppImageFailureKeepsNoSecretsAndDoesNotRetry(t *testing.T) {
297 testCases := []struct {
302 name: "upload failure stores a safe cause",
305 {name: "send failure stores a safe cause"},
307 for _, tc := range testCases {
308 t.Run(tc.name, func(t *testing.T) {
309 whatsapp, fake := whatsappStartTestLinked(t)
310 failure := errors.New("https://example.com/media?token=secret /private-media/key phone=" + whatsappTestNumber)
312 fake.uploadErr = failure
314 fake.sendErr = failure
317 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
318 whatsappTestConnect(t, fake)
320 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 1))
321 require.NoError(t, err)
324 answer, err := whatsapp.Send(t.Context(), "failed-photo", whatsappTestAna.String(), "Private caption.", name)
325 require.NoError(t, err)
326 require.Contains(t, answer, "media send failed")
327 require.NotContains(t, answer, "secret")
328 require.NotContains(t, answer, "/private-media")
329 require.NotContains(t, answer, whatsappTestNumber)
330 require.NotContains(t, answer, "Private caption")
333 require.Len(t, fake.Uploads(), 1)
336 require.Empty(t, fake.Sent())
338 require.Len(t, fake.Sent(), 1)
341 record, found := whatsapp.state.Send("failed-photo")
342 require.True(t, found)
343 require.Equal(t, whatsappFailed, record.State)
344 require.NotContains(t, record.Answer, "secret")
349 func TestWhatsAppImageInProgressRecordDoesNotUploadAgain(t *testing.T) {
350 whatsapp, fake := whatsappStartTestLinked(t)
351 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
352 whatsappTestConnect(t, fake)
354 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 2))
355 require.NoError(t, err)
357 digest := sha256.Sum256([]byte("The plan."))
358 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
359 ID: "interrupted-image",
360 Chat: whatsappTestAna.String(),
361 Digest: hex.EncodeToString(digest[:]),
363 State: whatsappSending,
366 answer, err := whatsapp.Send(t.Context(), "interrupted-image", whatsappTestAna.String(), "The plan.", name)
367 require.NoError(t, err)
368 require.Contains(t, answer, "its result is unknown")
369 require.Empty(t, fake.Uploads())
370 require.Empty(t, fake.Sent())
373 func TestTelegramReceivedPhotoCanBeShownAndSentToWhatsApp(t *testing.T) {
374 whatsapp, fake := whatsappStartTestLinked(t)
375 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
376 whatsappTestConnect(t, fake)
378 telegram := newTelegramFake(t)
379 picture := imageTestPNG(t, 5, 5)
380 telegram.Image("received", picture)
382 api := newTelegramAPI(telegram.URL, telegramTestToken)
383 text, err := telegramImageText(t.Context(), api, whatsapp.inbox.images, &TelegramMessage{
384 Caption: "The new plan.",
385 Photo: []TelegramPhoto{{
391 require.NoError(t, err)
393 names := imageFiles(text)
394 require.Len(t, names, 1)
396 bot, logs := telegramStartTestBot(t, telegram)
399 imageShow: &ImageShow{
400 store: whatsapp.inbox.images,
404 require.NotContains(t, box.run(t.Context(), "image_show", `{"image":"`+names[0]+`","caption":"The plan."}`), "error:")
405 require.Contains(t, box.run(t.Context(), "whatsapp_send", `{"id":"forward","chat":"15550002222@s.whatsapp.net","text":"The plan.","image":"`+names[0]+`"}`), "Sent to")
406 require.Equal(t, [][]byte{picture}, fake.Uploads())
407 require.Equal(t, "The plan.", fake.ImageSends()[0].GetCaption())
409 calls := telegram.Calls()
410 require.Equal(t, "sendPhoto", calls[len(calls)-1].method)
411 require.Equal(t, picture, calls[len(calls)-1].params["photo"])
412 require.Empty(t, logs.String())
415 func TestWhatsAppImageUploadEndsWithItsCallerOrTimeout(t *testing.T) {
416 synctest.Test(t, func(t *testing.T) {
417 client := &WhatsAppUploadWaitFake{WhatsAppFake: &WhatsAppFake{}}
419 _, err := whatsappImageMessage(t.Context(), client, imageTestPNG(t, 1, 1), "")
420 require.ErrorIs(t, err, context.DeadlineExceeded)
421 require.Equal(t, 2*time.Minute, time.Since(start))
423 ctx, cancel := context.WithCancel(t.Context())
429 _, err = whatsappImageMessage(ctx, client, imageTestPNG(t, 1, 1), "")
430 require.ErrorIs(t, err, context.Canceled)
431 require.Zero(t, time.Since(start))
435 type WhatsAppUploadWaitFake struct {
439 func (f *WhatsAppUploadWaitFake) Upload(ctx context.Context, _ []byte, _ whatsmeow.MediaType) (whatsmeow.UploadResponse, error) {
442 return whatsmeow.UploadResponse{}, ctx.Err()