+
+func TestWhatsAppImageSendUploadsOnceAndKeepsCaption(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+ whatsappTestConnect(t, fake)
+
+ picture := imageTestPNG(t, 4, 3)
+ name, err := whatsapp.inbox.images.Save(picture)
+ require.NoError(t, err)
+
+ box := &Toolbox{whatsapp: whatsapp}
+ arguments := `{"id":"photo","chat":"15550002222@s.whatsapp.net","text":"The plan.","image":"` + name + `"}`
+ answer := box.run(t.Context(), "whatsapp_send", arguments)
+ require.Contains(t, answer, "Sent to")
+ require.Contains(t, box.run(t.Context(), "whatsapp_send", arguments), "nothing was sent again")
+ require.Equal(t, [][]byte{picture}, fake.Uploads())
+ require.Len(t, fake.Sent(), 1)
+ require.Equal(t, "MSG1", fake.Sent()[0].id)
+ require.Empty(t, fake.Sent()[0].text)
+
+ messages := fake.ImageSends()
+ require.Len(t, messages, 1)
+
+ message := messages[0]
+ require.Equal(t, "The plan.", message.GetCaption())
+ require.Equal(t, "image/png", message.GetMimetype())
+ require.Equal(t, "https://example.com/image", message.GetURL())
+ require.Equal(t, "/image", message.GetDirectPath())
+ require.Equal(t, []byte("made-up key"), message.GetMediaKey())
+ require.Equal(t, []byte("made-up hash"), message.GetFileSHA256())
+ require.Equal(t, []byte("made-up encrypted hash"), message.GetFileEncSHA256())
+ require.Equal(t, uint64(len(picture)), message.GetFileLength())
+
+ state, err := openWhatsAppState(whatsapp.state.path)
+ require.NoError(t, err)
+
+ record, found := state.Send("photo")
+ require.True(t, found)
+ require.Equal(t, name, record.Image)
+ require.Equal(t, whatsappSent, record.State)
+
+ again, err := whatsappSend(t.Context(), whatsapp.link(), whatsapp.groups, state, whatsapp.inbox.images, whatsapp.clock, "photo", whatsappTestAna.String(), "The plan.", name)
+ require.NoError(t, err)
+ require.Contains(t, again, "nothing was sent again")
+ require.Len(t, fake.Uploads(), 1)
+ require.Len(t, fake.Sent(), 1)
+
+ other, err := whatsapp.inbox.images.Save(imageTestPNG(t, 1, 1))
+ require.NoError(t, err)
+
+ _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", other)
+ require.ErrorContains(t, err, "belongs to another message")
+
+ _, err = whatsapp.Send(t.Context(), "photo", whatsappTestAna.String(), "The plan.", "")
+ require.ErrorContains(t, err, "belongs to another message")
+ require.Len(t, fake.Uploads(), 1)
+}
+
+func TestWhatsAppImageSendAllowsEmptyCaptionAndRefusesUnknownImage(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+ whatsappTestConnect(t, fake)
+
+ _, err := whatsapp.Send(t.Context(), "unknown", whatsappTestAna.String(), "A caption.", "../image.png")
+ require.ErrorContains(t, err, "invalid image name")
+
+ _, found := whatsapp.state.Send("unknown")
+ require.False(t, found)
+ require.Empty(t, fake.Uploads())
+ require.Empty(t, fake.Sent())
+
+ name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 3, 2))
+ require.NoError(t, err)
+
+ _, err = whatsapp.Send(t.Context(), "no-caption", whatsappTestAna.String(), "", name)
+ require.NoError(t, err)
+ require.Len(t, fake.ImageSends(), 1)
+ require.Empty(t, fake.ImageSends()[0].GetCaption())
+}
+
+func TestWhatsAppImageFailureKeepsNoSecretsAndDoesNotRetry(t *testing.T) {
+ testCases := []struct {
+ name string
+ upload bool
+ }{
+ {
+ name: "upload failure stores a safe cause",
+ upload: true,
+ },
+ {name: "send failure stores a safe cause"},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ failure := errors.New("https://example.com/media?token=secret /private-media/key phone=" + whatsappTestNumber)
+ if tc.upload {
+ fake.uploadErr = failure
+ } else {
+ fake.sendErr = failure
+ }
+
+ daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+ whatsappTestConnect(t, fake)
+
+ name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 1))
+ require.NoError(t, err)
+
+ for range 2 {
+ answer, err := whatsapp.Send(t.Context(), "failed-photo", whatsappTestAna.String(), "Private caption.", name)
+ require.NoError(t, err)
+ require.Contains(t, answer, "media send failed")
+ require.NotContains(t, answer, "secret")
+ require.NotContains(t, answer, "/private-media")
+ require.NotContains(t, answer, whatsappTestNumber)
+ require.NotContains(t, answer, "Private caption")
+ }
+
+ require.Len(t, fake.Uploads(), 1)
+
+ if tc.upload {
+ require.Empty(t, fake.Sent())
+ } else {
+ require.Len(t, fake.Sent(), 1)
+ }
+
+ record, found := whatsapp.state.Send("failed-photo")
+ require.True(t, found)
+ require.Equal(t, whatsappFailed, record.State)
+ require.NotContains(t, record.Answer, "secret")
+ })
+ }
+}
+
+func TestWhatsAppImageInProgressRecordDoesNotUploadAgain(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+ whatsappTestConnect(t, fake)
+
+ name, err := whatsapp.inbox.images.Save(imageTestPNG(t, 2, 2))
+ require.NoError(t, err)
+
+ digest := sha256.Sum256([]byte("The plan."))
+ require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{
+ ID: "interrupted-image",
+ Chat: whatsappTestAna.String(),
+ Digest: hex.EncodeToString(digest[:]),
+ Image: name,
+ State: whatsappSending,
+ }))
+
+ answer, err := whatsapp.Send(t.Context(), "interrupted-image", whatsappTestAna.String(), "The plan.", name)
+ require.NoError(t, err)
+ require.Contains(t, answer, "its result is unknown")
+ require.Empty(t, fake.Uploads())
+ require.Empty(t, fake.Sent())
+}
+
+func TestTelegramReceivedPhotoCanBeShownAndSentToWhatsApp(t *testing.T) {
+ whatsapp, fake := whatsappStartTestLinked(t)
+ daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+ whatsappTestConnect(t, fake)
+
+ telegram := newTelegramFake(t)
+ picture := imageTestPNG(t, 5, 5)
+ telegram.Image("received", picture)
+
+ api := newTelegramAPI(telegram.URL, telegramTestToken)
+ text, err := telegramImageText(t.Context(), api, whatsapp.inbox.images, &TelegramMessage{
+ Caption: "The new plan.",
+ Photo: []TelegramPhoto{{
+ FileID: "received",
+ Width: 5,
+ Height: 5,
+ }},
+ })
+ require.NoError(t, err)
+
+ names := imageFiles(text)
+ require.Len(t, names, 1)
+
+ bot, logs := telegramStartTestBot(t, telegram)
+ box := &Toolbox{
+ whatsapp: whatsapp,
+ imageShow: &ImageShow{
+ store: whatsapp.inbox.images,
+ bot: bot,
+ },
+ }
+ require.NotContains(t, box.run(t.Context(), "image_show", `{"image":"`+names[0]+`","caption":"The plan."}`), "error:")
+ require.Contains(t, box.run(t.Context(), "whatsapp_send", `{"id":"forward","chat":"15550002222@s.whatsapp.net","text":"The plan.","image":"`+names[0]+`"}`), "Sent to")
+ require.Equal(t, [][]byte{picture}, fake.Uploads())
+ require.Equal(t, "The plan.", fake.ImageSends()[0].GetCaption())
+
+ calls := telegram.Calls()
+ require.Equal(t, "sendPhoto", calls[len(calls)-1].method)
+ require.Equal(t, picture, calls[len(calls)-1].params["photo"])
+ require.Empty(t, logs.String())
+}
+
+func TestWhatsAppImageUploadEndsWithItsCallerOrTimeout(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ client := &WhatsAppUploadWaitFake{WhatsAppFake: &WhatsAppFake{}}
+ start := time.Now()
+ _, err := whatsappImageMessage(t.Context(), client, imageTestPNG(t, 1, 1), "")
+ require.ErrorIs(t, err, context.DeadlineExceeded)
+ require.Equal(t, 2*time.Minute, time.Since(start))
+
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+
+ cancel()
+
+ start = time.Now()
+ _, err = whatsappImageMessage(ctx, client, imageTestPNG(t, 1, 1), "")
+ require.ErrorIs(t, err, context.Canceled)
+ require.Zero(t, time.Since(start))
+ })
+}
+
+type WhatsAppUploadWaitFake struct {
+ *WhatsAppFake
+}
+
+func (f *WhatsAppUploadWaitFake) Upload(ctx context.Context, _ []byte, _ whatsmeow.MediaType) (whatsmeow.UploadResponse, error) {
+ <-ctx.Done()
+
+ return whatsmeow.UploadResponse{}, ctx.Err()
+}