]> git.giacomolli.uk - onechat/blob - whatsapp_send_test.go
Keep test daemon sockets under the path limit
[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 TestWhatsAppSendCutByAStop(t *testing.T) {
92 whatsapp, fake := whatsappStartTestLinked(t)
93 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
94 whatsappTestConnect(t, fake)
95
96 digest := sha256.Sum256([]byte("hi"))
97 require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
98 ID: "keys",
99 Chat: whatsappTestAna.String(),
100 Digest: hex.EncodeToString(digest[:]),
101 State: whatsappSending,
102 }))
103
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())
108 }
109
110 func TestWhatsAppSendRefuses(t *testing.T) {
111 testCases := []struct {
112 name string
113 id string
114 chat string
115 text string
116 err string
117 }{
118 {
119 name: "no id",
120 id: " ",
121 chat: whatsappTestAna.String(),
122 text: "hi",
123 err: "the id must have 1 to 100 bytes",
124 },
125 {
126 name: "no text",
127 id: "a",
128 chat: whatsappTestAna.String(),
129 text: "",
130 err: "the text must have 1 to 65536 bytes",
131 },
132 {
133 name: "a name for a chat",
134 id: "a",
135 chat: "Ana Souza",
136 text: "hi",
137 err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`,
138 },
139 {
140 name: "a status broadcast",
141 id: "a",
142 chat: types.StatusBroadcastJID.String(),
143 text: "hi",
144 err: `"status@broadcast" is not a private chat or a group`,
145 },
146 {
147 name: "the chat of the user",
148 id: "a",
149 chat: whatsappTestOwn.String(),
150 text: "hi",
151 err: "this is the chat of the user with itself: send to another chat",
152 },
153 }
154
155 whatsapp, fake := whatsappStartTestLinked(t)
156 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
157 whatsappTestConnect(t, fake)
158
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)
163 })
164 }
165
166 require.Empty(t, fake.Sent())
167 }
168
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{}))
174
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")
177
178 require.True(t, fake.Emit(&events.Connected{}))
179
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)
183 }
184
185 func TestWhatsAppSendWithoutALink(t *testing.T) {
186 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
187
188 answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
189 require.NoError(t, err)
190 require.Equal(t, whatsappNotLinked, answer)
191 }
192
193 func TestAgentSendsAWhatsAppMessage(t *testing.T) {
194 whatsapp, fake := whatsappStartTestLinked(t)
195 whatsappTestContacts(t, whatsapp)
196
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))
201
202 return
203 }
204
205 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5))
206 }
207
208 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
209 client := terminalStartTestClient(t, socket)
210 whatsappTestConnect(t, fake)
211
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)
215 }
216
217 func TestWhatsAppImageSendUploadsOnceAndKeepsCaption(t *testing.T) {
218 whatsapp, fake := whatsappStartTestLinked(t)
219 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
220 whatsappTestConnect(t, fake)
221
222 picture := imageTestPNG(t, 4, 3)
223 name, err := whatsapp.inbox.images.Save(picture)
224 require.NoError(t, err)
225
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)
235
236 messages := fake.ImageSends()
237 require.Len(t, messages, 1)
238
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())
248
249 state, err := openWhatsAppState(whatsapp.state.path)
250 require.NoError(t, err)
251
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)
256
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)
262
263 other, err := whatsapp.inbox.images.Save(imageTestPNG(t, 1, 1))
264 require.NoError(t, err)
265
266 _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", other)
267 require.ErrorContains(t, err, "belongs to another message")
268
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)
272 }
273
274 func TestWhatsAppImageSendAllowsEmptyCaptionAndRefusesUnknownImage(t *testing.T) {
275 whatsapp, fake := whatsappStartTestLinked(t)
276 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
277 whatsappTestConnect(t, fake)
278
279 _, err := whatsapp.Send(t.Context(), "unknown", whatsappTestAna.String(), "A caption.", "../image.png")
280 require.ErrorContains(t, err, "invalid image name")
281
282 _, found := whatsapp.state.Send("unknown")
283 require.False(t, found)
284 require.Empty(t, fake.Uploads())
285 require.Empty(t, fake.Sent())
286
287 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 3, 2))
288 require.NoError(t, err)
289
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())
294 }
295
296 func TestWhatsAppImageFailureKeepsNoSecretsAndDoesNotRetry(t *testing.T) {
297 testCases := []struct {
298 name string
299 upload bool
300 }{
301 {
302 name: "upload failure stores a safe cause",
303 upload: true,
304 },
305 {name: "send failure stores a safe cause"},
306 }
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)
311 if tc.upload {
312 fake.uploadErr = failure
313 } else {
314 fake.sendErr = failure
315 }
316
317 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
318 whatsappTestConnect(t, fake)
319
320 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 1))
321 require.NoError(t, err)
322
323 for range 2 {
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")
331 }
332
333 require.Len(t, fake.Uploads(), 1)
334
335 if tc.upload {
336 require.Empty(t, fake.Sent())
337 } else {
338 require.Len(t, fake.Sent(), 1)
339 }
340
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")
345 })
346 }
347 }
348
349 func TestWhatsAppImageInProgressRecordDoesNotUploadAgain(t *testing.T) {
350 whatsapp, fake := whatsappStartTestLinked(t)
351 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
352 whatsappTestConnect(t, fake)
353
354 name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 2))
355 require.NoError(t, err)
356
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[:]),
362 Image: name,
363 State: whatsappSending,
364 }))
365
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())
371 }
372
373 func TestTelegramReceivedPhotoCanBeShownAndSentToWhatsApp(t *testing.T) {
374 whatsapp, fake := whatsappStartTestLinked(t)
375 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
376 whatsappTestConnect(t, fake)
377
378 telegram := newTelegramFake(t)
379 picture := imageTestPNG(t, 5, 5)
380 telegram.Image("received", picture)
381
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{{
386 FileID: "received",
387 Width: 5,
388 Height: 5,
389 }},
390 })
391 require.NoError(t, err)
392
393 names := imageFiles(text)
394 require.Len(t, names, 1)
395
396 bot, logs := telegramStartTestBot(t, telegram)
397 box := &Toolbox{
398 whatsapp: whatsapp,
399 imageShow: &ImageShow{
400 store: whatsapp.inbox.images,
401 bot: bot,
402 },
403 }
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())
408
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())
413 }
414
415 func TestWhatsAppImageUploadEndsWithItsCallerOrTimeout(t *testing.T) {
416 synctest.Test(t, func(t *testing.T) {
417 client := &WhatsAppUploadWaitFake{WhatsAppFake: &WhatsAppFake{}}
418 start := time.Now()
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))
422
423 ctx, cancel := context.WithCancel(t.Context())
424 defer cancel()
425
426 cancel()
427
428 start = time.Now()
429 _, err = whatsappImageMessage(ctx, client, imageTestPNG(t, 1, 1), "")
430 require.ErrorIs(t, err, context.Canceled)
431 require.Zero(t, time.Since(start))
432 })
433 }
434
435 type WhatsAppUploadWaitFake struct {
436 *WhatsAppFake
437 }
438
439 func (f *WhatsAppUploadWaitFake) Upload(ctx context.Context, _ []byte, _ whatsmeow.MediaType) (whatsmeow.UploadResponse, error) {
440 <-ctx.Done()
441
442 return whatsmeow.UploadResponse{}, ctx.Err()
443 }