X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/48d7bce28b18ecef4c52c98821675605464cecb9..195c3fbb075383ac47bba310eb701b02774e8d10:/whatsapp_send_test.go diff --git a/whatsapp_send_test.go b/whatsapp_send_test.go index 480e7db..65c2fc6 100644 --- a/whatsapp_send_test.go +++ b/whatsapp_send_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/sha256" "encoding/hex" "errors" @@ -8,6 +9,10 @@ import ( "path/filepath" "strings" "testing" + "testing/synctest" + "time" + + "go.mau.fi/whatsmeow" "github.com/stretchr/testify/require" "go.mau.fi/whatsmeow/types" @@ -20,15 +25,15 @@ func TestWhatsAppSendSendsOnceForEachID(t *testing.T) { 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{ @@ -62,7 +67,7 @@ func TestWhatsAppSendToAGroup(t *testing.T) { 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) } @@ -75,7 +80,7 @@ func TestWhatsAppSendThatFails(t *testing.T) { 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) } @@ -96,7 +101,7 @@ func TestWhatsAppSendCutByAStop(t *testing.T) { 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()) @@ -153,7 +158,7 @@ func TestWhatsAppSendRefuses(t *testing.T) { 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) }) } @@ -167,12 +172,12 @@ func TestWhatsAppSendWhileDisconnected(t *testing.T) { 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) } @@ -180,7 +185,7 @@ func TestWhatsAppSendWhileDisconnected(t *testing.T) { 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) } @@ -208,3 +213,231 @@ func TestAgentSendsAWhatsAppMessage(t *testing.T) { 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() +}