We build too many walls and not enough bridges

This commit is contained in:
Jakub
2020-04-08 12:59:16 +02:00
commit 17f4d6097a
494 changed files with 62753 additions and 0 deletions

57
test/context/accounts.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"os"
"github.com/ProtonMail/proton-bridge/test/accounts"
)
func newTestAccounts() *accounts.TestAccounts {
envFile := os.Getenv("TEST_ACCOUNTS")
data, err := accounts.Load(envFile)
if err != nil {
panic(err)
}
return data
}
func (ctx *TestContext) GetTestAccount(bddUserID string) *accounts.TestAccount {
return ctx.testAccounts.GetTestAccount(bddUserID)
}
func (ctx *TestContext) GetTestAccountWithAddress(bddUserID, addressID string) *accounts.TestAccount {
return ctx.testAccounts.GetTestAccountWithAddress(bddUserID, addressID)
}
func (ctx *TestContext) EnsureAddressID(bddUserID, addressOrAddressTestID string) string {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return addressOrAddressTestID
}
return account.EnsureAddressID(addressOrAddressTestID)
}
func (ctx *TestContext) EnsureAddress(bddUserID, addressOrAddressTestID string) string {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
return addressOrAddressTestID
}
return account.EnsureAddress(addressOrAddressTestID)
}

42
test/context/bddt.go Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)
type bddT struct {
err *multierror.Error
}
func (t *bddT) Errorf(msg string, args ...interface{}) {
err := fmt.Errorf(msg, args...)
t.err = multierror.Append(t.err, err)
}
func (t *bddT) FailNow() {
t.err = multierror.Append(t.err, errors.New("failed by calling FailNow"))
}
func (t *bddT) getErrors() error {
return t.err.ErrorOrNil()
}

84
test/context/bridge.go Normal file
View File

@ -0,0 +1,84 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"os"
"runtime"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
// GetBridge returns bridge instance.
func (ctx *TestContext) GetBridge() *bridge.Bridge {
return ctx.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.
func (ctx *TestContext) withBridgeInstance() {
pmapiFactory := func(userID string) bridge.PMAPIProvider {
return ctx.pmapiController.GetClient(userID)
}
ctx.bridge = newBridgeInstance(ctx.t, ctx.cfg, ctx.credStore, ctx.listener, pmapiFactory)
ctx.addCleanupChecked(ctx.bridge.ClearData, "Cleaning bridge data")
}
// RestartBridge closes store for each user and recreates a bridge instance the same way as `withBridgeInstance`.
// NOTE: This is a very problematic method. It doesn't stop the goroutines doing the event loop and the sync.
// These goroutines can continue to run and can cause problems or unexpected behaviour (especially
// regarding authorization, because if an auth fails, it will log out the user).
// To truly emulate bridge restart, we need a way to immediately stop those goroutines.
// I have added a channel that waits up to one second for the event loop to stop, but that isn't great.
func (ctx *TestContext) RestartBridge() error {
for _, user := range ctx.bridge.GetUsers() {
_ = user.GetStore().Close()
}
ctx.withBridgeInstance()
return nil
}
// newBridgeInstance creates a new bridge instance configured to use the given config/credstore.
func newBridgeInstance(
t *bddT,
cfg *fakeConfig,
credStore bridge.CredentialsStorer,
eventListener listener.Listener,
pmapiFactory bridge.PMAPIProviderFactory,
) *bridge.Bridge {
version := os.Getenv("VERSION")
bridge.UpdateCurrentUserAgent(version, runtime.GOOS, "", "")
panicHandler := &panicHandler{t: t}
pref := preferences.New(cfg)
return bridge.New(cfg, pref, panicHandler, eventListener, version, pmapiFactory, credStore)
}
// SetLastBridgeError sets the last error that occurred while executing a bridge action.
func (ctx *TestContext) SetLastBridgeError(err error) {
ctx.bridgeLastError = err
}
// GetLastBridgeError returns the last error that occurred while executing a bridge action.
func (ctx *TestContext) GetLastBridgeError() error {
return ctx.bridgeLastError
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"bytes"
"io/ioutil"
"runtime/pprof"
)
type panicHandler struct {
t *bddT
}
func newPanicHandler(t *bddT) *panicHandler {
return &panicHandler{
t: t,
}
}
// HandlePanic makes the panicHandler implement the panicHandler interface for bridge.
func (ph *panicHandler) HandlePanic() {
r := recover()
if r != nil {
ph.t.Errorf("panic: %s", r)
r := bytes.NewBufferString("")
_ = pprof.Lookup("goroutine").WriteTo(r, 2)
b, err := ioutil.ReadAll(r)
ph.t.Errorf("pprof details: %s %s", err, b)
ph.t.FailNow()
}
}

148
test/context/bridge_user.go Normal file
View File

@ -0,0 +1,148 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"fmt"
"math/rand"
"path/filepath"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
// 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)
if err != nil {
return errors.Wrap(err, "failed to login")
}
if auth.HasTwoFactor() {
if _, err := client.Auth2FA("2fa code", auth); err != nil {
return errors.Wrap(err, "failed to login with 2FA")
}
}
user, err := ctx.bridge.FinishLogin(client, auth, mailboxPassword)
if err != nil {
return errors.Wrap(err, "failed to finish login")
}
ctx.addCleanupChecked(user.Logout, "Logging out user")
return
}
// GetUser retrieves the bridge user matching the given query string.
func (ctx *TestContext) GetUser(username string) (*bridge.User, error) {
return ctx.bridge.GetUser(username)
}
// GetStore retrieves the store for given username.
func (ctx *TestContext) GetStore(username string) (*store.Store, error) {
user, err := ctx.GetUser(username)
if err != nil {
return nil, err
}
return user.GetStore(), nil
}
// GetStoreAddress retrieves the store address for given username and addressID.
func (ctx *TestContext) GetStoreAddress(username, addressID string) (*store.Address, error) {
store, err := ctx.GetStore(username)
if err != nil {
return nil, err
}
return store.GetAddress(addressID)
}
// GetStoreMailbox retrieves the store mailbox for given username, address ID and mailbox name.
func (ctx *TestContext) GetStoreMailbox(username, addressID, mailboxName string) (*store.Mailbox, error) {
address, err := ctx.GetStoreAddress(username, addressID)
if err != nil {
return nil, err
}
return address.GetMailbox(mailboxName)
}
// GetDatabaseFilePath returns the file path of the user's store file.
func (ctx *TestContext) GetDatabaseFilePath(userID string) string {
// We cannot use store to get information because we need to check db file also when user is deleted from bridge.
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(ctx.cfg.GetDBDir(), fileName)
}
// WaitForSync waits for sync to be done.
func (ctx *TestContext) WaitForSync(username string) error {
store, err := ctx.GetStore(username)
if err != nil {
return err
}
// First wait for ongoing sync to be done before starting and waiting for new one.
ctx.eventuallySyncIsFinished(store)
store.TestSync()
ctx.eventuallySyncIsFinished(store)
return nil
}
func (ctx *TestContext) eventuallySyncIsFinished(store *store.Store) {
assert.Eventually(ctx.t, func() bool { return !store.TestIsSyncRunning() }, 30*time.Second, 10*time.Millisecond)
}
// EventuallySyncIsFinishedForUsername will wait until sync is finished or
// deadline is reached see eventuallySyncIsFinished for timing
func (ctx *TestContext) EventuallySyncIsFinishedForUsername(username string) {
store, err := ctx.GetStore(username)
assert.Nil(ctx.t, err)
ctx.eventuallySyncIsFinished(store)
}
// LogoutUser logs out the given user.
func (ctx *TestContext) LogoutUser(query string) (err error) {
user, err := ctx.bridge.GetUser(query)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
if err = user.Logout(); err != nil {
return errors.Wrap(err, "failed to logout user")
}
return
}
// DeleteUser deletes the given user.
func (ctx *TestContext) DeleteUser(query string, deleteStore bool) (err error) {
user, err := ctx.bridge.GetUser(query)
if err != nil {
return errors.Wrap(err, "failed to get user")
}
if err = ctx.bridge.DeleteUser(user.ID(), deleteStore); err != nil {
err = errors.Wrap(err, "failed to delete user")
}
return
}

87
test/context/cleaner.go Normal file
View File

@ -0,0 +1,87 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"fmt"
"path/filepath"
"runtime"
"github.com/stretchr/testify/require"
)
// Cleaner is a test step that cleans up some stuff post-test.
type Cleaner struct {
// file is the filename of the caller.
file string
// lineNumber is the line number of the caller.
lineNumber int
// label is a descriptive label of the step being performed.
label string
// ctx is the TestContext on which the step operates.
ctx *TestContext
// cleanup is callback doing clean up.
cleanup func()
}
// Execute runs the cleaner operation.
func (c *Cleaner) Execute() {
c.ctx.logger.WithField("from", c.From()).Info(c.label)
c.cleanup()
}
// From returns the filepath and line number of the place where this cleaner was created.
func (c *Cleaner) From() string {
return fmt.Sprintf("%v:%v", c.file, c.lineNumber)
}
// addCleanup adds an operation to be performed at the end of the test.
func (ctx *TestContext) addCleanup(c func(), label string) {
cleaner := &Cleaner{
cleanup: c,
label: label,
ctx: ctx,
}
if _, file, line, ok := runtime.Caller(1); ok {
cleaner.file, cleaner.lineNumber = filepath.Base(file), line
}
ctx.cleanupSteps = append(ctx.cleanupSteps, cleaner)
}
// addCleanupChecked adds an operation that may return an error to be performed at the end of the test.
// If the operation fails, the test is failed.
func (ctx *TestContext) addCleanupChecked(f func() error, label string) {
checkedFunction := func() {
err := f()
require.NoError(ctx.t, err)
}
cleaner := &Cleaner{
cleanup: checkedFunction,
label: label,
ctx: ctx,
}
if _, file, line, ok := runtime.Caller(1); ok {
cleaner.file, cleaner.lineNumber = filepath.Base(file), line
}
ctx.cleanupSteps = append(ctx.cleanupSteps, cleaner)
}

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

@ -0,0 +1,91 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type fakeConfig struct {
dir string
tm *pmapi.TokenManager
}
// newFakeConfig creates a temporary folder for files.
// It's expected the test calls `ClearData` before finish to remove it from the file system.
func newFakeConfig() *fakeConfig {
dir, err := ioutil.TempDir("", "example")
if err != nil {
panic(err)
}
return &fakeConfig{
dir: dir,
tm: pmapi.NewTokenManager(),
}
}
func (c *fakeConfig) ClearData() error {
return os.RemoveAll(c.dir)
}
func (c *fakeConfig) GetAPIConfig() *pmapi.ClientConfig {
return &pmapi.ClientConfig{
AppVersion: "Bridge_" + os.Getenv("VERSION"),
ClientID: "bridge",
// TokenManager should not be required, but PMAPI still doesn't handle not-set cases everywhere.
TokenManager: c.tm,
}
}
func (c *fakeConfig) GetDBDir() string {
return c.dir
}
func (c *fakeConfig) GetLogDir() string {
return c.dir
}
func (c *fakeConfig) GetLogPrefix() string {
return "test"
}
func (c *fakeConfig) GetPreferencesPath() string {
return filepath.Join(c.dir, "prefs.json")
}
func (c *fakeConfig) GetTLSCertPath() string {
return filepath.Join(c.dir, "cert.pem")
}
func (c *fakeConfig) GetTLSKeyPath() string {
return filepath.Join(c.dir, "key.pem")
}
func (c *fakeConfig) GetEventsPath() string {
return filepath.Join(c.dir, "events.json")
}
func (c *fakeConfig) GetIMAPCachePath() string {
return filepath.Join(c.dir, "user_info.json")
}
func (c *fakeConfig) GetDefaultAPIPort() int {
return 21042
}
func (c *fakeConfig) GetDefaultIMAPPort() int {
return 21100 + rand.Intn(100)
}
func (c *fakeConfig) GetDefaultSMTPPort() int {
return 21200 + rand.Intn(100)
}

118
test/context/context.go Normal file
View File

@ -0,0 +1,118 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Package context allows integration tests to be written in a fluent, english-like way.
package context
import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/test/accounts"
"github.com/ProtonMail/proton-bridge/test/mocks"
"github.com/sirupsen/logrus"
)
type server interface {
ListenAndServe()
Close()
}
// TestContext manages a bridge test (mocked API, bridge instance, IMAP/SMTP servers, setup steps).
type TestContext struct {
// Base setup for the whole bridge (core & imap & smtp).
t *bddT
cfg *fakeConfig
listener listener.Listener
pmapiController PMAPIController // pmapiController is used to create pmapi clients (either real or fake) and control server state.
testAccounts *accounts.TestAccounts
// Bridge core related variables.
bridge *bridge.Bridge
bridgeLastError error
credStore bridge.CredentialsStorer
// IMAP related variables.
imapAddr string
imapServer server
imapClients map[string]*mocks.IMAPClient
imapLastResponses map[string]*mocks.IMAPResponse
// SMTP related variables.
smtpAddr string
smtpServer server
smtpClients map[string]*mocks.SMTPClient
smtpLastResponses map[string]*mocks.SMTPResponse
// These are the cleanup steps executed when Cleanup() is called.
cleanupSteps []*Cleaner
// logger allows logging of test labels to be handled differently (silenced/diverted/whatever).
logger logrus.FieldLogger
}
// New returns a new test TestContext.
func New() *TestContext {
setLogrusVerbosityFromEnv()
cfg := newFakeConfig()
ctx := &TestContext{
t: &bddT{},
cfg: cfg,
listener: listener.New(),
pmapiController: newPMAPIController(),
testAccounts: newTestAccounts(),
credStore: newFakeCredStore(),
imapClients: make(map[string]*mocks.IMAPClient),
imapLastResponses: make(map[string]*mocks.IMAPResponse),
smtpClients: make(map[string]*mocks.SMTPClient),
smtpLastResponses: make(map[string]*mocks.SMTPResponse),
logger: logrus.StandardLogger(),
}
// 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()
return ctx
}
// Cleanup runs through all cleanup steps.
// This can be a deferred call so that it is run even if the test steps failed the test.
func (ctx *TestContext) Cleanup() *TestContext {
for _, cleanup := range ctx.cleanupSteps {
cleanup.Execute()
}
return ctx
}
// GetPMAPIController returns API controller, either fake or live.
func (ctx *TestContext) GetPMAPIController() PMAPIController {
return ctx.pmapiController
}
// GetTestingT returns testing.T compatible struct.
func (ctx *TestContext) GetTestingT() *bddT { //nolint[golint]
return ctx.t
}
// GetTestingError returns error if test failed by using custom TestingT.
func (ctx *TestContext) GetTestingError() error {
return ctx.t.getErrors()
}

102
test/context/credentials.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"strings"
"github.com/ProtonMail/proton-bridge/internal/bridge/credentials"
)
// bridgePassword is password to be used for IMAP or SMTP under tests.
const bridgePassword = "bridgepassword"
type fakeCredStore struct {
credentials map[string]*credentials.Credentials
}
// newFakeCredStore returns a fake credentials store (optionally configured with the given credentials).
func newFakeCredStore(initCreds ...*credentials.Credentials) (c *fakeCredStore) {
c = &fakeCredStore{credentials: map[string]*credentials.Credentials{}}
for _, creds := range initCreds {
if creds == nil {
continue
}
c.credentials[creds.UserID] = &credentials.Credentials{}
*c.credentials[creds.UserID] = *creds
}
return
}
func (c *fakeCredStore) List() (userIDs []string, err error) {
keys := []string{}
for key := range c.credentials {
keys = append(keys, key)
}
return keys, nil
}
func (c *fakeCredStore) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error) {
bridgePassword := bridgePassword
if c, ok := c.credentials[userID]; ok {
bridgePassword = c.BridgePassword
}
c.credentials[userID] = &credentials.Credentials{
UserID: userID,
Name: userName,
Emails: strings.Join(emails, ";"),
APIToken: apiToken,
MailboxPassword: mailboxPassword,
BridgePassword: bridgePassword,
IsCombinedAddressMode: true, // otherwise by default starts in split mode
}
return c.Get(userID)
}
func (c *fakeCredStore) Get(userID string) (*credentials.Credentials, error) {
return c.credentials[userID], nil
}
func (c *fakeCredStore) SwitchAddressMode(userID string) error {
return nil
}
func (c *fakeCredStore) UpdateEmails(userID string, emails []string) error {
return nil
}
func (c *fakeCredStore) UpdateToken(userID, apiToken string) error {
creds, err := c.Get(userID)
if err != nil {
return err
}
creds.APIToken = apiToken
return nil
}
func (c *fakeCredStore) Logout(userID string) error {
c.credentials[userID].APIToken = ""
c.credentials[userID].MailboxPassword = ""
return nil
}
func (c *fakeCredStore) Delete(userID string) error {
delete(c.credentials, userID)
return nil
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"os"
"time"
)
const (
EnvName = "TEST_ENV"
EnvFake = "fake"
EnvLive = "live"
)
func (ctx *TestContext) EventLoopTimeout() time.Duration {
switch os.Getenv(EnvName) {
case EnvFake:
return 5 * time.Second
case EnvLive:
return 60 * time.Second
default:
panic("unknown env")
}
}

80
test/context/imap.go Normal file
View File

@ -0,0 +1,80 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"fmt"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/imap"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/test/mocks"
"github.com/stretchr/testify/require"
)
// GetIMAPClient gets the imap client by name; if it doesn't exist yet it creates it.
func (ctx *TestContext) GetIMAPClient(handle string) *mocks.IMAPClient {
if client, ok := ctx.imapClients[handle]; ok {
return client
}
return ctx.newIMAPClient(handle)
}
func (ctx *TestContext) newIMAPClient(handle string) *mocks.IMAPClient {
ctx.withIMAPServer()
client := mocks.NewIMAPClient(ctx.t, handle, ctx.imapAddr)
ctx.imapClients[handle] = client
ctx.addCleanup(client.Close, "Closing IMAP client")
return client
}
// withIMAPServer starts an imap server and connects it to the bridge instance.
// Every TestContext has this by default and thus this doesn't need to be exported.
func (ctx *TestContext) withIMAPServer() {
if ctx.imapServer != nil {
return
}
ph := newPanicHandler(ctx.t)
pref := preferences.New(ctx.cfg)
port := pref.GetInt(preferences.IMAPPortKey)
tls, _ := config.GetTLSConfig(ctx.cfg)
backend := imap.NewIMAPBackend(ph, ctx.listener, ctx.cfg, ctx.bridge)
server := imap.NewIMAPServer(true, true, port, tls, backend, ctx.listener)
go server.ListenAndServe()
require.NoError(ctx.t, waitForPort(port, 5*time.Second))
ctx.imapServer = server
ctx.imapAddr = fmt.Sprintf("%v:%v", bridge.Host, port)
ctx.addCleanup(ctx.imapServer.Close, "Closing IMAP server")
}
// SetIMAPLastResponse sets the last IMAP response that was received.
func (ctx *TestContext) SetIMAPLastResponse(handle string, resp *mocks.IMAPResponse) {
ctx.imapLastResponses[handle] = resp
}
// GetIMAPLastResponse returns the last IMAP response that was received.
func (ctx *TestContext) GetIMAPLastResponse(handle string) *mocks.IMAPResponse {
return ctx.imapLastResponses[handle]
}

View File

@ -0,0 +1,84 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"os"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/test/fakeapi"
"github.com/ProtonMail/proton-bridge/test/liveapi"
)
type PMAPIController interface {
GetClient(userID string) bridge.PMAPIProvider
TurnInternetConnectionOff()
TurnInternetConnectionOn()
AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error
AddUserLabel(username string, label *pmapi.Label) error
GetLabelIDs(username string, labelNames []string) ([]string, error)
AddUserMessage(username string, message *pmapi.Message) error
GetMessageID(username, messageIndex string) string
PrintCalls()
WasCalled(method, path string, expectedRequest []byte) bool
GetCalls(method, path string) [][]byte
}
func newPMAPIController() PMAPIController {
switch os.Getenv(EnvName) {
case EnvFake:
return newFakePMAPIController()
case EnvLive:
return newLivePMAPIController()
default:
panic("unknown env")
}
}
func newFakePMAPIController() PMAPIController {
return newFakePMAPIControllerWrap(fakeapi.NewController())
}
type fakePMAPIControllerWrap struct {
*fakeapi.Controller
}
func newFakePMAPIControllerWrap(controller *fakeapi.Controller) PMAPIController {
return &fakePMAPIControllerWrap{Controller: controller}
}
func (s *fakePMAPIControllerWrap) GetClient(userID string) bridge.PMAPIProvider {
return s.Controller.GetClient(userID)
}
func newLivePMAPIController() PMAPIController {
return newLiveAPIControllerWrap(liveapi.NewController())
}
type liveAPIControllerWrap struct {
*liveapi.Controller
}
func newLiveAPIControllerWrap(controller *liveapi.Controller) PMAPIController {
return &liveAPIControllerWrap{Controller: controller}
}
func (s *liveAPIControllerWrap) GetClient(userID string) bridge.PMAPIProvider {
return s.Controller.GetClient(userID)
}

81
test/context/smtp.go Normal file
View File

@ -0,0 +1,81 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"fmt"
"time"
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/smtp"
"github.com/ProtonMail/proton-bridge/pkg/config"
"github.com/ProtonMail/proton-bridge/test/mocks"
"github.com/stretchr/testify/require"
)
// GetSMTPClient gets the smtp client by name; if it doesn't exist yet it creates it.
func (ctx *TestContext) GetSMTPClient(handle string) *mocks.SMTPClient {
if client, ok := ctx.smtpClients[handle]; ok {
return client
}
return ctx.newSMTPClient(handle)
}
func (ctx *TestContext) newSMTPClient(handle string) *mocks.SMTPClient {
ctx.withSMTPServer()
client := mocks.NewSMTPClient(ctx.t, handle, ctx.smtpAddr)
ctx.smtpClients[handle] = client
ctx.addCleanup(client.Close, "Closing SMTP client")
return client
}
// withSMTPServer starts an smtp server and connects it to the bridge instance.
// Every TestContext has this by default and thus this doesn't need to be exported.
func (ctx *TestContext) withSMTPServer() {
if ctx.smtpServer != nil {
return
}
ph := newPanicHandler(ctx.t)
pref := preferences.New(ctx.cfg)
tls, _ := config.GetTLSConfig(ctx.cfg)
port := pref.GetInt(preferences.SMTPPortKey)
useSSL := pref.GetBool(preferences.SMTPSSLKey)
backend := smtp.NewSMTPBackend(ph, ctx.listener, pref, ctx.bridge)
server := smtp.NewSMTPServer(true, port, useSSL, tls, backend, ctx.listener)
go server.ListenAndServe()
require.NoError(ctx.t, waitForPort(port, 5*time.Second))
ctx.smtpServer = server
ctx.smtpAddr = fmt.Sprintf("%v:%v", bridge.Host, port)
ctx.addCleanup(ctx.smtpServer.Close, "Closing SMTP server")
}
// SetSMTPLastResponse sets the last SMTP response that was received.
func (ctx *TestContext) SetSMTPLastResponse(handle string, resp *mocks.SMTPResponse) {
ctx.smtpLastResponses[handle] = resp
}
// GetSMTPLastResponse returns the last IMAP response that was received.
func (ctx *TestContext) GetSMTPLastResponse(handle string) *mocks.SMTPResponse {
return ctx.smtpLastResponses[handle]
}

87
test/context/utils.go Normal file
View File

@ -0,0 +1,87 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package context
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
)
func waitForPort(port int, timeout time.Duration) error {
return waitUntilTrue(timeout, func() bool {
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(port), timeout)
if err != nil {
return false
}
if conn != nil {
if err := conn.Close(); err != nil {
return false
}
}
return true
})
}
// waitUntilTrue can use Eventually from
// https://godoc.org/github.com/stretchr/testify/require#Assertions.Eventually
func waitUntilTrue(timeout time.Duration, callback func() bool) error {
endTime := time.Now().Add(timeout)
for {
if time.Now().After(endTime) {
return fmt.Errorf("Timeout")
}
if callback() {
return nil
}
time.Sleep(50 * time.Millisecond)
}
}
func setLogrusVerbosityFromEnv() {
verbosityName := os.Getenv("VERBOSITY")
switch strings.ToLower(verbosityName) {
case "panic":
logrus.SetLevel(logrus.PanicLevel)
case "fatal":
logrus.SetLevel(logrus.FatalLevel)
case "error":
logrus.SetLevel(logrus.ErrorLevel)
case "warning", "warn":
logrus.SetLevel(logrus.WarnLevel)
case "info":
logrus.SetLevel(logrus.InfoLevel)
case "debug":
logrus.SetLevel(logrus.DebugLevel)
case "trace":
logrus.SetLevel(logrus.TraceLevel)
default:
logrus.SetLevel(logrus.FatalLevel)
}
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
FullTimestamp: true,
TimestampFormat: time.StampMilli,
})
}