// Copyright (c) 2020 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail 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. // // ProtonMail 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 ProtonMail Bridge. If not, see . package users import ( "fmt" "io/ioutil" "os" "runtime/debug" "testing" "time" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/users/credentials" usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks" "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks" gomock "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { if os.Getenv("VERBOSITY") == "fatal" { logrus.SetLevel(logrus.FatalLevel) } if os.Getenv("VERBOSITY") == "trace" { logrus.SetLevel(logrus.TraceLevel) } os.Exit(m.Run()) } var ( testAuth = &pmapi.Auth{ //nolint[gochecknoglobals] RefreshToken: "tok", } testAuthRefresh = &pmapi.Auth{ //nolint[gochecknoglobals] RefreshToken: "reftok", } testCredentials = &credentials.Credentials{ //nolint[gochecknoglobals] UserID: "user", Name: "username", Emails: "user@pm.me", APIToken: "token", MailboxPassword: "pass", BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, IsHidden: false, IsCombinedAddressMode: true, } testCredentialsSplit = &credentials.Credentials{ //nolint[gochecknoglobals] UserID: "users", Name: "usersname", Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me", APIToken: "token", MailboxPassword: "pass", BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, IsHidden: false, IsCombinedAddressMode: false, } testCredentialsDisconnected = &credentials.Credentials{ //nolint[gochecknoglobals] UserID: "user", Name: "username", Emails: "user@pm.me", APIToken: "", MailboxPassword: "", BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, IsHidden: false, IsCombinedAddressMode: true, } testPMAPIUser = &pmapi.User{ //nolint[gochecknoglobals] ID: "user", Name: "username", } testPMAPIAddress = &pmapi.Address{ //nolint[gochecknoglobals] ID: "testAddressID", Type: pmapi.OriginalAddress, Email: "user@pm.me", Receive: pmapi.CanReceive, } testPMAPIAddresses = []*pmapi.Address{ //nolint[gochecknoglobals] {ID: "usersAddress1ID", Email: "users@pm.me", Receive: pmapi.CanReceive, Type: pmapi.OriginalAddress}, {ID: "usersAddress2ID", Email: "anotheruser@pm.me", Receive: pmapi.CanReceive, Type: pmapi.AliasAddress}, {ID: "usersAddress3ID", Email: "alsouser@pm.me", Receive: pmapi.CanReceive, Type: pmapi.AliasAddress}, } testPMAPIEvent = &pmapi.Event{ // nolint[gochecknoglobals] EventID: "ACXDmTaBub14w==", } ) func waitForEvents() { // Wait for goroutine to add listener. // E.g. calling login to invoke firstsync event. Functions can end sooner than // goroutines call the listener mock. We need to wait a little bit before the end of // the test to capture all event calls. This allows us to detect whether there were // missing calls, or perhaps whether something was called too many times. time.Sleep(100 * time.Millisecond) } type mocks struct { t *testing.T ctrl *gomock.Controller config *usersmocks.MockConfiger PanicHandler *usersmocks.MockPanicHandler clientManager *usersmocks.MockClientManager credentialsStore *usersmocks.MockCredentialsStorer storeMaker *usersmocks.MockStoreMaker eventListener *MockListener pmapiClient *pmapimocks.MockClient storeCache *store.Cache } type fullStackReporter struct { T testing.TB } func (fr *fullStackReporter) Errorf(format string, args ...interface{}) { fmt.Printf("err: "+format+"\n", args...) fr.T.Fail() } func (fr *fullStackReporter) Fatalf(format string, args ...interface{}) { debug.PrintStack() fmt.Printf("fail: "+format+"\n", args...) fr.T.FailNow() } func initMocks(t *testing.T) mocks { var mockCtrl *gomock.Controller if os.Getenv("VERBOSITY") == "trace" { mockCtrl = gomock.NewController(&fullStackReporter{t}) } else { mockCtrl = gomock.NewController(t) } cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db") require.NoError(t, err, "could not get temporary file for store cache") m := mocks{ t: t, ctrl: mockCtrl, config: usersmocks.NewMockConfiger(mockCtrl), PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl), clientManager: usersmocks.NewMockClientManager(mockCtrl), credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl), storeMaker: usersmocks.NewMockStoreMaker(mockCtrl), eventListener: NewMockListener(mockCtrl), pmapiClient: pmapimocks.NewMockClient(mockCtrl), storeCache: store.NewCache(cacheFile.Name()), } // Called during clean-up. m.PanicHandler.EXPECT().HandlePanic().AnyTimes() // Set up store factory. m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) { dbFile, err := ioutil.TempFile("", "bridge-store-db-*.db") require.NoError(t, err, "could not get temporary file for store db") return store.New(m.PanicHandler, user, m.clientManager, m.eventListener, dbFile.Name(), m.storeCache) }).AnyTimes() m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes() return m } func testNewUsersWithUsers(t *testing.T, m mocks) *Users { // Events are asynchronous m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil).Times(2) m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil).Times(2) m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).Times(2) gomock.InOrder( m.credentialsStore.EXPECT().List().Return([]string{"user", "users"}, nil), // Init for user. m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), // Init for users. m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil), m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil), m.pmapiClient.EXPECT().AuthRefresh("token").Return(testAuthRefresh, nil), m.pmapiClient.EXPECT().Unlock([]byte("pass")).Return(nil), m.pmapiClient.EXPECT().ListLabels().Return([]*pmapi.Label{}, nil), m.pmapiClient.EXPECT().CountMessages("").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().Addresses().Return(testPMAPIAddresses), ) users := testNewUsers(t, m) user, _ := users.GetUser("user") mockAuthUpdate(user, "reftok", m) user, _ = users.GetUser("user") mockAuthUpdate(user, "reftok", m) return users } func testNewUsers(t *testing.T, m mocks) *Users { //nolint[unparam] m.config.EXPECT().GetVersion().Return("ver").AnyTimes() m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any()) m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth)) users := New(m.config, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker) waitForEvents() return users } func cleanUpUsersData(b *Users) { for _, user := range b.users { _ = user.clearStore() } } func TestClearData(t *testing.T) { m := initMocks(t) defer m.ctrl.Finish() m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient("users").Return(m.pmapiClient).MinTimes(1) users := testNewUsersWithUsers(t, m) defer cleanUpUsersData(users) m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "alsouser@pm.me") m.pmapiClient.EXPECT().Logout() m.credentialsStore.EXPECT().Logout("user").Return(nil) m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil) m.pmapiClient.EXPECT().Logout() m.credentialsStore.EXPECT().Logout("users").Return(nil) m.credentialsStore.EXPECT().Get("users").Return(testCredentialsSplit, nil) m.config.EXPECT().ClearData().Return(nil) require.NoError(t, users.ClearData()) waitForEvents() } func mockEventLoopNoAction(m mocks) { // Set up mocks for starting the store's event loop (in store.New). // The event loop runs in another goroutine so this might happen at any time. gomock.InOrder( m.pmapiClient.EXPECT().GetEvent("").Return(testPMAPIEvent, nil), m.pmapiClient.EXPECT().GetEvent(testPMAPIEvent.EventID).Return(testPMAPIEvent, nil), // Set up mocks for performing the initial store sync. m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil), ) }