forked from Silverfish/proton-bridge
GODT-1166: Reduce the number of auth for live test
- Changed: Do not reauth controller clients. - Changed: Verbosisty is set only once before run - Changed: AddUser takes TestAccount as argument - Added: Setup/clean up before/after test run - Added: Access to the current refresh token from pmapi.Client interface. - Added: Context function to add test a user to bridge without login, just call users.FinishLogin. - Added: PMAPIController.GetAuthClient returns authenticated client for username. - Added: Persistent clients does not loggout after every scenario. - Changed: Disabled no-internet tests.
This commit is contained in:
@ -217,3 +217,13 @@ func randomString(length int) string {
|
|||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(noise)[:length]
|
return base64.StdEncoding.EncodeToString(noise)[:length]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *client) GetCurrentAuth() *Auth {
|
||||||
|
return &Auth{
|
||||||
|
UserID: c.user.ID,
|
||||||
|
AuthRefresh: AuthRefresh{
|
||||||
|
UID: c.uid,
|
||||||
|
RefreshToken: c.ref,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -73,6 +73,8 @@ type Client interface {
|
|||||||
|
|
||||||
KeyRingForAddressID(string) (kr *crypto.KeyRing, err error)
|
KeyRingForAddressID(string) (kr *crypto.KeyRing, err error)
|
||||||
GetPublicKeysForEmail(context.Context, string) ([]PublicKey, bool, error)
|
GetPublicKeysForEmail(context.Context, string) ([]PublicKey, bool, error)
|
||||||
|
|
||||||
|
GetCurrentAuth() *Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthRefreshHandler func(*AuthRefresh)
|
type AuthRefreshHandler func(*AuthRefresh)
|
||||||
|
|||||||
@ -301,6 +301,20 @@ func (mr *MockClientMockRecorder) GetContactEmailByEmail(arg0, arg1, arg2, arg3
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockClient)(nil).GetContactEmailByEmail), arg0, arg1, arg2, arg3)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContactEmailByEmail", reflect.TypeOf((*MockClient)(nil).GetContactEmailByEmail), arg0, arg1, arg2, arg3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCurrentAuth mocks base method
|
||||||
|
func (m *MockClient) GetCurrentAuth() *pmapi.Auth {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetCurrentAuth")
|
||||||
|
ret0, _ := ret[0].(*pmapi.Auth)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentAuth indicates an expected call of GetCurrentAuth
|
||||||
|
func (mr *MockClientMockRecorder) GetCurrentAuth() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuth", reflect.TypeOf((*MockClient)(nil).GetCurrentAuth))
|
||||||
|
}
|
||||||
|
|
||||||
// GetEvent mocks base method
|
// GetEvent mocks base method
|
||||||
func (m *MockClient) GetEvent(arg0 context.Context, arg1 string) (*pmapi.Event, error) {
|
func (m *MockClient) GetEvent(arg0 context.Context, arg1 string) (*pmapi.Event, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.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
|
.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 GO111MODULE=on
|
||||||
export BRIDGE_VERSION:=1.5.5+integrationtests
|
export BRIDGE_VERSION:=1.8.2+integrationtests
|
||||||
export VERBOSITY?=fatal
|
export VERBOSITY?=fatal
|
||||||
export TEST_DATA=testdata
|
export TEST_DATA=testdata
|
||||||
export TEST_APP?=bridge
|
export TEST_APP?=bridge
|
||||||
|
|||||||
@ -29,6 +29,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func FeatureContext(s *godog.Suite) {
|
func FeatureContext(s *godog.Suite) {
|
||||||
|
s.BeforeSuite(context.BeforeRun)
|
||||||
|
s.AfterSuite(context.AfterRun)
|
||||||
|
|
||||||
s.BeforeScenario(beforeScenario)
|
s.BeforeScenario(beforeScenario)
|
||||||
s.AfterScenario(afterScenario)
|
s.AfterScenario(afterScenario)
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ func benchTestContext() (*context.TestContext, *mocks.IMAPClient) {
|
|||||||
panic("account " + username + " does not exist")
|
panic("account " + username + " does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
_ = ctx.GetPMAPIController().AddUser(account)
|
||||||
if err := ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()); err != nil {
|
if err := ctx.LoginUser(account.Username(), account.Password(), account.MailboxPassword()); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,8 +94,6 @@ type TestContext struct {
|
|||||||
|
|
||||||
// New returns a new test TestContext.
|
// New returns a new test TestContext.
|
||||||
func New(app string) *TestContext {
|
func New(app string) *TestContext {
|
||||||
setLogrusVerbosityFromEnv()
|
|
||||||
|
|
||||||
listener := listener.New()
|
listener := listener.New()
|
||||||
pmapiController, clientManager := newPMAPIController(app, listener)
|
pmapiController, clientManager := newPMAPIController(app, listener)
|
||||||
|
|
||||||
|
|||||||
40
test/context/globals.go
Normal file
40
test/context/globals.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) 2021 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/liveapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BeforeRun does necessary setup.
|
||||||
|
func BeforeRun() {
|
||||||
|
setLogrusVerbosityFromEnv()
|
||||||
|
|
||||||
|
if os.Getenv(EnvName) == EnvLive {
|
||||||
|
liveapi.SetupPersistentClients()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterRun does necessary cleanup.
|
||||||
|
func AfterRun() {
|
||||||
|
if os.Getenv(EnvName) == EnvLive {
|
||||||
|
liveapi.CleanupPersistentClients()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/internal/events"
|
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
"github.com/ProtonMail/proton-bridge/test/accounts"
|
||||||
"github.com/ProtonMail/proton-bridge/test/fakeapi"
|
"github.com/ProtonMail/proton-bridge/test/fakeapi"
|
||||||
"github.com/ProtonMail/proton-bridge/test/liveapi"
|
"github.com/ProtonMail/proton-bridge/test/liveapi"
|
||||||
)
|
)
|
||||||
@ -30,7 +31,8 @@ import (
|
|||||||
type PMAPIController interface {
|
type PMAPIController interface {
|
||||||
TurnInternetConnectionOff()
|
TurnInternetConnectionOff()
|
||||||
TurnInternetConnectionOn()
|
TurnInternetConnectionOn()
|
||||||
AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error
|
GetAuthClient(username string) pmapi.Client
|
||||||
|
AddUser(account *accounts.TestAccount) error
|
||||||
AddUserLabel(username string, label *pmapi.Label) error
|
AddUserLabel(username string, label *pmapi.Label) error
|
||||||
GetLabelIDs(username string, labelNames []string) ([]string, error)
|
GetLabelIDs(username string, labelNames []string) ([]string, error)
|
||||||
AddUserMessage(username string, message *pmapi.Message) (string, error)
|
AddUserMessage(username string, message *pmapi.Message) (string, error)
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-srp"
|
"github.com/ProtonMail/go-srp"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/store"
|
"github.com/ProtonMail/proton-bridge/internal/store"
|
||||||
"github.com/ProtonMail/proton-bridge/internal/users"
|
"github.com/ProtonMail/proton-bridge/internal/users"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -61,6 +62,18 @@ func (ctx *TestContext) LoginUser(username string, password, mailboxPassword []b
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FinishLogin prevents authentication if not necessary.
|
||||||
|
func (ctx *TestContext) FinishLogin(client pmapi.Client, mailboxPassword string) error {
|
||||||
|
user, err := ctx.users.FinishLogin(client, client.GetCurrentAuth(), mailboxPassword)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to finish login")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addCleanupChecked(user.Logout, "Logging out user")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUser retrieves the bridge user matching the given query string.
|
// GetUser retrieves the bridge user matching the given query string.
|
||||||
func (ctx *TestContext) GetUser(username string) (*users.User, error) {
|
func (ctx *TestContext) GetUser(username string) (*users.User, error) {
|
||||||
return ctx.users.GetUser(username)
|
return ctx.users.GetUser(username)
|
||||||
|
|||||||
@ -63,3 +63,13 @@ func (api *FakePMAPI) AuthDelete(_ context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *FakePMAPI) GetCurrentAuth() *pmapi.Auth {
|
||||||
|
return &pmapi.Auth{
|
||||||
|
UserID: api.userID,
|
||||||
|
AuthRefresh: pmapi.AuthRefresh{
|
||||||
|
UID: api.uid,
|
||||||
|
RefreshToken: api.ref,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Controller implements dummy PMAPIController interface without actual
|
||||||
|
// endpoint.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
// Internal states.
|
// Internal states.
|
||||||
lock *sync.RWMutex
|
lock *sync.RWMutex
|
||||||
|
|||||||
@ -22,8 +22,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
"github.com/ProtonMail/proton-bridge/test/accounts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var systemLabelNameToID = map[string]string{ //nolint[gochecknoglobals]
|
var systemLabelNameToID = map[string]string{ //nolint[gochecknoglobals]
|
||||||
@ -61,13 +63,15 @@ func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) e
|
|||||||
return api.ReorderAddresses(context.Background(), addressIDs)
|
return api.ReorderAddresses(context.Background(), addressIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error {
|
func (ctl *Controller) AddUser(account *accounts.TestAccount) error {
|
||||||
ctl.usersByUsername[user.Name] = &fakeUser{
|
ctl.usersByUsername[account.User().Name] = &fakeUser{
|
||||||
user: user,
|
user: account.User(),
|
||||||
password: password,
|
password: account.Password(),
|
||||||
has2FA: twoFAEnabled,
|
has2FA: account.IsTwoFAEnabled(),
|
||||||
}
|
}
|
||||||
ctl.addressesByUsername[user.Name] = addresses
|
ctl.addressesByUsername[account.User().Name] = account.Addresses()
|
||||||
|
ctl.createSession(account.User().Name, true)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,3 +185,15 @@ func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message,
|
|||||||
}
|
}
|
||||||
return messages, nil
|
return messages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctl *Controller) GetAuthClient(username string) pmapi.Client {
|
||||||
|
for uid, session := range ctl.sessionsByUID {
|
||||||
|
if session.username == username {
|
||||||
|
return ctl.clientManager.NewClient(uid, session.acc, session.ref, time.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl.log.WithField("username", username).Fatal("Cannot get authenticated client.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -51,24 +51,25 @@ func (ctl *Controller) checkScope(uid string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Controller) createSessionIfAuthorized(username string, password []byte) (*fakeSession, error) {
|
func (ctl *Controller) createSessionIfAuthorized(username string, password []byte) (*fakeSession, error) {
|
||||||
// get user
|
|
||||||
user, ok := ctl.usersByUsername[username]
|
user, ok := ctl.usersByUsername[username]
|
||||||
if !ok || !bytes.Equal(user.password, password) {
|
if !ok || !bytes.Equal(user.password, password) {
|
||||||
return nil, errWrongNameOrPassword
|
return nil, errWrongNameOrPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
// create session
|
return ctl.createSession(username, !user.has2FA), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Controller) createSession(username string, hasFullScope bool) *fakeSession {
|
||||||
session := &fakeSession{
|
session := &fakeSession{
|
||||||
username: username,
|
username: username,
|
||||||
uid: ctl.tokenGenerator.next("uid"),
|
uid: ctl.tokenGenerator.next("uid"),
|
||||||
acc: ctl.tokenGenerator.next("acc"),
|
acc: ctl.tokenGenerator.next("acc"),
|
||||||
ref: ctl.tokenGenerator.next("ref"),
|
ref: ctl.tokenGenerator.next("ref"),
|
||||||
hasFullScope: !user.has2FA,
|
hasFullScope: hasFullScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctl.sessionsByUID[session.uid] = session
|
ctl.sessionsByUID[session.uid] = session
|
||||||
|
return session
|
||||||
return session, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Controller) refreshSessionIfAuthorized(uid, ref string) (*fakeSession, error) {
|
func (ctl *Controller) refreshSessionIfAuthorized(uid, ref string) (*fakeSession, error) {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ Feature: IMAP auth
|
|||||||
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
||||||
|
|
||||||
Scenario: Authenticates with connected user that was loaded without internet
|
Scenario: Authenticates with connected user that was loaded without internet
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is no internet connection
|
And there is no internet connection
|
||||||
When bridge starts
|
When bridge starts
|
||||||
And the internet connection is restored
|
And the internet connection is restored
|
||||||
@ -28,13 +28,13 @@ Feature: IMAP auth
|
|||||||
Then "user" is connected
|
Then "user" is connected
|
||||||
|
|
||||||
Scenario: Authenticates with freshly logged-out user
|
Scenario: Authenticates with freshly logged-out user
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
When "user" logs out
|
When "user" logs out
|
||||||
And IMAP client authenticates "user"
|
And IMAP client authenticates "user"
|
||||||
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
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
|
Scenario: Authenticates user which was re-logged in
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
When "user" logs out
|
When "user" logs out
|
||||||
And IMAP client authenticates "user"
|
And IMAP client authenticates "user"
|
||||||
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
Then IMAP response is "IMAP error: NO account is logged out, use the app to login again"
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
Feature: Servers are closed when no internet
|
Feature: Servers are closed when no internet
|
||||||
|
|
||||||
|
# FIXME: Locally works, has lags on CI. Looks like it breaks other tests as well.
|
||||||
|
@ignore
|
||||||
Scenario: All connection are closed and then restored multiple times
|
Scenario: All connection are closed and then restored multiple times
|
||||||
Given there is connected user "user"
|
Given there is connected user "user"
|
||||||
And there is IMAP client "i1" logged in as "user"
|
And there is IMAP client "i1" logged in as "user"
|
||||||
And there is SMTP client "s1" logged in as "user"
|
And there is SMTP client "s1" logged in as "user"
|
||||||
When there is no internet connection
|
When there is no internet connection
|
||||||
And 1 second pass
|
And 3 seconds pass
|
||||||
Then IMAP client "i1" is logged out
|
Then IMAP client "i1" is logged out
|
||||||
And SMTP client "s1" is logged out
|
And SMTP client "s1" is logged out
|
||||||
Given the internet connection is restored
|
Given the internet connection is restored
|
||||||
And 1 second pass
|
And 3 seconds pass
|
||||||
And there is IMAP client "i2" logged in as "user"
|
And there is IMAP client "i2" logged in as "user"
|
||||||
And there is SMTP client "s2" logged in as "user"
|
And there is SMTP client "s2" logged in as "user"
|
||||||
When IMAP client "i2" gets info of "INBOX"
|
When IMAP client "i2" gets info of "INBOX"
|
||||||
@ -17,11 +19,11 @@ Feature: Servers are closed when no internet
|
|||||||
Then IMAP response to "i2" is "OK"
|
Then IMAP response to "i2" is "OK"
|
||||||
Then SMTP response to "s2" is "OK"
|
Then SMTP response to "s2" is "OK"
|
||||||
When there is no internet connection
|
When there is no internet connection
|
||||||
And 1 second pass
|
And 3 seconds pass
|
||||||
Then IMAP client "i2" is logged out
|
Then IMAP client "i2" is logged out
|
||||||
And SMTP client "s2" is logged out
|
And SMTP client "s2" is logged out
|
||||||
Given the internet connection is restored
|
Given the internet connection is restored
|
||||||
And 1 second pass
|
And 3 seconds pass
|
||||||
And there is IMAP client "i3" logged in as "user"
|
And there is IMAP client "i3" logged in as "user"
|
||||||
And there is SMTP client "s3" logged in as "user"
|
And there is SMTP client "s3" logged in as "user"
|
||||||
When IMAP client "i3" gets info of "INBOX"
|
When IMAP client "i3" gets info of "INBOX"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Feature: Start bridge
|
Feature: Start bridge
|
||||||
Scenario: Start with connected user, database file and internet connection
|
Scenario: Start with connected user, database file and internet connection
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is database file for "user"
|
And there is database file for "user"
|
||||||
When bridge starts
|
When bridge starts
|
||||||
Then "user" is connected
|
Then "user" is connected
|
||||||
@ -8,7 +8,7 @@ Feature: Start bridge
|
|||||||
And "user" has running event loop
|
And "user" has running event loop
|
||||||
|
|
||||||
Scenario: Start with connected user, database file and no internet connection
|
Scenario: Start with connected user, database file and no internet connection
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is database file for "user"
|
And there is database file for "user"
|
||||||
And there is no internet connection
|
And there is no internet connection
|
||||||
When bridge starts
|
When bridge starts
|
||||||
@ -17,7 +17,7 @@ Feature: Start bridge
|
|||||||
And "user" has running event loop
|
And "user" has running event loop
|
||||||
|
|
||||||
Scenario: Start with connected user, no database file and internet connection
|
Scenario: Start with connected user, no database file and internet connection
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
When bridge starts
|
When bridge starts
|
||||||
Then "user" is connected
|
Then "user" is connected
|
||||||
@ -25,7 +25,7 @@ Feature: Start bridge
|
|||||||
And "user" has running event loop
|
And "user" has running event loop
|
||||||
|
|
||||||
Scenario: Start with connected user, no database file and no internet connection
|
Scenario: Start with connected user, no database file and no internet connection
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
And there is no internet connection
|
And there is no internet connection
|
||||||
When bridge starts
|
When bridge starts
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
Feature: Delete user
|
Feature: Delete user
|
||||||
Scenario: Deleting connected user
|
Scenario: Deleting connected user
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
When user deletes "user"
|
When user deletes "user"
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
And "user" has database file
|
And "user" has database file
|
||||||
|
|
||||||
Scenario: Deleting connected user with cache
|
Scenario: Deleting connected user with cache
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
When user deletes "user" with cache
|
When user deletes "user" with cache
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
And "user" does not have database file
|
And "user" does not have database file
|
||||||
|
|
||||||
Scenario: Deleting connected user without database file
|
Scenario: Deleting connected user without database file
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
When user deletes "user" with cache
|
When user deletes "user" with cache
|
||||||
Then last response is "OK"
|
Then last response is "OK"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Feature: Re-login
|
Feature: Re-login
|
||||||
Scenario: Re-login with connected user and database file
|
Scenario: Re-login with connected user and database file
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is database file for "user"
|
And there is database file for "user"
|
||||||
When "user" logs in
|
When "user" logs in
|
||||||
Then last response is "failed to finish login: user is already connected"
|
Then last response is "failed to finish login: user is already connected"
|
||||||
@ -9,7 +9,7 @@ Feature: Re-login
|
|||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
Scenario: Re-login with connected user and no database file
|
Scenario: Re-login with connected user and no database file
|
||||||
Given there is connected user "user"
|
Given there is user "user" which just logged in
|
||||||
And there is no database file for "user"
|
And there is no database file for "user"
|
||||||
When "user" logs in
|
When "user" logs in
|
||||||
Then last response is "failed to finish login: user is already connected"
|
Then last response is "failed to finish login: user is already connected"
|
||||||
|
|||||||
@ -21,45 +21,36 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/internal/constants"
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Controller implements PMAPIController interface for specified endpoint.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
|
log *logrus.Entry
|
||||||
// Internal states.
|
// Internal states.
|
||||||
lock *sync.RWMutex
|
lock *sync.RWMutex
|
||||||
calls []*fakeCall
|
calls []*fakeCall
|
||||||
pmapiByUsername map[string]pmapi.Client
|
|
||||||
messageIDsByUsername map[string][]string
|
messageIDsByUsername map[string][]string
|
||||||
clientManager pmapi.Manager
|
|
||||||
|
|
||||||
// State controlled by test.
|
// State controlled by test.
|
||||||
noInternetConnection bool
|
noInternetConnection bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(app string) (*Controller, pmapi.Manager) {
|
func NewController(_ string) (*Controller, pmapi.Manager) {
|
||||||
cm := pmapi.New(pmapi.NewConfig(getAppVersionName(app), constants.Version))
|
|
||||||
controller := &Controller{
|
controller := &Controller{
|
||||||
|
log: logrus.WithField("pkg", "live-controller"),
|
||||||
lock: &sync.RWMutex{},
|
lock: &sync.RWMutex{},
|
||||||
calls: []*fakeCall{},
|
calls: []*fakeCall{},
|
||||||
pmapiByUsername: map[string]pmapi.Client{},
|
|
||||||
messageIDsByUsername: map[string][]string{},
|
messageIDsByUsername: map[string][]string{},
|
||||||
clientManager: cm,
|
|
||||||
|
|
||||||
noInternetConnection: false,
|
noInternetConnection: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.SetTransport(&fakeTransport{
|
persistentClients.manager.SetTransport(&fakeTransport{
|
||||||
ctl: controller,
|
ctl: controller,
|
||||||
transport: http.DefaultTransport,
|
transport: http.DefaultTransport,
|
||||||
})
|
})
|
||||||
|
|
||||||
return controller, cm
|
return controller, persistentClients.manager
|
||||||
}
|
|
||||||
|
|
||||||
func getAppVersionName(app string) string {
|
|
||||||
if app == "ie" {
|
|
||||||
return "importExport"
|
|
||||||
}
|
|
||||||
return app
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,9 +37,9 @@ var systemLabelNameToID = map[string]string{ //nolint[gochecknoglobals]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
|
func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
|
||||||
client, ok := ctl.pmapiByUsername[username]
|
client, err := getPersistentClient(username)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return fmt.Errorf("user %s does not exist", username)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
label.Exclusive = getLabelExclusive(label.Name)
|
label.Exclusive = getLabelExclusive(label.Name)
|
||||||
@ -68,9 +68,9 @@ func (ctl *Controller) getLabelID(username, labelName string) (string, error) {
|
|||||||
return labelID, nil
|
return labelID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, ok := ctl.pmapiByUsername[username]
|
client, err := getPersistentClient(username)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return "", fmt.Errorf("user %s does not exist", username)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
labels, err := client.ListLabels(context.Background())
|
labels, err := client.ListLabels(context.Background())
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package liveapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
messageUtils "github.com/ProtonMail/proton-bridge/pkg/message"
|
messageUtils "github.com/ProtonMail/proton-bridge/pkg/message"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
@ -31,9 +30,9 @@ func (ctl *Controller) AddUserMessage(username string, message *pmapi.Message) (
|
|||||||
return "", errors.New("add user messages with attachments is not implemented for live")
|
return "", errors.New("add user messages with attachments is not implemented for live")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, ok := ctl.pmapiByUsername[username]
|
client, err := getPersistentClient(username)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return "", fmt.Errorf("user %s does not exist", username)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.Flags == 0 {
|
if message.Flags == 0 {
|
||||||
@ -75,9 +74,9 @@ func (ctl *Controller) AddUserMessage(username string, message *pmapi.Message) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message, error) {
|
func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message, error) {
|
||||||
client, ok := ctl.pmapiByUsername[username]
|
client, err := getPersistentClient(username)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("user %s does not exist", username)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
page := 0
|
page := 0
|
||||||
|
|||||||
131
test/liveapi/persistent_clients.go
Normal file
131
test/liveapi/persistent_clients.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Copyright (c) 2021 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 liveapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/srp"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// persistentClients keeps authenticated clients for tests.
|
||||||
|
//
|
||||||
|
// We need to reduce the number of authentication done by live tests.
|
||||||
|
// Before every *scenario* we are creating and authenticating new client.
|
||||||
|
// This is not necessary for controller purposes. We can reuse the same clients
|
||||||
|
// for all tests.
|
||||||
|
//
|
||||||
|
//nolint[gochecknoglobals]
|
||||||
|
var persistentClients = struct {
|
||||||
|
manager pmapi.Manager
|
||||||
|
byName map[string]pmapi.Client
|
||||||
|
saltByName map[string]string
|
||||||
|
}{}
|
||||||
|
|
||||||
|
type persistentClient struct {
|
||||||
|
pmapi.Client
|
||||||
|
username string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthDelete is noop. All sessions will be closed in CleanupPersistentClients.
|
||||||
|
func (pc *persistentClient) AuthDelete(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthSalt returns cached string. Otherwise after some time there is an error:
|
||||||
|
//
|
||||||
|
// Access token does not have sufficient scope
|
||||||
|
//
|
||||||
|
// while all other routes works normally. Need to confirm with Aron that this
|
||||||
|
// is expected behaviour.
|
||||||
|
func (pc *persistentClient) AuthSalt(_ context.Context) (string, error) {
|
||||||
|
return persistentClients.saltByName[pc.username], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupPersistentClients() {
|
||||||
|
app := os.Getenv("TEST_APP")
|
||||||
|
|
||||||
|
persistentClients.manager = pmapi.New(pmapi.NewConfig(getAppVersionName(app), constants.Version))
|
||||||
|
persistentClients.manager.SetLogging(logrus.WithField("pkg", "liveapi"), logrus.GetLevel() == logrus.TraceLevel)
|
||||||
|
|
||||||
|
persistentClients.byName = map[string]pmapi.Client{}
|
||||||
|
persistentClients.saltByName = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppVersionName(app string) string {
|
||||||
|
if app == "ie" {
|
||||||
|
return "importExport"
|
||||||
|
}
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupPersistentClients() {
|
||||||
|
for username, client := range persistentClients.byName {
|
||||||
|
if err := client.AuthDelete(context.Background()); err != nil {
|
||||||
|
logrus.WithError(err).
|
||||||
|
WithField("username", username).
|
||||||
|
Error("Failed to logout persistent client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPersistentClient(username string, password, mailboxPassword []byte) (pmapi.Client, error) {
|
||||||
|
if cl, ok := persistentClients.byName[username]; ok {
|
||||||
|
return cl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srp.RandReader = rand.New(rand.NewSource(42)) //nolint[gosec] It is OK to use weaker random number generator here
|
||||||
|
|
||||||
|
client, _, err := persistentClients.manager.NewClientWithLogin(context.Background(), username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create new persistent client")
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := client.AuthSalt(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "persistent client: failed to get salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedMboxPass, err := pmapi.HashMailboxPassword(mailboxPassword, salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "persistent client: failed to hash mailbox password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Unlock(context.Background(), hashedMboxPass); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "persistent client: failed to unlock user")
|
||||||
|
}
|
||||||
|
|
||||||
|
persistentClients.byName[username] = client
|
||||||
|
persistentClients.saltByName[username] = salt
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPersistentClient(username string) (pmapi.Client, error) {
|
||||||
|
v, ok := persistentClients.byName[username]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("user %s does not exist", username)
|
||||||
|
}
|
||||||
|
return &persistentClient{v, username}, nil
|
||||||
|
}
|
||||||
@ -21,43 +21,42 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
"github.com/ProtonMail/proton-bridge/test/accounts"
|
||||||
"github.com/cucumber/godog"
|
"github.com/cucumber/godog"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error {
|
func (ctl *Controller) AddUser(account *accounts.TestAccount) error {
|
||||||
if twoFAEnabled {
|
if account.IsTwoFAEnabled() {
|
||||||
return godog.ErrPending
|
return godog.ErrPending
|
||||||
}
|
}
|
||||||
|
|
||||||
client, _, err := ctl.clientManager.NewClientWithLogin(context.Background(), user.Name, password)
|
client, err := addPersistentClient(account.User().Name, account.Password(), account.MailboxPassword())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to create new client")
|
return errors.Wrap(err, "failed to add persistent client")
|
||||||
}
|
}
|
||||||
|
|
||||||
salt, err := client.AuthSalt(context.Background())
|
if err := cleanup(client, account.Addresses()); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to get salt")
|
|
||||||
}
|
|
||||||
|
|
||||||
mailboxPassword, err := pmapi.HashMailboxPassword(password, salt)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to hash mailbox password")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Unlock(context.Background(), mailboxPassword); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to unlock user")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cleanup(client, addresses); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to clean user")
|
return errors.Wrap(err, "failed to clean user")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctl.pmapiByUsername[user.Name] = client
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) error {
|
func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) error {
|
||||||
return ctl.pmapiByUsername[user.Name].ReorderAddresses(context.Background(), addressIDs)
|
client, err := getPersistentClient(user.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return client.ReorderAddresses(context.Background(), addressIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *Controller) GetAuthClient(username string) pmapi.Client {
|
||||||
|
client, err := getPersistentClient(username)
|
||||||
|
if err != nil {
|
||||||
|
ctl.log.WithError(err).
|
||||||
|
WithField("username", username).
|
||||||
|
Fatal("Cannot get authenticated client")
|
||||||
|
}
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
func UsersSetupFeatureContext(s *godog.Suite) {
|
func UsersSetupFeatureContext(s *godog.Suite) {
|
||||||
s.Step(`^there is user "([^"]*)"$`, thereIsUser)
|
s.Step(`^there is user "([^"]*)"$`, thereIsUser)
|
||||||
s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser)
|
s.Step(`^there is connected user "([^"]*)"$`, thereIsConnectedUser)
|
||||||
|
s.Step(`^there is user "([^"]*)" which just logged in$`, thereIsUserWhichJustLoggedIn)
|
||||||
s.Step(`^there is disconnected user "([^"]*)"$`, thereIsDisconnectedUser)
|
s.Step(`^there is disconnected user "([^"]*)"$`, thereIsDisconnectedUser)
|
||||||
s.Step(`^there is database file for "([^"]*)"$`, thereIsDatabaseFileForUser)
|
s.Step(`^there is database file for "([^"]*)"$`, thereIsDatabaseFileForUser)
|
||||||
s.Step(`^there is no database file for "([^"]*)"$`, thereIsNoDatabaseFileForUser)
|
s.Step(`^there is no database file for "([^"]*)"$`, thereIsNoDatabaseFileForUser)
|
||||||
@ -39,7 +40,7 @@ func thereIsUser(bddUserID string) error {
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return godog.ErrPending
|
return godog.ErrPending
|
||||||
}
|
}
|
||||||
err := ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
err := ctx.GetPMAPIController().AddUser(account)
|
||||||
return internalError(err, "adding user %s", account.Username())
|
return internalError(err, "adding user %s", account.Username())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +49,21 @@ func thereIsConnectedUser(bddUserID string) error {
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return godog.ErrPending
|
return godog.ErrPending
|
||||||
}
|
}
|
||||||
err := ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
username := account.Username()
|
||||||
|
ctl := ctx.GetPMAPIController()
|
||||||
|
err := ctl.AddUser(account)
|
||||||
|
if err != nil {
|
||||||
|
return internalError(err, "adding user %s", username)
|
||||||
|
}
|
||||||
|
return ctx.FinishLogin(ctx.GetPMAPIController().GetAuthClient(username), account.MailboxPassword())
|
||||||
|
}
|
||||||
|
|
||||||
|
func thereIsUserWhichJustLoggedIn(bddUserID string) error {
|
||||||
|
account := ctx.GetTestAccount(bddUserID)
|
||||||
|
if account == nil {
|
||||||
|
return godog.ErrPending
|
||||||
|
}
|
||||||
|
err := ctx.GetPMAPIController().AddUser(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return internalError(err, "adding user %s", account.Username())
|
return internalError(err, "adding user %s", account.Username())
|
||||||
}
|
}
|
||||||
@ -60,7 +75,7 @@ func thereIsDisconnectedUser(bddUserID string) error {
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return godog.ErrPending
|
return godog.ErrPending
|
||||||
}
|
}
|
||||||
err := ctx.GetPMAPIController().AddUser(account.User(), account.Addresses(), account.Password(), account.IsTwoFAEnabled())
|
err := ctx.GetPMAPIController().AddUser(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return internalError(err, "adding user %s", account.Username())
|
return internalError(err, "adding user %s", account.Username())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user