]> git.giacomolli.uk - onechat/blob - whatsapp_send_test.go
Reflow comments over the reflow width
[onechat] / whatsapp_send_test.go
1 package main
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "net/http"
9 "path/filepath"
10 "strings"
11 "testing"
12 "testing/synctest"
13 "time"
14
15 "go.mau.fi/whatsmeow"
16
17 "github.com/stretchr/testify/require"
18 "go.mau.fi/whatsmeow/types"
19 "go.mau.fi/whatsmeow/types/events"
20 )
21
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)
27
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)
31
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)
35
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")
38
39 require.Equal(t, []WhatsAppFakeSend{
40 {
41 to: whatsappTestAna.String(),
42 text: "The keys are under the mat.",
43 id: "MSG1",
44 },
45 }, fake.Sent())
46
47 again, err := openWhatsAppState(filepath.Join(whatsapp.dir, whatsappStateFileName))
48 require.NoError(t, err)
49
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)
54 }
55
56 func TestWhatsAppSendToAGroup(t *testing.T) {
57 whatsapp, fake := whatsappStartTestLinked(t)
58 fake.groups = []*types.GroupInfo{
59 {
60 JID: whatsappTestFamily,
61 GroupName: types.GroupName{
62 Name: "Family",
63 },
64 },
65 }
66
67 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
68 whatsappTestConnect(t, fake)
69
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)
73 }
74
75 func TestWhatsAppSendThatFails(t *testing.T) {
76 whatsapp, fake := whatsappStartTestLinked(t)
77 fake.sendErr = errors.New("server error for " + whatsappTestNumber)
78
79 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
80 whatsappTestConnect(t, fake)
81
82 for range 2 {
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)
86 }
87
88 require.Len(t, fake.Sent(), 1)
89 }
90
91 func TestWhatsAppSendLogsEachSendWithoutTextOrNumbers(t *testing.T) {
92 dir := t.TempDir()
93 whatsappTestSession(t, dir, true)
94
95 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
96 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
97 whatsappTestConnect(t, fake)
98
99 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 4, 3))
100 require.NoError(t, err)
101
102 _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.", "")
103 require.NoError(t, err)
104
105 _, err = whatsapp.Send(t.Context(), "plan", whatsappTestAna.String(), "The plan.", name)
106 require.NoError(t, err)
107
108 fake.sendErr = errors.New("server error for " + whatsappTestNumber)
109 _, err = whatsapp.Send(t.Context(), "late", whatsappTestAna.String(), "Late.", "")
110 require.NoError(t, err)
111
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")
116
117 for _, secret := range []string{"keys", "plan", "Late", whatsappTestNumber, whatsappTestAna.User} {
118 require.NotContains(t, logs.String(), secret)
119 }
120 }
121
122 func TestWhatsAppSendCutByAStop(t *testing.T) {
123 whatsapp, fake := whatsappStartTestLinked(t)
124 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
125 whatsappTestConnect(t, fake)
126
127 digest := sha256.Sum256([]byte("hi"))
128 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
129 ID: "keys",
130 Chat: whatsappTestAna.String(),
131 Digest: hex.EncodeToString(digest[:]),
132 State: whatsappSending,
133 }))
134
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())
139 }
140
141 func TestWhatsAppSendRefuses(t *testing.T) {
142 testCases := []struct {
143 name string
144 id string
145 chat string
146 text string
147 err string
148 }{
149 {
150 name: "no id",
151 id: " ",
152 chat: whatsappTestAna.String(),
153 text: "hi",
154 err: "the id must have 1 to 100 bytes",
155 },
156 {
157 name: "no text",
158 id: "a",
159 chat: whatsappTestAna.String(),
160 text: "",
161 err: "the text must have 1 to 65536 bytes",
162 },
163 {
164 name: "a name for a chat",
165 id: "a",
166 chat: "Ana Souza",
167 text: "hi",
168 err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`,
169 },
170 {
171 name: "a status broadcast",
172 id: "a",
173 chat: types.StatusBroadcastJID.String(),
174 text: "hi",
175 err: `"status@broadcast" is not a private chat or a group`,
176 },
177 {
178 name: "the chat of the user",
179 id: "a",
180 chat: whatsappTestOwn.String(),
181 text: "hi",
182 err: "this is the chat of the user with itself: send to another chat",
183 },
184 }
185
186 whatsapp, fake := whatsappStartTestLinked(t)
187 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
188 whatsappTestConnect(t, fake)
189
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)
194 })
195 }
196
197 require.Empty(t, fake.Sent())
198 }
199
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{}))
205
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")
208
209 require.True(t, fake.Emit(&events.Connected{}))
210
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)
214 }
215
216 func TestWhatsAppSendWithoutALink(t *testing.T) {
217 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
218
219 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
220 require.NoError(t, err)
221 require.Equal(t, whatsappNotLinked, answer)
222 }
223
224 func TestAgentSendsAWhatsAppMessage(t *testing.T) {
225 whatsapp, fake := whatsappStartTestLinked(t)
226 whatsappTestContacts(t, whatsapp)
227
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))
232
233 return
234 }
235
236 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5))
237 }
238
239 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
240 client := terminalStartTestClient(t, socket)
241 whatsappTestConnect(t, fake)
242
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)
246 }
247
248 func TestWhatsAppImageSendUploadsOnceAndKeepsCaption(t *testing.T) {
249 whatsapp, fake := whatsappStartTestLinked(t)
250 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
251 whatsappTestConnect(t, fake)
252
253 picture := imageTestPNG(t, 4, 3)
254 name, err := whatsapp.inbox.images.Save(picture)
255 require.NoError(t, err)
256
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)
266
267 messages := fake.ImageSends()
268 require.Len(t, messages, 1)
269
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())
279
280 state, err := openWhatsAppState(whatsapp.state.path)
281 require.NoError(t, err)
282
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)
287
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)
293
294 other, err := whatsapp.inbox.images.Save(imageTestPNG(t, 1, 1))
295 require.NoError(t, err)
296
297 _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", other)
298 require.ErrorContains(t, err, "belongs to another message")
299
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)
303 }
304
305 func TestWhatsAppImageSendAllowsEmptyCaptionAndRefusesUnknownImage(t *testing.T) {
306 whatsapp, fake := whatsappStartTestLinked(t)
307 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
308 whatsappTestConnect(t, fake)
309
310 _, err := whatsapp.Send(t.Context(), "unknown", whatsappTestAna.String(), "A caption.", "../image.png")
311 require.ErrorContains(t, err, "invalid image name")
312
313 _, found := whatsapp.state.Send("unknown")
314 require.False(t, found)
315 require.Empty(t, fake.Uploads())
316 require.Empty(t, fake.Sent())
317
318 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 3, 2))
319 require.NoError(t, err)
320
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())
325 }
326
327 func TestWhatsAppImageFailureKeepsNoSecretsAndDoesNotRetry(t *testing.T) {
328 testCases := []struct {
329 name string
330 upload bool
331 }{
332 {
333 name: "upload failure stores a safe cause",
334 upload: true,
335 },
336 {name: "send failure stores a safe cause"},
337 }
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)
342 if tc.upload {
343 fake.uploadErr = failure
344 } else {
345 fake.sendErr = failure
346 }
347
348 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
349 whatsappTestConnect(t, fake)
350
351 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 1))
352 require.NoError(t, err)
353
354 for range 2 {
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")
362 }
363
364 require.Len(t, fake.Uploads(), 1)
365
366 if tc.upload {
367 require.Empty(t, fake.Sent())
368 } else {
369 require.Len(t, fake.Sent(), 1)
370 }
371
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")
376 })
377 }
378 }
379
380 func TestWhatsAppImageInProgressRecordDoesNotUploadAgain(t *testing.T) {
381 whatsapp, fake := whatsappStartTestLinked(t)
382 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
383 whatsappTestConnect(t, fake)
384
385 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 2))
386 require.NoError(t, err)
387
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[:]),
393 Image: name,
394 State: whatsappSending,
395 }))
396
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())
402 }
403
404 func TestTelegramReceivedPhotoCanBeShownAndSentToWhatsApp(t *testing.T) {
405 whatsapp, fake := whatsappStartTestLinked(t)
406 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
407 whatsappTestConnect(t, fake)
408
409 telegram := newTelegramFake(t)
410 picture := imageTestPNG(t, 5, 5)
411 telegram.Image("received", picture)
412
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{{
417 FileID: "received",
418 Width: 5,
419 Height: 5,
420 }},
421 })
422 require.NoError(t, err)
423
424 names := imageFiles(text)
425 require.Len(t, names, 1)
426
427 bot, logs := telegramStartTestBot(t, telegram)
428 box := &Toolbox{
429 whatsapp: whatsapp,
430 imageShow: &ImageShow{
431 store: whatsapp.inbox.images,
432 bot: bot,
433 },
434 }
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())
439
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())
444 }
445
446 func TestWhatsAppImageUploadEndsWithItsCallerOrTimeout(t *testing.T) {
447 synctest.Test(t, func(t *testing.T) {
448 client := &WhatsAppUploadWaitFake{WhatsAppFake: &WhatsAppFake{}}
449 start := time.Now()
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))
453
454 ctx, cancel := context.WithCancel(t.Context())
455 defer cancel()
456
457 cancel()
458
459 start = time.Now()
460 _, err = whatsappImageMessage(ctx, client, imageTestPNG(t, 1, 1), "")
461 require.ErrorIs(t, err, context.Canceled)
462 require.Zero(t, time.Since(start))
463 })
464 }
465
466 type WhatsAppUploadWaitFake struct {
467 *WhatsAppFake
468 }
469
470 func (f *WhatsAppUploadWaitFake) Upload(ctx context.Context, _ []byte, _ whatsmeow.MediaType) (whatsmeow.UploadResponse, error) {
471 <-ctx.Done()
472
473 return whatsmeow.UploadResponse{}, ctx.Err()
474 }