// Copyright (c) 2023 Proton AG // // This file is part of Proton Mail Bridge. // // Proton Mail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Proton Mail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Proton Mail Bridge. If not, see . package bridge_test import ( "context" "fmt" "net/http" "testing" "time" "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api/server" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/bradenaw/juniper/xslices" "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/stretchr/testify/require" ) func TestBridge_User_BadEvents(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { // Create a user. userID, addrID, err := s.CreateUser("user", password) require.NoError(t, err) labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder) require.NoError(t, err) // Create 10 messages for the user. withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { createNumMessages(ctx, t, c, addrID, labelID, 10) }) // The initial user should be fully synced. withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) defer done() userID, err := bridge.LoginFull(ctx, "user", password, nil, nil) require.NoError(t, err) require.Equal(t, userID, (<-syncCh).UserID) }) var messageIDs []string // Create 10 more messages for the user, generating events. withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10) }) // If bridge attempts to sync the new messages, it should get a BadRequest error. s.AddStatusHook(func(req *http.Request) (int, bool) { if xslices.Index(xslices.Map(messageIDs, func(messageID string) string { return "/mail/v4/messages/" + messageID }), req.URL.Path) < 0 { return 0, false } return http.StatusBadRequest, true }) // The user will continue to process events and will receive bad request errors. withMocks(t, func(mocks *bridge.Mocks) { mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1) // The user will eventually be logged out due to the bad request errors. withBridgeNoMocks(ctx, t, mocks, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge) { require.Eventually(t, func() bool { return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 0 }, 10*time.Second, 100*time.Millisecond) }) }) }) } func TestBridge_User_BadEvents_MessageLabelDeleted(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { // Create a user. userID, addrID, err := s.CreateUser("user", password) require.NoError(t, err) labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder) require.NoError(t, err) // Create 10 messages for the user. withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { createNumMessages(ctx, t, c, addrID, labelID, 10) }) // The initial user should be fully synced. withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) defer done() userID, err := bridge.LoginFull(ctx, "user", password, nil, nil) require.NoError(t, err) require.Equal(t, userID, (<-syncCh).UserID) }) // Create and delete 10 more messages for the user, generating delete events. withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { messageIDs := createNumMessages(ctx, t, c, addrID, labelID, 10) require.NoError(t, c.DeleteMessage(ctx, messageIDs...)) }) // Create and delete 10 labels for the user, generating delete events. withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { for i := 0; i < 10; i++ { label, err := c.CreateLabel(ctx, proton.CreateLabelReq{ Name: uuid.NewString(), Color: "#f66", Type: proton.LabelTypeLabel, }) require.NoError(t, err) require.NoError(t, c.DeleteLabel(ctx, label.ID)) } }) // The user will continue to process events and will not receive any bad request errors. withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { info, err := bridge.QueryUserInfo("user") require.NoError(t, err) client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() // Create a new label. withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { require.NoError(t, getErr(c.CreateLabel(ctx, proton.CreateLabelReq{ Name: "blabla", Color: "#f66", Type: proton.LabelTypeLabel, }))) }) // Wait for the label to be created. require.Eventually(t, func() bool { return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool { return mailbox.Name == "Labels/blabla" }) >= 0 }, 10*time.Second, 100*time.Millisecond) }) }) }