diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 17bebce5..00ba87a8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -77,7 +77,6 @@ dependency-updates: build-linux: stage: build - # Test build every time (= we want to know build is possible). only: - branches script: @@ -88,6 +87,18 @@ build-linux: - bridge_*.tgz expire_in: 2 week +build-ie-linux: + stage: build + only: + - branches + script: + - make build-ie + artifacts: + name: "bridge-linux-$CI_COMMIT_SHORT_SHA" + paths: + - bridge_*.tgz + expire_in: 2 week + build-darwin: stage: build only: @@ -114,6 +125,31 @@ build-darwin: - bridge_*.tgz expire_in: 2 week +build-ie-darwin: + stage: build + only: + - branches + before_script: + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null + - export PATH=/usr/local/bin:$PATH + - export PATH=/usr/local/opt/git/bin:$PATH + - export PATH=/usr/local/opt/make/libexec/gnubin:$PATH + - export PATH=/usr/local/opt/go@1.13/bin:$PATH + - export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH + - export GOPATH=~/go + - export PATH=$GOPATH/bin:$PATH + cache: {} + tags: + - macOS-bridge + script: + - make build-ie + artifacts: + name: "bridge-darwin-$CI_COMMIT_SHORT_SHA" + paths: + - bridge_*.tgz + expire_in: 2 week + build-windows: stage: build services: @@ -136,6 +172,30 @@ build-windows: - bridge_*.tgz expire_in: 2 week +build-ie-windows: + stage: build + services: + - docker:dind + only: + - branches + variables: + DOCKER_HOST: tcp://docker:2375 + script: + # We need to install docker because qtdeploy builds for windows inside a docker container. + # Docker will connect to the dockerd daemon provided by the runner service docker:dind at tcp://docker:2375. + - curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh + - apt-get update && apt-get -y install binutils-mingw-w64 tar gzip + - ln -s /usr/bin/x86_64-w64-mingw32-windres /usr/bin/windres + - go mod download + - TARGET_OS=windows make build-ie + artifacts: + name: "bridge-windows-$CI_COMMIT_SHORT_SHA" + paths: + - bridge_*.tgz + expire_in: 2 week + +# Stage: MIRROR + mirror-repo: stage: mirror only: diff --git a/Makefile b/Makefile index 0d959d64..56f40189 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,12 @@ TARGET_OS?=${GOOS} ## Build .PHONY: build build-ie build-nogui build-ie-nogui check-has-go -APP_VERSION?=$(shell git describe --abbrev=0 --tags)-git +BRIDGE_APP_VERSION?=1.4.0-git +IE_APP_VERSION?=1.0.0-git +APP_VERSION=${BRIDGE_APP_VERSION} +ifeq "${TARGET_CMD}" "Import-Export" + APP_VERSION=${IE_APP_VERSION} +endif REVISION:=$(shell git rev-parse --short=10 HEAD) BUILD_TIME:=$(shell date +%FT%T%z) diff --git a/README.md b/README.md index 059d018c..f512e1d6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ or ### Integration testing - `TEST_ENV`: set which env to use (fake or live) +- `TEST_APP`: set which app to test (bridge or ie) - `TEST_ACCOUNTS`: set JSON file with configured accounts - `TAGS`: set build tags for tests - `FEATURES`: set feature dir, file or scenario to test diff --git a/cmd/Desktop-Bridge/main.go b/cmd/Desktop-Bridge/main.go index 1c48218f..0dc459b3 100644 --- a/cmd/Desktop-Bridge/main.go +++ b/cmd/Desktop-Bridge/main.go @@ -35,18 +35,13 @@ package main */ import ( - "fmt" "io/ioutil" "os" - "os/exec" - "path/filepath" - "runtime" "runtime/pprof" - "strconv" - "strings" "github.com/ProtonMail/proton-bridge/internal/api" "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/cmd" "github.com/ProtonMail/proton-bridge/internal/cookies" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/frontend" @@ -54,104 +49,42 @@ import ( "github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/internal/smtp" "github.com/ProtonMail/proton-bridge/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/pkg/args" "github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/constants" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/updates" "github.com/allan-simon/go-singleinstance" - "github.com/getsentry/raven-go" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) -// cacheVersion is used for cache files such as lock, events, preferences, user_info, db files. -// Different number will drop old files and create new ones. -const cacheVersion = "c11" +const ( + // cacheVersion is used for cache files such as lock, events, preferences, user_info, db files. + // Different number will drop old files and create new ones. + cacheVersion = "c11" + + appName = "bridge" +) var ( log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals] - - // How many crashes in a row. - numberOfCrashes = 0 //nolint[gochecknoglobals] - - // After how many crashes bridge gives up starting. - maxAllowedCrashes = 10 //nolint[gochecknoglobals] ) func main() { - if err := raven.SetDSN(constants.DSNSentry); err != nil { - log.WithError(err).Errorln("Can not setup sentry DSN") - } - raven.SetRelease(constants.Revision) - - args.FilterProcessSerialNumberFromArgs() - filterRestartNumberFromArgs() - - app := cli.NewApp() - app.Name = "Protonmail Bridge" - app.Version = constants.BuildVersion - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "log-level, l", - Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"}, - cli.BoolFlag{ - Name: "no-window", - Usage: "Don't show window after start"}, - cli.BoolFlag{ - Name: "cli, c", - Usage: "Use command line interface"}, - cli.BoolFlag{ - Name: "noninteractive", - Usage: "Start Bridge entirely noninteractively"}, - cli.StringFlag{ - Name: "version-json, g", - Usage: "Generate json version file"}, - cli.BoolFlag{ - Name: "mem-prof, m", - Usage: "Generate memory profile"}, - cli.BoolFlag{ - Name: "cpu-prof, p", - Usage: "Generate CPU profile"}, - } - app.Usage = "ProtonMail IMAP and SMTP Bridge" - app.Action = run - - // Always log the basic info about current bridge. - logrus.SetLevel(logrus.InfoLevel) - log.WithField("version", constants.Version). - WithField("revision", constants.Revision). - WithField("runtime", runtime.GOOS). - WithField("build", constants.BuildTime). - WithField("args", os.Args). - WithField("appLong", app.Name). - WithField("appShort", constants.AppShortName). - Info("Run app") - if err := app.Run(os.Args); err != nil { - log.Error("Program exited with error: ", err) - } -} - -type panicHandler struct { - cfg *config.Config - err *error // Pointer to error of cli action. -} - -func (ph *panicHandler) HandlePanic() { - r := recover() - if r == nil { - return - } - - config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r)) - frontend.HandlePanic("ProtonMail Bridge") - - *ph.err = cli.NewExitError("Panic and restart", 255) - numberOfCrashes++ - log.Error("Restarting after panic") - restartApp() - os.Exit(255) + cmd.Main( + "ProtonMail Bridge", + "ProtonMail IMAP and SMTP Bridge", + []cli.Flag{ + cli.BoolFlag{ + Name: "no-window", + Usage: "Don't show window after start"}, + cli.BoolFlag{ + Name: "noninteractive", + Usage: "Start Bridge entirely noninteractively"}, + }, + run, + ) } // run initializes and starts everything in a precise order. @@ -159,13 +92,17 @@ func (ph *panicHandler) HandlePanic() { // IMPORTANT: ***Read the comments before CHANGING the order *** func run(context *cli.Context) (contextError error) { // nolint[funlen] // We need to have config instance to setup a logs, panic handler, etc ... - cfg := config.New(constants.AppShortName, constants.Version, constants.Revision, cacheVersion) + cfg := config.New(appName, constants.Version, constants.Revision, cacheVersion) // We want to know about any problem. Our PanicHandler calls sentry which is // not dependent on anything else. If that fails, it tries to create crash // report which will not be possible if no folder can be created. That's the // only problem we will not be notified about in any way. - panicHandler := &panicHandler{cfg, &contextError} + panicHandler := &cmd.PanicHandler{ + AppName: "ProtonMail Bridge", + Config: cfg, + Err: &contextError, + } defer panicHandler.HandlePanic() // First we need config and create necessary folder; it's dependency for everything. @@ -177,13 +114,6 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] logLevel := context.GlobalString("log-level") debugClient, debugServer := config.SetupLog(cfg, logLevel) - // Should be called after logs are configured but before preferences are created. - migratePreferencesFromC10(cfg) - - if err := cfg.ClearOldData(); err != nil { - log.Error("Cannot clear old data: ", err) - } - // Doesn't make sense to continue when Bridge was invoked with wrong arguments. // We should tell that to the user before we do anything else. if context.Args().First() != "" { @@ -193,21 +123,16 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] // It's safe to get version JSON file even when other instance is running. // (thus we put it before check of presence of other Bridge instance). - updates := updates.New( - constants.AppShortName, - constants.Version, - constants.Revision, - constants.BuildTime, - bridge.ReleaseNotes, - bridge.ReleaseFixedBugs, - cfg.GetUpdateDir(), - ) + updates := updates.NewBridge(cfg.GetUpdateDir()) if dir := context.GlobalString("version-json"); dir != "" { - generateVersionFiles(updates, dir) + cmd.GenerateVersionFiles(updates, dir) return nil } + // Should be called after logs are configured but before preferences are created. + migratePreferencesFromC10(cfg) + // ClearOldData before starting new bridge to do a proper setup. // // IMPORTANT: If you the change position of this you will need to wait @@ -234,7 +159,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] if err != nil { log.Warn("Bridge is already running") if err := api.CheckOtherInstanceAndFocus(pref.GetInt(preferences.APIPortKey), tls); err != nil { - numberOfCrashes = maxAllowedCrashes + cmd.DisableRestart() log.Error("Second instance: ", err) } return cli.NewExitError("Bridge is already running.", 3) @@ -254,7 +179,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] } if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile { - defer makeMemoryProfile() + defer cmd.MakeMemoryProfile() } // Now we initialize all Bridge parts. @@ -262,7 +187,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] eventListener := listener.New() events.SetupEvents(eventListener) - credentialsStore, credentialsError := credentials.NewStore("bridge") + credentialsStore, credentialsError := credentials.NewStore(appName) if credentialsError != nil { log.Error("Could not get credentials store: ", credentialsError) } @@ -338,7 +263,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] } if frontend.IsAppRestarting() { - restartApp() + cmd.RestartApp() } return nil @@ -348,7 +273,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] // It will happen only when c10/prefs.json exists and c11/prefs.json not. // No configuration changed between c10 and c11 versions. func migratePreferencesFromC10(cfg *config.Config) { - pref10Path := config.New(constants.AppShortName, constants.Version, constants.Revision, "c10").GetPreferencesPath() + pref10Path := config.New(appName, constants.Version, constants.Revision, "c10").GetPreferencesPath() if _, err := os.Stat(pref10Path); os.IsNotExist(err) { log.WithField("path", pref10Path).Trace("Old preferences does not exist, migration skipped") return @@ -374,68 +299,3 @@ func migratePreferencesFromC10(cfg *config.Config) { log.Info("Preferences migrated") } - -// generateVersionFiles writes a JSON file with details about current build. -// Those files are used for upgrading the app. -func generateVersionFiles(updates *updates.Updates, dir string) { - log.Info("Generating version files") - for _, goos := range []string{"windows", "darwin", "linux"} { - log.Debug("Generating JSON for ", goos) - if err := updates.CreateJSONAndSign(dir, goos); err != nil { - log.Error(err) - } - } -} - -func makeMemoryProfile() { - name := "./mem.pprof" - f, err := os.Create(name) - if err != nil { - log.Error("Could not create memory profile: ", err) - } - if abs, err := filepath.Abs(name); err == nil { - name = abs - } - log.Info("Writing memory profile to ", name) - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Error("Could not write memory profile: ", err) - } - _ = f.Close() -} - -// filterRestartNumberFromArgs removes flag with a number how many restart we already did. -// See restartApp how that number is used. -func filterRestartNumberFromArgs() { - tmp := os.Args[:0] - for i, arg := range os.Args { - if !strings.HasPrefix(arg, "--restart_") { - tmp = append(tmp, arg) - continue - } - var err error - numberOfCrashes, err = strconv.Atoi(os.Args[i][10:]) - if err != nil { - numberOfCrashes = maxAllowedCrashes - } - } - os.Args = tmp -} - -// restartApp starts a new instance in background. -func restartApp() { - if numberOfCrashes >= maxAllowedCrashes { - log.Error("Too many crashes") - return - } - if exeFile, err := os.Executable(); err == nil { - arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes)) - cmd := exec.Command(exeFile, arguments...) //nolint[gosec] - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - if err := cmd.Start(); err != nil { - log.Error("Restart failed: ", err) - } - } -} diff --git a/cmd/Import-Export/main.go b/cmd/Import-Export/main.go index f605afc7..ac653ca3 100644 --- a/cmd/Import-Export/main.go +++ b/cmd/Import-Export/main.go @@ -18,108 +18,40 @@ package main import ( - "fmt" "os" - "os/exec" - "path/filepath" - "runtime" "runtime/pprof" - "strconv" - "strings" + "github.com/ProtonMail/proton-bridge/internal/cmd" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/frontend" "github.com/ProtonMail/proton-bridge/internal/importexport" "github.com/ProtonMail/proton-bridge/internal/users/credentials" - "github.com/ProtonMail/proton-bridge/pkg/args" "github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/constants" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/updates" - "github.com/getsentry/raven-go" + "github.com/allan-simon/go-singleinstance" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) +const ( + appName = "importExport" + appNameDash = "import-export" +) + var ( log = logrus.WithField("pkg", "main") //nolint[gochecknoglobals] - - // How many crashes in a row. - numberOfCrashes = 0 //nolint[gochecknoglobals] - - // After how many crashes import/export gives up starting. - maxAllowedCrashes = 10 //nolint[gochecknoglobals] ) func main() { - constants.AppShortName = "importExport" //TODO - - if err := raven.SetDSN(constants.DSNSentry); err != nil { - log.WithError(err).Errorln("Can not setup sentry DSN") - } - raven.SetRelease(constants.Revision) - - args.FilterProcessSerialNumberFromArgs() - filterRestartNumberFromArgs() - - app := cli.NewApp() - app.Name = "Protonmail Import/Export" - app.Version = constants.BuildVersion - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "log-level, l", - Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"}, - cli.BoolFlag{ - Name: "cli, c", - Usage: "Use command line interface"}, - cli.StringFlag{ - Name: "version-json, g", - Usage: "Generate json version file"}, - cli.BoolFlag{ - Name: "mem-prof, m", - Usage: "Generate memory profile"}, - cli.BoolFlag{ - Name: "cpu-prof, p", - Usage: "Generate CPU profile"}, - } - app.Usage = "ProtonMail Import/Export" - app.Action = run - - // Always log the basic info about current import/export. - logrus.SetLevel(logrus.InfoLevel) - log.WithField("version", constants.Version). - WithField("revision", constants.Revision). - WithField("runtime", runtime.GOOS). - WithField("build", constants.BuildTime). - WithField("args", os.Args). - WithField("appLong", app.Name). - WithField("appShort", constants.AppShortName). - Info("Run app") - if err := app.Run(os.Args); err != nil { - log.Error("Program exited with error: ", err) - } -} - -type panicHandler struct { - cfg *config.Config - err *error // Pointer to error of cli action. -} - -func (ph *panicHandler) HandlePanic() { - r := recover() - if r == nil { - return - } - - config.HandlePanic(ph.cfg, fmt.Sprintf("Recover: %v", r)) - frontend.HandlePanic("ProtonMail Import-Export") - - *ph.err = cli.NewExitError("Panic and restart", 255) - numberOfCrashes++ - log.Error("Restarting after panic") - restartApp() - os.Exit(255) + cmd.Main( + "ProtonMail Import/Export", + "ProtonMail Import/Export tool", + nil, + run, + ) } // run initializes and starts everything in a precise order. @@ -127,13 +59,17 @@ func (ph *panicHandler) HandlePanic() { // IMPORTANT: ***Read the comments before CHANGING the order *** func run(context *cli.Context) (contextError error) { // nolint[funlen] // We need to have config instance to setup a logs, panic handler, etc ... - cfg := config.New(constants.AppShortName, constants.Version, constants.Revision, "") + cfg := config.New(appName, constants.Version, constants.Revision, "") // We want to know about any problem. Our PanicHandler calls sentry which is // not dependent on anything else. If that fails, it tries to create crash // report which will not be possible if no folder can be created. That's the // only problem we will not be notified about in any way. - panicHandler := &panicHandler{cfg, &contextError} + panicHandler := &cmd.PanicHandler{ + AppName: "ProtonMail Import/Export", + Config: cfg, + Err: &contextError, + } defer panicHandler.HandlePanic() // First we need config and create necessary folder; it's dependency for everything. @@ -154,21 +90,22 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] // It's safe to get version JSON file even when other instance is running. // (thus we put it before check of presence of other Import/Export instance). - updates := updates.New( - constants.AppShortName, - constants.Version, - constants.Revision, - constants.BuildTime, - importexport.ReleaseNotes, - importexport.ReleaseFixedBugs, - cfg.GetUpdateDir(), - ) + updates := updates.NewImportExport(cfg.GetUpdateDir()) if dir := context.GlobalString("version-json"); dir != "" { - generateVersionFiles(updates, dir) + cmd.GenerateVersionFiles(updates, dir) return nil } + // Now we can try to proceed with starting the import/export. First we need to ensure + // this is the only instance. If not, we will end and focus the existing one. + lock, err := singleinstance.CreateLockFile(cfg.GetLockPath()) + if err != nil { + log.Warn("Import/Export is already running") + return cli.NewExitError("Import/Export is already running.", 3) + } + defer lock.Close() //nolint[errcheck] + // In case user wants to do CPU or memory profiles... if doCPUProfile := context.GlobalBool("cpu-prof"); doCPUProfile { f, err := os.Create("cpu.pprof") @@ -182,7 +119,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] } if doMemoryProfile := context.GlobalBool("mem-prof"); doMemoryProfile { - defer makeMemoryProfile() + defer cmd.MakeMemoryProfile() } // Now we initialize all Import/Export parts. @@ -190,7 +127,7 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] eventListener := listener.New() events.SetupEvents(eventListener) - credentialsStore, credentialsError := credentials.NewStore("import-export") + credentialsStore, credentialsError := credentials.NewStore(appNameDash) if credentialsError != nil { log.Error("Could not get credentials store: ", credentialsError) } @@ -224,73 +161,8 @@ func run(context *cli.Context) (contextError error) { // nolint[funlen] } if frontend.IsAppRestarting() { - restartApp() + cmd.RestartApp() } return nil } - -// generateVersionFiles writes a JSON file with details about current build. -// Those files are used for upgrading the app. -func generateVersionFiles(updates *updates.Updates, dir string) { - log.Info("Generating version files") - for _, goos := range []string{"windows", "darwin", "linux"} { - log.Debug("Generating JSON for ", goos) - if err := updates.CreateJSONAndSign(dir, goos); err != nil { - log.Error(err) - } - } -} - -func makeMemoryProfile() { - name := "./mem.pprof" - f, err := os.Create(name) - if err != nil { - log.Error("Could not create memory profile: ", err) - } - if abs, err := filepath.Abs(name); err == nil { - name = abs - } - log.Info("Writing memory profile to ", name) - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Error("Could not write memory profile: ", err) - } - _ = f.Close() -} - -// filterRestartNumberFromArgs removes flag with a number how many restart we already did. -// See restartApp how that number is used. -func filterRestartNumberFromArgs() { - tmp := os.Args[:0] - for i, arg := range os.Args { - if !strings.HasPrefix(arg, "--restart_") { - tmp = append(tmp, arg) - continue - } - var err error - numberOfCrashes, err = strconv.Atoi(os.Args[i][10:]) - if err != nil { - numberOfCrashes = maxAllowedCrashes - } - } - os.Args = tmp -} - -// restartApp starts a new instance in background. -func restartApp() { - if numberOfCrashes >= maxAllowedCrashes { - log.Error("Too many crashes") - return - } - if exeFile, err := os.Executable(); err == nil { - arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes)) - cmd := exec.Command(exeFile, arguments...) //nolint[gosec] - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - if err := cmd.Start(); err != nil { - log.Error("Restart failed: ", err) - } - } -} diff --git a/go.mod b/go.mod index cb4c80ba..c499d754 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/emersion/go-imap-specialuse v0.0.0-20200722111535-598ff00e4075 github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 github.com/emersion/go-mbox v1.0.0 + github.com/emersion/go-message v0.11.1 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe github.com/emersion/go-vcard v0.0.0-20190105225839-8856043f13c5 // indirect diff --git a/pkg/args/args.go b/internal/cmd/args.go similarity index 90% rename from pkg/args/args.go rename to internal/cmd/args.go index 12edbb20..79532e0d 100644 --- a/pkg/args/args.go +++ b/internal/cmd/args.go @@ -15,16 +15,16 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -package args +package cmd import ( "os" "strings" ) -// FilterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber +// filterProcessSerialNumberFromArgs removes additional flag from MacOS. More info ProcessSerialNumber // http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/Process_Manager/prmref_main/data_type_5.html#//apple_ref/doc/uid/TP30000208/C001951 -func FilterProcessSerialNumberFromArgs() { +func filterProcessSerialNumberFromArgs() { tmp := os.Args[:0] for _, arg := range os.Args { if !strings.Contains(arg, "-psn_") { diff --git a/internal/cmd/main.go b/internal/cmd/main.go new file mode 100644 index 00000000..e83d806b --- /dev/null +++ b/internal/cmd/main.go @@ -0,0 +1,86 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package cmd + +import ( + "os" + "runtime" + + "github.com/ProtonMail/proton-bridge/pkg/constants" + "github.com/getsentry/raven-go" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + log = logrus.WithField("pkg", "cmd") //nolint[gochecknoglobals] + + baseFlags = []cli.Flag{ //nolint[gochecknoglobals] + cli.StringFlag{ + Name: "log-level, l", + Usage: "Set the log level (one of panic, fatal, error, warn, info, debug, debug-client, debug-server)"}, + cli.BoolFlag{ + Name: "cli, c", + Usage: "Use command line interface"}, + cli.StringFlag{ + Name: "version-json, g", + Usage: "Generate json version file"}, + cli.BoolFlag{ + Name: "mem-prof, m", + Usage: "Generate memory profile"}, + cli.BoolFlag{ + Name: "cpu-prof, p", + Usage: "Generate CPU profile"}, + } +) + +// Main sets up Sentry, filters out unwanted args, creates app and runs it. +func Main(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) { + if err := raven.SetDSN(constants.DSNSentry); err != nil { + log.WithError(err).Errorln("Can not setup sentry DSN") + } + raven.SetRelease(constants.Revision) + + filterProcessSerialNumberFromArgs() + filterRestartNumberFromArgs() + + app := newApp(appName, usage, extraFlags, run) + + logrus.SetLevel(logrus.InfoLevel) + log.WithField("version", constants.Version). + WithField("revision", constants.Revision). + WithField("build", constants.BuildTime). + WithField("runtime", runtime.GOOS). + WithField("args", os.Args). + WithField("appName", app.Name). + Info("Run app") + + if err := app.Run(os.Args); err != nil { + log.Error("Program exited with error: ", err) + } +} + +func newApp(appName, usage string, extraFlags []cli.Flag, run func(*cli.Context) error) *cli.App { + app := cli.NewApp() + app.Name = appName + app.Usage = usage + app.Version = constants.BuildVersion + app.Flags = append(baseFlags, extraFlags...) //nolint[gocritic] + app.Action = run + return app +} diff --git a/internal/cmd/memory_profile.go b/internal/cmd/memory_profile.go new file mode 100644 index 00000000..5b09bf54 --- /dev/null +++ b/internal/cmd/memory_profile.go @@ -0,0 +1,43 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package cmd + +import ( + "os" + "path/filepath" + "runtime" + "runtime/pprof" +) + +// MakeMemoryProfile generates memory pprof. +func MakeMemoryProfile() { + name := "./mem.pprof" + f, err := os.Create(name) + if err != nil { + log.Error("Could not create memory profile: ", err) + } + if abs, err := filepath.Abs(name); err == nil { + name = abs + } + log.Info("Writing memory profile to ", name) + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Error("Could not write memory profile: ", err) + } + _ = f.Close() +} diff --git a/internal/cmd/restart.go b/internal/cmd/restart.go new file mode 100644 index 00000000..abd3036e --- /dev/null +++ b/internal/cmd/restart.go @@ -0,0 +1,108 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package cmd + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/ProtonMail/proton-bridge/internal/frontend" + "github.com/ProtonMail/proton-bridge/pkg/config" + "github.com/urfave/cli" +) + +const ( + // After how many crashes app gives up starting. + maxAllowedCrashes = 10 +) + +var ( + // How many crashes happened so far in a row. + // It will be filled from args by `filterRestartNumberFromArgs`. + // Every call of `HandlePanic` will increase this number. + // Then it will be passed as argument to the next try by `RestartApp`. + numberOfCrashes = 0 //nolint[gochecknoglobals] +) + +// filterRestartNumberFromArgs removes flag with a number how many restart we already did. +// See restartApp how that number is used. +func filterRestartNumberFromArgs() { + tmp := os.Args[:0] + for i, arg := range os.Args { + if !strings.HasPrefix(arg, "--restart_") { + tmp = append(tmp, arg) + continue + } + var err error + numberOfCrashes, err = strconv.Atoi(os.Args[i][10:]) + if err != nil { + numberOfCrashes = maxAllowedCrashes + } + } + os.Args = tmp +} + +// DisableRestart disables restart once `RestartApp` is called. +func DisableRestart() { + numberOfCrashes = maxAllowedCrashes +} + +// RestartApp starts a new instance in background. +func RestartApp() { + if numberOfCrashes >= maxAllowedCrashes { + log.Error("Too many crashes") + return + } + if exeFile, err := os.Executable(); err == nil { + arguments := append(os.Args[1:], fmt.Sprintf("--restart_%d", numberOfCrashes)) + cmd := exec.Command(exeFile, arguments...) //nolint[gosec] + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if err := cmd.Start(); err != nil { + log.Error("Restart failed: ", err) + } + } +} + +// PanicHandler defines HandlePanic which can be used anywhere in defer. +type PanicHandler struct { + AppName string + Config *config.Config + Err *error // Pointer to error of cli action. +} + +// HandlePanic should be called in defer to ensure restart of app after error. +func (ph *PanicHandler) HandlePanic() { + r := recover() + if r == nil { + return + } + + config.HandlePanic(ph.Config, fmt.Sprintf("Recover: %v", r)) + frontend.HandlePanic(ph.AppName) + + *ph.Err = cli.NewExitError("Panic and restart", 255) + numberOfCrashes++ + log.Error("Restarting after panic") + RestartApp() + os.Exit(255) +} diff --git a/internal/cmd/version_file.go b/internal/cmd/version_file.go new file mode 100644 index 00000000..fe0e8e9c --- /dev/null +++ b/internal/cmd/version_file.go @@ -0,0 +1,32 @@ +// Copyright (c) 2020 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package cmd + +import "github.com/ProtonMail/proton-bridge/pkg/updates" + +// GenerateVersionFiles writes a JSON file with details about current build. +// Those files are used for upgrading the app. +func GenerateVersionFiles(updates *updates.Updates, dir string) { + log.Info("Generating version files") + for _, goos := range []string{"windows", "darwin", "linux"} { + log.Debug("Generating JSON for ", goos) + if err := updates.CreateJSONAndSign(dir, goos); err != nil { + log.Error(err) + } + } +} diff --git a/internal/frontend/cli-ie/frontend.go b/internal/frontend/cli-ie/frontend.go index 7bb11586..cf769fe6 100644 --- a/internal/frontend/cli-ie/frontend.go +++ b/internal/frontend/cli-ie/frontend.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Package cli provides CLI interface of the Bridge. +// Package cli provides CLI interface of the Import/Export. package cli import ( @@ -227,7 +227,11 @@ func (f *frontendCLI) Loop(credentialsError error) error { return credentialsError } - f.Print(`Welcome to ProtonMail Import/Export interactive shell`) + f.Print(` +Welcome to ProtonMail Import/Export interactive shell + +WARNING: CLI is experimental feature and does not cover all functionality yet. + `) f.Run() return nil } diff --git a/internal/frontend/cli-ie/updates.go b/internal/frontend/cli-ie/updates.go index b30da468..62332dc8 100644 --- a/internal/frontend/cli-ie/updates.go +++ b/internal/frontend/cli-ie/updates.go @@ -20,7 +20,7 @@ package cli import ( "strings" - "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/importexport" "github.com/ProtonMail/proton-bridge/pkg/updates" "github.com/abiosoft/ishell" ) @@ -47,7 +47,7 @@ func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) { } func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) { - f.Println(bold("ProtonMail Bridge "+versionInfo.Version), "\n") + f.Println(bold("ProtonMail Import/Export "+versionInfo.Version), "\n") if versionInfo.ReleaseNotes != "" { f.Println(bold("Release Notes")) f.Println(versionInfo.ReleaseNotes) @@ -59,7 +59,7 @@ func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) { } func (f *frontendCLI) printCredits(c *ishell.Context) { - for _, pkg := range strings.Split(bridge.Credits, ";") { + for _, pkg := range strings.Split(importexport.Credits, ";") { f.Println(pkg) } } diff --git a/internal/frontend/cli-ie/utils.go b/internal/frontend/cli-ie/utils.go index f5a97000..6f01d2fe 100644 --- a/internal/frontend/cli-ie/utils.go +++ b/internal/frontend/cli-ie/utils.go @@ -98,7 +98,7 @@ func (f *frontendCLI) notifyNeedUpgrade() { func (f *frontendCLI) notifyCredentialsError() { // Print in 80-column width. - f.Println("ProtonMail Bridge is not able to detect a supported password manager") + f.Println("ProtonMail Import/Export is not able to detect a supported password manager") f.Println("(pass, gnome-keyring). Please install and set up a supported password manager") f.Println("and restart the application.") } @@ -109,7 +109,7 @@ func (f *frontendCLI) notifyCertIssue() { be insecure. Description: -ProtonMail Bridge was not able to establish a secure connection to Proton +ProtonMail Import/Export was not able to establish a secure connection to Proton servers due to a TLS certificate error. This means your connection may potentially be insecure and susceptible to monitoring by third parties. diff --git a/internal/frontend/cli/updates.go b/internal/frontend/cli/updates.go index 55d35e16..b30da468 100644 --- a/internal/frontend/cli/updates.go +++ b/internal/frontend/cli/updates.go @@ -47,7 +47,7 @@ func (f *frontendCLI) printLocalReleaseNotes(c *ishell.Context) { } func (f *frontendCLI) printReleaseNotes(versionInfo updates.VersionInfo) { - f.Println(bold("ProtonMail Import/Export "+versionInfo.Version), "\n") + f.Println(bold("ProtonMail Bridge "+versionInfo.Version), "\n") if versionInfo.ReleaseNotes != "" { f.Println(bold("Release Notes")) f.Println(versionInfo.ReleaseNotes) diff --git a/internal/frontend/qt-common/path_status.go b/internal/frontend/qt-common/path_status.go index e6845f92..bf76d6d8 100644 --- a/internal/frontend/qt-common/path_status.go +++ b/internal/frontend/qt-common/path_status.go @@ -67,12 +67,12 @@ func CheckPathStatus(path string) int { tmpFile += "tmp" _, err = os.Lstat(tmpFile) } - err = os.Mkdir(tmpFile, 0777) + err = os.Mkdir(tmpFile, 0750) if err != nil { stat |= PathWrongPermissions return int(stat) } - os.Remove(tmpFile) + _ = os.Remove(tmpFile) } else { stat |= PathNotADir } diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go index 87eed1d7..311da61d 100644 --- a/internal/importexport/credits.go +++ b/internal/importexport/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Code generated by ./credits.sh at Thu 04 Jun 2020 04:19:16 PM CEST. DO NOT EDIT. +// Code generated by ./credits.sh at Mon Jul 13 14:02:21 CEST 2020. DO NOT EDIT. package importexport -const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" +const Credits = "github.com/0xAX/notificator;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-smtp;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/andybalholm/cascadia;github.com/certifi/gocertifi;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/raven-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/jhillyerd/enmime;github.com/kardianos/osext;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" diff --git a/internal/importexport/release_notes.go b/internal/importexport/release_notes.go index 456f472c..71713e72 100644 --- a/internal/importexport/release_notes.go +++ b/internal/importexport/release_notes.go @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Code generated by ./release-notes.sh at 'Thu Jun 4 15:54:31 CEST 2020'. DO NOT EDIT. +// Code generated by ./release-notes.sh at 'Thu Jun 25 10:06:16 CEST 2020'. DO NOT EDIT. package importexport diff --git a/internal/transfer/mailbox.go b/internal/transfer/mailbox.go index b4fbaf46..430db7c9 100644 --- a/internal/transfer/mailbox.go +++ b/internal/transfer/mailbox.go @@ -41,13 +41,9 @@ func (m Mailbox) Hash() string { // LeastUsedColor is intended to return color for creating a new inbox or label func LeastUsedColor(mailboxes []Mailbox) string { usedColors := []string{} - - if mailboxes != nil { - for _, m := range mailboxes { - usedColors = append(usedColors, m.Color) - } + for _, m := range mailboxes { + usedColors = append(usedColors, m.Color) } - return pmapi.LeastUsedColor(usedColors) } diff --git a/internal/transfer/message.go b/internal/transfer/message.go index 5c939305..fec7be38 100644 --- a/internal/transfer/message.go +++ b/internal/transfer/message.go @@ -56,6 +56,10 @@ type MessageStatus struct { Time time.Time } +func (status *MessageStatus) String() string { + return fmt.Sprintf("%s (%s, %s, %s): %s", status.SourceID, status.Subject, status.From, status.Time, status.GetErrorMessage()) +} + func (status *MessageStatus) setDetailsFromHeader(header mail.Header) { dec := &mime.WordDecoder{} diff --git a/internal/transfer/progress.go b/internal/transfer/progress.go index ee463f45..28c37f38 100644 --- a/internal/transfer/progress.go +++ b/internal/transfer/progress.go @@ -139,8 +139,10 @@ func (p *Progress) messageExported(messageID string, body []byte, err error) { p.log.WithField("id", messageID).WithError(err).Debug("Message exported") status := p.messageStatuses[messageID] - status.exported = true status.exportErr = err + if err == nil { + status.exported = true + } if len(body) > 0 { status.bodyHash = fmt.Sprintf("%x", sha256.Sum256(body)) @@ -166,8 +168,10 @@ func (p *Progress) messageImported(messageID, importID string, err error) { p.log.WithField("id", messageID).WithError(err).Debug("Message imported") p.messageStatuses[messageID].targetID = importID - p.messageStatuses[messageID].imported = true p.messageStatuses[messageID].importErr = err + if err == nil { + p.messageStatuses[messageID].imported = true + } // Import is the last step, now we can log the result to the report file. p.logMessage(messageID) diff --git a/internal/transfer/progress_test.go b/internal/transfer/progress_test.go index 68baeb16..3ec170d2 100644 --- a/internal/transfer/progress_test.go +++ b/internal/transfer/progress_test.go @@ -73,8 +73,8 @@ func TestProgressAddingMessages(t *testing.T) { failed, imported, exported, added, _ := progress.GetCounts() a.Equal(t, uint(4), added) - a.Equal(t, uint(4), exported) - a.Equal(t, uint(3), imported) + a.Equal(t, uint(2), exported) + a.Equal(t, uint(2), imported) a.Equal(t, uint(3), failed) errorsMap := map[string]string{} diff --git a/internal/transfer/provider_imap_source.go b/internal/transfer/provider_imap_source.go index 59dbf9ac..82d13f1c 100644 --- a/internal/transfer/provider_imap_source.go +++ b/internal/transfer/provider_imap_source.go @@ -68,7 +68,7 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres continue } - messagesInfo := p.loadMessagesInfo(rule, progress, mailbox.UidValidity) + messagesInfo := p.loadMessagesInfo(rule, progress, mailbox.UidValidity, mailbox.Messages) res[rule.SourceMailbox.Name] = messagesInfo progress.updateCount(rule.SourceMailbox.Name, uint(len(messagesInfo))) } @@ -76,7 +76,7 @@ func (p *IMAPProvider) loadMessageInfoMap(rules transferRules, progress *Progres return res } -func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity uint32) map[string]imapMessageInfo { +func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValidity, count uint32) map[string]imapMessageInfo { messagesInfo := map[string]imapMessageInfo{} pageStart := uint32(1) @@ -86,6 +86,11 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid break } + // Some servers do not accept message sequence number higher than the total count. + if pageEnd > count { + pageEnd = count + } + seqSet := &imap.SeqSet{} seqSet.AddRange(pageStart, pageEnd) @@ -114,7 +119,7 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid } progress.callWrap(func() error { - return p.fetch(seqSet, items, processMessageCallback) + return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback) }) if pageMsgCount < imapPageSize { @@ -145,7 +150,6 @@ func (p *IMAPProvider) transferTo(rule *Rule, messagesInfo map[string]imapMessag if seqSetSize != 0 && (seqSetSize+messageInfo.size) > imapMaxFetchSize { log.WithField("mailbox", rule.SourceMailbox.Name).WithField("seq", seqSet).WithField("size", seqSetSize).Debug("Fetching messages") - p.exportMessages(rule, progress, ch, seqSet, uidToID) seqSet = &imap.SeqSet{} @@ -157,6 +161,11 @@ func (p *IMAPProvider) transferTo(rule *Rule, messagesInfo map[string]imapMessag seqSetSize += messageInfo.size uidToID[messageInfo.uid] = messageInfo.id } + + if len(uidToID) != 0 { + log.WithField("mailbox", rule.SourceMailbox.Name).WithField("seq", seqSet).WithField("size", seqSetSize).Debug("Fetching messages") + p.exportMessages(rule, progress, ch, seqSet, uidToID) + } } func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<- Message, seqSet *imap.SeqSet, uidToID map[uint32]string) { @@ -188,7 +197,7 @@ func (p *IMAPProvider) exportMessages(rule *Rule, progress *Progress, ch chan<- } progress.callWrap(func() error { - return p.uidFetch(seqSet, items, processMessageCallback) + return p.uidFetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback) }) } diff --git a/internal/transfer/provider_imap_utils.go b/internal/transfer/provider_imap_utils.go index 364e9ecc..36b3cb2c 100644 --- a/internal/transfer/provider_imap_utils.go +++ b/internal/transfer/provider_imap_utils.go @@ -49,16 +49,11 @@ func (l *imapErrorLogger) Println(v ...interface{}) { l.log.Errorln(v...) } -type imapDebugLogger struct { //nolint[unused] - log *logrus.Entry -} - -func (l *imapDebugLogger) Write(data []byte) (int, error) { - l.log.Trace(string(data)) - return len(data), nil -} - func (p *IMAPProvider) ensureConnection(callback func() error) error { + return p.ensureConnectionAndSelection(callback, "") +} + +func (p *IMAPProvider) ensureConnectionAndSelection(callback func() error, ensureSelectedIn string) error { var callErr error for i := 1; i <= imapRetries; i++ { callErr = callback() @@ -66,8 +61,8 @@ func (p *IMAPProvider) ensureConnection(callback func() error) error { return nil } - log.WithField("attempt", i).WithError(callErr).Warning("Call failed, trying reconnect") - err := p.tryReconnect() + log.WithField("attempt", i).WithError(callErr).Warning("IMAP call failed, trying reconnect") + err := p.tryReconnect(ensureSelectedIn) if err != nil { return err } @@ -75,7 +70,7 @@ func (p *IMAPProvider) ensureConnection(callback func() error) error { return errors.Wrap(callErr, "too many retries") } -func (p *IMAPProvider) tryReconnect() error { +func (p *IMAPProvider) tryReconnect(ensureSelectedIn string) error { start := time.Now() var previousErr error for { @@ -84,6 +79,7 @@ func (p *IMAPProvider) tryReconnect() error { } err := pmapi.CheckConnection() + log.WithError(err).Debug("Connection check") if err != nil { time.Sleep(imapReconnectSleep) previousErr = err @@ -91,24 +87,47 @@ func (p *IMAPProvider) tryReconnect() error { } err = p.reauth() + log.WithError(err).Debug("Reauth") if err != nil { time.Sleep(imapReconnectSleep) previousErr = err continue } + if ensureSelectedIn != "" { + _, err = p.client.Select(ensureSelectedIn, true) + log.WithError(err).Debug("Reselect") + if err != nil { + previousErr = err + continue + } + } + break } return nil } func (p *IMAPProvider) reauth() error { - if _, err := p.client.Capability(); err != nil { - state := p.client.State() - log.WithField("addr", p.addr).WithField("state", state).WithError(err).Debug("Reconnecting") - p.client = nil + var state imap.ConnState + + // In some cases once go-imap fails, we cannot issue another command + // because it would dead-lock. Let's simply ignore it, we want to open + // new connection anyway. + ch := make(chan struct{}) + go func() { + defer close(ch) + if _, err := p.client.Capability(); err != nil { + state = p.client.State() + } + }() + select { + case <-ch: + case <-time.After(30 * time.Second): } + log.WithField("addr", p.addr).WithField("state", state).Debug("Reconnecting") + p.client = nil return p.auth() } @@ -121,15 +140,25 @@ func (p *IMAPProvider) auth() error { //nolint[funlen] return errors.Wrap(err, "failed to dial server") } - client, err := imapClient.DialTLS(p.addr, nil) + var client *imapClient.Client + var err error + host, _, _ := net.SplitHostPort(p.addr) + if host == "127.0.0.1" { + client, err = imapClient.Dial(p.addr) + } else { + client, err = imapClient.DialTLS(p.addr, nil) + } if err != nil { return errors.Wrap(err, "failed to connect to server") } + client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")} - // Logrus have Writer helper but it fails for big messages because of - // bufio.MaxScanTokenSize limit. - // This spams a lot, uncomment once needed during development. - //client.SetDebug(&imapDebugLogger{logrus.WithField("pkg", "imap-client")}) + // Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit. + // Also, this spams a lot, uncomment once needed during development. + //client.SetDebug(imap.NewDebugWriter( + // logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel), + // logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel), + //)) p.client = client log.Info("Connected") @@ -205,16 +234,16 @@ func (p *IMAPProvider) selectIn(mailboxName string) (mailbox *imap.MailboxStatus return } -func (p *IMAPProvider) fetch(seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error { - return p.fetchHelper(false, seqSet, items, processMessageCallback) +func (p *IMAPProvider) fetch(ensureSelectedIn string, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error { + return p.fetchHelper(false, ensureSelectedIn, seqSet, items, processMessageCallback) } -func (p *IMAPProvider) uidFetch(seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error { - return p.fetchHelper(true, seqSet, items, processMessageCallback) +func (p *IMAPProvider) uidFetch(ensureSelectedIn string, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error { + return p.fetchHelper(true, ensureSelectedIn, seqSet, items, processMessageCallback) } -func (p *IMAPProvider) fetchHelper(uid bool, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error { - return p.ensureConnection(func() error { +func (p *IMAPProvider) fetchHelper(uid bool, ensureSelectedIn string, seqSet *imap.SeqSet, items []imap.FetchItem, processMessageCallback func(m *imap.Message)) error { + return p.ensureConnectionAndSelection(func() error { messagesCh := make(chan *imap.Message) doneCh := make(chan error) @@ -232,5 +261,5 @@ func (p *IMAPProvider) fetchHelper(uid bool, seqSet *imap.SeqSet, items []imap.F err := <-doneCh return err - }) + }, ensureSelectedIn) } diff --git a/internal/transfer/provider_mbox_target.go b/internal/transfer/provider_mbox_target.go index 610f8cf2..44f450f3 100644 --- a/internal/transfer/provider_mbox_target.go +++ b/internal/transfer/provider_mbox_target.go @@ -45,7 +45,7 @@ func (p *MBOXProvider) TransferFrom(rules transferRules, progress *Progress, ch defer log.Info("Finished transfer from channel to MBOX") for msg := range ch { - for progress.shouldStop() { + if progress.shouldStop() { break } diff --git a/internal/transfer/provider_pmapi.go b/internal/transfer/provider_pmapi.go index f6f14e27..61f708de 100644 --- a/internal/transfer/provider_pmapi.go +++ b/internal/transfer/provider_pmapi.go @@ -20,7 +20,7 @@ package transfer import ( "sort" - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" ) @@ -31,6 +31,9 @@ type PMAPIProvider struct { userID string addressID string keyRing *crypto.KeyRing + + importMsgReqMap map[string]*pmapi.ImportMsgReq // Key is msg transfer ID. + importMsgReqSize int } // NewPMAPIProvider returns new PMAPIProvider. @@ -45,6 +48,9 @@ func NewPMAPIProvider(clientManager ClientManager, userID, addressID string) (*P userID: userID, addressID: addressID, keyRing: keyRing, + + importMsgReqMap: map[string]*pmapi.ImportMsgReq{}, + importMsgReqSize: 0, }, nil } diff --git a/internal/transfer/provider_pmapi_source.go b/internal/transfer/provider_pmapi_source.go index 240a4a84..6c383812 100644 --- a/internal/transfer/provider_pmapi_source.go +++ b/internal/transfer/provider_pmapi_source.go @@ -19,6 +19,7 @@ package transfer import ( "fmt" + "sync" pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" @@ -32,11 +33,21 @@ func (p *PMAPIProvider) TransferTo(rules transferRules, progress *Progress, ch c log.Info("Started transfer from PMAPI to channel") defer log.Info("Finished transfer from PMAPI to channel") - go p.loadCounts(rules, progress) + // TransferTo cannot end sooner than loadCounts goroutine because + // loadCounts writes to channel in progress which would be closed. + // That can happen for really small accounts. + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + p.loadCounts(rules, progress) + }() for rule := range rules.iterateActiveRules() { p.transferTo(rule, progress, ch, rules.skipEncryptedMessages) } + + wg.Wait() } func (p *PMAPIProvider) loadCounts(rules transferRules, progress *Progress) { diff --git a/internal/transfer/provider_pmapi_target.go b/internal/transfer/provider_pmapi_target.go index 1034caa4..7e146825 100644 --- a/internal/transfer/provider_pmapi_target.go +++ b/internal/transfer/provider_pmapi_target.go @@ -28,6 +28,11 @@ import ( "github.com/pkg/errors" ) +const ( + pmapiImportBatchMaxItems = 10 + pmapiImportBatchMaxSize = 25 * 1000 * 1000 // 25 MB +) + // DefaultMailboxes returns the default mailboxes for default rules if no other is found. func (p *PMAPIProvider) DefaultMailboxes(_ Mailbox) []Mailbox { return []Mailbox{{ @@ -67,18 +72,19 @@ func (p *PMAPIProvider) TransferFrom(rules transferRules, progress *Progress, ch defer log.Info("Finished transfer from channel to PMAPI") for msg := range ch { - for progress.shouldStop() { + if progress.shouldStop() { break } - var importedID string - var err error if p.isMessageDraft(msg) { - importedID, err = p.importDraft(msg, rules.globalMailbox) + p.transferDraft(rules, progress, msg) } else { - importedID, err = p.importMessage(msg, rules.globalMailbox) + p.transferMessage(rules, progress, msg) } - progress.messageImported(msg.ID, importedID, err) + } + + if len(p.importMsgReqMap) > 0 { + p.importMessages(progress) } } @@ -91,6 +97,11 @@ func (p *PMAPIProvider) isMessageDraft(msg Message) bool { return false } +func (p *PMAPIProvider) transferDraft(rules transferRules, progress *Progress, msg Message) { + importedID, err := p.importDraft(msg, rules.globalMailbox) + progress.messageImported(msg.ID, importedID, err) +} + func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string, error) { message, attachmentReaders, err := p.parseMessage(msg) if err != nil { @@ -138,15 +149,30 @@ func (p *PMAPIProvider) importDraft(msg Message, globalMailbox *Mailbox) (string return draft.ID, nil } -func (p *PMAPIProvider) importMessage(msg Message, globalMailbox *Mailbox) (string, error) { +func (p *PMAPIProvider) transferMessage(rules transferRules, progress *Progress, msg Message) { + importMsgReq, err := p.generateImportMsgReq(msg, rules.globalMailbox) + if err != nil { + progress.messageImported(msg.ID, "", err) + return + } + + importMsgReqSize := len(importMsgReq.Body) + if p.importMsgReqSize+importMsgReqSize > pmapiImportBatchMaxSize || len(p.importMsgReqMap) == pmapiImportBatchMaxItems { + p.importMessages(progress) + } + p.importMsgReqMap[msg.ID] = importMsgReq + p.importMsgReqSize += importMsgReqSize +} + +func (p *PMAPIProvider) generateImportMsgReq(msg Message, globalMailbox *Mailbox) (*pmapi.ImportMsgReq, error) { message, attachmentReaders, err := p.parseMessage(msg) if err != nil { - return "", errors.Wrap(err, "failed to parse message") + return nil, errors.Wrap(err, "failed to parse message") } body, err := p.encryptMessage(message, attachmentReaders) if err != nil { - return "", errors.Wrap(err, "failed to encrypt message") + return nil, errors.Wrap(err, "failed to encrypt message") } unread := 0 @@ -165,26 +191,14 @@ func (p *PMAPIProvider) importMessage(msg Message, globalMailbox *Mailbox) (stri labelIDs = append(labelIDs, globalMailbox.ID) } - importMsgReq := &pmapi.ImportMsgReq{ + return &pmapi.ImportMsgReq{ AddressID: p.addressID, Body: body, Unread: unread, Time: message.Time, Flags: computeMessageFlags(labelIDs), LabelIDs: labelIDs, - } - - results, err := p.importRequest([]*pmapi.ImportMsgReq{importMsgReq}) - if err != nil { - return "", errors.Wrap(err, "failed to import messages") - } - if len(results) == 0 { - return "", errors.New("import ended with no result") - } - if results[0].Error != nil { - return "", errors.Wrap(results[0].Error, "failed to import message") - } - return results[0].MessageID, nil + }, nil } func (p *PMAPIProvider) parseMessage(msg Message) (*pmapi.Message, []io.Reader, error) { @@ -218,3 +232,65 @@ func computeMessageFlags(labels []string) (flag int64) { return flag } + +func (p *PMAPIProvider) importMessages(progress *Progress) { + if progress.shouldStop() { + return + } + + importMsgIDs := []string{} + importMsgRequests := []*pmapi.ImportMsgReq{} + for msgID, req := range p.importMsgReqMap { + importMsgIDs = append(importMsgIDs, msgID) + importMsgRequests = append(importMsgRequests, req) + } + + log.WithField("msgIDs", importMsgIDs).WithField("size", p.importMsgReqSize).Debug("Importing messages") + results, err := p.importRequest(importMsgRequests) + + // In case the whole request failed, try to import every message one by one. + if err != nil || len(results) == 0 { + log.WithError(err).Warning("Importing messages failed, trying one by one") + for msgID, req := range p.importMsgReqMap { + importedID, err := p.importMessage(progress, req) + progress.messageImported(msgID, importedID, err) + } + return + } + + // In case request passed but some messages failed, try to import the failed ones alone. + for index, result := range results { + msgID := importMsgIDs[index] + if result.Error != nil { + log.WithError(result.Error).WithField("msg", msgID).Warning("Importing message failed, trying alone") + req := importMsgRequests[index] + importedID, err := p.importMessage(progress, req) + progress.messageImported(msgID, importedID, err) + } else { + progress.messageImported(msgID, result.MessageID, nil) + } + } + + p.importMsgReqMap = map[string]*pmapi.ImportMsgReq{} + p.importMsgReqSize = 0 +} + +func (p *PMAPIProvider) importMessage(progress *Progress, req *pmapi.ImportMsgReq) (importedID string, importedErr error) { + progress.callWrap(func() error { + results, err := p.importRequest([]*pmapi.ImportMsgReq{req}) + if err != nil { + return errors.Wrap(err, "failed to import messages") + } + if len(results) == 0 { + importedErr = errors.New("import ended with no result") + return nil // This should not happen, only when there is bug which means we should skip this one. + } + if results[0].Error != nil { + importedErr = errors.Wrap(results[0].Error, "failed to import message") + return nil // Call passed but API refused this message, skip this one. + } + importedID = results[0].MessageID + return nil + }) + return +} diff --git a/internal/transfer/provider_pmapi_test.go b/internal/transfer/provider_pmapi_test.go index 6d529fae..76ce8327 100644 --- a/internal/transfer/provider_pmapi_test.go +++ b/internal/transfer/provider_pmapi_test.go @@ -178,17 +178,16 @@ func setupPMAPIClientExpectationForExport(m *mocks) { func setupPMAPIClientExpectationForImport(m *mocks) { m.pmapiClient.EXPECT().KeyRingForAddressID(gomock.Any()).Return(m.keyring, nil).AnyTimes() m.pmapiClient.EXPECT().Import(gomock.Any()).DoAndReturn(func(requests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) { - r.Equal(m.t, 1, len(requests)) - - request := requests[0] - for _, msgID := range []string{"msg1", "msg2"} { - if bytes.Contains(request.Body, []byte(msgID)) { - return []*pmapi.ImportMsgRes{{MessageID: msgID, Error: nil}}, nil + results := []*pmapi.ImportMsgRes{} + for _, request := range requests { + for _, msgID := range []string{"msg1", "msg2"} { + if bytes.Contains(request.Body, []byte(msgID)) { + results = append(results, &pmapi.ImportMsgRes{MessageID: msgID, Error: nil}) + } } } - r.Fail(m.t, "No message found") - return nil, nil - }).Times(2) + return results, nil + }).AnyTimes() } func setupPMAPIClientExpectationForImportDraft(m *mocks) { diff --git a/internal/transfer/provider_pmapi_utils.go b/internal/transfer/provider_pmapi_utils.go index 65bf8817..0633e52e 100644 --- a/internal/transfer/provider_pmapi_utils.go +++ b/internal/transfer/provider_pmapi_utils.go @@ -39,7 +39,7 @@ func (p *PMAPIProvider) ensureConnection(callback func() error) error { return nil } - log.WithField("attempt", i).WithError(callErr).Warning("Call failed, trying reconnect") + log.WithField("attempt", i).WithError(callErr).Warning("API call failed, trying reconnect") err := p.tryReconnect() if err != nil { return err @@ -57,6 +57,7 @@ func (p *PMAPIProvider) tryReconnect() error { } err := p.clientManager.CheckConnection() + log.WithError(err).Debug("Connection check") if err != nil { time.Sleep(pmapiReconnectSleep) previousErr = err diff --git a/internal/transfer/transfer_test.go b/internal/transfer/transfer_test.go index 9a7c0bf7..2d0128f9 100644 --- a/internal/transfer/transfer_test.go +++ b/internal/transfer/transfer_test.go @@ -18,11 +18,10 @@ package transfer import ( - "bytes" "io/ioutil" "testing" - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" transfermocks "github.com/ProtonMail/proton-bridge/internal/transfer/mocks" pmapimocks "github.com/ProtonMail/proton-bridge/pkg/pmapi/mocks" gomock "github.com/golang/mock/gomock" @@ -62,11 +61,12 @@ func newTestKeyring() *crypto.KeyRing { if err != nil { panic(err) } - userKey, err := crypto.ReadArmoredKeyRing(bytes.NewReader(data)) + key, err := crypto.NewKeyFromArmored(string(data)) if err != nil { panic(err) } - if err := userKey.Unlock([]byte("testpassphrase")); err != nil { + userKey, err := crypto.NewKeyRing(key) + if err != nil { panic(err) } return userKey diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 02a46e8b..19c617b4 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -29,9 +29,6 @@ var ( // BuildTime stamp of the build. BuildTime = "" - // AppShortName to make setup. - AppShortName = "bridge" - // DSNSentry client keys to be able to report crashes to Sentry. DSNSentry = "" diff --git a/pkg/message/build.go b/pkg/message/build.go index f25cd09b..290da24e 100644 --- a/pkg/message/build.go +++ b/pkg/message/build.go @@ -27,7 +27,7 @@ import ( "mime/quotedprintable" "net/textproto" - "github.com/ProtonMail/gopenpgp/crypto" + "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/emersion/go-textwrapper" openpgperrors "golang.org/x/crypto/openpgp/errors" diff --git a/pkg/pmapi/clientmanager.go b/pkg/pmapi/clientmanager.go index b351347f..a2a91e88 100644 --- a/pkg/pmapi/clientmanager.go +++ b/pkg/pmapi/clientmanager.go @@ -317,9 +317,8 @@ func (cm *ClientManager) CheckConnection() error { retStatus := make(chan error) retAPI := make(chan error) - // Check protonstatus.com without SSL for performance reasons. vpn_status endpoint is fast and - // returns only OK; this endpoint is not known by the public. We check the connection only. - go checkConnection(client, "http://protonstatus.com/vpn_status", retStatus) + // vpn_status endpoint is fast and returns only OK. We check the connection only. + go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus) // Check of API reachability also uses a fast endpoint. go checkConnection(client, cm.GetRootURL()+"/tests/ping", retAPI) @@ -351,7 +350,7 @@ func (cm *ClientManager) CheckConnection() error { func CheckConnection() error { client := &http.Client{Timeout: time.Second * 10} retStatus := make(chan error) - checkConnection(client, "http://protonstatus.com/vpn_status", retStatus) + go checkConnection(client, "https://protonstatus.com/vpn_status", retStatus) return <-retStatus } diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 2b0a9607..07463d49 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -27,6 +27,9 @@ import ( "runtime" "strings" + "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/importexport" + "github.com/ProtonMail/proton-bridge/pkg/constants" "github.com/kardianos/osext" "github.com/sirupsen/logrus" ) @@ -57,7 +60,6 @@ var ( ) type Updates struct { - appName string version string revision string buildTime string @@ -68,26 +70,44 @@ type Updates struct { installerFileBaseName string // File for initial install or manual reinstall. per goos [exe, dmg, sh]. versionFileBaseName string // Text file containing information about current file. per goos [_linux,_darwin,_windows].json (have .sig file). updateFileBaseName string // File for automatic update. per goos [_linux,_darwin,_windows].tgz (have .sig file). - macAppBundleName string // For update procedure. + linuxFileBaseName string // Prefix of linux package names. + macAppBundleName string // Name of Mac app file in the bundle for update procedure. cachedNewerVersion *VersionInfo // To have info about latest version even when the internet connection drops. } -// New inits Updates struct. -// `appName` should be in camelCase format for file names. For installer files is converted to CamelCase. -func New(appName, version, revision, buildTime, releaseNotes, releaseFixedBugs, updateTempDir string) *Updates { +// NewBridge inits Updates struct for bridge. +func NewBridge(updateTempDir string) *Updates { return &Updates{ - appName: appName, - version: version, - revision: revision, - buildTime: buildTime, - releaseNotes: releaseNotes, - releaseFixedBugs: releaseFixedBugs, + version: constants.Version, + revision: constants.Revision, + buildTime: constants.BuildTime, + releaseNotes: bridge.ReleaseNotes, + releaseFixedBugs: bridge.ReleaseFixedBugs, updateTempDir: updateTempDir, - landingPagePath: appName + "/download", - installerFileBaseName: strings.Title(appName) + "-Installer", + landingPagePath: "bridge/download", + installerFileBaseName: "Bridge-Installer", versionFileBaseName: "current_version", - updateFileBaseName: appName + "_upgrade", - macAppBundleName: "ProtonMail " + strings.Title(appName) + ".app", // For update procedure. + updateFileBaseName: "bridge_upgrade", + linuxFileBaseName: "protonmail-bridge", + macAppBundleName: "ProtonMail Bridge.app", + } +} + +// NewImportExport inits Updates struct for import/export. +func NewImportExport(updateTempDir string) *Updates { + return &Updates{ + version: constants.Version, + revision: constants.Revision, + buildTime: constants.BuildTime, + releaseNotes: importexport.ReleaseNotes, + releaseFixedBugs: importexport.ReleaseFixedBugs, + updateTempDir: updateTempDir, + landingPagePath: "blog/import-export-beta/", + installerFileBaseName: "Import-Export-Installer", + versionFileBaseName: "current_version_ie", + updateFileBaseName: "ie_upgrade", + linuxFileBaseName: "protonmail-import-export", + macAppBundleName: "ProtonMail Import-Export.app", } } @@ -165,7 +185,7 @@ func (u *Updates) getLocalVersion(goos string) VersionInfo { } if goos == "linux" { - pkgName := "protonmail-" + u.appName + pkgName := u.linuxFileBaseName pkgRel := "1" pkgBase := strings.Join([]string{Host, DownloadPath, pkgName}, "/") diff --git a/pkg/updates/updates_test.go b/pkg/updates/updates_test.go index 220864d1..c0e89b40 100644 --- a/pkg/updates/updates_test.go +++ b/pkg/updates/updates_test.go @@ -174,5 +174,11 @@ func TestStartUpgrade(t *testing.T) { } func newTestUpdates(version string) *Updates { - return New("bridge", version, "rev123", "42", "• new feature", "• fixed foo", testUpdateDir) + u := NewBridge(testUpdateDir) + u.version = version + u.revision = "rev123" + u.buildTime = "42" + u.releaseNotes = "• new feature" + u.releaseFixedBugs = "• fixed foo" + return u } diff --git a/test/Makefile b/test/Makefile index be965951..0a512f97 100644 --- a/test/Makefile +++ b/test/Makefile @@ -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. diff --git a/test/api_actions_test.go b/test/api_actions_test.go new file mode 100644 index 00000000..5bab3ddf --- /dev/null +++ b/test/api_actions_test.go @@ -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 . + +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 +} diff --git a/test/api_checks_test.go b/test/api_checks_test.go index 089f7767..b2f6d456 100644 --- a/test/api_checks_test.go +++ b/test/api_checks_test.go @@ -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 +} diff --git a/test/api_setup_test.go b/test/api_setup_test.go new file mode 100644 index 00000000..71232f87 --- /dev/null +++ b/test/api_setup_test.go @@ -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 . + +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 +} diff --git a/test/bdd_test.go b/test/bdd_test.go index 17104b51..0b072183 100644 --- a/test/bdd_test.go +++ b/test/bdd_test.go @@ -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() diff --git a/test/benchmarks/bench_test.go b/test/benchmarks/bench_test.go index c157dc3b..6e6486ea 100644 --- a/test/benchmarks/bench_test.go +++ b/test/benchmarks/bench_test.go @@ -27,7 +27,7 @@ import ( ) func benchTestContext() (*context.TestContext, *mocks.IMAPClient) { - ctx := context.New() + ctx := context.New("bridge") username := "user" account := ctx.GetTestAccount(username) diff --git a/test/bridge_actions_test.go b/test/bridge_actions_test.go index 0c2bb449..5b93ca4d 100644 --- a/test/bridge_actions_test.go +++ b/test/bridge_actions_test.go @@ -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) -} diff --git a/test/common_checks_test.go b/test/common_checks_test.go new file mode 100644 index 00000000..bf1690e5 --- /dev/null +++ b/test/common_checks_test.go @@ -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 . + +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() +} diff --git a/test/context/bridge.go b/test/context/bridge.go index 2d03de8b..711b5f51 100644 --- a/test/context/bridge.go +++ b/test/context/bridge.go @@ -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 -} diff --git a/test/context/config.go b/test/context/config.go index 71dfe89e..1b2e646e 100644 --- a/test/context/config.go +++ b/test/context/config.go @@ -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") } diff --git a/test/context/context.go b/test/context/context.go index e6db35c7..b614625c 100644 --- a/test/context/context.go +++ b/test/context/context.go @@ -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 +} diff --git a/test/context/importexport.go b/test/context/importexport.go new file mode 100644 index 00000000..6b468d52 --- /dev/null +++ b/test/context/importexport.go @@ -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 . + +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) +} diff --git a/test/context/pmapi_controller.go b/test/context/pmapi_controller.go index 769771fe..ea36af35 100644 --- a/test/context/pmapi_controller.go +++ b/test/context/pmapi_controller.go @@ -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 diff --git a/test/context/transfer.go b/test/context/transfer.go new file mode 100644 index 00000000..a2e792ae --- /dev/null +++ b/test/context/transfer.go @@ -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 . + +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 +} diff --git a/test/context/bridge_user.go b/test/context/users.go similarity index 90% rename from test/context/bridge_user.go rename to test/context/users.go index cdfcbd02..934fd213 100644 --- a/test/context/bridge_user.go +++ b/test/context/users.go @@ -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") } diff --git a/test/fakeapi/controller_control.go b/test/fakeapi/controller_control.go index fb4ac497..dafaa67c 100644 --- a/test/fakeapi/controller_control.go +++ b/test/fakeapi/controller_control.go @@ -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 +} diff --git a/test/fakeapi/keyring_userKey b/test/fakeapi/keyring_userKey new file mode 100644 index 00000000..976d2be2 --- /dev/null +++ b/test/fakeapi/keyring_userKey @@ -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----- \ No newline at end of file diff --git a/test/fakeapi/messages.go b/test/fakeapi/messages.go index 3fa76943..557fdf85 100644 --- a/test/fakeapi/messages.go +++ b/test/fakeapi/messages.go @@ -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) diff --git a/test/features/imap/auth.feature b/test/features/bridge/imap/auth.feature similarity index 97% rename from test/features/imap/auth.feature rename to test/features/bridge/imap/auth.feature index 8898b50a..3191d1e3 100644 --- a/test/features/imap/auth.feature +++ b/test/features/bridge/imap/auth.feature @@ -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" diff --git a/test/features/imap/idle/basic.feature b/test/features/bridge/imap/idle/basic.feature similarity index 100% rename from test/features/imap/idle/basic.feature rename to test/features/bridge/imap/idle/basic.feature diff --git a/test/features/imap/idle/two_users.feature b/test/features/bridge/imap/idle/two_users.feature similarity index 100% rename from test/features/imap/idle/two_users.feature rename to test/features/bridge/imap/idle/two_users.feature diff --git a/test/features/imap/mailbox/create.feature b/test/features/bridge/imap/mailbox/create.feature similarity index 100% rename from test/features/imap/mailbox/create.feature rename to test/features/bridge/imap/mailbox/create.feature diff --git a/test/features/imap/mailbox/delete.feature b/test/features/bridge/imap/mailbox/delete.feature similarity index 100% rename from test/features/imap/mailbox/delete.feature rename to test/features/bridge/imap/mailbox/delete.feature diff --git a/test/features/imap/mailbox/info.feature b/test/features/bridge/imap/mailbox/info.feature similarity index 100% rename from test/features/imap/mailbox/info.feature rename to test/features/bridge/imap/mailbox/info.feature diff --git a/test/features/imap/mailbox/list.feature b/test/features/bridge/imap/mailbox/list.feature similarity index 100% rename from test/features/imap/mailbox/list.feature rename to test/features/bridge/imap/mailbox/list.feature diff --git a/test/features/imap/mailbox/rename.feature b/test/features/bridge/imap/mailbox/rename.feature similarity index 100% rename from test/features/imap/mailbox/rename.feature rename to test/features/bridge/imap/mailbox/rename.feature diff --git a/test/features/imap/mailbox/select.feature b/test/features/bridge/imap/mailbox/select.feature similarity index 100% rename from test/features/imap/mailbox/select.feature rename to test/features/bridge/imap/mailbox/select.feature diff --git a/test/features/imap/mailbox/status.feature b/test/features/bridge/imap/mailbox/status.feature similarity index 100% rename from test/features/imap/mailbox/status.feature rename to test/features/bridge/imap/mailbox/status.feature diff --git a/test/features/imap/message/copy.feature b/test/features/bridge/imap/message/copy.feature similarity index 100% rename from test/features/imap/message/copy.feature rename to test/features/bridge/imap/message/copy.feature diff --git a/test/features/imap/message/create.feature b/test/features/bridge/imap/message/create.feature similarity index 100% rename from test/features/imap/message/create.feature rename to test/features/bridge/imap/message/create.feature diff --git a/test/features/imap/message/delete.feature b/test/features/bridge/imap/message/delete.feature similarity index 100% rename from test/features/imap/message/delete.feature rename to test/features/bridge/imap/message/delete.feature diff --git a/test/features/imap/message/fetch.feature b/test/features/bridge/imap/message/fetch.feature similarity index 100% rename from test/features/imap/message/fetch.feature rename to test/features/bridge/imap/message/fetch.feature diff --git a/test/features/imap/message/import.feature b/test/features/bridge/imap/message/import.feature similarity index 100% rename from test/features/imap/message/import.feature rename to test/features/bridge/imap/message/import.feature diff --git a/test/features/imap/message/move.feature b/test/features/bridge/imap/message/move.feature similarity index 100% rename from test/features/imap/message/move.feature rename to test/features/bridge/imap/message/move.feature diff --git a/test/features/imap/message/search.feature b/test/features/bridge/imap/message/search.feature similarity index 100% rename from test/features/imap/message/search.feature rename to test/features/bridge/imap/message/search.feature diff --git a/test/features/imap/message/update.feature b/test/features/bridge/imap/message/update.feature similarity index 100% rename from test/features/imap/message/update.feature rename to test/features/bridge/imap/message/update.feature diff --git a/test/features/smtp/auth.feature b/test/features/bridge/smtp/auth.feature similarity index 100% rename from test/features/smtp/auth.feature rename to test/features/bridge/smtp/auth.feature diff --git a/test/features/smtp/send/bcc.feature b/test/features/bridge/smtp/send/bcc.feature similarity index 100% rename from test/features/smtp/send/bcc.feature rename to test/features/bridge/smtp/send/bcc.feature diff --git a/test/features/smtp/send/failures.feature b/test/features/bridge/smtp/send/failures.feature similarity index 100% rename from test/features/smtp/send/failures.feature rename to test/features/bridge/smtp/send/failures.feature diff --git a/test/features/smtp/send/html.feature b/test/features/bridge/smtp/send/html.feature similarity index 100% rename from test/features/smtp/send/html.feature rename to test/features/bridge/smtp/send/html.feature diff --git a/test/features/smtp/send/html_att.feature b/test/features/bridge/smtp/send/html_att.feature similarity index 100% rename from test/features/smtp/send/html_att.feature rename to test/features/bridge/smtp/send/html_att.feature diff --git a/test/features/smtp/send/plain.feature b/test/features/bridge/smtp/send/plain.feature similarity index 100% rename from test/features/smtp/send/plain.feature rename to test/features/bridge/smtp/send/plain.feature diff --git a/test/features/smtp/send/plain_att.feature b/test/features/bridge/smtp/send/plain_att.feature similarity index 100% rename from test/features/smtp/send/plain_att.feature rename to test/features/bridge/smtp/send/plain_att.feature diff --git a/test/features/smtp/send/same_message.feature b/test/features/bridge/smtp/send/same_message.feature similarity index 100% rename from test/features/smtp/send/same_message.feature rename to test/features/bridge/smtp/send/same_message.feature diff --git a/test/features/smtp/send/two_messages.feature b/test/features/bridge/smtp/send/two_messages.feature similarity index 100% rename from test/features/smtp/send/two_messages.feature rename to test/features/bridge/smtp/send/two_messages.feature diff --git a/test/features/bridge/addressmode.feature b/test/features/bridge/users/addressmode.feature similarity index 98% rename from test/features/bridge/addressmode.feature rename to test/features/bridge/users/addressmode.feature index f99c4f03..be8e25f8 100644 --- a/test/features/bridge/addressmode.feature +++ b/test/features/bridge/users/addressmode.feature @@ -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 | diff --git a/test/features/bridge/deleteuser.feature b/test/features/bridge/users/delete.feature similarity index 62% rename from test/features/bridge/deleteuser.feature rename to test/features/bridge/users/delete.feature index 61fa6d3b..770ddf6d 100644 --- a/test/features/bridge/deleteuser.feature +++ b/test/features/bridge/users/delete.feature @@ -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" diff --git a/test/features/bridge/login.feature b/test/features/bridge/users/login.feature similarity index 59% rename from test/features/bridge/login.feature rename to test/features/bridge/users/login.feature index a74cd1d1..8f89bee1 100644 --- a/test/features/bridge/login.feature +++ b/test/features/bridge/users/login.feature @@ -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 diff --git a/test/features/bridge/relogin.feature b/test/features/bridge/users/relogin.feature similarity index 70% rename from test/features/bridge/relogin.feature rename to test/features/bridge/users/relogin.feature index aeb4dd06..e0b43d79 100644 --- a/test/features/bridge/relogin.feature +++ b/test/features/bridge/users/relogin.feature @@ -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 diff --git a/test/features/bridge/sync.feature b/test/features/bridge/users/sync.feature similarity index 97% rename from test/features/bridge/sync.feature rename to test/features/bridge/users/sync.feature index 6824200d..09c616d8 100644 --- a/test/features/bridge/sync.feature +++ b/test/features/bridge/users/sync.feature @@ -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 | diff --git a/test/features/ie/transfer/export_eml.feature b/test/features/ie/transfer/export_eml.feature new file mode 100644 index 00000000..50a7917c --- /dev/null +++ b/test/features/ie/transfer/export_eml.feature @@ -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 | diff --git a/test/features/ie/transfer/export_mbox.feature b/test/features/ie/transfer/export_mbox.feature new file mode 100644 index 00000000..a0143eed --- /dev/null +++ b/test/features/ie/transfer/export_mbox.feature @@ -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 | diff --git a/test/features/ie/transfer/import_eml.feature b/test/features/ie/transfer/import_eml.feature new file mode 100644 index 00000000..5bd1cae9 --- /dev/null +++ b/test/features/ie/transfer/import_eml.feature @@ -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 + To: Internal Bridge + + 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 diff --git a/test/features/ie/transfer/import_export.feature b/test/features/ie/transfer/import_export.feature new file mode 100644 index 00000000..5cd5d29e --- /dev/null +++ b/test/features/ie/transfer/import_export.feature @@ -0,0 +1,49 @@ +Feature: Import/Export + 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 diff --git a/test/features/ie/transfer/import_imap.feature b/test/features/ie/transfer/import_imap.feature new file mode 100644 index 00000000..f3766348 --- /dev/null +++ b/test/features/ie/transfer/import_imap.feature @@ -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 + To: Internal Bridge + + 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 diff --git a/test/features/ie/transfer/import_mbox.feature b/test/features/ie/transfer/import_mbox.feature new file mode 100644 index 00000000..90388992 --- /dev/null +++ b/test/features/ie/transfer/import_mbox.feature @@ -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 + To: Internal Bridge + + 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 diff --git a/test/features/ie/users/delete.feature b/test/features/ie/users/delete.feature new file mode 100644 index 00000000..34ffe4b0 --- /dev/null +++ b/test/features/ie/users/delete.feature @@ -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" diff --git a/test/features/ie/users/login.feature b/test/features/ie/users/login.feature new file mode 100644 index 00000000..e74cc7b2 --- /dev/null +++ b/test/features/ie/users/login.feature @@ -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 diff --git a/test/features/ie/users/relogin.feature b/test/features/ie/users/relogin.feature new file mode 100644 index 00000000..d55e3eea --- /dev/null +++ b/test/features/ie/users/relogin.feature @@ -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 diff --git a/test/liveapi/messages.go b/test/liveapi/messages.go index f064cffd..1d682aaa 100644 --- a/test/liveapi/messages.go +++ b/test/liveapi/messages.go @@ -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 +} diff --git a/test/mocks/imap.go b/test/mocks/imap_client.go similarity index 100% rename from test/mocks/imap.go rename to test/mocks/imap_client.go diff --git a/test/mocks/imap_response.go b/test/mocks/imap_response.go index 75c36e7c..5945f405 100644 --- a/test/mocks/imap_response.go +++ b/test/mocks/imap_response.go @@ -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() diff --git a/test/mocks/imap_server.go b/test/mocks/imap_server.go new file mode 100644 index 00000000..b47f5ce2 --- /dev/null +++ b/test/mocks/imap_server.go @@ -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 . + +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") +} diff --git a/test/store_checks_test.go b/test/store_checks_test.go index f90ea344..bb5aebcd 100644 --- a/test/store_checks_test.go +++ b/test/store_checks_test.go @@ -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 { diff --git a/test/store_setup_test.go b/test/store_setup_test.go index 9db508e0..ff456c36 100644 --- a/test/store_setup_test.go +++ b/test/store_setup_test.go @@ -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) } diff --git a/test/transfer_actions_test.go b/test/transfer_actions_test.go new file mode 100644 index 00000000..ae049c2e --- /dev/null +++ b/test/transfer_actions_test.go @@ -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 . + +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) +} diff --git a/test/transfer_checks_test.go b/test/transfer_checks_test.go new file mode 100644 index 00000000..7452f8eb --- /dev/null +++ b/test/transfer_checks_test.go @@ -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 . + +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] } diff --git a/test/transfer_setup_test.go b/test/transfer_setup_test.go new file mode 100644 index 00000000..b52bec46 --- /dev/null +++ b/test/transfer_setup_test.go @@ -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 . + +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") +} diff --git a/test/users_actions_test.go b/test/users_actions_test.go new file mode 100644 index 00000000..98fa229c --- /dev/null +++ b/test/users_actions_test.go @@ -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 . + +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) +} diff --git a/test/bridge_checks_test.go b/test/users_checks_test.go similarity index 77% rename from test/bridge_checks_test.go rename to test/users_checks_test.go index 980fecba..b816c1dc 100644 --- a/test/bridge_checks_test.go +++ b/test/users_checks_test.go @@ -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() } diff --git a/test/bridge_setup_test.go b/test/users_setup_test.go similarity index 89% rename from test/bridge_setup_test.go rename to test/users_setup_test.go index 261d27a8..495e574c 100644 --- a/test/bridge_setup_test.go +++ b/test/users_setup_test.go @@ -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 }