package main
import (
+ "context"
"crypto/sha256"
"encoding/hex"
"errors"
"path/filepath"
"strings"
"testing"
+ "testing/synctest"
+ "time"
+
+ "go.mau.fi/whatsmeow"
"github.com/stretchr/testify/require"
"go.mau.fi/whatsmeow/types"
daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
whatsappTestConnect(t, fake)
- answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.")
+ answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.", "")
require.NoError(t, err)
require.Equal(t, "Sent to Ana Souza at 12:05.", answer)
- answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.")
+ answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.", "")
require.NoError(t, err)
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)
- _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.")
+ _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.", "")
require.EqualError(t, err, "the id keys belongs to another message: give a new id for each message")
require.Equal(t, []WhatsAppFakeSend{
daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
whatsappTestConnect(t, fake)
- answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.")
+ answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.", "")
require.NoError(t, err)
require.Equal(t, "Sent to Family at 12:05.", answer)
}
whatsappTestConnect(t, fake)
for range 2 {
- answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
+ answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
require.NoError(t, err)
require.True(t, strings.HasPrefix(answer, "The send failed: server error for OWN."), answer)
}
State: whatsappSending,
}))
- answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
+ answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
require.NoError(t, err)
require.Contains(t, answer, "its result is unknown")
require.Empty(t, fake.Sent())
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text)
+ _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text, "")
require.EqualError(t, err, tc.err)
})
}
whatsappTestConnect(t, fake)
require.True(t, fake.Emit(&events.Disconnected{}))
- _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
+ _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later")
require.True(t, fake.Emit(&events.Connected{}))
- answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
+ answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
require.NoError(t, err)
require.Equal(t, "Sent to +15550002222 at 12:05.", answer)
}
func TestWhatsAppSendWithoutALink(t *testing.T) {
whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
- answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi")
+ answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi", "")
require.NoError(t, err)
require.Equal(t, whatsappNotLinked, answer)
}
client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]")
require.Len(t, fake.Sent(), 1)
}
+
+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()
+}