Merge branch 'release/congo' into devel

This commit is contained in:
Jakub
2020-09-08 09:37:05 +02:00
286 changed files with 22512 additions and 1057 deletions

View File

@ -1,25 +1,38 @@
.PHONY: check-has-go install-godog test test-live test-debug test-live-debug
.PHONY: check-go check-godog install-godog test test-bridge test-ie test-live test-live-bridge test-live-ie test-stage test-debug test-live-debug bench
export GO111MODULE=on
export BRIDGE_VERSION:=1.3.0-integrationtests
export VERBOSITY?=fatal
export TEST_DATA=testdata
export TEST_APP?=bridge
check-has-go:
# Tests do not run in parallel. This will overrule user settings.
MAKEFLAGS=-j1
check-go:
@which go || (echo "Install Go-lang!" && exit 1)
install-godog: check-has-go
check-godog:
@which godog || $(MAKE) install-godog
install-godog: check-go
go get github.com/cucumber/godog/cmd/godog@v0.8.1
test:
which godog || $(MAKE) install-godog
TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
test: test-bridge test-ie
test-bridge: FEATURES ?= features/bridge
test-bridge: check-godog
TEST_APP=bridge TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
test-ie: FEATURES ?= features/ie
test-ie: check-godog
TEST_APP=ie TEST_ENV=fake TEST_ACCOUNTS=accounts/fake.json godog --tags="~@ignore" $(FEATURES)
# Doesn't work in parallel!
# Provide TEST_ACCOUNTS with your accounts.
test-live:
which godog || $(MAKE) install-godog
TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
test-live: test-live-bridge test-live-ie
test-live-bridge: FEATURES ?= features/bridge
test-live-bridge: check-godog
TEST_APP=bridge TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
test-live-ie: FEATURES ?= features/ie
test-live-ie: check-godog
TEST_APP=ie TEST_ENV=live godog --tags="~@ignore && ~@ignore-live" $(FEATURES)
# Doesn't work in parallel!
# Provide TEST_ACCOUNTS with your accounts.

45
test/api_actions_test.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"time"
"github.com/cucumber/godog"
)
func APIActionsFeatureContext(s *godog.Suite) {
s.Step(`^the internet connection is lost$`, theInternetConnectionIsLost)
s.Step(`^the internet connection is restored$`, theInternetConnectionIsRestored)
s.Step(`^(\d+) seconds pass$`, secondsPass)
}
func theInternetConnectionIsLost() error {
ctx.GetPMAPIController().TurnInternetConnectionOff()
return nil
}
func theInternetConnectionIsRestored() error {
ctx.GetPMAPIController().TurnInternetConnectionOn()
return nil
}
func secondsPass(seconds int) error {
time.Sleep(time.Duration(seconds) * time.Second)
return nil
}

View File

@ -29,6 +29,8 @@ import (
func APIChecksFeatureContext(s *godog.Suite) {
s.Step(`^API endpoint "([^"]*)" is called with:$`, apiIsCalledWith)
s.Step(`^message is sent with API call:$`, messageIsSentWithAPICall)
s.Step(`^API mailbox "([^"]*)" for "([^"]*)" has messages$`, apiMailboxForUserHasMessages)
s.Step(`^API mailbox "([^"]*)" for address "([^"]*)" of "([^"]*)" has messages$`, apiMailboxForAddressOfUserHasMessages)
}
func apiIsCalledWith(endpoint string, data *gherkin.DocString) error {
@ -77,3 +79,41 @@ func checkAllRequiredFieldsForSendingMessage(request []byte) bool {
}
return true
}
func apiMailboxForUserHasMessages(mailboxName, bddUserID string, messages *gherkin.DataTable) error {
return apiMailboxForAddressOfUserHasMessages(mailboxName, "", bddUserID, messages)
}
func apiMailboxForAddressOfUserHasMessages(mailboxName, bddAddressID, bddUserID string, messages *gherkin.DataTable) error {
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
if account == nil {
return godog.ErrPending
}
labelIDs, err := ctx.GetPMAPIController().GetLabelIDs(account.Username(), []string{mailboxName})
if err != nil {
return internalError(err, "getting label %s for %s", mailboxName, account.Username())
}
labelID := labelIDs[0]
pmapiMessages, err := ctx.GetPMAPIController().GetMessages(account.Username(), labelID)
if err != nil {
return err
}
head := messages.Rows[0].Cells
for _, row := range messages.Rows[1:] {
found, err := messagesContainsMessageRow(account, pmapiMessages, head, row)
if err != nil {
return err
}
if !found {
rowMap := map[string]string{}
for idx, cell := range row.Cells {
rowMap[head[idx].Value] = cell.Value
}
return fmt.Errorf("message %v not found", rowMap)
}
}
return nil
}

31
test/api_setup_test.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"github.com/cucumber/godog"
)
func APISetupFeatureContext(s *godog.Suite) {
s.Step(`^there is no internet connection$`, thereIsNoInternetConnection)
}
func thereIsNoInternetConnection() error {
ctx.GetPMAPIController().TurnInternetConnectionOff()
return nil
}

View File

@ -18,19 +18,27 @@
package tests
import (
"os"
"github.com/ProtonMail/proton-bridge/test/context"
"github.com/cucumber/godog"
)
const (
timeFormat = "2006-01-02T15:04:05"
)
func FeatureContext(s *godog.Suite) {
s.BeforeScenario(beforeScenario)
s.AfterScenario(afterScenario)
APIActionsFeatureContext(s)
APIChecksFeatureContext(s)
APISetupFeatureContext(s)
BridgeActionsFeatureContext(s)
BridgeChecksFeatureContext(s)
BridgeSetupFeatureContext(s)
CommonChecksFeatureContext(s)
IMAPActionsAuthFeatureContext(s)
IMAPActionsMailboxFeatureContext(s)
@ -45,18 +53,32 @@ func FeatureContext(s *godog.Suite) {
StoreActionsFeatureContext(s)
StoreChecksFeatureContext(s)
StoreSetupFeatureContext(s)
TransferActionsFeatureContext(s)
TransferChecksFeatureContext(s)
TransferSetupFeatureContext(s)
UsersActionsFeatureContext(s)
UsersSetupFeatureContext(s)
UsersChecksFeatureContext(s)
}
var ctx *context.TestContext //nolint[gochecknoglobals]
func beforeScenario(scenario interface{}) {
ctx = context.New()
// bridge or ie. With godog 0.10.x and later it can be determined from
// scenario.Uri and its file location.
app := os.Getenv("TEST_APP")
ctx = context.New(app)
}
func afterScenario(scenario interface{}, err error) {
if err != nil {
for _, user := range ctx.GetBridge().GetUsers() {
user.GetStore().TestDumpDB(ctx.GetTestingT())
for _, user := range ctx.GetUsers().GetUsers() {
store := user.GetStore()
if store != nil {
store.TestDumpDB(ctx.GetTestingT())
}
}
}
ctx.Cleanup()

View File

@ -27,7 +27,7 @@ import (
)
func benchTestContext() (*context.TestContext, *mocks.IMAPClient) {
ctx := context.New()
ctx := context.New("bridge")
username := "user"
account := ctx.GetTestAccount(username)

View File

@ -18,28 +18,16 @@
package tests
import (
"time"
"github.com/cucumber/godog"
)
func BridgeActionsFeatureContext(s *godog.Suite) {
s.Step(`^bridge starts$`, bridgeStarts)
s.Step(`^bridge syncs "([^"]*)"$`, bridgeSyncsUser)
s.Step(`^"([^"]*)" logs in to bridge$`, userLogsInToBridge)
s.Step(`^"([^"]*)" logs in to bridge with bad password$`, userLogsInToBridgeWithBadPassword)
s.Step(`^"([^"]*)" logs out from bridge$`, userLogsOutFromBridge)
s.Step(`^"([^"]*)" changes the address mode$`, userChangesTheAddressMode)
s.Step(`^user deletes "([^"]*)" from bridge$`, userDeletesUserFromBridge)
s.Step(`^user deletes "([^"]*)" from bridge with cache$`, userDeletesUserFromBridgeWithCache)
s.Step(`^the internet connection is lost$`, theInternetConnectionIsLost)
s.Step(`^the internet connection is restored$`, theInternetConnectionIsRestored)
s.Step(`^(\d+) seconds pass$`, secondsPass)
s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
}
func bridgeStarts() error {
ctx.SetLastBridgeError(ctx.RestartBridge())
ctx.SetLastError(ctx.RestartBridge())
return nil
}
@ -51,113 +39,6 @@ func bridgeSyncsUser(bddUserID string) error {
if err := ctx.WaitForSync(account.Username()); err != nil {
return internalError(err, "waiting for sync")
}
ctx.SetLastBridgeError(ctx.GetTestingError())
ctx.SetLastError(ctx.GetTestingError())
return nil
}
func userLogsInToBridge(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
ctx.SetLastBridgeError(ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()))
return nil
}
func userLogsInToBridgeWithBadPassword(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
ctx.SetLastBridgeError(ctx.LoginUser(account.Username(), "you shall not pass!", "123"))
return nil
}
func userLogsOutFromBridge(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
ctx.SetLastBridgeError(ctx.LogoutUser(account.Username()))
return nil
}
func userChangesTheAddressMode(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
if err := bridgeUser.SwitchAddressMode(); err != nil {
return err
}
ctx.EventuallySyncIsFinishedForUsername(account.Username())
return nil
}
func userDeletesUserFromBridge(bddUserID string) error {
return deleteUserFromBridge(bddUserID, false)
}
func userDeletesUserFromBridgeWithCache(bddUserID string) error {
return deleteUserFromBridge(bddUserID, true)
}
func deleteUserFromBridge(bddUserID string, cache bool) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
return ctx.GetBridge().DeleteUser(bridgeUser.ID(), cache)
}
func theInternetConnectionIsLost() error {
ctx.GetPMAPIController().TurnInternetConnectionOff()
return nil
}
func theInternetConnectionIsRestored() error {
ctx.GetPMAPIController().TurnInternetConnectionOn()
return nil
}
func secondsPass(seconds int) error {
time.Sleep(time.Duration(seconds) * time.Second)
return nil
}
func swapsAddressWithAddress(bddUserID, bddAddressID1, bddAddressID2 string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
address1ID := account.GetAddressID(bddAddressID1)
address2ID := account.GetAddressID(bddAddressID2)
addressIDs := make([]string, len(*account.Addresses()))
var address1Index, address2Index int
for i, v := range *account.Addresses() {
if v.ID == address1ID {
address1Index = i
}
if v.ID == address2ID {
address2Index = i
}
addressIDs[i] = v.ID
}
addressIDs[address1Index], addressIDs[address2Index] = addressIDs[address2Index], addressIDs[address1Index]
ctx.ReorderAddresses(account.Username(), bddAddressID1, bddAddressID2)
return ctx.GetPMAPIController().ReorderAddresses(account.User(), addressIDs)
}

View File

@ -0,0 +1,37 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"github.com/cucumber/godog"
a "github.com/stretchr/testify/assert"
)
func CommonChecksFeatureContext(s *godog.Suite) {
s.Step(`^last response is "([^"]*)"$`, lastResponseIs)
}
func lastResponseIs(expectedResponse string) error {
err := ctx.GetLastError()
if expectedResponse == "OK" {
a.NoError(ctx.GetTestingT(), err)
} else {
a.EqualError(ctx.GetTestingT(), err, expectedResponse)
}
return ctx.GetTestingError()
}

View File

@ -32,9 +32,10 @@ func (ctx *TestContext) GetBridge() *bridge.Bridge {
}
// withBridgeInstance creates a bridge instance for use in the test.
// Every TestContext has this by default and thus this doesn't need to be exported.
// TestContext has this by default once called with env variable TEST_APP=bridge.
func (ctx *TestContext) withBridgeInstance() {
ctx.bridge = newBridgeInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, ctx.clientManager)
ctx.users = ctx.bridge.Users
ctx.addCleanupChecked(ctx.bridge.ClearData, "Cleaning bridge data")
}
@ -69,13 +70,3 @@ func newBridgeInstance(
pref := preferences.New(cfg)
return bridge.New(cfg, pref, panicHandler, eventListener, clientManager, credStore)
}
// SetLastBridgeError sets the last error that occurred while executing a bridge action.
func (ctx *TestContext) SetLastBridgeError(err error) {
ctx.bridgeLastError = err
}
// GetLastBridgeError returns the last error that occurred while executing a bridge action.
func (ctx *TestContext) GetLastBridgeError() error {
return ctx.bridgeLastError
}

View File

@ -77,6 +77,9 @@ func (c *fakeConfig) GetLogPrefix() string {
func (c *fakeConfig) GetPreferencesPath() string {
return filepath.Join(c.dir, "prefs.json")
}
func (c *fakeConfig) GetTransferDir() string {
return c.dir
}
func (c *fakeConfig) GetTLSCertPath() string {
return filepath.Join(c.dir, "cert.pem")
}

View File

@ -20,6 +20,8 @@ package context
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -46,10 +48,12 @@ type TestContext struct {
pmapiController PMAPIController
clientManager *pmapi.ClientManager
// Bridge core related variables.
bridge *bridge.Bridge
bridgeLastError error
credStore users.CredentialsStorer
// Core related variables.
bridge *bridge.Bridge
importExport *importexport.ImportExport
users *users.Users
credStore users.CredentialsStorer
lastError error
// IMAP related variables.
imapAddr string
@ -63,6 +67,12 @@ type TestContext struct {
smtpClients map[string]*mocks.SMTPClient
smtpLastResponses map[string]*mocks.SMTPResponse
// Transfer related variables.
transferLocalRootForImport string
transferLocalRootForExport string
transferRemoteIMAPServer *mocks.IMAPServer
transferProgress *transfer.Progress
// These are the cleanup steps executed when Cleanup() is called.
cleanupSteps []*Cleaner
@ -71,7 +81,7 @@ type TestContext struct {
}
// New returns a new test TestContext.
func New() *TestContext {
func New(app string) *TestContext {
setLogrusVerbosityFromEnv()
cfg := newFakeConfig()
@ -96,8 +106,15 @@ func New() *TestContext {
// Ensure that the config is cleaned up after the test is over.
ctx.addCleanupChecked(cfg.ClearData, "Cleaning bridge config data")
// Create bridge instance under test.
ctx.withBridgeInstance()
// Create bridge or import-export instance under test.
switch app {
case "bridge":
ctx.withBridgeInstance()
case "ie":
ctx.withImportExportInstance()
default:
panic("unknown app: " + app)
}
return ctx
}
@ -125,3 +142,13 @@ func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint]
func (ctx *TestContext) GetTestingError() error {
return ctx.t.getErrors()
}
// SetLastError sets the last error that occurred while executing an action.
func (ctx *TestContext) SetLastError(err error) {
ctx.lastError = err
}
// GetLastError returns the last error that occurred while executing an action.
func (ctx *TestContext) GetLastError() error {
return ctx.lastError
}

View File

@ -0,0 +1,48 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package context
import (
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
// GetImportExport returns import-export instance.
func (ctx *TestContext) GetImportExport() *importexport.ImportExport {
return ctx.importExport
}
// withImportExportInstance creates a import-export instance for use in the test.
// TestContext has this by default once called with env variable TEST_APP=ie.
func (ctx *TestContext) withImportExportInstance() {
ctx.importExport = newImportExportInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, ctx.clientManager)
ctx.users = ctx.importExport.Users
}
// newImportExportInstance creates a new import-export instance configured to use the given config/credstore.
func newImportExportInstance(
t *bddT,
cfg importexport.Configer,
credStore users.CredentialsStorer,
eventListener listener.Listener,
clientManager users.ClientManager,
) *importexport.ImportExport {
panicHandler := &panicHandler{t: t}
return importexport.New(cfg, panicHandler, eventListener, clientManager, credStore)
}

View File

@ -33,6 +33,7 @@ type PMAPIController interface {
GetLabelIDs(username string, labelNames []string) ([]string, error)
AddUserMessage(username string, message *pmapi.Message) error
GetMessageID(username, messageIndex string) string
GetMessages(username, labelID string) ([]*pmapi.Message, error)
ReorderAddresses(user *pmapi.User, addressIDs []string) error
PrintCalls()
WasCalled(method, path string, expectedRequest []byte) bool

91
test/context/transfer.go Normal file
View File

@ -0,0 +1,91 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package context
import (
"io/ioutil"
"math/rand"
"os"
"strconv"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/test/mocks"
)
// SetTransferProgress sets transfer progress.
func (ctx *TestContext) SetTransferProgress(progress *transfer.Progress) {
ctx.transferProgress = progress
}
// GetTransferProgress returns transfer progress.
func (ctx *TestContext) GetTransferProgress() *transfer.Progress {
return ctx.transferProgress
}
// GetTransferLocalRootForImport creates temporary root for importing
// if it not exists yet, and returns its path.
func (ctx *TestContext) GetTransferLocalRootForImport() string {
if ctx.transferLocalRootForImport != "" {
return ctx.transferLocalRootForImport
}
root := ctx.createLocalRoot()
ctx.transferLocalRootForImport = root
return root
}
// GetTransferLocalRootForExport creates temporary root for exporting
// if it not exists yet, and returns its path.
func (ctx *TestContext) GetTransferLocalRootForExport() string {
if ctx.transferLocalRootForExport != "" {
return ctx.transferLocalRootForExport
}
root := ctx.createLocalRoot()
ctx.transferLocalRootForExport = root
return root
}
func (ctx *TestContext) createLocalRoot() string {
root, err := ioutil.TempDir("", "transfer")
if err != nil {
panic("failed to create temp transfer root: " + err.Error())
}
ctx.addCleanupChecked(func() error {
return os.RemoveAll(root)
}, "Cleaning transfer data")
return root
}
// GetTransferRemoteIMAPServer creates mocked IMAP server if it not created yet, and returns it.
func (ctx *TestContext) GetTransferRemoteIMAPServer() *mocks.IMAPServer {
if ctx.transferRemoteIMAPServer != nil {
return ctx.transferRemoteIMAPServer
}
port := 21300 + rand.Intn(100)
ctx.transferRemoteIMAPServer = mocks.NewIMAPServer("user", "pass", "127.0.0.1", strconv.Itoa(port))
ctx.transferRemoteIMAPServer.Start()
ctx.addCleanupChecked(func() error {
ctx.transferRemoteIMAPServer.Stop()
return nil
}, "Cleaning transfer IMAP server")
return ctx.transferRemoteIMAPServer
}

View File

@ -30,11 +30,16 @@ import (
"github.com/stretchr/testify/assert"
)
// GetUsers returns users instance.
func (ctx *TestContext) GetUsers() *users.Users {
return ctx.users
}
// LoginUser logs in the user with the given username, password, and mailbox password.
func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (err error) {
srp.RandReader = rand.New(rand.NewSource(42))
client, auth, err := ctx.bridge.Login(username, password)
client, auth, err := ctx.users.Login(username, password)
if err != nil {
return errors.Wrap(err, "failed to login")
}
@ -45,7 +50,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
}
}
user, err := ctx.bridge.FinishLogin(client, auth, mailboxPassword)
user, err := ctx.users.FinishLogin(client, auth, mailboxPassword)
if err != nil {
return errors.Wrap(err, "failed to finish login")
}
@ -57,7 +62,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
// GetUser retrieves the bridge user matching the given query string.
func (ctx *TestContext) GetUser(username string) (*users.User, error) {
return ctx.bridge.GetUser(username)
return ctx.users.GetUser(username)
}
// GetStore retrieves the store for given username.
@ -100,6 +105,9 @@ func (ctx *TestContext) WaitForSync(username string) error {
if err != nil {
return err
}
if store == nil {
return nil
}
// First wait for ongoing sync to be done before starting and waiting for new one.
ctx.eventuallySyncIsFinished(store)
store.TestSync()
@ -121,7 +129,7 @@ func (ctx *TestContext) EventuallySyncIsFinishedForUsername(username string) {
// LogoutUser logs out the given user.
func (ctx *TestContext) LogoutUser(query string) (err error) {
user, err := ctx.bridge.GetUser(query)
user, err := ctx.users.GetUser(query)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
@ -135,12 +143,12 @@ func (ctx *TestContext) LogoutUser(query string) (err error) {
// DeleteUser deletes the given user.
func (ctx *TestContext) DeleteUser(query string, deleteStore bool) (err error) {
user, err := ctx.bridge.GetUser(query)
user, err := ctx.users.GetUser(query)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
if err = ctx.bridge.DeleteUser(user.ID(), deleteStore); err != nil {
if err = ctx.users.DeleteUser(user.ID(), deleteStore); err != nil {
err = errors.Wrap(err, "failed to delete user")
}

View File

@ -159,3 +159,17 @@ func (ctl *Controller) resetUsers() {
func (ctl *Controller) GetMessageID(username, messageIndex string) string {
return messageIndex
}
func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message, error) {
messages := []*pmapi.Message{}
for _, fakeAPI := range ctl.fakeAPIs {
if fakeAPI.username == username {
for _, message := range fakeAPI.messages {
if labelID == "" || message.HasLabelID(labelID) {
messages = append(messages, message)
}
}
}
}
return messages, nil
}

View File

@ -0,0 +1,62 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v4.4.5
Comment: testpassphrase
xcLYBFzGzhEBCADBxfqTFMqfQzT77A5tuuhPFwPq8dfC2evs8u1OvTqFbztY
5FOuSxzduyeDqQ1Fx6dKEOKgcYE8t1Uh4VSS7z6bTdY8j9yrL81kCVB46sE1
OzStzyx/5l7OdH/pM4F+aKslnLvqlw0UeJr+UNizVtOCEUaNfVjPK3cc1ocx
v+36K4RnnyfEtjUW9gDZbhgaF02G5ILHmWmbgM7I+77gCd2wI0EdY9s/JZQ+
VmkMFqoMdY9PyBchoOIPUkkGQi1SaF4IEzMaAUSbnCYkHHY/SbfDTcR46VGq
cXlkB1rq5xskaUQ9r+giCC/K4pc7bBkI1lQ7ADVuWvdrWnWapK0FO6CfABEB
AAEAB/0YPhPJ0phA/EWviN+16bmGVOZNaVapjt2zMMybWmrtEQv3OeWgO3nP
4cohRi/zaCBCphcm+dxbLhftW7AFi/9PVcR09436MB+oTCQFugpUWw+4TmA5
BidxTpDxf4X2vH3rquQLBufWL6U7JlPeKAGL1xZ2aCq0DIeOk5D+xTjZizV2
GIyQRVCLWb+LfDmvvcp3Y94X60KXdBAMuS1ZMKcY3Sl8VAXNB4KQsC/kByzf
6FCB097XZRYV7lvJJQ7+6Wisb3yVi8sEQx2sFm5fAp+0qi3a6zRTEp49r6Hr
gyWViH5zOOpA7DcNwx1Bwhi7GG0tak6EUnnKUNLfOupglcphBADmpXCgT4nc
uSBYTiZSVcB/ICCkTxVsHL1WcXtPK2Ikzussx2n9kb0rapvuC0YLipX9lUkQ
fyeC3jQJeCyN79AkDGkOfWaESueT2hM0Po+RwDgMibKn6yJ1zebz4Lc2J3C9
oVFcAnql+9KyGsAPn03fyQzDnvhNnJvHJi4Hx8AWoQQA1xLoXeVBjRi0IjjU
E6Mqaq5RLEog4kXRp86VSSEGHBwyIYnDiM//gjseo/CXuVyHwL7UXitp8s1B
D1uE3APrhqUS66fD5pkF+z+RcSqiIv7I76NJ24Cdg38L6seGSjOHrq7/dEeG
K6WqfQUCEjta3yNSg7pXb2wn2WZqKIK+rz8EALZRuMXeql/FtO3Cjb0sv7oT
9dLP4cn1bskGRJ+Vok9lfCERbfXGccoAk3V+qSfpHgKxsebkRbUhf+trOGnw
tW+kBWo/5hYGQuN+A9JogSJViT+nuZyE+x1/rKswDFmlMSdf2GIDARWIV0gc
b1yOEwUmNBSthPcnFXvBr4BG3XTtNPTNLSJhcm9uMjEtM0BzYWRlbWJlLm9y
ZyIgPGFyb24yMS0zQHNhZGVtYmUub3JnPsLAdQQQAQgAHwUCXMbOEQYLCQcI
AwIEFQgKAgMWAgECGQECGwMCHgEACgkQZ/B4v2b2xB6XUgf/dHGRHimyMR78
QYbEm2cuaEvOtq4a+J6Zv3P4VOWAbvkGWS9LDKSvVi60vq4oYOmF54HgPzur
nA4OtZDf0HKwQK45VZ7CYD693o70jkKPrAAJG3yTsbesfiS7RbFyGKzKJ7EL
nsUIJkfgm/SlKmXU/u8MOBO5Wg7/TcsS33sRWHl90j+9jbhqdl92R+vY/CwC
ieFkQA7/TDv1u+NAalH+Lpkd8AIuEcki+TAogZ7oi/SnofwnoB7BxRm+mIkp
ZZhIDSCaPOzLG8CSZ81d3HVHhqbf8dh0DFKFoUYyKdbOqIkNWWASf+c/ZEme
IWcekY8hqwf/raZ56tGM/bRwYPcotMfC1wRcxs4RAQgAsMb5/ELWmrfPy3ba
5qif+RXhGSbjitATNgHpoPUHrfTC7cn4JWHqehoXLAQpFAoKd+O/ZNpZozK9
ilpqGUx05yMw06jNQEhYIbgIF4wzPpz02Lp6YeMwdF5LF+Rw83PHdHrA/wRV
/QjL04+kZnN+G5HmzMlhFY+oZSpL+Gp1bTXgtAVDkhCnMB5tP2VwULMGyJ+X
vRYxwTK2CrLjIVZv5n1VYY+caCowU6j/XFqvlCJj+G5oV+UhFOWffaMRXhOh
a64RrhqT1Np7wCLvLMP2wpys9xlMcLQJLqDNxqOTp504V7dm67ncC0fKUsT4
m4oTktnxKPd6MU+4VYveaLCquwARAQABAAf4u9s7gpGErs1USxmDO9TlyGZK
aBlri8nMf3s+hOJCOo3cRaRHJBfdY6pu/baG6H6JTsWzeY4MHwr6N+dhVIEh
FPMa9EZAjagyc4GugxWGiMVTfU+2AEfdrdynhQKMgXSctnnNCdkRuX0nwqb3
nlupm1hsz2ze4+Wg0BKSLS0FQdoUbITdJUR69OHr4dNJVHWYI0JSBx4SdhV3
y9163dDvmc+lW9AEaD53vyZWfzCHZxsR/gI32VmT0z5gn1t8w9AOdXo2lA1H
bf7wh4/qCyujGu64ToZtiEny/GCyM6PofLtiZuJNLw3s/y+B2tKv22aTJ760
+Gib1xB9WcWjKyrxBADoeCyq+nHGrl0CwOkmjanlFymgo7mnBOXuiFOvGrKk
M1meMU1TI4TEBWkVnDVMcSejgjAf/bX1dtouba1tMAMu7DlaV/0EwbSADRel
RSqEbIzIOys+y9TY/BMI/uCKNyEKHvu1KUXADb+CBpdBpCfMBWDANFlo9xLz
Ajcmu2dyawQAwquwC0VXQcvzfs+Hd5au5XvHdm1KidOiAdu6PH1SrOgenIN4
lkEjHrJD9jmloO2/GVcxDBB2pmf0B4HEg7DuY9LXBrksP5eSbbRc5+UH1HUv
u82AqQnfNKTd/jae+lLwaOS++ohtwMkkD6W0LdWnHPjyyXg4Oi9zPID3asRu
3PED/3CYyjl6S8GTMY4FNH7Yxu9+NV2xpKE92Hf8K/hnYlmSSVKDCEeOJtLt
BkkcSqY6liCNSMmJdVyAF2GrR+zmDac7UQRssf57oOWtSsGozt0aqJXuspMT
6aB+P1UhZ8Ly9rWZNiJ0jwyfnQNOLCYDaqjFmiSpqrNnJ2Q1Xge3+k80P9DC
wF8EGAEIAAkFAlzGzhECGwwACgkQZ/B4v2b2xB5wlwgAjZA1zdv5irFjyWVo
4/itONtyO1NbdpyYpcct7vD0oV+a4wahQP0J3Kk1GhZ5tvAoZF/jakQQOM5o
GjUYpXAGnr09Mv9EiQ2pDwXc2yq0WfXnGxNrpzOqdtV+IqY9NYkl55Tme7x+
WRvrkPSUeUsyEGvxwR1stdv8eg9jUmxdl8Io3PYoFJJlrM/6aXeC1r3KOj7q
XAnR0XHJ+QBSNKCWLlQv5hui9BKfcLiVKFK/dNhs82nRyhPr4sWFw6MTqdAK
4zkn7l0jmy6Evi1AiiGPiHPnxeNErnofOIEh4REQj00deZADHrixTLtx2FuR
uaSC3IcBmBsj1fNb4eYXElILjQ==
=fMOl
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -20,6 +20,7 @@ package fakeapi
import (
"bytes"
"fmt"
"io/ioutil"
"net/mail"
"time"
@ -67,7 +68,7 @@ func (api *FakePMAPI) ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Messa
for idx := 0; idx < len(api.messages); idx++ {
var message *pmapi.Message
if !*filter.Desc {
if filter.Desc == nil || !*filter.Desc {
message = api.messages[idx]
if filter.BeginID == "" || message.ID == filter.BeginID {
skipByIDBegin = false
@ -81,7 +82,7 @@ func (api *FakePMAPI) ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Messa
if skipByIDBegin || skipByIDEnd {
continue
}
if !*filter.Desc {
if filter.Desc == nil || !*filter.Desc {
if message.ID == filter.EndID {
skipByIDEnd = true
}
@ -189,36 +190,60 @@ func (api *FakePMAPI) SendMessage(messageID string, sendMessageRequest *pmapi.Se
}
func (api *FakePMAPI) Import(importMessageRequests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
if err := api.checkAndRecordCall(POST, "/import", importMessageRequests); err != nil {
return nil, err
}
msgRes := []*pmapi.ImportMsgRes{}
for _, msgReq := range importMessageRequests {
mailMessage, err := mail.ReadMessage(bytes.NewBuffer(msgReq.Body))
message, err := api.generateMessageFromImportRequest(msgReq)
if err != nil {
msgRes = append(msgRes, &pmapi.ImportMsgRes{
Error: err,
})
}
messageID := api.controller.messageIDGenerator.next("")
message := &pmapi.Message{
ID: messageID,
AddressID: msgReq.AddressID,
Sender: &mail.Address{Address: mailMessage.Header.Get("From")},
ToList: []*mail.Address{{Address: mailMessage.Header.Get("To")}},
Subject: mailMessage.Header.Get("Subject"),
Unread: msgReq.Unread,
LabelIDs: msgReq.LabelIDs,
Body: string(msgReq.Body),
Flags: msgReq.Flags,
Time: msgReq.Time,
continue
}
msgRes = append(msgRes, &pmapi.ImportMsgRes{
Error: nil,
MessageID: messageID,
MessageID: message.ID,
})
api.addMessage(message)
}
return msgRes, nil
}
func (api *FakePMAPI) generateMessageFromImportRequest(msgReq *pmapi.ImportMsgReq) (*pmapi.Message, error) {
mailMessage, err := mail.ReadMessage(bytes.NewBuffer(msgReq.Body))
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(mailMessage.Body)
if err != nil {
return nil, err
}
sender, err := mail.ParseAddress(mailMessage.Header.Get("From"))
if err != nil {
return nil, err
}
toList, err := mail.ParseAddressList(mailMessage.Header.Get("To"))
if err != nil {
return nil, err
}
messageID := api.controller.messageIDGenerator.next("")
return &pmapi.Message{
ID: messageID,
AddressID: msgReq.AddressID,
Sender: sender,
ToList: toList,
Subject: mailMessage.Header.Get("Subject"),
Unread: msgReq.Unread,
LabelIDs: append(msgReq.LabelIDs, pmapi.AllMailLabel),
Body: string(body),
Header: mailMessage.Header,
Flags: msgReq.Flags,
Time: msgReq.Time,
}, nil
}
func (api *FakePMAPI) addMessage(message *pmapi.Message) {
api.messages = append(api.messages, message)
api.addEventMessage(pmapi.EventCreate, message)

View File

@ -23,16 +23,8 @@ import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (api *FakePMAPI) ReportBugWithEmailClient(os, osVersion, title, description, username, email, emailClient string) error {
return api.checkInternetAndRecordCall(POST, "/reports/bug", &pmapi.ReportReq{
OS: os,
OSVersion: osVersion,
Title: title,
Description: description,
Username: username,
Email: email,
Browser: emailClient,
})
func (api *FakePMAPI) Report(report pmapi.ReportReq) error {
return api.checkInternetAndRecordCall(POST, "/reports/bug", report)
}
func (api *FakePMAPI) SendSimpleMetric(category, action, label string) error {

View File

@ -29,16 +29,16 @@ Feature: IMAP auth
Scenario: Authenticates with freshly logged-out user
Given there is connected user "user"
When "user" logs out from bridge
When "user" logs out
And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
Scenario: Authenticates user which was re-logged in
Given there is connected user "user"
When "user" logs out from bridge
When "user" logs out
And IMAP client authenticates "user"
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
When "user" logs in to bridge
When "user" logs in
And IMAP client authenticates "user"
Then IMAP response is "OK"
When IMAP client selects "INBOX"

View File

@ -26,7 +26,7 @@ Feature: Address mode
Scenario: Switch address mode from combined to split mode
Given there is "userMoreAddresses" in "combined" address mode
When "userMoreAddresses" changes the address mode
Then bridge response is "OK"
Then last response is "OK"
And "userMoreAddresses" has address mode in "split" mode
And mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
| from | to | subject |
@ -38,7 +38,7 @@ Feature: Address mode
Scenario: Switch address mode from split to combined mode
Given there is "userMoreAddresses" in "split" address mode
When "userMoreAddresses" changes the address mode
Then bridge response is "OK"
Then last response is "OK"
And "userMoreAddresses" has address mode in "combined" mode
And mailbox "Folders/mbox" for address "primary" of "userMoreAddresses" has messages
| from | to | subject |

View File

@ -1,36 +1,36 @@
Feature: Delete user
Scenario: Deleting connected user
Given there is connected user "user"
When user deletes "user" from bridge
Then bridge response is "OK"
When user deletes "user"
Then last response is "OK"
And "user" has database file
Scenario: Deleting connected user with cache
Given there is connected user "user"
When user deletes "user" from bridge with cache
Then bridge response is "OK"
When user deletes "user" with cache
Then last response is "OK"
And "user" does not have database file
Scenario: Deleting connected user without database file
Given there is connected user "user"
And there is no database file for "user"
When user deletes "user" from bridge with cache
Then bridge response is "OK"
When user deletes "user" with cache
Then last response is "OK"
Scenario: Deleting disconnected user
Given there is disconnected user "user"
When user deletes "user" from bridge
Then bridge response is "OK"
When user deletes "user"
Then last response is "OK"
And "user" has database file
Scenario: Deleting disconnected user with cache
Given there is disconnected user "user"
When user deletes "user" from bridge with cache
Then bridge response is "OK"
When user deletes "user" with cache
Then last response is "OK"
And "user" does not have database file
Scenario: Deleting disconnected user without database file
Given there is disconnected user "user"
And there is no database file for "user"
When user deletes "user" from bridge with cache
Then bridge response is "OK"
When user deletes "user" with cache
Then last response is "OK"

View File

@ -1,47 +1,47 @@
Feature: Login to bridge for the first time
Scenario: Normal bridge login
Feature: Login for the first time
Scenario: Normal login
Given there is user "user"
When "user" logs in to bridge
Then bridge response is "OK"
When "user" logs in
Then last response is "OK"
And "user" is connected
And "user" has database file
And "user" has running event loop
Scenario: Login with bad username
When "user" logs in to bridge with bad password
Then bridge response is "failed to login: Incorrect login credentials. Please try again"
When "user" logs in with bad password
Then last response is "failed to login: Incorrect login credentials. Please try again"
Scenario: Login with bad password
Given there is user "user"
When "user" logs in to bridge with bad password
Then bridge response is "failed to login: Incorrect login credentials. Please try again"
When "user" logs in with bad password
Then last response is "failed to login: Incorrect login credentials. Please try again"
Scenario: Login without internet connection
Given there is no internet connection
When "user" logs in to bridge
Then bridge response is "failed to login: cannot reach the server"
When "user" logs in
Then last response is "failed to login: cannot reach the server"
@ignore-live
Scenario: Login user with 2FA
Given there is user "user2fa"
When "user2fa" logs in to bridge
Then bridge response is "OK"
When "user2fa" logs in
Then last response is "OK"
And "user2fa" is connected
And "user2fa" has database file
And "user2fa" has running event loop
Scenario: Login user with capital letters in address
Given there is user "userAddressWithCapitalLetter"
When "userAddressWithCapitalLetter" logs in to bridge
Then bridge response is "OK"
When "userAddressWithCapitalLetter" logs in
Then last response is "OK"
And "userAddressWithCapitalLetter" is connected
And "userAddressWithCapitalLetter" has database file
And "userAddressWithCapitalLetter" has running event loop
Scenario: Login user with more addresses
Given there is user "userMoreAddresses"
When "userMoreAddresses" logs in to bridge
Then bridge response is "OK"
When "userMoreAddresses" logs in
Then last response is "OK"
And "userMoreAddresses" is connected
And "userMoreAddresses" has database file
And "userMoreAddresses" has running event loop
@ -49,8 +49,8 @@ Feature: Login to bridge for the first time
@ignore-live
Scenario: Login user with disabled primary address
Given there is user "userDisabledPrimaryAddress"
When "userDisabledPrimaryAddress" logs in to bridge
Then bridge response is "OK"
When "userDisabledPrimaryAddress" logs in
Then last response is "OK"
And "userDisabledPrimaryAddress" is connected
And "userDisabledPrimaryAddress" has database file
And "userDisabledPrimaryAddress" has running event loop
@ -58,9 +58,9 @@ Feature: Login to bridge for the first time
Scenario: Login two users
Given there is user "user"
And there is user "userMoreAddresses"
When "user" logs in to bridge
Then bridge response is "OK"
When "user" logs in
Then last response is "OK"
And "user" is connected
When "userMoreAddresses" logs in to bridge
Then bridge response is "OK"
When "userMoreAddresses" logs in
Then last response is "OK"
And "userMoreAddresses" is connected

View File

@ -1,9 +1,9 @@
Feature: Re-login to bridge
Feature: Re-login
Scenario: Re-login with connected user and database file
Given there is connected user "user"
And there is database file for "user"
When "user" logs in to bridge
Then bridge response is "failed to finish login: user is already connected"
When "user" logs in
Then last response is "failed to finish login: user is already connected"
And "user" is connected
And "user" has running event loop
@ -11,8 +11,8 @@ Feature: Re-login to bridge
Scenario: Re-login with connected user and no database file
Given there is connected user "user"
And there is no database file for "user"
When "user" logs in to bridge
Then bridge response is "failed to finish login: user is already connected"
When "user" logs in
Then last response is "failed to finish login: user is already connected"
And "user" is connected
And "user" has database file
And "user" has running event loop
@ -20,16 +20,16 @@ Feature: Re-login to bridge
Scenario: Re-login with disconnected user and database file
Given there is disconnected user "user"
And there is database file for "user"
When "user" logs in to bridge
Then bridge response is "OK"
When "user" logs in
Then last response is "OK"
And "user" is connected
And "user" has running event loop
Scenario: Re-login with disconnected user and no database file
Given there is disconnected user "user"
And there is no database file for "user"
When "user" logs in to bridge
Then bridge response is "OK"
When "user" logs in
Then last response is "OK"
And "user" is connected
And "user" has database file
And "user" has running event loop

View File

@ -28,7 +28,7 @@ Feature: Sync bridge
Scenario: Sync in combined mode
And there is "userMoreAddresses" in "combined" address mode
When bridge syncs "userMoreAddresses"
Then bridge response is "OK"
Then last response is "OK"
And "userMoreAddresses" has the following messages
| mailboxes | messages |
| INBOX | 1101 |
@ -43,7 +43,7 @@ Feature: Sync bridge
Scenario: Sync in split mode
And there is "userMoreAddresses" in "split" address mode
When bridge syncs "userMoreAddresses"
Then bridge response is "OK"
Then last response is "OK"
And "userMoreAddresses" has the following messages
| address | mailboxes | messages |
| primary | INBOX | 1001 |

View File

@ -0,0 +1,43 @@
Feature: Export to EML files
Background:
Given there is connected user "user"
And there is "user" with mailbox "Folders/Foo"
And there are messages in mailbox "INBOX" for "user"
| from | to | subject | time |
| bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
And there are messages in mailbox "Folders/Foo" for "user"
| from | to | subject | time |
| foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
Scenario: Export all
When user "user" exports to EML files
Then progress result is "OK"
# Every message is also in All Mail.
And transfer exported 8 messages
And transfer imported 8 messages
And transfer failed for 0 messages
And transfer exported messages
| folder | from | to | subject | time |
| Inbox | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
| Foo | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
| All Mail | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
| All Mail | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| All Mail | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| All Mail | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
Scenario: Export only Foo with time limit
When user "user" exports to EML files with rules
| source | target | from | to |
| Foo | | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
Then progress result is "OK"
And transfer exported 2 messages
And transfer imported 2 messages
And transfer failed for 0 messages
And transfer exported messages
| folder | from | to | subject | time |
| Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |

View File

@ -0,0 +1,43 @@
Feature: Export to MBOX files
Background:
Given there is connected user "user"
And there is "user" with mailbox "Folders/Foo"
And there are messages in mailbox "INBOX" for "user"
| from | to | subject | time |
| bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
And there are messages in mailbox "Folders/Foo" for "user"
| from | to | subject | time |
| foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
Scenario: Export all
When user "user" exports to MBOX files
Then progress result is "OK"
# Every message is also in All Mail.
And transfer exported 8 messages
And transfer imported 8 messages
And transfer failed for 0 messages
And transfer exported messages
| folder | from | to | subject | time |
| Inbox | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
| Foo | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
| All Mail | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
| All Mail | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| All Mail | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| All Mail | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
Scenario: Export only Foo with time limit
When user "user" exports to MBOX files with rules
| source | target | from | to |
| Foo | | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
Then progress result is "OK"
And transfer exported 2 messages
And transfer imported 2 messages
And transfer failed for 0 messages
And transfer exported messages
| folder | from | to | subject | time |
| Foo | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Foo | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |

View File

@ -0,0 +1,60 @@
Feature: Import from EML files
Background:
Given there is connected user "user"
And there is "user" with mailbox "Folders/Foo"
And there is "user" with mailbox "Folders/Bar"
And there are EML files
| file | from | to | subject | time |
| Foo/one.eml | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| Foo/two.eml | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Sub/Foo/three.eml | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
And there is EML file "Inbox/hello.eml"
"""
Subject: hello
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
hello
"""
Scenario: Import all
When user "user" imports local files
Then progress result is "OK"
And transfer exported 4 messages
And transfer imported 4 messages
And transfer failed for 0 messages
And API mailbox "INBOX" for "user" has messages
| from | to | subject |
| bridgetest@pm.test | test@protonmail.com | hello |
And API mailbox "Folders/Foo" for "user" has messages
| from | to | subject |
| foo@example.com | bridgetest@protonmail.com | one |
| bar@example.com | bridgetest@protonmail.com | two |
| bar@example.com | bridgetest@protonmail.com | three |
Scenario: Import only Foo to Bar with time limit
When user "user" imports local files with rules
| source | target | from | to |
| Foo | Bar | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
Then progress result is "OK"
And transfer exported 2 messages
And transfer imported 2 messages
And transfer failed for 0 messages
And API mailbox "Folders/Bar" for "user" has messages
| from | to | subject |
| bar@example.com | bridgetest@protonmail.com | two |
| bar@example.com | bridgetest@protonmail.com | three |
Scenario: Import broken EML message
Given there is EML file "Broken/broken.eml"
"""
Content-type: image/png
"""
When user "user" imports local files with rules
| source | target |
| Broken | Foo |
Then progress result is "OK"
And transfer exported 1 messages
And transfer imported 0 messages
And transfer failed for 1 messages

View File

@ -0,0 +1,49 @@
Feature: Import-Export app
Background:
Given there is connected user "user"
And there is "user" with mailbox "Folders/Foo"
And there is "user" with mailbox "Folders/Bar"
Scenario: EML -> PM -> EML
Given there are EML files
| file | from | to | subject | time |
| Inbox/hello.eml | bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
| Foo/one.eml | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| Foo/two.eml | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Sub/Foo/three.eml | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
When user "user" imports local files
Then progress result is "OK"
And transfer failed for 0 messages
And transfer imported 4 messages
When user "user" exports to EML files
Then progress result is "OK"
And transfer failed for 0 messages
# Every message is also in All Mail.
And transfer imported 8 messages
And exported messages match the original ones
Scenario: MBOX -> PM -> MBOX
Given there is MBOX file "Inbox.mbox" with messages
| from | to | subject | time |
| bridgetest@pm.test | test@protonmail.com | hello | 2020-01-01T12:00:00 |
And there is MBOX file "Foo.mbox" with messages
| from | to | subject | time |
| foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
When user "user" imports local files
Then progress result is "OK"
And transfer failed for 0 messages
And transfer imported 4 messages
When user "user" exports to MBOX files
Then progress result is "OK"
And transfer failed for 0 messages
# Every message is also in All Mail.
And transfer imported 8 messages
And exported messages match the original ones

View File

@ -0,0 +1,79 @@
Feature: Import from IMAP server
Background:
Given there is connected user "user"
And there is "user" with mailbox "Folders/Foo"
And there is "user" with mailbox "Folders/Bar"
And there are IMAP mailboxes
| name |
| Inbox |
| Foo |
| Broken |
And there are IMAP messages
| mailbox | seqnum | uid | from | to | subject | time |
| Foo | 1 | 12 | foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| Foo | 2 | 14 | bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
| Foo | 3 | 15 | bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
And there is IMAP message in mailbox "Inbox" with seq 1, uid 42, time "2020-01-01T12:34:56" and subject "hello"
"""
Subject: hello
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
hello
"""
Scenario: Import all
When user "user" imports remote messages
Then progress result is "OK"
And transfer exported 4 messages
And transfer imported 4 messages
And transfer failed for 0 messages
And API mailbox "INBOX" for "user" has messages
| from | to | subject |
| bridgetest@pm.test | test@protonmail.com | hello |
And API mailbox "Folders/Foo" for "user" has messages
| from | to | subject |
| foo@example.com | bridgetest@protonmail.com | one |
| bar@example.com | bridgetest@protonmail.com | two |
| bar@example.com | bridgetest@protonmail.com | three |
Scenario: Import only Foo to Bar with time limit
When user "user" imports remote messages with rules
| source | target | from | to |
| Foo | Bar | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
Then progress result is "OK"
And transfer exported 2 messages
And transfer imported 2 messages
And transfer failed for 0 messages
And API mailbox "Folders/Bar" for "user" has messages
| from | to | subject |
| bar@example.com | bridgetest@protonmail.com | two |
| bar@example.com | bridgetest@protonmail.com | three |
# Note we need to have message which we can parse and use in go-imap
# but which has problem on our side. Used example with missing boundary
# is real example which we want to solve one day. Probabl this test
# can be removed once we import any time of message or switch is to
# something we will never allow.
Scenario: Import broken message
Given there is IMAP message in mailbox "Broken" with seq 1, uid 42, time "2020-01-01T12:34:56" and subject "broken"
"""
Subject: missing boundary end
Content-Type: multipart/related; boundary=boundary
--boundary
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=utf-8
body
"""
When user "user" imports remote messages with rules
| source | target |
| Broken | Foo |
Then progress result is "OK"
And transfer exported 1 messages
And transfer imported 0 messages
And transfer failed for 1 messages

View File

@ -0,0 +1,64 @@
Feature: Import from MBOX files
Background:
Given there is connected user "user"
And there is "user" with mailbox "Folders/Foo"
And there is "user" with mailbox "Folders/Bar"
And there is MBOX file "Foo.mbox" with messages
| from | to | subject | time |
| foo@example.com | bridgetest@protonmail.com | one | 2020-01-01T12:00:00 |
| bar@example.com | bridgetest@protonmail.com | two | 2020-01-01T13:00:00 |
And there is MBOX file "Sub/Foo.mbox" with messages
| from | to | subject | time |
| bar@example.com | bridgetest@protonmail.com | three | 2020-01-01T12:30:00 |
And there is MBOX file "Inbox.mbox"
"""
From bridgetest@pm.test Thu Feb 20 20:20:20 2020
Subject: hello
From: Bridge Test <bridgetest@pm.test>
To: Internal Bridge <test@protonmail.com>
hello
"""
Scenario: Import all
When user "user" imports local files
Then progress result is "OK"
And transfer exported 4 messages
And transfer imported 4 messages
And transfer failed for 0 messages
And API mailbox "INBOX" for "user" has messages
| from | to | subject |
| bridgetest@pm.test | test@protonmail.com | hello |
And API mailbox "Folders/Foo" for "user" has messages
| from | to | subject |
| foo@example.com | bridgetest@protonmail.com | one |
| bar@example.com | bridgetest@protonmail.com | two |
| bar@example.com | bridgetest@protonmail.com | three |
Scenario: Import only Foo to Bar with time limit
When user "user" imports local files with rules
| source | target | from | to |
| Foo | Bar | 2020-01-01T12:10:00 | 2020-01-01T13:00:00 |
Then progress result is "OK"
And transfer exported 2 messages
And transfer imported 2 messages
And transfer failed for 0 messages
And API mailbox "Folders/Bar" for "user" has messages
| from | to | subject |
| bar@example.com | bridgetest@protonmail.com | two |
| bar@example.com | bridgetest@protonmail.com | three |
Scenario: Import broken message
Given there is MBOX file "Broken.mbox"
"""
From bridgetest@pm.test Thu Feb 20 20:20:20 2020
Content-type: image/png
"""
When user "user" imports local files with rules
| source | target |
| Broken | Foo |
Then progress result is "OK"
And transfer exported 1 messages
And transfer imported 0 messages
And transfer failed for 1 messages

View File

@ -0,0 +1,20 @@
Feature: Delete user
Scenario: Deleting connected user
Given there is connected user "user"
When user deletes "user"
Then last response is "OK"
Scenario: Deleting connected user with cache
Given there is connected user "user"
When user deletes "user" with cache
Then last response is "OK"
Scenario: Deleting disconnected user
Given there is disconnected user "user"
When user deletes "user"
Then last response is "OK"
Scenario: Deleting disconnected user with cache
Given there is disconnected user "user"
When user deletes "user" with cache
Then last response is "OK"

View File

@ -0,0 +1,56 @@
Feature: Login for the first time
Scenario: Normal login
Given there is user "user"
When "user" logs in
Then last response is "OK"
And "user" is connected
Scenario: Login with bad username
When "user" logs in with bad password
Then last response is "failed to login: Incorrect login credentials. Please try again"
Scenario: Login with bad password
Given there is user "user"
When "user" logs in with bad password
Then last response is "failed to login: Incorrect login credentials. Please try again"
Scenario: Login without internet connection
Given there is no internet connection
When "user" logs in
Then last response is "failed to login: cannot reach the server"
@ignore-live
Scenario: Login user with 2FA
Given there is user "user2fa"
When "user2fa" logs in
Then last response is "OK"
And "user2fa" is connected
Scenario: Login user with capital letters in address
Given there is user "userAddressWithCapitalLetter"
When "userAddressWithCapitalLetter" logs in
Then last response is "OK"
And "userAddressWithCapitalLetter" is connected
Scenario: Login user with more addresses
Given there is user "userMoreAddresses"
When "userMoreAddresses" logs in
Then last response is "OK"
And "userMoreAddresses" is connected
@ignore-live
Scenario: Login user with disabled primary address
Given there is user "userDisabledPrimaryAddress"
When "userDisabledPrimaryAddress" logs in
Then last response is "OK"
And "userDisabledPrimaryAddress" is connected
Scenario: Login two users
Given there is user "user"
And there is user "userMoreAddresses"
When "user" logs in
Then last response is "OK"
And "user" is connected
When "userMoreAddresses" logs in
Then last response is "OK"
And "userMoreAddresses" is connected

View File

@ -0,0 +1,12 @@
Feature: Re-login
Scenario: Re-login with connected user
Given there is connected user "user"
When "user" logs in
Then last response is "failed to finish login: user is already connected"
And "user" is connected
Scenario: Re-login with disconnected user
Given there is disconnected user "user"
When "user" logs in
Then last response is "OK"
And "user" is connected

View File

@ -135,3 +135,31 @@ func (ctl *Controller) GetMessageID(username, messageIndex string) string {
}
return ctl.messageIDsByUsername[username][idx-1]
}
func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message, error) {
client, ok := ctl.pmapiByUsername[username]
if !ok {
return nil, fmt.Errorf("user %s does not exist", username)
}
page := 0
messages := []*pmapi.Message{}
for {
// ListMessages returns empty result, not error, asking for page out of range.
pageMessages, _, err := client.ListMessages(&pmapi.MessagesFilter{
Page: page,
PageSize: 150,
LabelID: labelID,
})
if err != nil {
return nil, errors.Wrap(err, "failed to list messages")
}
messages = append(messages, pageMessages...)
if len(pageMessages) < 150 {
break
}
}
return messages, nil
}

View File

@ -18,13 +18,13 @@
package mocks
import (
"bufio"
"fmt"
"io"
"regexp"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
a "github.com/stretchr/testify/assert"
)
@ -37,7 +37,7 @@ type IMAPResponse struct {
done bool
}
func (ir *IMAPResponse) sendCommand(reqTag string, reqIndex int, command string, debug *debug, conn io.Writer, response *bufio.Reader) {
func (ir *IMAPResponse) sendCommand(reqTag string, reqIndex int, command string, debug *debug, conn io.Writer, response imap.StringReader) {
defer func() { ir.done = true }()
tstart := time.Now()

227
test/mocks/imap_server.go Normal file
View File

@ -0,0 +1,227 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package mocks
import (
"fmt"
"net"
"strings"
"time"
"github.com/emersion/go-imap"
imapbackend "github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type IMAPServer struct {
Username string
Password string
Host string
Port string
mailboxes []string
messages map[string][]*imap.Message // Key is mailbox.
server *imapserver.Server
}
func NewIMAPServer(username, password, host, port string) *IMAPServer {
return &IMAPServer{
Username: username,
Password: password,
Host: host,
Port: port,
mailboxes: []string{},
messages: map[string][]*imap.Message{},
}
}
func (s *IMAPServer) AddMailbox(mailboxName string) {
s.mailboxes = append(s.mailboxes, mailboxName)
s.messages[strings.ToLower(mailboxName)] = []*imap.Message{}
}
func (s *IMAPServer) AddMessage(mailboxName string, message *imap.Message) {
mailboxName = strings.ToLower(mailboxName)
s.messages[mailboxName] = append(s.messages[mailboxName], message)
}
func (s *IMAPServer) Start() {
server := imapserver.New(&IMAPBackend{server: s})
server.Addr = net.JoinHostPort(s.Host, s.Port)
server.AllowInsecureAuth = true
server.ErrorLog = logrus.WithField("pkg", "imap-server")
server.Debug = logrus.WithField("pkg", "imap-server").WriterLevel(logrus.DebugLevel)
server.AutoLogout = 30 * time.Minute
s.server = server
go func() {
err := server.ListenAndServe()
logrus.WithError(err).Warn("IMAP server stopped")
}()
time.Sleep(100 * time.Millisecond)
}
func (s *IMAPServer) Stop() {
_ = s.server.Close()
}
type IMAPBackend struct {
server *IMAPServer
}
func (b *IMAPBackend) Login(connInfo *imap.ConnInfo, username, password string) (imapbackend.User, error) {
if username != b.server.Username || password != b.server.Password {
return nil, errors.New("invalid credentials")
}
return &IMAPUser{
server: b.server,
username: username,
}, nil
}
type IMAPUser struct {
server *IMAPServer
username string
}
func (u *IMAPUser) Username() string {
return u.username
}
func (u *IMAPUser) ListMailboxes(subscribed bool) ([]imapbackend.Mailbox, error) {
mailboxes := []imapbackend.Mailbox{}
for _, mailboxName := range u.server.mailboxes {
mailboxes = append(mailboxes, &IMAPMailbox{
server: u.server,
name: mailboxName,
})
}
return mailboxes, nil
}
func (u *IMAPUser) GetMailbox(name string) (imapbackend.Mailbox, error) {
name = strings.ToLower(name)
_, ok := u.server.messages[name]
if !ok {
return nil, fmt.Errorf("mailbox %s not found", name)
}
return &IMAPMailbox{
server: u.server,
name: name,
}, nil
}
func (u *IMAPUser) CreateMailbox(name string) error {
return errors.New("not supported: create mailbox")
}
func (u *IMAPUser) DeleteMailbox(name string) error {
return errors.New("not supported: delete mailbox")
}
func (u *IMAPUser) RenameMailbox(existingName, newName string) error {
return errors.New("not supported: rename mailbox")
}
func (u *IMAPUser) Logout() error {
return nil
}
type IMAPMailbox struct {
server *IMAPServer
name string
attributes []string
}
func (m *IMAPMailbox) Name() string {
return m.name
}
func (m *IMAPMailbox) Info() (*imap.MailboxInfo, error) {
return &imap.MailboxInfo{
Name: m.name,
Attributes: m.attributes,
}, nil
}
func (m *IMAPMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, error) {
status := imap.NewMailboxStatus(m.name, items)
status.UidValidity = 1
status.Messages = uint32(len(m.server.messages[m.name]))
return status, nil
}
func (m *IMAPMailbox) SetSubscribed(subscribed bool) error {
return errors.New("not supported: set subscribed")
}
func (m *IMAPMailbox) Check() error {
return errors.New("not supported: check")
}
func (m *IMAPMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan<- *imap.Message) error {
defer func() {
close(ch)
}()
for index, message := range m.server.messages[m.name] {
seqNum := uint32(index + 1)
var id uint32
if uid {
id = message.Uid
} else {
id = seqNum
}
if seqset.Contains(id) {
msg := imap.NewMessage(seqNum, items)
msg.Envelope = message.Envelope
msg.BodyStructure = message.BodyStructure
msg.Body = message.Body
msg.Size = message.Size
msg.Uid = message.Uid
ch <- msg
}
}
return nil
}
func (m *IMAPMailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]uint32, error) {
return nil, errors.New("not supported: search")
}
func (m *IMAPMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
return errors.New("not supported: create")
}
func (m *IMAPMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
return errors.New("not supported: update flags")
}
func (m *IMAPMailbox) CopyMessages(uid bool, seqset *imap.SeqSet, dest string) error {
return errors.New("not supported: copy")
}
func (m *IMAPMailbox) Expunge() error {
return errors.New("not supported: expunge")
}

View File

@ -172,12 +172,12 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []*pm
matches := true
for n, cell := range row.Cells {
switch head[n].Value {
case "from":
case "from": //nolint[goconst]
address := ctx.EnsureAddress(account.Username(), cell.Value)
if !areAddressesSame(message.Sender.Address, address) {
matches = false
}
case "to":
case "to": //nolint[goconst]
for _, address := range strings.Split(cell.Value, ",") {
address = ctx.EnsureAddress(account.Username(), address)
for _, to := range message.ToList {
@ -197,7 +197,7 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []*pm
}
}
}
case "subject":
case "subject": //nolint[goconst]
expectedSubject := cell.Value
if expectedSubject == "" {
expectedSubject = "(No Subject)"
@ -205,7 +205,7 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []*pm
if message.Subject != expectedSubject {
matches = false
}
case "body":
case "body": //nolint[goconst]
if message.Body != cell.Value {
matches = false
}
@ -238,7 +238,7 @@ func areAddressesSame(first, second string) bool {
if err != nil {
return false
}
return firstAddress.String() == secondAddress.String()
return firstAddress.Address == secondAddress.Address
}
func messagesInMailboxForUserIsMarkedAsRead(messageIDs, mailboxName, bddUserID string) error {

View File

@ -22,6 +22,7 @@ import (
"net/mail"
"strconv"
"strings"
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/cucumber/godog"
@ -63,6 +64,9 @@ func thereIsUserWithMailbox(bddUserID, mailboxName string) error {
if err != nil {
return internalError(err, "getting store of %s", account.Username())
}
if store == nil {
return nil
}
return internalError(store.RebuildMailboxes(), "rebuilding mailboxes")
}
@ -122,6 +126,12 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd
if cell.Value == "true" {
message.LabelIDs = append(message.LabelIDs, "10")
}
case "time": //nolint[goconst]
date, err := time.Parse(timeFormat, cell.Value)
if err != nil {
return internalError(err, "parsing time")
}
message.Time = date.Unix()
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}

View File

@ -0,0 +1,227 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"fmt"
"time"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/cucumber/godog"
"github.com/cucumber/godog/gherkin"
)
func TransferActionsFeatureContext(s *godog.Suite) {
s.Step(`^user "([^"]*)" imports local files$`, userImportsLocalFiles)
s.Step(`^user "([^"]*)" imports local files with rules$`, userImportsLocalFilesWithRules)
s.Step(`^user "([^"]*)" imports local files to address "([^"]*)"$`, userImportsLocalFilesToAddress)
s.Step(`^user "([^"]*)" imports local files to address "([^"]*)" with rules$`, userImportsLocalFilesToAddressWithRules)
s.Step(`^user "([^"]*)" imports remote messages$`, userImportsRemoteMessages)
s.Step(`^user "([^"]*)" imports remote messages with rules$`, userImportsRemoteMessagesWithRules)
s.Step(`^user "([^"]*)" imports remote messages to address "([^"]*)"$`, userImportsRemoteMessagesToAddress)
s.Step(`^user "([^"]*)" imports remote messages to address "([^"]*)" with rules$`, userImportsRemoteMessagesToAddressWithRules)
s.Step(`^user "([^"]*)" exports to EML files$`, userExportsToEMLFiles)
s.Step(`^user "([^"]*)" exports to EML files with rules$`, userExportsToEMLFilesWithRules)
s.Step(`^user "([^"]*)" exports address "([^"]*)" to EML files$`, userExportsAddressToEMLFiles)
s.Step(`^user "([^"]*)" exports address "([^"]*)" to EML files with rules$`, userExportsAddressToEMLFilesWithRules)
s.Step(`^user "([^"]*)" exports to MBOX files$`, userExportsToMBOXFiles)
s.Step(`^user "([^"]*)" exports to MBOX files with rules$`, userExportsToMBOXFilesWithRules)
s.Step(`^user "([^"]*)" exports address "([^"]*)" to MBOX files$`, userExportsAddressToMBOXFiles)
s.Step(`^user "([^"]*)" exports address "([^"]*)" to MBOX files with rules$`, userExportsAddressToMBOXFilesWithRules)
}
// Local import.
func userImportsLocalFiles(bddUserID string) error {
return userImportsLocalFilesToAddressWithRules(bddUserID, "", nil)
}
func userImportsLocalFilesWithRules(bddUserID string, rules *gherkin.DataTable) error {
return userImportsLocalFilesToAddressWithRules(bddUserID, "", rules)
}
func userImportsLocalFilesToAddress(bddUserID, bddAddressID string) error {
return userImportsLocalFilesToAddressWithRules(bddUserID, bddAddressID, nil)
}
func userImportsLocalFilesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
path := ctx.GetTransferLocalRootForImport()
return ctx.GetImportExport().GetLocalImporter(address, path)
})
}
// Remote import.
func userImportsRemoteMessages(bddUserID string) error {
return userImportsRemoteMessagesToAddressWithRules(bddUserID, "", nil)
}
func userImportsRemoteMessagesWithRules(bddUserID string, rules *gherkin.DataTable) error {
return userImportsRemoteMessagesToAddressWithRules(bddUserID, "", rules)
}
func userImportsRemoteMessagesToAddress(bddUserID, bddAddressID string) error {
return userImportsRemoteMessagesToAddressWithRules(bddUserID, bddAddressID, nil)
}
func userImportsRemoteMessagesToAddressWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
imapServer := ctx.GetTransferRemoteIMAPServer()
return ctx.GetImportExport().GetRemoteImporter(address, imapServer.Username, imapServer.Password, imapServer.Host, imapServer.Port)
})
}
// EML export.
func userExportsToEMLFiles(bddUserID string) error {
return userExportsAddressToEMLFilesWithRules(bddUserID, "", nil)
}
func userExportsToEMLFilesWithRules(bddUserID string, rules *gherkin.DataTable) error {
return userExportsAddressToEMLFilesWithRules(bddUserID, "", rules)
}
func userExportsAddressToEMLFiles(bddUserID, bddAddressID string) error {
return userExportsAddressToEMLFilesWithRules(bddUserID, bddAddressID, nil)
}
func userExportsAddressToEMLFilesWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
path := ctx.GetTransferLocalRootForExport()
return ctx.GetImportExport().GetEMLExporter(address, path)
})
}
// MBOX export.
func userExportsToMBOXFiles(bddUserID string) error {
return userExportsAddressToMBOXFilesWithRules(bddUserID, "", nil)
}
func userExportsToMBOXFilesWithRules(bddUserID string, rules *gherkin.DataTable) error {
return userExportsAddressToMBOXFilesWithRules(bddUserID, "", rules)
}
func userExportsAddressToMBOXFiles(bddUserID, bddAddressID string) error {
return userExportsAddressToMBOXFilesWithRules(bddUserID, bddAddressID, nil)
}
func userExportsAddressToMBOXFilesWithRules(bddUserID, bddAddressID string, rules *gherkin.DataTable) error {
return doTransfer(bddUserID, bddAddressID, rules, func(address string) (*transfer.Transfer, error) {
path := ctx.GetTransferLocalRootForExport()
return ctx.GetImportExport().GetMBOXExporter(address, path)
})
}
// Helpers.
func doTransfer(bddUserID, bddAddressID string, rules *gherkin.DataTable, getTransferrer func(string) (*transfer.Transfer, error)) error {
account := ctx.GetTestAccountWithAddress(bddUserID, bddAddressID)
if account == nil {
return godog.ErrPending
}
transferrer, err := getTransferrer(account.Address())
if err != nil {
return internalError(err, "failed to init transfer")
}
if err := setRules(transferrer, rules); err != nil {
return internalError(err, "failed to set rules")
}
progress := transferrer.Start()
ctx.SetTransferProgress(progress)
return nil
}
func setRules(transferrer *transfer.Transfer, rules *gherkin.DataTable) error {
if rules == nil {
return nil
}
transferrer.ResetRules()
allSourceMailboxes, err := transferrer.SourceMailboxes()
if err != nil {
return internalError(err, "failed to get source mailboxes")
}
allTargetMailboxes, err := transferrer.TargetMailboxes()
if err != nil {
return internalError(err, "failed to get target mailboxes")
}
head := rules.Rows[0].Cells
for _, row := range rules.Rows[1:] {
source := ""
target := ""
fromTime := int64(0)
toTime := int64(0)
for n, cell := range row.Cells {
switch head[n].Value {
case "source":
source = cell.Value
case "target":
target = cell.Value
case "from":
date, err := time.Parse(timeFormat, cell.Value)
if err != nil {
return internalError(err, "failed to parse from time")
}
fromTime = date.Unix()
case "to":
date, err := time.Parse(timeFormat, cell.Value)
if err != nil {
return internalError(err, "failed to parse to time")
}
toTime = date.Unix()
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
sourceMailbox, err := getMailboxByName(allSourceMailboxes, source)
if err != nil {
return internalError(err, "failed to match source mailboxes")
}
// Empty target means the same as source. Useful for exports.
targetMailboxes := []transfer.Mailbox{}
if target == "" {
targetMailboxes = append(targetMailboxes, sourceMailbox)
} else {
targetMailbox, err := getMailboxByName(allTargetMailboxes, target)
if err != nil {
return internalError(err, "failed to match target mailboxes")
}
targetMailboxes = append(targetMailboxes, targetMailbox)
}
if err := transferrer.SetRule(sourceMailbox, targetMailboxes, fromTime, toTime); err != nil {
return internalError(err, "failed to set rule")
}
}
return nil
}
func getMailboxByName(mailboxes []transfer.Mailbox, name string) (transfer.Mailbox, error) {
for _, mailbox := range mailboxes {
if mailbox.Name == name {
return mailbox, nil
}
}
return transfer.Mailbox{}, fmt.Errorf("mailbox %s not found", name)
}

View File

@ -0,0 +1,267 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"fmt"
"io"
"io/ioutil"
"net/mail"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/cucumber/godog"
"github.com/cucumber/godog/gherkin"
"github.com/emersion/go-mbox"
"github.com/emersion/go-message"
"github.com/pkg/errors"
a "github.com/stretchr/testify/assert"
)
func TransferChecksFeatureContext(s *godog.Suite) {
s.Step(`^progress result is "([^"]*)"$`, progressFinishedWith)
s.Step(`^transfer exported (\d+) messages$`, transferExportedNumberOfMessages)
s.Step(`^transfer imported (\d+) messages$`, transferImportedNumberOfMessages)
s.Step(`^transfer failed for (\d+) messages$`, transferFailedForNumberOfMessages)
s.Step(`^transfer exported messages$`, transferExportedMessages)
s.Step(`^exported messages match the original ones$`, exportedMessagesMatchTheOriginalOnes)
}
func progressFinishedWith(wantResponse string) error {
progress := ctx.GetTransferProgress()
// Wait till transport is finished.
for range progress.GetUpdateChannel() {
}
err := progress.GetFatalError()
if wantResponse == "OK" {
a.NoError(ctx.GetTestingT(), err)
} else {
a.EqualError(ctx.GetTestingT(), err, wantResponse)
}
return ctx.GetTestingError()
}
func transferExportedNumberOfMessages(wantCount int) error {
progress := ctx.GetTransferProgress()
_, _, exported, _, _ := progress.GetCounts() //nolint[dogsled]
a.Equal(ctx.GetTestingT(), uint(wantCount), exported)
return ctx.GetTestingError()
}
func transferImportedNumberOfMessages(wantCount int) error {
progress := ctx.GetTransferProgress()
_, imported, _, _, _ := progress.GetCounts() //nolint[dogsled]
a.Equal(ctx.GetTestingT(), uint(wantCount), imported)
return ctx.GetTestingError()
}
func transferFailedForNumberOfMessages(wantCount int) error {
progress := ctx.GetTransferProgress()
failedMessages := progress.GetFailedMessages()
a.Equal(ctx.GetTestingT(), wantCount, len(failedMessages), "failed messages: %v", failedMessages)
return ctx.GetTestingError()
}
func transferExportedMessages(messages *gherkin.DataTable) error {
expectedMessages := map[string][]MessageAttributes{}
head := messages.Rows[0].Cells
for _, row := range messages.Rows[1:] {
folder := ""
msg := MessageAttributes{}
for n, cell := range row.Cells {
switch head[n].Value {
case "folder":
folder = cell.Value
case "subject":
msg.subject = cell.Value
case "from":
msg.from = cell.Value
case "to":
msg.to = []string{cell.Value}
case "time":
date, err := time.Parse(timeFormat, cell.Value)
if err != nil {
return internalError(err, "failed to parse time")
}
msg.date = date.Unix()
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
expectedMessages[folder] = append(expectedMessages[folder], msg)
sort.Sort(BySubject(expectedMessages[folder]))
}
exportRoot := ctx.GetTransferLocalRootForExport()
exportedMessages, err := readMessages(exportRoot)
if err != nil {
return errors.Wrap(err, "scanning exported messages")
}
a.Equal(ctx.GetTestingT(), expectedMessages, exportedMessages)
return ctx.GetTestingError()
}
func exportedMessagesMatchTheOriginalOnes() error {
importRoot := ctx.GetTransferLocalRootForImport()
exportRoot := ctx.GetTransferLocalRootForExport()
importMessages, err := readMessages(importRoot)
if err != nil {
return errors.Wrap(err, "scanning messages for import")
}
exportMessages, err := readMessages(exportRoot)
if err != nil {
return errors.Wrap(err, "scanning exported messages")
}
delete(exportMessages, "All Mail") // Ignore All Mail.
a.Equal(ctx.GetTestingT(), importMessages, exportMessages)
return ctx.GetTestingError()
}
func readMessages(root string) (map[string][]MessageAttributes, error) {
files, err := ioutil.ReadDir(root)
if err != nil {
return nil, err
}
messagesPerLabel := map[string][]MessageAttributes{}
for _, file := range files {
if !file.IsDir() {
fileReader, err := os.Open(filepath.Join(root, file.Name()))
if err != nil {
return nil, errors.Wrap(err, "opening file")
}
if filepath.Ext(file.Name()) == ".eml" {
label := filepath.Base(root)
msg, err := readMessageAttributes(fileReader)
if err != nil {
return nil, err
}
messagesPerLabel[label] = append(messagesPerLabel[label], msg)
sort.Sort(BySubject(messagesPerLabel[label]))
} else if filepath.Ext(file.Name()) == ".mbox" {
label := strings.TrimSuffix(file.Name(), ".mbox")
mboxReader := mbox.NewReader(fileReader)
for {
msgReader, err := mboxReader.NextMessage()
if err == io.EOF {
break
} else if err != nil {
return nil, errors.Wrap(err, "reading next message")
}
msg, err := readMessageAttributes(msgReader)
if err != nil {
return nil, err
}
messagesPerLabel[label] = append(messagesPerLabel[label], msg)
}
sort.Sort(BySubject(messagesPerLabel[label]))
}
} else {
subfolderRoot := filepath.Join(root, file.Name())
subfolderMessagesPerLabel, err := readMessages(subfolderRoot)
if err != nil {
return nil, err
}
for key, value := range subfolderMessagesPerLabel {
messagesPerLabel[key] = append(messagesPerLabel[key], value...)
sort.Sort(BySubject(messagesPerLabel[key]))
}
}
}
return messagesPerLabel, nil
}
type MessageAttributes struct {
subject string
from string
to []string
date int64
}
func readMessageAttributes(fileReader io.Reader) (MessageAttributes, error) {
entity, err := message.Read(fileReader)
if err != nil {
return MessageAttributes{}, errors.Wrap(err, "reading file")
}
date, err := parseTime(entity.Header.Get("date"))
if err != nil {
return MessageAttributes{}, errors.Wrap(err, "parsing date")
}
from, err := parseAddress(entity.Header.Get("from"))
if err != nil {
return MessageAttributes{}, errors.Wrap(err, "parsing from")
}
to, err := parseAddresses(entity.Header.Get("to"))
if err != nil {
return MessageAttributes{}, errors.Wrap(err, "parsing to")
}
return MessageAttributes{
subject: entity.Header.Get("subject"),
from: from,
to: to,
date: date.Unix(),
}, nil
}
func parseTime(input string) (time.Time, error) {
for _, format := range []string{time.RFC1123, time.RFC1123Z} {
t, err := time.Parse(format, input)
if err == nil {
return t, nil
}
}
return time.Time{}, errors.New("Unrecognized time format")
}
func parseAddresses(input string) ([]string, error) {
addresses, err := mail.ParseAddressList(input)
if err != nil {
return nil, err
}
result := []string{}
for _, address := range addresses {
result = append(result, address.Address)
}
return result, nil
}
func parseAddress(input string) (string, error) {
address, err := mail.ParseAddress(input)
if err != nil {
return "", err
}
return address.Address, nil
}
// BySubject implements sort.Interface based on the subject field.
type BySubject []MessageAttributes
func (a BySubject) Len() int { return len(a) }
func (a BySubject) Less(i, j int) bool { return a[i].subject < a[j].subject }
func (a BySubject) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

261
test/transfer_setup_test.go Normal file
View File

@ -0,0 +1,261 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"bytes"
"fmt"
"net/textproto"
"os"
"path/filepath"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/pkg/message"
"github.com/cucumber/godog"
"github.com/cucumber/godog/gherkin"
"github.com/emersion/go-imap"
"github.com/emersion/go-mbox"
)
func TransferSetupFeatureContext(s *godog.Suite) {
s.Step(`^there are EML files$`, thereAreEMLFiles)
s.Step(`^there is EML file "([^"]*)"$`, thereIsEMLFile)
s.Step(`^there is MBOX file "([^"]*)" with messages$`, thereIsMBOXFileWithMessages)
s.Step(`^there is MBOX file "([^"]*)"$`, thereIsMBOXFile)
s.Step(`^there are IMAP mailboxes$`, thereAreIMAPMailboxes)
s.Step(`^there are IMAP messages$`, thereAreIMAPMessages)
s.Step(`^there is IMAP message in mailbox "([^"]*)" with seq (\d+), uid (\d+), time "([^"]*)" and subject "([^"]*)"$`, thereIsIMAPMessage)
}
func thereAreEMLFiles(messages *gherkin.DataTable) error {
head := messages.Rows[0].Cells
for _, row := range messages.Rows[1:] {
fileName := ""
for n, cell := range row.Cells {
switch head[n].Value {
case "file":
fileName = cell.Value
case "from", "to", "subject", "time", "body":
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
body := getBodyFromDataRow(head, row)
if err := createFile(fileName, body); err != nil {
return err
}
}
return nil
}
func thereIsEMLFile(fileName string, message *gherkin.DocString) error {
return createFile(fileName, message.Content)
}
func thereIsMBOXFileWithMessages(fileName string, messages *gherkin.DataTable) error {
mboxBuffer := &bytes.Buffer{}
mboxWriter := mbox.NewWriter(mboxBuffer)
head := messages.Rows[0].Cells
for _, row := range messages.Rows[1:] {
from := ""
for n, cell := range row.Cells {
switch head[n].Value {
case "from":
from = cell.Value
case "to", "subject", "time", "body":
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
body := getBodyFromDataRow(head, row)
messageWriter, err := mboxWriter.CreateMessage(from, time.Now())
if err != nil {
return err
}
_, err = messageWriter.Write([]byte(body))
if err != nil {
return err
}
}
return createFile(fileName, mboxBuffer.String())
}
func thereIsMBOXFile(fileName string, messages *gherkin.DocString) error {
return createFile(fileName, messages.Content)
}
func thereAreIMAPMailboxes(mailboxes *gherkin.DataTable) error {
imapServer := ctx.GetTransferRemoteIMAPServer()
head := mailboxes.Rows[0].Cells
for _, row := range mailboxes.Rows[1:] {
mailboxName := ""
for n, cell := range row.Cells {
switch head[n].Value {
case "name":
mailboxName = cell.Value
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
imapServer.AddMailbox(mailboxName)
}
return nil
}
func thereAreIMAPMessages(messages *gherkin.DataTable) (err error) {
imapServer := ctx.GetTransferRemoteIMAPServer()
head := messages.Rows[0].Cells
for _, row := range messages.Rows[1:] {
mailboxName := ""
date := time.Now()
subject := ""
seqNum := 0
uid := 0
for n, cell := range row.Cells {
switch head[n].Value {
case "mailbox":
mailboxName = cell.Value
case "uid":
uid, err = strconv.Atoi(cell.Value)
if err != nil {
return internalError(err, "failed to parse uid")
}
case "seqnum":
seqNum, err = strconv.Atoi(cell.Value)
if err != nil {
return internalError(err, "failed to parse seqnum")
}
case "time":
date, err = time.Parse(timeFormat, cell.Value)
if err != nil {
return internalError(err, "failed to parse time")
}
case "subject":
subject = cell.Value
case "from", "to", "body":
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
body := getBodyFromDataRow(head, row)
imapMessage, err := getIMAPMessage(seqNum, uid, date, subject, body)
if err != nil {
return err
}
imapServer.AddMessage(mailboxName, imapMessage)
}
return nil
}
func thereIsIMAPMessage(mailboxName string, seqNum, uid int, dateValue, subject string, message *gherkin.DocString) error {
imapServer := ctx.GetTransferRemoteIMAPServer()
date, err := time.Parse(timeFormat, dateValue)
if err != nil {
return internalError(err, "failed to parse time")
}
imapMessage, err := getIMAPMessage(seqNum, uid, date, subject, message.Content)
if err != nil {
return err
}
imapServer.AddMessage(mailboxName, imapMessage)
return nil
}
func getBodyFromDataRow(head []*gherkin.TableCell, row *gherkin.TableRow) string {
body := "hello"
headers := textproto.MIMEHeader{}
for n, cell := range row.Cells {
switch head[n].Value {
case "from":
headers.Set("from", cell.Value)
case "to":
headers.Set("to", cell.Value)
case "subject":
headers.Set("subject", cell.Value)
case "time":
date, err := time.Parse(timeFormat, cell.Value)
if err != nil {
panic(err)
}
headers.Set("date", date.Format(time.RFC1123))
case "body":
body = cell.Value
}
}
buffer := &bytes.Buffer{}
_ = message.WriteHeader(buffer, headers)
return buffer.String() + body + "\n\n"
}
func getIMAPMessage(seqNum, uid int, date time.Time, subject, body string) (*imap.Message, error) {
reader := bytes.NewBufferString(body)
bodyStructure, err := message.NewBodyStructure(reader)
if err != nil {
return nil, internalError(err, "failed to parse body structure")
}
imapBodyStructure, err := bodyStructure.IMAPBodyStructure([]int{})
if err != nil {
return nil, internalError(err, "failed to parse body structure")
}
bodySection, _ := imap.ParseBodySectionName("BODY[]")
return &imap.Message{
SeqNum: uint32(seqNum),
Uid: uint32(uid),
Size: uint32(len(body)),
Envelope: &imap.Envelope{
Date: date,
Subject: subject,
},
BodyStructure: imapBodyStructure,
Body: map[*imap.BodySectionName]imap.Literal{
bodySection: bytes.NewBufferString(body),
},
}, nil
}
func createFile(fileName, body string) error {
root := ctx.GetTransferLocalRootForImport()
filePath := filepath.Join(root, fileName)
dirPath := filepath.Dir(filePath)
err := os.MkdirAll(dirPath, os.ModePerm)
if err != nil {
return internalError(err, "failed to create dir")
}
f, err := os.Create(filePath)
if err != nil {
return internalError(err, "failed to create file")
}
defer f.Close() //nolint
_, err = f.WriteString(body)
return internalError(err, "failed to write to file")
}

125
test/users_actions_test.go Normal file
View File

@ -0,0 +1,125 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.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 <https://www.gnu.org/licenses/>.
package tests
import (
"github.com/cucumber/godog"
)
func UsersActionsFeatureContext(s *godog.Suite) {
s.Step(`^"([^"]*)" logs in$`, userLogsIn)
s.Step(`^"([^"]*)" logs in with bad password$`, userLogsInWithBadPassword)
s.Step(`^"([^"]*)" logs out$`, userLogsOut)
s.Step(`^"([^"]*)" changes the address mode$`, userChangesTheAddressMode)
s.Step(`^user deletes "([^"]*)"$`, userDeletesUser)
s.Step(`^user deletes "([^"]*)" with cache$`, userDeletesUserWithCache)
s.Step(`^"([^"]*)" swaps address "([^"]*)" with address "([^"]*)"$`, swapsAddressWithAddress)
}
func userLogsIn(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
ctx.SetLastError(ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()))
return nil
}
func userLogsInWithBadPassword(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
ctx.SetLastError(ctx.LoginUser(account.Username(), "you shall not pass!", "123"))
return nil
}
func userLogsOut(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
ctx.SetLastError(ctx.LogoutUser(account.Username()))
return nil
}
func userChangesTheAddressMode(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
if err := user.SwitchAddressMode(); err != nil {
return err
}
ctx.EventuallySyncIsFinishedForUsername(account.Username())
return nil
}
func userDeletesUser(bddUserID string) error {
return deleteUser(bddUserID, false)
}
func userDeletesUserWithCache(bddUserID string) error {
return deleteUser(bddUserID, true)
}
func deleteUser(bddUserID string, cache bool) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
ctx.SetLastError(ctx.GetUsers().DeleteUser(user.ID(), cache))
return nil
}
func swapsAddressWithAddress(bddUserID, bddAddressID1, bddAddressID2 string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
address1ID := account.GetAddressID(bddAddressID1)
address2ID := account.GetAddressID(bddAddressID2)
addressIDs := make([]string, len(*account.Addresses()))
var address1Index, address2Index int
for i, v := range *account.Addresses() {
if v.ID == address1ID {
address1Index = i
}
if v.ID == address2ID {
address2Index = i
}
addressIDs[i] = v.ID
}
addressIDs[address1Index], addressIDs[address2Index] = addressIDs[address2Index], addressIDs[address1Index]
ctx.ReorderAddresses(account.Username(), bddAddressID1, bddAddressID2)
return ctx.GetPMAPIController().ReorderAddresses(account.User(), addressIDs)
}

View File

@ -24,8 +24,7 @@ import (
a "github.com/stretchr/testify/assert"
)
func BridgeChecksFeatureContext(s *godog.Suite) {
s.Step(`^bridge response is "([^"]*)"$`, bridgeResponseIs)
func UsersChecksFeatureContext(s *godog.Suite) {
s.Step(`^"([^"]*)" has address mode in "([^"]*)" mode$`, userHasAddressModeInMode)
s.Step(`^"([^"]*)" is disconnected$`, userIsDisconnected)
s.Step(`^"([^"]*)" is connected$`, userIsConnected)
@ -39,27 +38,17 @@ func BridgeChecksFeatureContext(s *godog.Suite) {
s.Step(`^"([^"]*)" has API auth$`, isAuthorized)
}
func bridgeResponseIs(expectedResponse string) error {
err := ctx.GetLastBridgeError()
if expectedResponse == "OK" {
a.NoError(ctx.GetTestingT(), err)
} else {
a.EqualError(ctx.GetTestingT(), err, expectedResponse)
}
return ctx.GetTestingError()
}
func userHasAddressModeInMode(bddUserID, wantAddressMode string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
addressMode := "split"
if bridgeUser.IsCombinedAddressMode() {
if user.IsCombinedAddressMode() {
addressMode = "combined"
}
a.Equal(ctx.GetTestingT(), wantAddressMode, addressMode)
@ -71,12 +60,12 @@ func userIsDisconnected(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.Eventually(ctx.GetTestingT(), func() bool {
return !bridgeUser.IsConnected()
return !user.IsConnected()
}, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}
@ -87,13 +76,13 @@ func userIsConnected(bddUserID string) error {
return godog.ErrPending
}
t := ctx.GetTestingT()
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.Eventually(ctx.GetTestingT(), bridgeUser.IsConnected, 5*time.Second, 10*time.Millisecond)
a.NotEmpty(t, bridgeUser.GetPrimaryAddress())
a.NotEmpty(t, bridgeUser.GetStoreAddresses())
a.Eventually(ctx.GetTestingT(), user.IsConnected, 5*time.Second, 10*time.Millisecond)
a.NotEmpty(t, user.GetPrimaryAddress())
a.NotEmpty(t, user.GetStoreAddresses())
return ctx.GetTestingError()
}
@ -122,11 +111,11 @@ func userHasLoadedStore(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.NotNil(ctx.GetTestingT(), bridgeUser.GetStore())
a.NotNil(ctx.GetTestingT(), user.GetStore())
return ctx.GetTestingError()
}
@ -135,11 +124,11 @@ func userDoesNotHaveLoadedStore(bddUserID string) error {
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.Nil(ctx.GetTestingT(), bridgeUser.GetStore())
a.Nil(ctx.GetTestingT(), user.GetStore())
return ctx.GetTestingError()
}
@ -173,28 +162,28 @@ func userDoesNotHaveRunningEventLoop(bddUserID string) error {
return ctx.GetTestingError()
}
func isAuthorized(accountName string) error {
account := ctx.GetTestAccount(accountName)
func isAuthorized(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.Eventually(ctx.GetTestingT(), bridgeUser.IsAuthorized, 5*time.Second, 10*time.Millisecond)
a.Eventually(ctx.GetTestingT(), user.IsAuthorized, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}
func isNotAuthorized(accountName string) error {
account := ctx.GetTestAccount(accountName)
func isNotAuthorized(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
a.Eventually(ctx.GetTestingT(), func() bool { return !bridgeUser.IsAuthorized() }, 5*time.Second, 10*time.Millisecond)
a.Eventually(ctx.GetTestingT(), func() bool { return !user.IsAuthorized() }, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}

View File

@ -25,8 +25,7 @@ import (
a "github.com/stretchr/testify/assert"
)
func BridgeSetupFeatureContext(s *godog.Suite) {
s.Step(`^there is no internet connection$`, thereIsNoInternetConnection)
func UsersSetupFeatureContext(s *godog.Suite) {
s.Step(`^there is user "([^"]*)"$`, thereIsUser)
s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser)
s.Step(`^there is disconnected user "([^"]*)"$`, thereIsDisconnectedUser)
@ -35,11 +34,6 @@ func BridgeSetupFeatureContext(s *godog.Suite) {
s.Step(`^there is "([^"]*)" in "([^"]*)" address mode$`, thereIsUserWithAddressMode)
}
func thereIsNoInternetConnection() error {
ctx.GetPMAPIController().TurnInternetConnectionOff()
return nil
}
func thereIsUser(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
@ -87,7 +81,11 @@ func thereIsDisconnectedUser(bddUserID string) error {
// logout is also called and if we would do login at the same time, it
// wouldn't work. 100 ms after event loop is stopped should be enough.
a.Eventually(ctx.GetTestingT(), func() bool {
return !user.GetStore().TestGetEventLoop().IsRunning()
store := user.GetStore()
if store == nil {
return true
}
return !store.TestGetEventLoop().IsRunning()
}, 1*time.Second, 10*time.Millisecond)
time.Sleep(100 * time.Millisecond)
return ctx.GetTestingError()
@ -120,20 +118,20 @@ func thereIsUserWithAddressMode(bddUserID, wantAddressMode string) error {
if account == nil {
return godog.ErrPending
}
bridgeUser, err := ctx.GetUser(account.Username())
user, err := ctx.GetUser(account.Username())
if err != nil {
return internalError(err, "getting user %s", account.Username())
}
addressMode := "split"
if bridgeUser.IsCombinedAddressMode() {
if user.IsCombinedAddressMode() {
addressMode = "combined"
}
if wantAddressMode != addressMode {
err := bridgeUser.SwitchAddressMode()
err := user.SwitchAddressMode()
if err != nil {
return internalError(err, "switching mode")
}
}
ctx.EventuallySyncIsFinishedForUsername(bridgeUser.Username())
ctx.EventuallySyncIsFinishedForUsername(user.Username())
return nil
}