GODT-35: New pmapi client and manager using resty

This commit is contained in:
James Houlahan
2021-02-22 18:23:51 +01:00
committed by Jakub
parent 1d538e8540
commit 2284e9ede1
163 changed files with 3333 additions and 8124 deletions

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/sentry"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// GetBridge returns bridge instance.
@ -52,7 +53,6 @@ func (ctx *TestContext) RestartBridge() error {
_ = user.GetStore().Close()
}
ctx.bridge.StopWatchers()
time.Sleep(50 * time.Millisecond)
ctx.withBridgeInstance()
@ -68,7 +68,7 @@ func newBridgeInstance(
settings *fakeSettings,
credStore users.CredentialsStorer,
eventListener listener.Listener,
clientManager users.ClientManager,
clientManager pmapi.Manager,
) *bridge.Bridge {
sentryReporter := sentry.NewReporter("bridge", constants.Version, useragent.New())
panicHandler := &panicHandler{t: t}

View File

@ -23,7 +23,6 @@ import (
"github.com/ProtonMail/proton-bridge/internal/bridge"
"github.com/ProtonMail/proton-bridge/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/internal/constants"
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/transfer"
"github.com/ProtonMail/proton-bridge/internal/users"
@ -53,7 +52,7 @@ type TestContext struct {
// pmapiController is used to control real or fake pmapi clients.
// The clients are created by the clientManager.
pmapiController PMAPIController
clientManager *pmapi.ClientManager
clientManager pmapi.Manager
// Core related variables.
bridge *bridge.Bridge
@ -99,10 +98,7 @@ func New(app string) *TestContext {
userAgent := useragent.New()
cm := pmapi.NewClientManager(
pmapi.GetAPIConfig(getConfigName(app), constants.Version),
userAgent,
)
pmapiController, clientManager := newPMAPIController()
ctx := &TestContext{
t: &bddT{},
@ -111,8 +107,8 @@ func New(app string) *TestContext {
settings: newFakeSettings(),
listener: listener.New(),
userAgent: userAgent,
pmapiController: newPMAPIController(cm),
clientManager: cm,
pmapiController: pmapiController,
clientManager: clientManager,
testAccounts: newTestAccounts(),
credStore: newFakeCredStore(),
imapClients: make(map[string]*mocks.IMAPClient),
@ -164,7 +160,7 @@ func (ctx *TestContext) GetPMAPIController() PMAPIController {
}
// GetClientManager returns client manager being used for testing.
func (ctx *TestContext) GetClientManager() *pmapi.ClientManager {
func (ctx *TestContext) GetClientManager() pmapi.Manager {
return ctx.clientManager
}

View File

@ -51,7 +51,7 @@ func (c *fakeCredStore) List() (userIDs []string, err error) {
return keys, nil
}
func (c *fakeCredStore) Add(userID, userName, apiToken, mailboxPassword string, emails []string) (*credentials.Credentials, error) {
func (c *fakeCredStore) Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*credentials.Credentials, error) {
bridgePassword := bridgePassword
if c, ok := c.credentials[userID]; ok {
bridgePassword = c.BridgePassword
@ -60,7 +60,7 @@ func (c *fakeCredStore) Add(userID, userName, apiToken, mailboxPassword string,
UserID: userID,
Name: userName,
Emails: strings.Join(emails, ";"),
APIToken: apiToken,
APIToken: uid + ":" + ref,
MailboxPassword: mailboxPassword,
BridgePassword: bridgePassword,
IsCombinedAddressMode: true, // otherwise by default starts in split mode
@ -73,36 +73,38 @@ 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) SwitchAddressMode(userID string) (*credentials.Credentials, error) {
// FIXME(conman): Why is this empty?
return c.credentials[userID], nil
}
func (c *fakeCredStore) UpdateEmails(userID string, emails []string) error {
return nil
func (c *fakeCredStore) UpdateEmails(userID string, emails []string) (*credentials.Credentials, error) {
// FIXME(conman): Why is this empty?
return c.credentials[userID], nil
}
func (c *fakeCredStore) UpdatePassword(userID, password string) error {
func (c *fakeCredStore) UpdatePassword(userID, password string) (*credentials.Credentials, error) {
creds, err := c.Get(userID)
if err != nil {
return err
return nil, err
}
creds.MailboxPassword = password
return nil
return creds, nil
}
func (c *fakeCredStore) UpdateToken(userID, apiToken string) error {
func (c *fakeCredStore) UpdateToken(userID, uid, ref string) (*credentials.Credentials, error) {
creds, err := c.Get(userID)
if err != nil {
return err
return nil, err
}
creds.APIToken = apiToken
return nil
creds.APIToken = uid + ":" + ref
return creds, nil
}
func (c *fakeCredStore) Logout(userID string) error {
func (c *fakeCredStore) Logout(userID string) (*credentials.Credentials, error) {
c.credentials[userID].APIToken = ""
c.credentials[userID].MailboxPassword = ""
return nil
return c.credentials[userID], nil
}
func (c *fakeCredStore) Delete(userID string) error {

View File

@ -21,6 +21,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/importexport"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// GetImportExport returns import-export instance.
@ -42,7 +43,7 @@ func newImportExportInstance(
cache importexport.Cacher,
credStore users.CredentialsStorer,
eventListener listener.Listener,
clientManager users.ClientManager,
clientManager pmapi.Manager,
) *importexport.ImportExport {
panicHandler := &panicHandler{t: t}
return importexport.New(locations, cache, panicHandler, eventListener, clientManager, credStore)

View File

@ -39,37 +39,15 @@ type PMAPIController interface {
GetCalls(method, path string) [][]byte
}
func newPMAPIController(cm *pmapi.ClientManager) PMAPIController {
func newPMAPIController() (PMAPIController, pmapi.Manager) {
switch os.Getenv(EnvName) {
case EnvFake:
return newFakePMAPIController(cm)
return fakeapi.NewController()
case EnvLive:
return newLivePMAPIController(cm)
return liveapi.NewController()
default:
panic("unknown env")
}
}
func newFakePMAPIController(cm *pmapi.ClientManager) PMAPIController {
return newFakePMAPIControllerWrap(fakeapi.NewController(cm))
}
type fakePMAPIControllerWrap struct {
*fakeapi.Controller
}
func newFakePMAPIControllerWrap(controller *fakeapi.Controller) PMAPIController {
return &fakePMAPIControllerWrap{Controller: controller}
}
func newLivePMAPIController(cm *pmapi.ClientManager) PMAPIController {
return newLiveAPIControllerWrap(liveapi.NewController(cm))
}
type liveAPIControllerWrap struct {
*liveapi.Controller
}
func newLiveAPIControllerWrap(controller *liveapi.Controller) PMAPIController {
return &liveAPIControllerWrap{Controller: controller}
}

View File

@ -0,0 +1,65 @@
package context
import (
"context"
"net/http"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/go-resty/resty/v2"
)
func newLivePMAPIManager() pmapi.Manager {
return pmapi.New(pmapi.DefaultConfig)
}
func newFakePMAPIManager() pmapi.Manager {
return &fakePMAPIManager{}
}
type fakePMAPIManager struct{}
func (*fakePMAPIManager) NewClient(string, string, string, time.Time) pmapi.Client {
panic("TODO")
}
func (*fakePMAPIManager) NewClientWithRefresh(context.Context, string, string) (pmapi.Client, *pmapi.Auth, error) {
panic("TODO")
}
func (*fakePMAPIManager) NewClientWithLogin(context.Context, string, string) (pmapi.Client, *pmapi.Auth, error) {
panic("TODO")
}
func (*fakePMAPIManager) DownloadAndVerify(kr *crypto.KeyRing, url, sig string) ([]byte, error) {
panic("TODO")
}
func (*fakePMAPIManager) ReportBug(context.Context, pmapi.ReportBugReq) error {
panic("TODO")
}
func (*fakePMAPIManager) SendSimpleMetric(context.Context, string, string, string) error {
panic("TODO")
}
func (*fakePMAPIManager) SetLogger(resty.Logger) {
panic("TODO")
}
func (*fakePMAPIManager) SetTransport(http.RoundTripper) {
panic("TODO")
}
func (*fakePMAPIManager) SetCookieJar(http.CookieJar) {
panic("TODO")
}
func (*fakePMAPIManager) SetRetryCount(int) {
panic("TODO")
}
func (*fakePMAPIManager) AddConnectionObserver(pmapi.ConnectionObserver) {
panic("TODO")
}

View File

@ -18,6 +18,7 @@
package context
import (
"context"
"fmt"
"math/rand"
"path/filepath"
@ -25,6 +26,7 @@ import (
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/pkg/srp"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
@ -36,7 +38,7 @@ func (ctx *TestContext) GetUsers() *users.Users {
}
// LoginUser logs in the user with the given username, password, and mailbox password.
func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (err error) {
func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) error {
srp.RandReader = rand.New(rand.NewSource(42)) //nolint[gosec] It is OK to use weaker random number generator here
client, auth, err := ctx.users.Login(username, password)
@ -44,8 +46,8 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
return errors.Wrap(err, "failed to login")
}
if auth.HasTwoFactor() {
if err := client.Auth2FA("2fa code", auth); err != nil {
if auth.TwoFA.Enabled == pmapi.TOTPEnabled {
if err := client.Auth2FA(context.TODO(), pmapi.Auth2FAReq{TwoFactorCode: "2fa code"}); err != nil {
return errors.Wrap(err, "failed to login with 2FA")
}
}
@ -57,7 +59,7 @@ func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) (e
ctx.addCleanupChecked(user.Logout, "Logging out user")
return
return nil
}
// GetUser retrieves the bridge user matching the given query string.

View File

@ -19,6 +19,7 @@ package fakeapi
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
@ -53,7 +54,7 @@ func newTestAttachment(iAtt int, msgID string) *pmapi.Attachment {
}
}
func (api *FakePMAPI) GetAttachment(attachmentID string) (io.ReadCloser, error) {
func (api *FakePMAPI) GetAttachment(_ context.Context, attachmentID string) (io.ReadCloser, error) {
if err := api.checkAndRecordCall(GET, "/mail/v4/attachments/"+attachmentID, nil); err != nil {
return nil, err
}
@ -65,7 +66,7 @@ func (api *FakePMAPI) GetAttachment(attachmentID string) (io.ReadCloser, error)
return ioutil.NopCloser(r), nil
}
func (api *FakePMAPI) CreateAttachment(attachment *pmapi.Attachment, data io.Reader, signature io.Reader) (*pmapi.Attachment, error) {
func (api *FakePMAPI) CreateAttachment(_ context.Context, attachment *pmapi.Attachment, data io.Reader, signature io.Reader) (*pmapi.Attachment, error) {
if err := api.checkAndRecordCall(POST, "/mail/v4/attachments", nil); err != nil {
return nil, err
}
@ -76,7 +77,3 @@ func (api *FakePMAPI) CreateAttachment(attachment *pmapi.Attachment, data io.Rea
attachment.KeyPackets = base64.StdEncoding.EncodeToString(bytes)
return attachment, nil
}
func (api *FakePMAPI) DeleteAttachment(attID string) error {
return api.checkAndRecordCall(DELETE, "/mail/v4/attachments/"+attID, nil)
}

View File

@ -18,76 +18,23 @@
package fakeapi
import (
"strings"
"context"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (api *FakePMAPI) SetAuths(auths chan<- *pmapi.Auth) {
api.auths = auths
}
func (api *FakePMAPI) AuthInfo(username string) (*pmapi.AuthInfo, error) {
if err := api.checkInternetAndRecordCall(POST, "/auth/info", &pmapi.AuthInfoReq{
Username: username,
}); err != nil {
return nil, err
}
authInfo := &pmapi.AuthInfo{}
user, ok := api.controller.usersByUsername[username]
if !ok {
// If username is wrong, API server will return empty but
// positive response
return authInfo, nil
}
authInfo.TwoFA = user.get2FAInfo()
return authInfo, nil
}
func (api *FakePMAPI) Auth(username, password string, authInfo *pmapi.AuthInfo) (*pmapi.Auth, error) {
if err := api.checkInternetAndRecordCall(POST, "/auth", &pmapi.AuthReq{
Username: username,
}); err != nil {
return nil, err
}
session, err := api.controller.createSessionIfAuthorized(username, password)
if err != nil {
return nil, err
}
api.setUID(session.uid)
if err := api.setUser(username); err != nil {
return nil, err
}
user := api.controller.usersByUsername[username]
auth := &pmapi.Auth{
TwoFA: user.get2FAInfo(),
RefreshToken: session.refreshToken,
ExpiresIn: 86400, // seconds
}
auth.DANGEROUSLYSetUID(session.uid)
api.sendAuth(auth)
return auth, nil
}
func (api *FakePMAPI) Auth2FA(twoFactorCode string, auth *pmapi.Auth) error {
if err := api.checkInternetAndRecordCall(POST, "/auth/2fa", &pmapi.Auth2FAReq{
TwoFactorCode: twoFactorCode,
}); err != nil {
func (api *FakePMAPI) Auth2FA(_ context.Context, req pmapi.Auth2FAReq) error {
if err := api.checkAndRecordCall(POST, "/auth/2fa", req); err != nil {
return err
}
if api.uid == "" {
return pmapi.ErrInvalidToken
return pmapi.ErrUnauthorized
}
session, ok := api.controller.sessionsByUID[api.uid]
if !ok {
return pmapi.ErrInvalidToken
return pmapi.ErrUnauthorized
}
session.hasFullScope = true
@ -95,92 +42,24 @@ func (api *FakePMAPI) Auth2FA(twoFactorCode string, auth *pmapi.Auth) error {
return nil
}
func (api *FakePMAPI) AuthRefresh(token string) (*pmapi.Auth, error) {
if api.lastToken == "" {
api.lastToken = token
}
split := strings.Split(token, ":")
if len(split) != 2 {
return nil, pmapi.ErrInvalidToken
}
if err := api.checkInternetAndRecordCall(POST, "/auth/refresh", &pmapi.AuthRefreshReq{
ResponseType: "token",
GrantType: "refresh_token",
UID: split[0],
RefreshToken: split[1],
RedirectURI: "https://protonmail.ch",
State: "random_string",
}); err != nil {
return nil, err
}
session, ok := api.controller.sessionsByUID[split[0]]
if !ok || session.refreshToken != split[1] {
api.log.WithField("token", token).
WithField("session", session).
Warn("Refresh token failed")
// The API server will respond normal error not 401 (check api)
// i.e. should not use `sendAuth(nil)`
api.setUID("")
return nil, pmapi.ErrInvalidToken
}
api.setUID(split[0])
if err := api.setUser(session.username); err != nil {
return nil, err
}
api.controller.refreshTheTokensForSession(session)
api.lastToken = split[0] + ":" + session.refreshToken
auth := &pmapi.Auth{
RefreshToken: session.refreshToken,
ExpiresIn: 86400,
}
auth.DANGEROUSLYSetUID(session.uid)
api.sendAuth(auth)
return auth, nil
}
func (api *FakePMAPI) AuthSalt() (string, error) {
if err := api.checkInternetAndRecordCall(GET, "/keys/salts", nil); err != nil {
func (api *FakePMAPI) AuthSalt(_ context.Context) (string, error) {
if err := api.checkAndRecordCall(GET, "/keys/salts", nil); err != nil {
return "", err
}
return "", nil
}
func (api *FakePMAPI) Logout() {
api.controller.clientManager.LogoutClient(api.userID)
func (api *FakePMAPI) AddAuthHandler(handler pmapi.AuthHandler) {
api.authHandlers = append(api.authHandlers, handler)
}
func (api *FakePMAPI) IsConnected() bool {
return api.uid != "" && api.lastToken != ""
}
func (api *FakePMAPI) DeleteAuth() error {
func (api *FakePMAPI) AuthDelete(_ context.Context) error {
if err := api.checkAndRecordCall(DELETE, "/auth", nil); err != nil {
return err
}
api.controller.deleteSession(api.uid)
return nil
}
func (api *FakePMAPI) ClearData() {
if api.userKeyRing != nil {
api.userKeyRing.ClearPrivateParams()
api.userKeyRing = nil
}
for addrID, addr := range api.addrKeyRing {
if addr != nil {
addr.ClearPrivateParams()
delete(api.addrKeyRing, addrID)
}
}
api.unsetUser()
}

View File

@ -18,6 +18,7 @@
package fakeapi
import (
"context"
"fmt"
"net/url"
"strconv"
@ -29,7 +30,7 @@ func (api *FakePMAPI) DecryptAndVerifyCards(cards []pmapi.Card) ([]pmapi.Card, e
return cards, nil
}
func (api *FakePMAPI) GetContactEmailByEmail(email string, page int, pageSize int) ([]pmapi.ContactEmail, error) {
func (api *FakePMAPI) GetContactEmailByEmail(_ context.Context, email string, page int, pageSize int) ([]pmapi.ContactEmail, error) {
v := url.Values{}
v.Set("Page", strconv.Itoa(page))
if pageSize > 0 {
@ -42,7 +43,7 @@ func (api *FakePMAPI) GetContactEmailByEmail(email string, page int, pageSize in
return []pmapi.ContactEmail{}, nil
}
func (api *FakePMAPI) GetContactByID(contactID string) (pmapi.Contact, error) {
func (api *FakePMAPI) GetContactByID(_ context.Context, contactID string) (pmapi.Contact, error) {
if err := api.checkAndRecordCall(GET, "/contacts/"+contactID, nil); err != nil {
return pmapi.Contact{}, err
}

View File

@ -32,7 +32,7 @@ type Controller struct {
labelIDGenerator idGenerator
messageIDGenerator idGenerator
tokenGenerator idGenerator
clientManager *pmapi.ClientManager
clientManager pmapi.Manager
// State controlled by test.
noInternetConnection bool
@ -46,7 +46,7 @@ type Controller struct {
log *logrus.Entry
}
func NewController(cm *pmapi.ClientManager) *Controller {
func NewController() (*Controller, pmapi.Manager) {
controller := &Controller{
lock: &sync.RWMutex{},
fakeAPIs: []*FakePMAPI{},
@ -54,7 +54,6 @@ func NewController(cm *pmapi.ClientManager) *Controller {
labelIDGenerator: 100, // We cannot use system label IDs.
messageIDGenerator: 0,
tokenGenerator: 1000, // No specific reason; 1000 simply feels right.
clientManager: cm,
noInternetConnection: false,
usersByUsername: map[string]*fakeUser{},
@ -67,11 +66,11 @@ func NewController(cm *pmapi.ClientManager) *Controller {
log: logrus.WithField("pkg", "fakeapi-controller"),
}
cm.SetClientConstructor(func(userID string) pmapi.Client {
fakeAPI := New(controller, userID)
controller.fakeAPIs = append(controller.fakeAPIs, fakeAPI)
return fakeAPI
})
cm := &fakePMAPIManager{
controller: controller,
}
return controller
controller.clientManager = cm
return controller, cm
}

View File

@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/nsf/jsondiff"
)
@ -39,23 +40,31 @@ type fakeCall struct {
request []byte
}
func (ctl *Controller) recordCall(method method, path string, req interface{}) {
func (ctl *Controller) recordCall(method method, path string, req interface{}) error {
ctl.lock.Lock()
defer ctl.lock.Unlock()
request := []byte{}
var request []byte
if req != nil {
var err error
request, err = json.Marshal(req)
if err != nil {
panic(err)
if request, err = json.Marshal(req); err != nil {
return err
}
}
ctl.calls = append(ctl.calls, &fakeCall{
method: method,
path: path,
request: request,
})
if ctl.noInternetConnection {
return pmapi.ErrNoConnection
}
return nil
}
func (ctl *Controller) PrintCalls() {

View File

@ -18,6 +18,7 @@
package fakeapi
import (
"context"
"errors"
"fmt"
"strings"
@ -51,7 +52,7 @@ func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) e
return errors.New("no such user")
}
return api.ReorderAddresses(addressIDs)
return api.ReorderAddresses(context.TODO(), addressIDs)
}
func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error {

View File

@ -19,16 +19,36 @@ package fakeapi
import (
"errors"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type fakeSession struct {
username string
uid, refreshToken string
hasFullScope bool
username string
uid, acc, ref string
hasFullScope bool
}
var errWrongNameOrPassword = errors.New("Incorrect login credentials. Please try again") //nolint[stylecheck]
func (ctl *Controller) checkAccessToken(uid, acc string) bool {
session, ok := ctl.sessionsByUID[uid]
if !ok {
return false
}
return session.uid == uid && session.acc == acc
}
func (ctl *Controller) checkScope(uid string) bool {
session, ok := ctl.sessionsByUID[uid]
if !ok {
return false
}
return session.hasFullScope
}
func (ctl *Controller) createSessionIfAuthorized(username, password string) (*fakeSession, error) {
// get user
user, ok := ctl.usersByUsername[username]
@ -40,16 +60,32 @@ func (ctl *Controller) createSessionIfAuthorized(username, password string) (*fa
session := &fakeSession{
username: username,
uid: ctl.tokenGenerator.next("uid"),
acc: ctl.tokenGenerator.next("acc"),
ref: ctl.tokenGenerator.next("ref"),
hasFullScope: !user.has2FA,
}
ctl.refreshTheTokensForSession(session)
ctl.sessionsByUID[session.uid] = session
return session, nil
}
func (ctl *Controller) refreshTheTokensForSession(session *fakeSession) {
session.refreshToken = ctl.tokenGenerator.next("refresh")
func (ctl *Controller) refreshSessionIfAuthorized(uid, ref string) (*fakeSession, error) {
session, ok := ctl.sessionsByUID[uid]
if !ok {
return nil, pmapi.ErrUnauthorized
}
if ref != session.ref {
return nil, pmapi.ErrUnauthorized
}
session.ref = ctl.tokenGenerator.next("ref")
session.acc = ctl.tokenGenerator.next("acc")
ctl.sessionsByUID[session.uid] = session
return session, nil
}
func (ctl *Controller) deleteSession(uid string) {

View File

@ -24,14 +24,3 @@ type fakeUser struct {
password string
has2FA bool
}
func (fu *fakeUser) get2FAInfo() *pmapi.TwoFactorInfo {
twoFAEnabled := 0
if fu.has2FA {
twoFAEnabled = 1
}
return &pmapi.TwoFactorInfo{
Enabled: twoFAEnabled,
TOTP: 0,
}
}

View File

@ -17,9 +17,13 @@
package fakeapi
import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
import (
"context"
func (api *FakePMAPI) CountMessages(addressID string) ([]*pmapi.MessagesCount, error) {
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (api *FakePMAPI) CountMessages(_ context.Context, addressID string) ([]*pmapi.MessagesCount, error) {
if err := api.checkAndRecordCall(GET, "/mail/v4/messages/count?AddressID="+addressID, nil); err != nil {
return nil, err
}
@ -43,10 +47,16 @@ func (api *FakePMAPI) getCounts(addressID string) []*pmapi.MessagesCount {
counts.Unread++
}
} else {
var unread int
if message.Unread == pmapi.True {
unread = 1
}
allCounts[labelID] = &pmapi.MessagesCount{
LabelID: labelID,
Total: 1,
Unread: message.Unread,
Unread: unread,
}
}
}

View File

@ -18,10 +18,12 @@
package fakeapi
import (
"context"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (api *FakePMAPI) GetEvent(eventID string) (*pmapi.Event, error) {
func (api *FakePMAPI) GetEvent(_ context.Context, eventID string) (*pmapi.Event, error) {
if err := api.checkAndRecordCall(GET, "/events/"+eventID, nil); err != nil {
return nil, err
}

View File

@ -34,28 +34,64 @@ type FakePMAPI struct {
controller *Controller
eventIDGenerator idGenerator
auths chan<- *pmapi.Auth
user *pmapi.User
userKeyRing *crypto.KeyRing
addresses *pmapi.AddressList
addrKeyRing map[string]*crypto.KeyRing
labels []*pmapi.Label
messages []*pmapi.Message
events []*pmapi.Event
authHandlers []pmapi.AuthHandler
user *pmapi.User
userKeyRing *crypto.KeyRing
addresses *pmapi.AddressList
addrKeyRing map[string]*crypto.KeyRing
labels []*pmapi.Label
messages []*pmapi.Message
events []*pmapi.Event
// uid represents the API UID. It is the unique session ID.
uid, lastToken string
uid string
acc string // FIXME(conman): Check this is correct!
ref string // FIXME(conman): Check this is correct!
log *logrus.Entry
}
func New(controller *Controller, userID string) *FakePMAPI {
fakePMAPI := &FakePMAPI{
func newFakePMAPI(controller *Controller, userID, uid, acc, ref string) *FakePMAPI {
return &FakePMAPI{
controller: controller,
log: logrus.WithField("pkg", "fakeapi"),
log: logrus.WithField("pkg", "fakeapi").WithField("uid", uid),
uid: uid,
acc: acc, // FIXME(conman): This should be checked!
ref: ref, // FIXME(conman): This should be checked!
userID: userID,
addrKeyRing: make(map[string]*crypto.KeyRing),
}
}
func NewFakePMAPI(controller *Controller, username, userID, uid, acc, ref string) (*FakePMAPI, error) {
user, ok := controller.usersByUsername[username]
if !ok {
return nil, fmt.Errorf("user %s does not exist", username)
}
addresses, ok := controller.addressesByUsername[username]
if !ok {
addresses = &pmapi.AddressList{}
}
labels, ok := controller.labelsByUsername[username]
if !ok {
labels = []*pmapi.Label{}
}
messages, ok := controller.messagesByUsername[username]
if !ok {
messages = []*pmapi.Message{}
}
fakePMAPI := newFakePMAPI(controller, userID, uid, acc, ref)
fakePMAPI.log = fakePMAPI.log.WithField("username", username)
fakePMAPI.username = username
fakePMAPI.user = user.user
fakePMAPI.addresses = addresses
fakePMAPI.labels = labels
fakePMAPI.messages = messages
fakePMAPI.addEvent(&pmapi.Event{
EventID: fakePMAPI.eventIDGenerator.last("event"),
@ -63,7 +99,7 @@ func New(controller *Controller, userID string) *FakePMAPI {
More: 0,
})
return fakePMAPI
return fakePMAPI, nil
}
func (api *FakePMAPI) CloseConnections() {
@ -74,54 +110,24 @@ func (api *FakePMAPI) checkAndRecordCall(method method, path string, request int
api.controller.locker.Lock()
defer api.controller.locker.Unlock()
if err := api.checkInternetAndRecordCall(method, path, request); err != nil {
api.log.WithField(string(method), path).Trace("CALL")
if err := api.controller.recordCall(method, path, request); err != nil {
return err
}
// Try re-auth
if api.uid == "" && api.lastToken != "" {
api.log.WithField("lastToken", api.lastToken).Warn("Handling unauthorized status")
if _, err := api.AuthRefresh(api.lastToken); err != nil {
return err
}
// FIXME(conman): This needs to match conman behaviour. Should try auth refresh somehow.
if !api.controller.checkAccessToken(api.uid, api.acc) {
return pmapi.ErrUnauthorized
}
// Check client is authenticated. There is difference between
// * invalid token
// * and missing token
// but API treats it the same
if api.uid == "" {
return pmapi.ErrInvalidToken
}
// Any route (except Auth and AuthRefresh) can end with wrong
// token and it should be translated into logout
session, ok := api.controller.sessionsByUID[api.uid]
if !ok {
api.setUID("") // all consecutive requests will not send auth nil
api.sendAuth(nil)
return pmapi.ErrInvalidToken
} else if !session.hasFullScope {
// This is exact error string from the server (at least from documentation).
if path != "/auth/2fa" && !api.controller.checkScope(api.uid) {
return errors.New("Access token does not have sufficient scope") //nolint[stylecheck]
}
return nil
}
func (api *FakePMAPI) checkInternetAndRecordCall(method method, path string, request interface{}) error {
api.log.WithField(string(method), path).Trace("CALL")
api.controller.recordCall(method, path, request)
if api.controller.noInternetConnection {
return pmapi.ErrAPINotReachable
}
return nil
}
func (api *FakePMAPI) sendAuth(auth *pmapi.Auth) {
api.controller.clientManager.HandleAuth(pmapi.ClientAuth{UserID: api.userID, Auth: auth})
}
func (api *FakePMAPI) setUser(username string) error {
api.username = username
api.log = api.log.WithField("username", username)
@ -153,14 +159,9 @@ func (api *FakePMAPI) setUser(username string) error {
return nil
}
func (api *FakePMAPI) setUID(uid string) {
api.uid = uid
api.log = api.log.WithField("uid", api.uid)
api.log.Info("UID updated")
}
func (api *FakePMAPI) unsetUser() {
api.setUID("")
api.uid = ""
api.acc = "" // FIXME(conman): This should be checked!
api.user = nil
api.labels = nil
api.messages = nil

View File

@ -17,7 +17,11 @@
package fakeapi
import "github.com/ProtonMail/proton-bridge/pkg/pmapi"
import (
"context"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
// publicKey is used from pmapi unit tests.
// For now we need just some key, no need to have some specific one.
@ -55,7 +59,7 @@ a+hqY4Jr/a7ui40S+7xYRHKL/7ZAS4/grWllhU3dbNrwSzrOKwrA/U0/9t73
-----END PGP PUBLIC KEY BLOCK-----
`
func (api *FakePMAPI) GetPublicKeysForEmail(email string) (keys []pmapi.PublicKey, internal bool, err error) {
func (api *FakePMAPI) GetPublicKeysForEmail(_ context.Context, email string) (keys []pmapi.PublicKey, internal bool, err error) {
if err := api.checkAndRecordCall(GET, "/keys?Email="+email, nil); err != nil {
return nil, false, err
}

View File

@ -18,6 +18,7 @@
package fakeapi
import (
"context"
"fmt"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -32,14 +33,14 @@ func (api *FakePMAPI) isLabelFolder(labelID string) bool {
return labelID == pmapi.InboxLabel || labelID == pmapi.ArchiveLabel || labelID == pmapi.SentLabel
}
func (api *FakePMAPI) ListLabels() ([]*pmapi.Label, error) {
func (api *FakePMAPI) ListLabels(context.Context) ([]*pmapi.Label, error) {
if err := api.checkAndRecordCall(GET, "/labels/1", nil); err != nil {
return nil, err
}
return api.labels, nil
}
func (api *FakePMAPI) CreateLabel(label *pmapi.Label) (*pmapi.Label, error) {
func (api *FakePMAPI) CreateLabel(_ context.Context, label *pmapi.Label) (*pmapi.Label, error) {
if err := api.checkAndRecordCall(POST, "/labels", &pmapi.LabelReq{Label: label}); err != nil {
return nil, err
}
@ -61,7 +62,7 @@ func (api *FakePMAPI) CreateLabel(label *pmapi.Label) (*pmapi.Label, error) {
return label, nil
}
func (api *FakePMAPI) UpdateLabel(label *pmapi.Label) (*pmapi.Label, error) {
func (api *FakePMAPI) UpdateLabel(_ context.Context, label *pmapi.Label) (*pmapi.Label, error) {
if err := api.checkAndRecordCall(PUT, "/labels", &pmapi.LabelReq{Label: label}); err != nil {
return nil, err
}
@ -81,7 +82,7 @@ func (api *FakePMAPI) UpdateLabel(label *pmapi.Label) (*pmapi.Label, error) {
return nil, fmt.Errorf("label %s does not exist", label.ID)
}
func (api *FakePMAPI) DeleteLabel(labelID string) error {
func (api *FakePMAPI) DeleteLabel(_ context.Context, labelID string) error {
if err := api.checkAndRecordCall(DELETE, "/labels/"+labelID, nil); err != nil {
return err
}

164
test/fakeapi/manager.go Normal file
View File

@ -0,0 +1,164 @@
package fakeapi
import (
"context"
"net/http"
"net/url"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/go-resty/resty/v2"
)
type fakePMAPIManager struct {
controller *Controller
}
func (m *fakePMAPIManager) NewClient(uid string, acc string, ref string, _ time.Time) pmapi.Client {
session, ok := m.controller.sessionsByUID[uid]
if !ok {
return newFakePMAPI(m.controller, "", "", "", "")
}
user, ok := m.controller.usersByUsername[session.username]
if !ok {
return newFakePMAPI(m.controller, "", "", "", "")
}
client, err := NewFakePMAPI(m.controller, session.username, user.user.ID, session.uid, session.acc, session.ref)
if err != nil {
return newFakePMAPI(m.controller, "", "", "", "")
}
m.controller.fakeAPIs = append(m.controller.fakeAPIs, client)
return client
}
func (m *fakePMAPIManager) NewClientWithRefresh(_ context.Context, uid, ref string) (pmapi.Client, *pmapi.Auth, error) {
if err := m.controller.recordCall(POST, "/auth/refresh", &pmapi.AuthRefreshReq{
UID: uid,
RefreshToken: ref,
ResponseType: "token",
GrantType: "refresh_token",
RedirectURI: "https://protonmail.ch",
State: "random_string",
}); err != nil {
return nil, nil, err
}
session, err := m.controller.refreshSessionIfAuthorized(uid, ref)
if err != nil {
return nil, nil, pmapi.ErrUnauthorized
}
user, ok := m.controller.usersByUsername[session.username]
if !ok {
return nil, nil, errWrongNameOrPassword
}
client, err := NewFakePMAPI(m.controller, session.username, user.user.ID, session.uid, session.acc, session.ref)
if err != nil {
return nil, nil, err
}
m.controller.fakeAPIs = append(m.controller.fakeAPIs, client)
auth := &pmapi.Auth{
UID: session.uid,
AccessToken: session.acc,
RefreshToken: session.ref,
ExpiresIn: 86400, // seconds,
}
if user.has2FA {
auth.TwoFA = pmapi.TwoFAInfo{
Enabled: pmapi.TOTPEnabled,
}
}
return client, auth, nil
}
func (m *fakePMAPIManager) NewClientWithLogin(_ context.Context, username string, password string) (pmapi.Client, *pmapi.Auth, error) {
if err := m.controller.recordCall(POST, "/auth/info", &pmapi.GetAuthInfoReq{Username: username}); err != nil {
return nil, nil, err
}
// If username is wrong, API server will return empty but positive response.
// However, we will fail to create a client, so we return error here.
user, ok := m.controller.usersByUsername[username]
if !ok {
return nil, nil, errWrongNameOrPassword
}
if err := m.controller.recordCall(POST, "/auth", &pmapi.AuthReq{Username: username}); err != nil {
return nil, nil, err
}
session, err := m.controller.createSessionIfAuthorized(username, password)
if err != nil {
return nil, nil, err
}
client, err := NewFakePMAPI(m.controller, username, user.user.ID, session.uid, session.acc, session.ref)
if err != nil {
return nil, nil, err
}
m.controller.fakeAPIs = append(m.controller.fakeAPIs, client)
auth := &pmapi.Auth{
UID: session.uid,
AccessToken: session.acc,
RefreshToken: session.ref,
ExpiresIn: 86400, // seconds,
}
if user.has2FA {
auth.TwoFA = pmapi.TwoFAInfo{
Enabled: pmapi.TOTPEnabled,
}
}
return client, auth, nil
}
func (*fakePMAPIManager) DownloadAndVerify(kr *crypto.KeyRing, url, sig string) ([]byte, error) {
panic("TODO")
}
func (*fakePMAPIManager) ReportBug(context.Context, pmapi.ReportBugReq) error {
panic("TODO")
}
func (m *fakePMAPIManager) SendSimpleMetric(_ context.Context, cat string, act string, lab string) error {
v := url.Values{}
v.Set("Category", cat)
v.Set("Action", act)
v.Set("Label", lab)
return m.controller.recordCall(GET, "/metrics?"+v.Encode(), nil)
}
func (*fakePMAPIManager) SetLogger(resty.Logger) {
panic("TODO")
}
func (*fakePMAPIManager) SetTransport(http.RoundTripper) {
panic("TODO")
}
func (*fakePMAPIManager) SetCookieJar(http.CookieJar) {
panic("TODO")
}
func (*fakePMAPIManager) SetRetryCount(int) {
panic("TODO")
}
func (*fakePMAPIManager) AddConnectionObserver(pmapi.ConnectionObserver) {
panic("TODO")
}

View File

@ -19,6 +19,7 @@ package fakeapi
import (
"bytes"
"context"
"fmt"
"time"
@ -29,7 +30,7 @@ import (
var errWasNotUpdated = errors.New("message was not updated")
func (api *FakePMAPI) GetMessage(apiID string) (*pmapi.Message, error) {
func (api *FakePMAPI) GetMessage(_ context.Context, apiID string) (*pmapi.Message, error) {
if err := api.checkAndRecordCall(GET, "/mail/v4/messages/"+apiID, nil); err != nil {
return nil, err
}
@ -49,7 +50,7 @@ func (api *FakePMAPI) GetMessage(apiID string) (*pmapi.Message, error) {
// * ID
// * Attachments
// * AutoWildcard
func (api *FakePMAPI) ListMessages(filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
func (api *FakePMAPI) ListMessages(_ context.Context, filter *pmapi.MessagesFilter) ([]*pmapi.Message, int, error) {
if err := api.checkAndRecordCall(GET, "/mail/v4/messages", filter); err != nil {
return nil, 0, err
}
@ -131,10 +132,14 @@ func isMessageMatchingFilter(filter *pmapi.MessagesFilter, message *pmapi.Messag
return false
}
if filter.Unread != nil {
wantUnread := 0
var wantUnread pmapi.Boolean
if *filter.Unread {
wantUnread = 1
wantUnread = pmapi.True
} else {
wantUnread = pmapi.False
}
if message.Unread != wantUnread {
return false
}
@ -150,7 +155,7 @@ func copyFilteredMessage(message *pmapi.Message) *pmapi.Message {
return filteredMessage
}
func (api *FakePMAPI) CreateDraft(message *pmapi.Message, parentID string, action int) (*pmapi.Message, error) {
func (api *FakePMAPI) CreateDraft(ctx context.Context, message *pmapi.Message, parentID string, action int) (*pmapi.Message, error) {
if err := api.checkAndRecordCall(POST, "/mail/v4/messages", &pmapi.DraftReq{
Message: message,
ParentID: parentID,
@ -160,7 +165,7 @@ func (api *FakePMAPI) CreateDraft(message *pmapi.Message, parentID string, actio
return nil, err
}
if parentID != "" {
if _, err := api.GetMessage(parentID); err != nil {
if _, err := api.GetMessage(ctx, parentID); err != nil {
return nil, err
}
}
@ -174,11 +179,11 @@ func (api *FakePMAPI) CreateDraft(message *pmapi.Message, parentID string, actio
return message, nil
}
func (api *FakePMAPI) SendMessage(messageID string, sendMessageRequest *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error) {
func (api *FakePMAPI) SendMessage(ctx context.Context, messageID string, sendMessageRequest *pmapi.SendMessageReq) (sent, parent *pmapi.Message, err error) {
if err := api.checkAndRecordCall(POST, "/mail/v4/messages/"+messageID, sendMessageRequest); err != nil {
return nil, nil, err
}
message, err := api.GetMessage(messageID)
message, err := api.GetMessage(ctx, messageID)
if err != nil {
return nil, nil, errors.Wrap(err, "draft does not exist")
}
@ -188,7 +193,7 @@ func (api *FakePMAPI) SendMessage(messageID string, sendMessageRequest *pmapi.Se
return message, nil, nil
}
func (api *FakePMAPI) Import(importMessageRequests []*pmapi.ImportMsgReq) ([]*pmapi.ImportMsgRes, error) {
func (api *FakePMAPI) Import(_ context.Context, importMessageRequests pmapi.ImportMsgReqs) ([]*pmapi.ImportMsgRes, error) {
if err := api.checkAndRecordCall(POST, "/import", importMessageRequests); err != nil {
return nil, err
}
@ -211,7 +216,7 @@ func (api *FakePMAPI) Import(importMessageRequests []*pmapi.ImportMsgReq) ([]*pm
}
func (api *FakePMAPI) generateMessageFromImportRequest(msgReq *pmapi.ImportMsgReq) (*pmapi.Message, error) {
m, _, _, _, err := message.Parse(bytes.NewReader(msgReq.Body)) // nolint[dogsled]
m, _, _, _, err := message.Parse(bytes.NewReader(msgReq.Message)) // nolint[dogsled]
if err != nil {
return nil, err
}
@ -230,16 +235,16 @@ func (api *FakePMAPI) generateMessageFromImportRequest(msgReq *pmapi.ImportMsgRe
return &pmapi.Message{
ID: messageID,
ExternalID: m.ExternalID,
AddressID: msgReq.AddressID,
AddressID: msgReq.Metadata.AddressID,
Sender: m.Sender,
ToList: m.ToList,
Subject: m.Subject,
Unread: msgReq.Unread,
Unread: msgReq.Metadata.Unread,
LabelIDs: api.generateLabelIDsFromImportRequest(msgReq),
Body: m.Body,
Header: m.Header,
Flags: msgReq.Flags,
Time: msgReq.Time,
Flags: msgReq.Metadata.Flags,
Time: msgReq.Metadata.Time,
}, nil
}
@ -248,17 +253,17 @@ func (api *FakePMAPI) generateMessageFromImportRequest(msgReq *pmapi.ImportMsgRe
func (api *FakePMAPI) generateLabelIDsFromImportRequest(msgReq *pmapi.ImportMsgReq) []string {
isInSentOrInbox := false
labelIDs := []string{pmapi.AllMailLabel}
for _, labelID := range msgReq.LabelIDs {
for _, labelID := range msgReq.Metadata.LabelIDs {
if labelID == pmapi.InboxLabel || labelID == pmapi.SentLabel {
isInSentOrInbox = true
} else {
labelIDs = append(labelIDs, labelID)
}
}
if isInSentOrInbox && (msgReq.Flags&pmapi.FlagSent) != 0 {
if isInSentOrInbox && (msgReq.Metadata.Flags&pmapi.FlagSent) != 0 {
labelIDs = append(labelIDs, pmapi.SentLabel)
}
if isInSentOrInbox && (msgReq.Flags&pmapi.FlagReceived) != 0 {
if isInSentOrInbox && (msgReq.Metadata.Flags&pmapi.FlagReceived) != 0 {
labelIDs = append(labelIDs, pmapi.InboxLabel)
}
return labelIDs
@ -287,7 +292,7 @@ func (api *FakePMAPI) addMessage(message *pmapi.Message) {
api.addEventMessage(pmapi.EventCreate, message)
}
func (api *FakePMAPI) DeleteMessages(apiIDs []string) error {
func (api *FakePMAPI) DeleteMessages(_ context.Context, apiIDs []string) error {
err := api.deleteMessages(PUT, "/mail/v4/messages/delete", &pmapi.MessagesActionReq{
IDs: apiIDs,
}, func(message *pmapi.Message) bool {
@ -304,7 +309,7 @@ func (api *FakePMAPI) DeleteMessages(apiIDs []string) error {
return nil
}
func (api *FakePMAPI) EmptyFolder(labelID string, addressID string) error {
func (api *FakePMAPI) EmptyFolder(_ context.Context, labelID string, addressID string) error {
err := api.deleteMessages(DELETE, "/mail/v4/messages/empty?LabelID="+labelID+"&AddressID="+addressID, nil, func(message *pmapi.Message) bool {
return hasItem(message.LabelIDs, labelID) && message.AddressID == addressID
})
@ -340,7 +345,7 @@ func (api *FakePMAPI) deleteMessages(method method, path string, request interfa
return nil
}
func (api *FakePMAPI) LabelMessages(apiIDs []string, labelID string) error {
func (api *FakePMAPI) LabelMessages(_ context.Context, apiIDs []string, labelID string) error {
return api.updateMessages(PUT, "/mail/v4/messages/label", &pmapi.LabelMessagesReq{
IDs: apiIDs,
LabelID: labelID,
@ -366,7 +371,7 @@ func (api *FakePMAPI) LabelMessages(apiIDs []string, labelID string) error {
})
}
func (api *FakePMAPI) UnlabelMessages(apiIDs []string, labelID string) error {
func (api *FakePMAPI) UnlabelMessages(_ context.Context, apiIDs []string, labelID string) error {
return api.updateMessages(PUT, "/mail/v4/messages/unlabel", &pmapi.LabelMessagesReq{
IDs: apiIDs,
LabelID: labelID,
@ -384,7 +389,7 @@ func (api *FakePMAPI) UnlabelMessages(apiIDs []string, labelID string) error {
})
}
func (api *FakePMAPI) MarkMessagesRead(apiIDs []string) error {
func (api *FakePMAPI) MarkMessagesRead(_ context.Context, apiIDs []string) error {
return api.updateMessages(PUT, "/mail/v4/messages/read", &pmapi.MessagesActionReq{
IDs: apiIDs,
}, apiIDs, func(message *pmapi.Message) error {
@ -396,7 +401,7 @@ func (api *FakePMAPI) MarkMessagesRead(apiIDs []string) error {
})
}
func (api *FakePMAPI) MarkMessagesUnread(apiIDs []string) error {
func (api *FakePMAPI) MarkMessagesUnread(_ context.Context, apiIDs []string) error {
err := api.updateMessages(PUT, "/mail/v4/messages/unread", &pmapi.MessagesActionReq{
IDs: apiIDs,
}, apiIDs, func(message *pmapi.Message) error {

View File

@ -1,40 +0,0 @@
// 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 fakeapi
import (
"net/url"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (api *FakePMAPI) Report(report pmapi.ReportReq) error {
return api.checkInternetAndRecordCall(POST, "/reports/bug", report)
}
func (api *FakePMAPI) SendSimpleMetric(category, action, label string) error {
v := url.Values{}
v.Set("Category", category)
v.Set("Action", action)
v.Set("Label", label)
return api.checkInternetAndRecordCall(GET, "/metrics?"+v.Encode(), nil)
}
func (api *FakePMAPI) ReportSentryCrash(err error) error {
return nil
}

View File

@ -18,11 +18,13 @@
package fakeapi
import (
"context"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func (api *FakePMAPI) GetMailSettings() (pmapi.MailSettings, error) {
func (api *FakePMAPI) GetMailSettings(context.Context) (pmapi.MailSettings, error) {
if err := api.checkAndRecordCall(GET, "/mail/v4/settings", nil); err != nil {
return pmapi.MailSettings{}, err
}
@ -33,7 +35,7 @@ func (api *FakePMAPI) IsUnlocked() bool {
return api.userKeyRing != nil
}
func (api *FakePMAPI) Unlock(passphrase []byte) (err error) {
func (api *FakePMAPI) Unlock(_ context.Context, passphrase []byte) (err error) {
if api.userKeyRing != nil {
return
}
@ -63,19 +65,19 @@ func (api *FakePMAPI) Unlock(passphrase []byte) (err error) {
return nil
}
func (api *FakePMAPI) ReloadKeys(passphrase []byte) (err error) {
if _, err = api.UpdateUser(); err != nil {
func (api *FakePMAPI) ReloadKeys(ctx context.Context, passphrase []byte) (err error) {
if _, err = api.UpdateUser(ctx); err != nil {
return
}
return api.Unlock(passphrase)
return api.Unlock(ctx, passphrase)
}
func (api *FakePMAPI) CurrentUser() (*pmapi.User, error) {
return api.UpdateUser()
func (api *FakePMAPI) CurrentUser(ctx context.Context) (*pmapi.User, error) {
return api.UpdateUser(ctx)
}
func (api *FakePMAPI) UpdateUser() (*pmapi.User, error) {
func (api *FakePMAPI) UpdateUser(context.Context) (*pmapi.User, error) {
if err := api.checkAndRecordCall(GET, "/users", nil); err != nil {
return nil, err
}
@ -83,14 +85,14 @@ func (api *FakePMAPI) UpdateUser() (*pmapi.User, error) {
return api.user, nil
}
func (api *FakePMAPI) GetAddresses() (pmapi.AddressList, error) {
func (api *FakePMAPI) GetAddresses(context.Context) (pmapi.AddressList, error) {
if err := api.checkAndRecordCall(GET, "/addresses", nil); err != nil {
return nil, err
}
return *api.addresses, nil
}
func (api *FakePMAPI) ReorderAddresses(addressIDs []string) error {
func (api *FakePMAPI) ReorderAddresses(_ context.Context, addressIDs []string) error {
if err := api.checkAndRecordCall(PUT, "/addresses/order", nil); err != nil {
return err
}

View File

@ -6,7 +6,6 @@ Feature: Start bridge
Then "user" is connected
And "user" has loaded store
And "user" has running event loop
And "user" has API auth
Scenario: Start with connected user, database file and no internet connection
Given there is connected user "user"
@ -16,7 +15,6 @@ Feature: Start bridge
Then "user" is connected
And "user" has loaded store
And "user" has running event loop
And "user" does not have API auth
@ignore
Scenario: Start with connected user, no database file and internet connection
@ -26,7 +24,7 @@ Feature: Start bridge
Then "user" is connected
And "user" has loaded store
And "user" has running event loop
And "user" has API auth
And "user" is connected
@ignore
Scenario: Start with connected user, no database file and no internet connection
@ -35,7 +33,6 @@ Feature: Start bridge
And there is no internet connection
When bridge starts
Then "user" is disconnected
And "user" does not have API auth
Scenario: Start with disconnected user, database file and internet connection
Given there is disconnected user "user"
@ -44,7 +41,6 @@ Feature: Start bridge
Then "user" is disconnected
And "user" has loaded store
And "user" does not have running event loop
And "user" does not have API auth
Scenario: Start with disconnected user, database file and no internet connection
Given there is disconnected user "user"
@ -54,7 +50,6 @@ Feature: Start bridge
Then "user" is disconnected
And "user" has loaded store
And "user" does not have running event loop
And "user" does not have API auth
@ignore
Scenario: Start with disconnected user, no database file and internet connection
@ -64,7 +59,6 @@ Feature: Start bridge
Then "user" is disconnected
And "user" does not have loaded store
And "user" does not have running event loop
And "user" does not have API auth
@ignore
Scenario: Start with disconnected user, no database file and no internet connection
@ -75,4 +69,3 @@ Feature: Start bridge
Then "user" is disconnected
And "user" does not have loaded store
And "user" does not have running event loop
And "user" does not have API auth

View File

@ -21,7 +21,7 @@ Feature: Login for the first time
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"
Then last response is "failed to login: no internet connection"
@ignore-live
Scenario: Login user with 2FA

View File

@ -19,7 +19,7 @@ Feature: Login for the first time
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"
Then last response is "failed to login: no internet connection"
@ignore-live
Scenario: Login user with 2FA

View File

@ -18,6 +18,7 @@
package liveapi
import (
"context"
"time"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
@ -43,7 +44,7 @@ func cleanup(client pmapi.Client, addresses *pmapi.AddressList) error {
func cleanSystemFolders(client pmapi.Client) error {
for _, labelID := range []string{pmapi.InboxLabel, pmapi.SentLabel, pmapi.ArchiveLabel, pmapi.AllMailLabel, pmapi.DraftLabel} {
for {
messages, total, err := client.ListMessages(&pmapi.MessagesFilter{
messages, total, err := client.ListMessages(context.TODO(), &pmapi.MessagesFilter{
PageSize: 150,
LabelID: labelID,
})
@ -60,7 +61,7 @@ func cleanSystemFolders(client pmapi.Client) error {
messageIDs = append(messageIDs, message.ID)
}
if err := client.DeleteMessages(messageIDs); err != nil {
if err := client.DeleteMessages(context.TODO(), messageIDs); err != nil {
return errors.Wrap(err, "failed to delete messages")
}
@ -73,7 +74,7 @@ func cleanSystemFolders(client pmapi.Client) error {
}
func cleanCustomLables(client pmapi.Client) error {
labels, err := client.ListLabels()
labels, err := client.ListLabels(context.TODO())
if err != nil {
return errors.Wrap(err, "failed to list labels")
}
@ -82,7 +83,7 @@ func cleanCustomLables(client pmapi.Client) error {
if err := emptyFolder(client, label.ID); err != nil {
return errors.Wrap(err, "failed to empty label")
}
if err := client.DeleteLabel(label.ID); err != nil {
if err := client.DeleteLabel(context.TODO(), label.ID); err != nil {
return errors.Wrap(err, "failed to delete label")
}
}
@ -92,7 +93,7 @@ func cleanCustomLables(client pmapi.Client) error {
func cleanTrash(client pmapi.Client) error {
for {
_, total, err := client.ListMessages(&pmapi.MessagesFilter{
_, total, err := client.ListMessages(context.TODO(), &pmapi.MessagesFilter{
PageSize: 1,
LabelID: pmapi.TrashLabel,
})
@ -114,12 +115,12 @@ func cleanTrash(client pmapi.Client) error {
}
func emptyFolder(client pmapi.Client, labelID string) error {
err := client.EmptyFolder(labelID, "")
err := client.EmptyFolder(context.TODO(), labelID, "")
if err != nil {
return err
}
for {
_, total, err := client.ListMessages(&pmapi.MessagesFilter{
_, total, err := client.ListMessages(context.TODO(), &pmapi.MessagesFilter{
PageSize: 1,
LabelID: labelID,
})
@ -141,5 +142,5 @@ func reorderAddresses(client pmapi.Client, addresses *pmapi.AddressList) error {
addressIDs = append(addressIDs, address.ID)
}
return client.ReorderAddresses(addressIDs)
return client.ReorderAddresses(context.TODO(), addressIDs)
}

View File

@ -30,27 +30,29 @@ type Controller struct {
calls []*fakeCall
pmapiByUsername map[string]pmapi.Client
messageIDsByUsername map[string][]string
clientManager *pmapi.ClientManager
clientManager pmapi.Manager
// State controlled by test.
noInternetConnection bool
}
func NewController(cm *pmapi.ClientManager) *Controller {
func NewController() (*Controller, pmapi.Manager) {
controller := &Controller{
lock: &sync.RWMutex{},
calls: []*fakeCall{},
pmapiByUsername: map[string]pmapi.Client{},
messageIDsByUsername: map[string][]string{},
clientManager: cm,
noInternetConnection: false,
}
cm.SetRoundTripper(&fakeTransport{
// FIXME(conman): Set connect values here?
cm := pmapi.New(pmapi.DefaultConfig)
cm.SetTransport(&fakeTransport{
ctl: controller,
transport: http.DefaultTransport,
})
return controller
return controller, cm
}

View File

@ -18,6 +18,7 @@
package liveapi
import (
"context"
"fmt"
"strings"
@ -44,7 +45,7 @@ func (ctl *Controller) AddUserLabel(username string, label *pmapi.Label) error {
label.Exclusive = getLabelExclusive(label.Name)
label.Name = getLabelNameWithoutPrefix(label.Name)
label.Color = pmapi.LabelColors[0]
if _, err := client.CreateLabel(label); err != nil {
if _, err := client.CreateLabel(context.TODO(), label); err != nil {
return errors.Wrap(err, "failed to create label")
}
return nil
@ -72,7 +73,7 @@ func (ctl *Controller) getLabelID(username, labelName string) (string, error) {
return "", fmt.Errorf("user %s does not exist", username)
}
labels, err := client.ListLabels()
labels, err := client.ListLabels(context.TODO())
if err != nil {
return "", errors.Wrap(err, "failed to list labels")
}

View File

@ -18,6 +18,7 @@
package liveapi
import (
"context"
"fmt"
messageUtils "github.com/ProtonMail/proton-bridge/pkg/message"
@ -50,15 +51,17 @@ func (ctl *Controller) AddUserMessage(username string, message *pmapi.Message) (
}
req := &pmapi.ImportMsgReq{
AddressID: message.AddressID,
Body: body,
Unread: message.Unread,
Time: message.Time,
Flags: message.Flags,
LabelIDs: message.LabelIDs,
Metadata: &pmapi.ImportMetadata{
AddressID: message.AddressID,
Unread: message.Unread,
Time: message.Time,
Flags: message.Flags,
LabelIDs: message.LabelIDs,
},
Message: body,
}
results, err := client.Import([]*pmapi.ImportMsgReq{req})
results, err := client.Import(context.TODO(), pmapi.ImportMsgReqs{req})
if err != nil {
return "", errors.Wrap(err, "failed to make an import")
}
@ -82,7 +85,7 @@ func (ctl *Controller) GetMessages(username, labelID string) ([]*pmapi.Message,
for {
// ListMessages returns empty result, not error, asking for page out of range.
pageMessages, _, err := client.ListMessages(&pmapi.MessagesFilter{
pageMessages, _, err := client.ListMessages(context.TODO(), &pmapi.MessagesFilter{
Page: page,
PageSize: 150,
LabelID: labelID,

View File

@ -18,6 +18,8 @@
package liveapi
import (
"context"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/cucumber/godog"
"github.com/pkg/errors"
@ -28,19 +30,12 @@ func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, p
return godog.ErrPending
}
client := ctl.clientManager.GetClient(user.ID)
authInfo, err := client.AuthInfo(user.Name)
client, _, err := ctl.clientManager.NewClientWithLogin(context.TODO(), user.Name, password)
if err != nil {
return errors.Wrap(err, "failed to get auth info")
return errors.Wrap(err, "failed to create new client")
}
_, err = client.Auth(user.Name, password, authInfo)
if err != nil {
return errors.Wrap(err, "failed to auth user")
}
salt, err := client.AuthSalt()
salt, err := client.AuthSalt(context.TODO())
if err != nil {
return errors.Wrap(err, "failed to get salt")
}
@ -50,7 +45,7 @@ func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, p
return errors.Wrap(err, "failed to hash mailbox password")
}
if err := client.Unlock([]byte(mailboxPassword)); err != nil {
if err := client.Unlock(context.TODO(), mailboxPassword); err != nil {
return errors.Wrap(err, "failed to unlock user")
}
@ -64,7 +59,5 @@ func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, p
}
func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) error {
client := ctl.clientManager.GetClient(user.ID)
return client.ReorderAddresses(addressIDs)
return ctl.pmapiByUsername[user.Name].ReorderAddresses(context.TODO(), addressIDs)
}

View File

@ -255,10 +255,14 @@ func messagesContainsMessageRow(account *accounts.TestAccount, allMessages []int
matches = false
}
case "read":
unread := 1
var unread pmapi.Boolean
if cell.Value == "true" { //nolint[goconst]
unread = 0
unread = pmapi.False
} else {
unread = pmapi.True
}
if message.Unread != unread {
matches = false
}

View File

@ -173,10 +173,14 @@ func processMessageTableCell(column, cellValue, username string, message *pmapi.
case "body":
message.Body = cellValue
case "read":
unread := 1
if cellValue == "true" {
unread = 0
var unread pmapi.Boolean
if cellValue == "true" { //nolint[goconst]
unread = false
} else {
unread = true
}
message.Unread = unread
case "starred":
if cellValue == "true" {

View File

@ -34,8 +34,10 @@ func UsersChecksFeatureContext(s *godog.Suite) {
s.Step(`^"([^"]*)" does not have loaded store$`, userDoesNotHaveLoadedStore)
s.Step(`^"([^"]*)" has running event loop$`, userHasRunningEventLoop)
s.Step(`^"([^"]*)" does not have running event loop$`, userDoesNotHaveRunningEventLoop)
s.Step(`^"([^"]*)" does not have API auth$`, isNotAuthorized)
s.Step(`^"([^"]*)" has API auth$`, isAuthorized)
// FIXME(conman): Write tests for new "auth" system.
// s.Step(`^"([^"]*)" does not have API auth$`, isNotAuthorized)
// s.Step(`^"([^"]*)" has API auth$`, isAuthorized)
}
func userHasAddressModeInMode(bddUserID, wantAddressMode string) error {
@ -162,6 +164,7 @@ func userDoesNotHaveRunningEventLoop(bddUserID string) error {
return ctx.GetTestingError()
}
/*
func isAuthorized(bddUserID string) error {
account := ctx.GetTestAccount(bddUserID)
if account == nil {
@ -187,3 +190,4 @@ func isNotAuthorized(bddUserID string) error {
a.Eventually(ctx.GetTestingT(), func() bool { return !user.IsAuthorized() }, 5*time.Second, 10*time.Millisecond)
return ctx.GetTestingError()
}
*/