mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
GODT-1913: pass reporter to gluon, limit restarts, add crash handlers.
This commit is contained in:
8
Makefile
8
Makefile
@ -245,7 +245,10 @@ coverage: test
|
|||||||
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
go tool cover -html=/tmp/coverage.out -o=coverage.html
|
||||||
|
|
||||||
mocks:
|
mocks:
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/bridge TLSReporter,ProxyController,Autostarter > internal/bridge/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
|
||||||
|
mv tmp internal/bridge/mocks/mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v2/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||||
|
|
||||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
||||||
@ -312,6 +315,9 @@ endif
|
|||||||
run-nogui: build-nogui clean-vendor gofiles
|
run-nogui: build-nogui clean-vendor gofiles
|
||||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||||
|
|
||||||
|
run-debug:
|
||||||
|
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
||||||
|
|
||||||
clean-vendor:
|
clean-vendor:
|
||||||
rm -rf ./vendor
|
rm -rf ./vendor
|
||||||
|
|
||||||
|
|||||||
@ -183,20 +183,27 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
// Load the cookies from the vault.
|
// Load the cookies from the vault.
|
||||||
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
||||||
// Create a new bridge instance.
|
// Create a new bridge instance.
|
||||||
return withBridge(c, exe, locations, version, identifier, reporter, vault, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
return withBridge(
|
||||||
if insecure {
|
c, exe, locations, version,
|
||||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
identifier, crashHandler, reporter,
|
||||||
b.PushError(bridge.ErrVaultInsecure)
|
vault, cookieJar, func(
|
||||||
}
|
b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||||
|
if insecure {
|
||||||
|
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||||
|
b.PushError(bridge.ErrVaultInsecure)
|
||||||
|
}
|
||||||
|
|
||||||
if corrupt {
|
if corrupt {
|
||||||
logrus.Warn("The vault is corrupt and has been wiped")
|
logrus.Warn("The vault is corrupt and has been wiped")
|
||||||
b.PushError(bridge.ErrVaultCorrupt)
|
b.PushError(bridge.ErrVaultCorrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the frontend.
|
// Run the frontend.
|
||||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh)
|
return runFrontend(c, crashHandler, restarter,
|
||||||
})
|
locations, b, eventCh,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v2/internal/crash"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/dialer"
|
"github.com/ProtonMail/proton-bridge/v2/internal/dialer"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||||
@ -51,7 +52,8 @@ func withBridge( //nolint:funlen
|
|||||||
locations *locations.Locations,
|
locations *locations.Locations,
|
||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
identifier *useragent.UserAgent,
|
identifier *useragent.UserAgent,
|
||||||
_ *sentry.Reporter,
|
crashHandler *crash.Handler,
|
||||||
|
reporter *sentry.Reporter,
|
||||||
vault *vault.Vault,
|
vault *vault.Vault,
|
||||||
cookieJar http.CookieJar,
|
cookieJar http.CookieJar,
|
||||||
fn func(*bridge.Bridge, <-chan events.Event) error,
|
fn func(*bridge.Bridge, <-chan events.Event) error,
|
||||||
@ -104,6 +106,10 @@ func withBridge( //nolint:funlen
|
|||||||
dialer.CreateTransportWithDialer(proxyDialer),
|
dialer.CreateTransportWithDialer(proxyDialer),
|
||||||
proxyDialer,
|
proxyDialer,
|
||||||
|
|
||||||
|
// Crash and report stuff
|
||||||
|
crashHandler,
|
||||||
|
reporter,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||||
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
c.String(flagLogIMAP) == "server" || c.String(flagLogIMAP) == "all",
|
||||||
|
|||||||
233
internal/async/group.go
Normal file
233
internal/async/group.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
// Copyright (c) 2022 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail 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.
|
||||||
|
//
|
||||||
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package async
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PanicHandler interface {
|
||||||
|
HandlePanic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group is forked and improved version of "github.com/bradenaw/juniper/xsync.Group".
|
||||||
|
//
|
||||||
|
// It manages a group of goroutines. The main change to original is posibility
|
||||||
|
// to wait passed function to finish without canceling it's context and adding
|
||||||
|
// PanicHandler.
|
||||||
|
type Group struct {
|
||||||
|
baseCtx context.Context
|
||||||
|
ctx context.Context
|
||||||
|
jobCtx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
finish context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
panicHandler PanicHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGroup returns a Group ready for use. The context passed to any of the f functions will be a
|
||||||
|
// descendant of ctx.
|
||||||
|
func NewGroup(ctx context.Context, panicHandler PanicHandler) *Group {
|
||||||
|
bgCtx, cancel := context.WithCancel(ctx)
|
||||||
|
jobCtx, finish := context.WithCancel(ctx)
|
||||||
|
return &Group{
|
||||||
|
baseCtx: ctx,
|
||||||
|
ctx: bgCtx,
|
||||||
|
jobCtx: jobCtx,
|
||||||
|
cancel: cancel,
|
||||||
|
finish: finish,
|
||||||
|
panicHandler: panicHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once calls f once from another goroutine.
|
||||||
|
func (g *Group) Once(f func(ctx context.Context)) {
|
||||||
|
g.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer g.handlePanic()
|
||||||
|
|
||||||
|
f(g.ctx)
|
||||||
|
g.wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// jitterDuration returns a random duration in [d - jitter, d + jitter].
|
||||||
|
func jitterDuration(d time.Duration, jitter time.Duration) time.Duration {
|
||||||
|
return d + time.Duration(float64(jitter)*((rand.Float64()*2)-1)) //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodic spawns a goroutine that calls f once per interval +/- jitter.
|
||||||
|
func (g *Group) Periodic(
|
||||||
|
interval time.Duration,
|
||||||
|
jitter time.Duration,
|
||||||
|
f func(ctx context.Context),
|
||||||
|
) {
|
||||||
|
g.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer g.handlePanic()
|
||||||
|
|
||||||
|
defer g.wg.Done()
|
||||||
|
|
||||||
|
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if g.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-g.jobCtx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Reset(jitterDuration(interval, jitter))
|
||||||
|
f(g.ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger spawns a goroutine which calls f whenever the returned function is called. If f is
|
||||||
|
// already running when triggered, f will run again immediately when it finishes.
|
||||||
|
func (g *Group) Trigger(f func(ctx context.Context)) func() {
|
||||||
|
c := make(chan struct{}, 1)
|
||||||
|
g.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer g.handlePanic()
|
||||||
|
|
||||||
|
defer g.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if g.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-g.jobCtx.Done():
|
||||||
|
return
|
||||||
|
case <-c:
|
||||||
|
}
|
||||||
|
f(g.ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
select {
|
||||||
|
case c <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeriodicOrTrigger spawns a goroutine which calls f whenever the returned function is called. If
|
||||||
|
// f is already running when triggered, f will run again immediately when it finishes. Also calls f
|
||||||
|
// when it has been interval+/-jitter since the last trigger.
|
||||||
|
func (g *Group) PeriodicOrTrigger(
|
||||||
|
interval time.Duration,
|
||||||
|
jitter time.Duration,
|
||||||
|
f func(ctx context.Context),
|
||||||
|
) func() {
|
||||||
|
c := make(chan struct{}, 1)
|
||||||
|
g.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer g.handlePanic()
|
||||||
|
|
||||||
|
defer g.wg.Done()
|
||||||
|
|
||||||
|
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if g.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-g.jobCtx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
t.Reset(jitterDuration(interval, jitter))
|
||||||
|
case <-c:
|
||||||
|
if !t.Stop() {
|
||||||
|
<-t.C
|
||||||
|
}
|
||||||
|
t.Reset(jitterDuration(interval, jitter))
|
||||||
|
}
|
||||||
|
f(g.ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
select {
|
||||||
|
case c <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) resetCtx() {
|
||||||
|
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||||
|
g.ctx, g.cancel = context.WithCancel(g.baseCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel is send to all of the spawn goroutines and ends periodic
|
||||||
|
// or trigger routines.
|
||||||
|
func (g *Group) Cancel() {
|
||||||
|
g.cancel()
|
||||||
|
g.finish()
|
||||||
|
g.resetCtx()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish will ends all periodic or polls routines. It will let
|
||||||
|
// currently running functions to finish (cancel is not sent).
|
||||||
|
//
|
||||||
|
// It is not safe to call Wait concurrently with any other method on g.
|
||||||
|
func (g *Group) Finish() {
|
||||||
|
g.finish()
|
||||||
|
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelAndWait cancels the context passed to any of the spawned goroutines and waits for all spawned
|
||||||
|
// goroutines to exit.
|
||||||
|
//
|
||||||
|
// It is not safe to call Wait concurrently with any other method on g.
|
||||||
|
func (g *Group) CancelAndWait() {
|
||||||
|
g.finish()
|
||||||
|
g.cancel()
|
||||||
|
g.wg.Wait()
|
||||||
|
g.resetCtx()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitToFinish will ends all periodic or polls routines. It will wait for
|
||||||
|
// currently running functions to finish (cancel is not sent).
|
||||||
|
//
|
||||||
|
// It is not safe to call Wait concurrently with any other method on g.
|
||||||
|
func (g *Group) WaitToFinish() {
|
||||||
|
g.finish()
|
||||||
|
g.wg.Wait()
|
||||||
|
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) handlePanic() {
|
||||||
|
if g.panicHandler != nil {
|
||||||
|
g.panicHandler.HandlePanic()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/watcher"
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||||
@ -39,7 +40,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/bradenaw/juniper/xsync"
|
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -90,6 +90,12 @@ type Bridge struct {
|
|||||||
// locator is the bridge's locator.
|
// locator is the bridge's locator.
|
||||||
locator Locator
|
locator Locator
|
||||||
|
|
||||||
|
// crashHandler
|
||||||
|
crashHandler async.PanicHandler
|
||||||
|
|
||||||
|
// reporter
|
||||||
|
reporter reporter.Reporter
|
||||||
|
|
||||||
// watchers holds all registered event watchers.
|
// watchers holds all registered event watchers.
|
||||||
watchers []*watcher.Watcher[events.Event]
|
watchers []*watcher.Watcher[events.Event]
|
||||||
watchersLock sync.RWMutex
|
watchersLock sync.RWMutex
|
||||||
@ -103,7 +109,7 @@ type Bridge struct {
|
|||||||
logSMTP bool
|
logSMTP bool
|
||||||
|
|
||||||
// tasks manages the bridge's goroutines.
|
// tasks manages the bridge's goroutines.
|
||||||
tasks *xsync.Group
|
tasks *async.Group
|
||||||
|
|
||||||
// goLoad triggers a load of disconnected users from the vault.
|
// goLoad triggers a load of disconnected users from the vault.
|
||||||
goLoad func()
|
goLoad func()
|
||||||
@ -126,6 +132,8 @@ func New( //nolint:funlen
|
|||||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||||
proxyCtl ProxyController, // the DoH controller
|
proxyCtl ProxyController, // the DoH controller
|
||||||
|
crashHandler async.PanicHandler,
|
||||||
|
reporter reporter.Reporter,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||||
logSMTP bool, // whether to log SMTP activity
|
logSMTP bool, // whether to log SMTP activity
|
||||||
@ -141,7 +149,7 @@ func New( //nolint:funlen
|
|||||||
)
|
)
|
||||||
|
|
||||||
// tasks holds all the bridge's background tasks.
|
// tasks holds all the bridge's background tasks.
|
||||||
tasks := xsync.NewGroup(context.Background())
|
tasks := async.NewGroup(context.Background(), crashHandler)
|
||||||
|
|
||||||
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
||||||
imapEventCh := make(chan imapEvents.Event)
|
imapEventCh := make(chan imapEvents.Event)
|
||||||
@ -156,6 +164,8 @@ func New( //nolint:funlen
|
|||||||
autostarter,
|
autostarter,
|
||||||
updater,
|
updater,
|
||||||
curVersion,
|
curVersion,
|
||||||
|
crashHandler,
|
||||||
|
reporter,
|
||||||
|
|
||||||
api,
|
api,
|
||||||
identifier,
|
identifier,
|
||||||
@ -189,7 +199,7 @@ func New( //nolint:funlen
|
|||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
func newBridge(
|
func newBridge(
|
||||||
tasks *xsync.Group,
|
tasks *async.Group,
|
||||||
imapEventCh chan imapEvents.Event,
|
imapEventCh chan imapEvents.Event,
|
||||||
|
|
||||||
locator Locator,
|
locator Locator,
|
||||||
@ -197,6 +207,8 @@ func newBridge(
|
|||||||
autostarter Autostarter,
|
autostarter Autostarter,
|
||||||
updater Updater,
|
updater Updater,
|
||||||
curVersion *semver.Version,
|
curVersion *semver.Version,
|
||||||
|
crashHandler async.PanicHandler,
|
||||||
|
reporter reporter.Reporter,
|
||||||
|
|
||||||
api *liteapi.Manager,
|
api *liteapi.Manager,
|
||||||
identifier Identifier,
|
identifier Identifier,
|
||||||
@ -218,6 +230,7 @@ func newBridge(
|
|||||||
gluonDir,
|
gluonDir,
|
||||||
curVersion,
|
curVersion,
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
|
reporter,
|
||||||
logIMAPClient,
|
logIMAPClient,
|
||||||
logIMAPServer,
|
logIMAPServer,
|
||||||
imapEventCh,
|
imapEventCh,
|
||||||
@ -253,6 +266,9 @@ func newBridge(
|
|||||||
newVersion: curVersion,
|
newVersion: curVersion,
|
||||||
newVersionLock: safe.NewRWMutex(),
|
newVersionLock: safe.NewRWMutex(),
|
||||||
|
|
||||||
|
crashHandler: crashHandler,
|
||||||
|
reporter: reporter,
|
||||||
|
|
||||||
focusService: focusService,
|
focusService: focusService,
|
||||||
autostarter: autostarter,
|
autostarter: autostarter,
|
||||||
locator: locator,
|
locator: locator,
|
||||||
@ -410,7 +426,7 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// Stop all ongoing tasks.
|
// Stop all ongoing tasks.
|
||||||
bridge.tasks.Wait()
|
bridge.tasks.CancelAndWait()
|
||||||
|
|
||||||
// Close the focus service.
|
// Close the focus service.
|
||||||
bridge.focusService.Close()
|
bridge.focusService.Close()
|
||||||
|
|||||||
@ -484,6 +484,8 @@ func withBridge(
|
|||||||
mocks.TLSReporter,
|
mocks.TLSReporter,
|
||||||
liteapi.NewDialer(netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
liteapi.NewDialer(netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
||||||
mocks.ProxyCtl,
|
mocks.ProxyCtl,
|
||||||
|
mocks.CrashHandler,
|
||||||
|
mocks.Reporter,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
false,
|
false,
|
||||||
|
|||||||
@ -29,12 +29,12 @@ import (
|
|||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
"github.com/ProtonMail/proton-bridge/v2/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
"github.com/bradenaw/juniper/xsync"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -194,9 +194,10 @@ func newIMAPServer(
|
|||||||
gluonDir string,
|
gluonDir string,
|
||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
tlsConfig *tls.Config,
|
tlsConfig *tls.Config,
|
||||||
|
reporter reporter.Reporter,
|
||||||
logClient, logServer bool,
|
logClient, logServer bool,
|
||||||
eventCh chan<- imapEvents.Event,
|
eventCh chan<- imapEvents.Event,
|
||||||
tasks *xsync.Group,
|
tasks *async.Group,
|
||||||
) (*gluon.Server, error) {
|
) (*gluon.Server, error) {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"gluonDir": gluonDir,
|
"gluonDir": gluonDir,
|
||||||
@ -233,6 +234,7 @@ func newIMAPServer(
|
|||||||
gluon.WithDataDir(gluonDir),
|
gluon.WithDataDir(gluonDir),
|
||||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||||
getGluonVersionInfo(version),
|
getGluonVersionInfo(version),
|
||||||
|
gluon.WithReporter(reporter),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -21,6 +21,9 @@ type Mocks struct {
|
|||||||
|
|
||||||
Updater *TestUpdater
|
Updater *TestUpdater
|
||||||
Autostarter *mocks.MockAutostarter
|
Autostarter *mocks.MockAutostarter
|
||||||
|
|
||||||
|
CrashHandler *mocks.MockPanicHandler
|
||||||
|
Reporter *mocks.MockReporter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||||
@ -33,11 +36,17 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
|||||||
|
|
||||||
Updater: NewTestUpdater(version, minAuto),
|
Updater: NewTestUpdater(version, minAuto),
|
||||||
Autostarter: mocks.NewMockAutostarter(ctl),
|
Autostarter: mocks.NewMockAutostarter(ctl),
|
||||||
|
|
||||||
|
CrashHandler: mocks.NewMockPanicHandler(ctl),
|
||||||
|
Reporter: mocks.NewMockReporter(ctl),
|
||||||
}
|
}
|
||||||
|
|
||||||
// When getting the TLS issue channel, we want to return the test channel.
|
// When getting the TLS issue channel, we want to return the test channel.
|
||||||
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
||||||
|
|
||||||
|
// This is called at he end of any go-routine:
|
||||||
|
mocks.CrashHandler.EXPECT().HandlePanic().AnyTimes()
|
||||||
|
|
||||||
return mocks
|
return mocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
internal/bridge/mocks/async_mocks.go
Normal file
46
internal/bridge/mocks/async_mocks.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/ProtonMail/proton-bridge/v2/internal/async (interfaces: PanicHandler)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockPanicHandler is a mock of PanicHandler interface.
|
||||||
|
type MockPanicHandler struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockPanicHandlerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockPanicHandlerMockRecorder is the mock recorder for MockPanicHandler.
|
||||||
|
type MockPanicHandlerMockRecorder struct {
|
||||||
|
mock *MockPanicHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockPanicHandler creates a new mock instance.
|
||||||
|
func NewMockPanicHandler(ctrl *gomock.Controller) *MockPanicHandler {
|
||||||
|
mock := &MockPanicHandler{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockPanicHandlerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePanic mocks base method.
|
||||||
|
func (m *MockPanicHandler) HandlePanic() {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "HandlePanic")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePanic indicates an expected call of HandlePanic.
|
||||||
|
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
|
||||||
|
}
|
||||||
90
internal/bridge/mocks/gluon_mocks.go
Normal file
90
internal/bridge/mocks/gluon_mocks.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/ProtonMail/gluon/reporter (interfaces: Reporter)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockReporter is a mock of Reporter interface.
|
||||||
|
type MockReporter struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockReporterMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockReporterMockRecorder is the mock recorder for MockReporter.
|
||||||
|
type MockReporterMockRecorder struct {
|
||||||
|
mock *MockReporter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockReporter creates a new mock instance.
|
||||||
|
func NewMockReporter(ctrl *gomock.Controller) *MockReporter {
|
||||||
|
mock := &MockReporter{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockReporterMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockReporter) EXPECT() *MockReporterMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportException mocks base method.
|
||||||
|
func (m *MockReporter) ReportException(arg0 interface{}) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReportException", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportException indicates an expected call of ReportException.
|
||||||
|
func (mr *MockReporterMockRecorder) ReportException(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportException", reflect.TypeOf((*MockReporter)(nil).ReportException), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportExceptionWithContext mocks base method.
|
||||||
|
func (m *MockReporter) ReportExceptionWithContext(arg0 interface{}, arg1 map[string]interface{}) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReportExceptionWithContext", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportExceptionWithContext indicates an expected call of ReportExceptionWithContext.
|
||||||
|
func (mr *MockReporterMockRecorder) ReportExceptionWithContext(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportExceptionWithContext", reflect.TypeOf((*MockReporter)(nil).ReportExceptionWithContext), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportMessage mocks base method.
|
||||||
|
func (m *MockReporter) ReportMessage(arg0 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReportMessage", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportMessage indicates an expected call of ReportMessage.
|
||||||
|
func (mr *MockReporterMockRecorder) ReportMessage(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessage", reflect.TypeOf((*MockReporter)(nil).ReportMessage), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportMessageWithContext mocks base method.
|
||||||
|
func (m *MockReporter) ReportMessageWithContext(arg0 string, arg1 map[string]interface{}) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReportMessageWithContext", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportMessageWithContext indicates an expected call of ReportMessageWithContext.
|
||||||
|
func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
|
||||||
|
}
|
||||||
109
internal/bridge/mocks/matcher.go
Normal file
109
internal/bridge/mocks/matcher.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) 2022 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail 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.
|
||||||
|
//
|
||||||
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type refreshContextMatcher struct {
|
||||||
|
wantRefresh liteapi.RefreshFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRefreshContextMatcher(refreshFlag liteapi.RefreshFlag) *refreshContextMatcher { //nolint:revive
|
||||||
|
return &refreshContextMatcher{wantRefresh: refreshFlag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *refreshContextMatcher) Matches(x interface{}) bool {
|
||||||
|
context, ok := x.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
i, ok := context["EventLoop"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
el, ok := i.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
vID, ok := el["EventID"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := vID.(string)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
vRefresh, ok := el["Refresh"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh, ok := vRefresh.(liteapi.RefreshFlag)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return refresh == m.wantRefresh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *refreshContextMatcher) String() string {
|
||||||
|
return `map[string]interface which contains "Refresh" field with value liteapi.RefreshAll`
|
||||||
|
}
|
||||||
|
|
||||||
|
type closedConnectionMatcher struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClosedConnectionMatcher() *closedConnectionMatcher { //nolint:revive
|
||||||
|
return &closedConnectionMatcher{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *closedConnectionMatcher) Matches(x interface{}) bool {
|
||||||
|
context, ok := x.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
vErr, ok := context["error"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err, ok := vErr.(error)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(err.Error(), "used of closed network connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *closedConnectionMatcher) String() string {
|
||||||
|
return "map containing error of closed network connection"
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap/client"
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
"gitlab.protontech.ch/go/liteapi/server"
|
"gitlab.protontech.ch/go/liteapi/server"
|
||||||
@ -43,6 +44,8 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@ -139,6 +139,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
|||||||
bridge.vault.GetGluonDir(),
|
bridge.vault.GetGluonDir(),
|
||||||
bridge.curVersion,
|
bridge.curVersion,
|
||||||
bridge.tlsConfig,
|
bridge.tlsConfig,
|
||||||
|
bridge.reporter,
|
||||||
bridge.logIMAPClient,
|
bridge.logIMAPClient,
|
||||||
bridge.logIMAPServer,
|
bridge.logIMAPServer,
|
||||||
bridge.imapEventCh,
|
bridge.imapEventCh,
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
"github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
"gitlab.protontech.ch/go/liteapi/server"
|
"gitlab.protontech.ch/go/liteapi/server"
|
||||||
@ -69,6 +70,8 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
|
|
||||||
// If we then connect an IMAP client, it should see all the messages.
|
// If we then connect an IMAP client, it should see all the messages.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
info, err := bridge.GetUserInfo(userID)
|
info, err := bridge.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.Connected)
|
require.True(t, info.Connected)
|
||||||
@ -93,6 +96,8 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
|
|
||||||
// Login the user; its sync should fail.
|
// Login the user; its sync should fail.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
{
|
{
|
||||||
syncCh, done := chToType[events.Event, events.SyncFailed](bridge.GetEvents(events.SyncFailed{}))
|
syncCh, done := chToType[events.Event, events.SyncFailed](bridge.GetEvents(events.SyncFailed{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|||||||
@ -101,6 +101,10 @@ func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
|||||||
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*liteapi.Client, liteapi.Auth, error) {
|
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*liteapi.Client, liteapi.Auth, error) {
|
||||||
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
||||||
|
|
||||||
|
if username == "crash@bandicoot" {
|
||||||
|
panic("Your wish is my command.. I crash!")
|
||||||
|
}
|
||||||
|
|
||||||
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, liteapi.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
return nil, liteapi.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||||
@ -415,6 +419,8 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
vault,
|
vault,
|
||||||
client,
|
client,
|
||||||
apiUser,
|
apiUser,
|
||||||
|
bridge.crashHandler,
|
||||||
|
bridge.reporter,
|
||||||
bridge.vault.SyncWorkers(),
|
bridge.vault.SyncWorkers(),
|
||||||
bridge.vault.SyncBuffer(),
|
bridge.vault.SyncBuffer(),
|
||||||
bridge.vault.GetShowAllMail(),
|
bridge.vault.GetShowAllMail(),
|
||||||
|
|||||||
@ -24,8 +24,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
|
||||||
|
mocksPkg "github.com/ProtonMail/proton-bridge/v2/internal/bridge/mocks"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
"gitlab.protontech.ch/go/liteapi/server"
|
"gitlab.protontech.ch/go/liteapi/server"
|
||||||
@ -612,6 +614,11 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
|||||||
func TestBridge_User_Refresh(t *testing.T) {
|
func TestBridge_User_Refresh(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *liteapi.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *liteapi.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
||||||
|
gomock.Eq("Warning: refresh occurred"),
|
||||||
|
mocksPkg.NewRefreshContextMatcher(liteapi.RefreshAll),
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
// Get a channel of sync started events.
|
// Get a channel of sync started events.
|
||||||
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|||||||
@ -49,6 +49,8 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
|
|||||||
// if events occurred before streaming started, they've been queued. Now that the stream channel is available
|
// if events occurred before streaming started, they've been queued. Now that the stream channel is available
|
||||||
// we can flush the queued
|
// we can flush the queued
|
||||||
go func() {
|
go func() {
|
||||||
|
defer s.panicHandler.HandlePanic()
|
||||||
|
|
||||||
s.eventQueueMutex.Lock()
|
s.eventQueueMutex.Lock()
|
||||||
defer s.eventQueueMutex.Unlock()
|
defer s.eventQueueMutex.Unlock()
|
||||||
for _, event := range s.eventQueue {
|
for _, event := range s.eventQueue {
|
||||||
|
|||||||
@ -88,3 +88,17 @@ func (mr *MockInstallerMockRecorder) InstallUpdate(arg0, arg1 interface{}) *gomo
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallUpdate", reflect.TypeOf((*MockInstaller)(nil).InstallUpdate), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallUpdate", reflect.TypeOf((*MockInstaller)(nil).InstallUpdate), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAlreadyInstalled mocks base method.
|
||||||
|
func (m *MockInstaller) IsAlreadyInstalled(arg0 *semver.Version) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "IsAlreadyInstalled", arg0)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAlreadyInstalled indicates an expected call of IsAlreadyInstalled.
|
||||||
|
func (mr *MockInstallerMockRecorder) IsAlreadyInstalled(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAlreadyInstalled", reflect.TypeOf((*MockInstaller)(nil).IsAlreadyInstalled), arg0)
|
||||||
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import (
|
|||||||
// handleAPIEvent handles the given liteapi.Event.
|
// handleAPIEvent handles the given liteapi.Event.
|
||||||
func (user *User) handleAPIEvent(ctx context.Context, event liteapi.Event) error {
|
func (user *User) handleAPIEvent(ctx context.Context, event liteapi.Event) error {
|
||||||
if event.Refresh&liteapi.RefreshMail != 0 {
|
if event.Refresh&liteapi.RefreshMail != 0 {
|
||||||
return user.handleRefreshEvent(ctx)
|
return user.handleRefreshEvent(ctx, event.Refresh, event.EventID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.User != nil {
|
if event.User != nil {
|
||||||
@ -66,8 +66,23 @@ func (user *User) handleAPIEvent(ctx context.Context, event liteapi.Event) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) handleRefreshEvent(ctx context.Context) error {
|
func (user *User) handleRefreshEvent(ctx context.Context, refresh liteapi.RefreshFlag, eventID string) error {
|
||||||
user.log.Info("Handling refresh event")
|
l := user.log.WithFields(logrus.Fields{
|
||||||
|
"eventID": eventID,
|
||||||
|
"refresh": refresh,
|
||||||
|
})
|
||||||
|
|
||||||
|
l.Info("Handling refresh event")
|
||||||
|
|
||||||
|
context := map[string]interface{}{
|
||||||
|
"EventLoop": map[string]interface{}{
|
||||||
|
"EventID": eventID,
|
||||||
|
"Refresh": refresh,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if sentryErr := user.reporter.ReportMessageWithContext("Warning: refresh occurred", context); sentryErr != nil {
|
||||||
|
l.WithError(sentryErr).Error("Failed to report refresh to sentry")
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel and restart ongoing syncs.
|
// Cancel and restart ongoing syncs.
|
||||||
user.abortable.Abort()
|
user.abortable.Abort()
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/queue"
|
"github.com/ProtonMail/gluon/queue"
|
||||||
|
gluonReporter "github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||||
@ -38,7 +39,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/bradenaw/juniper/xsync"
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
@ -71,7 +71,9 @@ type User struct {
|
|||||||
updateCh map[string]*queue.QueuedChannel[imap.Update]
|
updateCh map[string]*queue.QueuedChannel[imap.Update]
|
||||||
updateChLock safe.RWMutex
|
updateChLock safe.RWMutex
|
||||||
|
|
||||||
tasks *xsync.Group
|
reporter gluonReporter.Reporter
|
||||||
|
|
||||||
|
tasks *async.Group
|
||||||
abortable async.Abortable
|
abortable async.Abortable
|
||||||
goSync func()
|
goSync func()
|
||||||
goPoll func()
|
goPoll func()
|
||||||
@ -89,6 +91,8 @@ func New(
|
|||||||
encVault *vault.User,
|
encVault *vault.User,
|
||||||
client *liteapi.Client,
|
client *liteapi.Client,
|
||||||
apiUser liteapi.User,
|
apiUser liteapi.User,
|
||||||
|
crashHandler async.PanicHandler,
|
||||||
|
reporter gluonReporter.Reporter,
|
||||||
syncWorkers, syncBuffer int,
|
syncWorkers, syncBuffer int,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
) (*User, error) { //nolint:funlen
|
) (*User, error) { //nolint:funlen
|
||||||
@ -156,7 +160,9 @@ func New(
|
|||||||
updateCh: updateCh,
|
updateCh: updateCh,
|
||||||
updateChLock: safe.NewRWMutex(),
|
updateChLock: safe.NewRWMutex(),
|
||||||
|
|
||||||
tasks: xsync.NewGroup(context.Background()),
|
reporter: reporter,
|
||||||
|
|
||||||
|
tasks: async.NewGroup(context.Background(), crashHandler),
|
||||||
|
|
||||||
syncWorkers: syncWorkers,
|
syncWorkers: syncWorkers,
|
||||||
syncBuffer: syncBuffer,
|
syncBuffer: syncBuffer,
|
||||||
@ -519,6 +525,10 @@ func (user *User) SendMail(authID string, from string, to []string, r io.Reader)
|
|||||||
// CheckAuth returns whether the given email and password can be used to authenticate over IMAP or SMTP with this user.
|
// CheckAuth returns whether the given email and password can be used to authenticate over IMAP or SMTP with this user.
|
||||||
// It returns the address ID of the authenticated address.
|
// It returns the address ID of the authenticated address.
|
||||||
func (user *User) CheckAuth(email string, password []byte) (string, error) {
|
func (user *User) CheckAuth(email string, password []byte) (string, error) {
|
||||||
|
if email == "crash@bandicoot" {
|
||||||
|
panic("your wish is my command.. I crash")
|
||||||
|
}
|
||||||
|
|
||||||
dec, err := b64Decode(password)
|
dec, err := b64Decode(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to decode password: %w", err)
|
return "", fmt.Errorf("failed to decode password: %w", err)
|
||||||
@ -551,7 +561,7 @@ func (user *User) OnStatusDown(context.Context) {
|
|||||||
|
|
||||||
// Logout logs the user out from the API.
|
// Logout logs the user out from the API.
|
||||||
func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
||||||
user.tasks.Wait()
|
user.tasks.CancelAndWait()
|
||||||
|
|
||||||
if withAPI {
|
if withAPI {
|
||||||
if err := user.client.AuthDelete(ctx); err != nil {
|
if err := user.client.AuthDelete(ctx); err != nil {
|
||||||
@ -569,7 +579,7 @@ func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
|||||||
// Close closes ongoing connections and cleans up resources.
|
// Close closes ongoing connections and cleans up resources.
|
||||||
func (user *User) Close() {
|
func (user *User) Close() {
|
||||||
// Stop any ongoing background tasks.
|
// Stop any ongoing background tasks.
|
||||||
user.tasks.Wait()
|
user.tasks.CancelAndWait()
|
||||||
|
|
||||||
// Close the user's API client.
|
// Close the user's API client.
|
||||||
user.client.Close()
|
user.client.Close()
|
||||||
|
|||||||
@ -23,10 +23,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v2/internal/bridge/mocks"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/tests"
|
"github.com/ProtonMail/proton-bridge/v2/tests"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
"gitlab.protontech.ch/go/liteapi/server"
|
"gitlab.protontech.ch/go/liteapi/server"
|
||||||
@ -139,16 +141,26 @@ func TestUser_Deauth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_Refresh(t *testing.T) {
|
func TestUser_Refresh(t *testing.T) {
|
||||||
|
ctl := gomock.NewController(t)
|
||||||
|
mockReporter := mocks.NewMockReporter(ctl)
|
||||||
|
|
||||||
withAPI(t, context.Background(), func(ctx context.Context, s *server.Server, m *liteapi.Manager) {
|
withAPI(t, context.Background(), func(ctx context.Context, s *server.Server, m *liteapi.Manager) {
|
||||||
withAccount(t, s, "username", "password", []string{"email@pm.me"}, func(userID string, addrIDs []string) {
|
withAccount(t, s, "username", "password", []string{"email@pm.me"}, func(userID string, addrIDs []string) {
|
||||||
withUser(t, ctx, s, m, "username", "password", func(user *User) {
|
withUser(t, ctx, s, m, "username", "password", func(user *User) {
|
||||||
|
user.reporter = mockReporter
|
||||||
|
|
||||||
|
mockReporter.EXPECT().ReportMessageWithContext(
|
||||||
|
gomock.Eq("Warning: refresh occurred"),
|
||||||
|
mocks.NewRefreshContextMatcher(liteapi.RefreshAll),
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
// Get the event channel.
|
// Get the event channel.
|
||||||
eventCh := user.GetEventCh()
|
eventCh := user.GetEventCh()
|
||||||
|
|
||||||
// Revoke the user's auth token.
|
// Send refresh event
|
||||||
require.NoError(t, s.RefreshUser(user.ID(), liteapi.RefreshAll))
|
require.NoError(t, s.RefreshUser(user.ID(), liteapi.RefreshAll))
|
||||||
|
|
||||||
// The user should eventually be logged out.
|
// The user should eventually re-synced.
|
||||||
require.Eventually(t, func() bool { _, ok := (<-eventCh).(events.UserRefreshed); return ok }, 5*time.Second, 100*time.Millisecond)
|
require.Eventually(t, func() bool { _, ok := (<-eventCh).(events.UserRefreshed); return ok }, 5*time.Second, 100*time.Millisecond)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -203,7 +215,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *liteapi.M
|
|||||||
vaultUser, err := vault.AddUser(apiUser.ID, username, apiAuth.UID, apiAuth.RefreshToken, saltedKeyPass)
|
vaultUser, err := vault.AddUser(apiUser.ID, username, apiAuth.UID, apiAuth.RefreshToken, saltedKeyPass)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
|
|
||||||
user, err := New(ctx, vaultUser, client, apiUser, vault.SyncWorkers(), vault.SyncBuffer(), true)
|
user, err := New(ctx, vaultUser, client, apiUser, nil, nil, vault.SyncWorkers(), vault.SyncBuffer(), true)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const BridgeCrashCount = "BRIDGE_CRASH_COUNT"
|
const BridgeCrashCount = "BRIDGE_CRASH_COUNT"
|
||||||
|
const MaxCrashRestarts = 10
|
||||||
|
|
||||||
type Restarter struct {
|
type Restarter struct {
|
||||||
restart bool
|
restart bool
|
||||||
@ -72,14 +73,29 @@ func (restarter *Restarter) Restart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := execabs.Command(restarter.exe, xslices.Join(os.Args[1:], restarter.flags)...) //nolint:gosec
|
cmd := execabs.Command(restarter.exe, xslices.Join(os.Args[1:], restarter.flags)...) //nolint:gosec
|
||||||
|
l := logrus.WithFields(logrus.Fields{
|
||||||
|
"exe": restarter.exe,
|
||||||
|
"crashCount": env[BridgeCrashCount],
|
||||||
|
"args": cmd.Args,
|
||||||
|
})
|
||||||
|
|
||||||
|
if nCrash, err := strconv.Atoi(env[BridgeCrashCount]); err != nil {
|
||||||
|
l.WithError(err).Error("Crash count is not integer, ignoring")
|
||||||
|
return
|
||||||
|
} else if nCrash >= MaxCrashRestarts {
|
||||||
|
l.Error("Crash count is too high, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Env = getEnvList(env)
|
cmd.Env = getEnvList(env)
|
||||||
|
|
||||||
|
l.Warn("Restarting")
|
||||||
|
|
||||||
if err := run(cmd); err != nil {
|
if err := run(cmd); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to restart")
|
l.WithError(err).Error("Failed to restart")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -125,6 +125,7 @@ func TestFeatures(testingT *testing.T) {
|
|||||||
ctx.Step(`^bridge sends an update installed event for version "([^"]*)"$`, s.bridgeSendsAnUpdateInstalledEventForVersion)
|
ctx.Step(`^bridge sends an update installed event for version "([^"]*)"$`, s.bridgeSendsAnUpdateInstalledEventForVersion)
|
||||||
ctx.Step(`^bridge sends an update not available event$`, s.bridgeSendsAnUpdateNotAvailableEvent)
|
ctx.Step(`^bridge sends an update not available event$`, s.bridgeSendsAnUpdateNotAvailableEvent)
|
||||||
ctx.Step(`^bridge sends a forced update event$`, s.bridgeSendsAForcedUpdateEvent)
|
ctx.Step(`^bridge sends a forced update event$`, s.bridgeSendsAForcedUpdateEvent)
|
||||||
|
ctx.Step(`^bridge reports a message with "([^"]*)"$`, s.bridgeReportsMessage)
|
||||||
|
|
||||||
// ==== FRONTEND ====
|
// ==== FRONTEND ====
|
||||||
ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion)
|
ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion)
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *scenario) bridgeStarts() error {
|
func (s *scenario) bridgeStarts() error {
|
||||||
@ -263,6 +264,16 @@ func (s *scenario) bridgeSendsAForcedUpdateEvent() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scenario) bridgeReportsMessage(message string) error {
|
||||||
|
s.t.reporter.removeMatchingRecords(
|
||||||
|
gomock.Eq(false),
|
||||||
|
gomock.Eq(message),
|
||||||
|
gomock.Any(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *scenario) theUserHidesAllMail() error {
|
func (s *scenario) theUserHidesAllMail() error {
|
||||||
return s.t.bridge.SetShowAllMail(false)
|
return s.t.bridge.SetShowAllMail(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,6 +159,8 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
|
|||||||
t.mocks.TLSReporter,
|
t.mocks.TLSReporter,
|
||||||
liteapi.NewDialer(t.netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
liteapi.NewDialer(t.netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
||||||
t.mocks.ProxyCtl,
|
t.mocks.ProxyCtl,
|
||||||
|
t.mocks.CrashHandler,
|
||||||
|
t.reporter,
|
||||||
|
|
||||||
// Logging stuff
|
// Logging stuff
|
||||||
logIMAP,
|
logIMAP,
|
||||||
|
|||||||
@ -21,9 +21,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,3 +107,123 @@ func (t *testCtx) createMessages(ctx context.Context, username, addrID string, r
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type reportRecord struct {
|
||||||
|
isException bool
|
||||||
|
message string
|
||||||
|
context reporter.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type reportRecorder struct {
|
||||||
|
assert *assert.Assertions
|
||||||
|
reports []reportRecord
|
||||||
|
|
||||||
|
lock sync.Locker
|
||||||
|
isClosed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReportRecorder(tb testing.TB) *reportRecorder {
|
||||||
|
return &reportRecorder{
|
||||||
|
assert: assert.New(tb),
|
||||||
|
reports: []reportRecord{},
|
||||||
|
lock: &sync.Mutex{},
|
||||||
|
isClosed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) add(isException bool, message string, context reporter.Context) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
l := logrus.WithFields(logrus.Fields{
|
||||||
|
"isException": isException,
|
||||||
|
"message": message,
|
||||||
|
"context": context,
|
||||||
|
"pkg": "test/reportRecorder",
|
||||||
|
})
|
||||||
|
|
||||||
|
if r.isClosed {
|
||||||
|
l.Warn("Reporter closed, report skipped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.reports = append(r.reports, reportRecord{
|
||||||
|
isException: isException,
|
||||||
|
message: message,
|
||||||
|
context: context,
|
||||||
|
})
|
||||||
|
|
||||||
|
l.Warn("Report recorded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) close() {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
r.isClosed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) assertEmpty() {
|
||||||
|
r.assert.Empty(r.reports)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) removeMatchingRecords(isException, message, context gomock.Matcher, n int) {
|
||||||
|
if n == 0 {
|
||||||
|
n = len(r.reports)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.reports = xslices.Filter(r.reports, func(rec reportRecord) bool {
|
||||||
|
if n <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logrus.WithFields(logrus.Fields{
|
||||||
|
"rec": rec,
|
||||||
|
})
|
||||||
|
if !isException.Matches(rec.isException) {
|
||||||
|
l.WithField("matcher", isException).Debug("Not matching")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !message.Matches(rec.message) {
|
||||||
|
l.WithField("matcher", message).Debug("Not matching")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !context.Matches(rec.context) {
|
||||||
|
l.WithField("matcher", context).Debug("Not matching")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
n--
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) ReportException(data any) error {
|
||||||
|
r.add(true, "exception", reporter.Context{"data": data})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) ReportMessage(message string) error {
|
||||||
|
r.add(false, message, reporter.Context{})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) ReportMessageWithContext(message string, context reporter.Context) error {
|
||||||
|
r.add(false, message, context)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reportRecorder) ReportExceptionWithContext(data any, context reporter.Context) error {
|
||||||
|
if context == nil {
|
||||||
|
context = reporter.Context{}
|
||||||
|
}
|
||||||
|
|
||||||
|
context["data"] = data
|
||||||
|
|
||||||
|
r.add(true, "exception", context)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
"gitlab.protontech.ch/go/liteapi/server"
|
"gitlab.protontech.ch/go/liteapi/server"
|
||||||
@ -51,6 +52,7 @@ type testCtx struct {
|
|||||||
version *semver.Version
|
version *semver.Version
|
||||||
mocks *bridge.Mocks
|
mocks *bridge.Mocks
|
||||||
events *eventCollector
|
events *eventCollector
|
||||||
|
reporter *reportRecorder
|
||||||
|
|
||||||
// bridge holds the bridge app under test.
|
// bridge holds the bridge app under test.
|
||||||
bridge *bridge.Bridge
|
bridge *bridge.Bridge
|
||||||
@ -105,6 +107,7 @@ func newTestCtx(tb testing.TB) *testCtx {
|
|||||||
version: defaultVersion,
|
version: defaultVersion,
|
||||||
mocks: bridge.NewMocks(tb, defaultVersion, defaultVersion),
|
mocks: bridge.NewMocks(tb, defaultVersion, defaultVersion),
|
||||||
events: newEventCollector(),
|
events: newEventCollector(),
|
||||||
|
reporter: newReportRecorder(tb),
|
||||||
|
|
||||||
userIDByName: make(map[string]string),
|
userIDByName: make(map[string]string),
|
||||||
userAddrByEmail: make(map[string]map[string]string),
|
userAddrByEmail: make(map[string]map[string]string),
|
||||||
@ -324,4 +327,16 @@ func (t *testCtx) close(ctx context.Context) {
|
|||||||
t.api.Close()
|
t.api.Close()
|
||||||
|
|
||||||
t.events.close()
|
t.events.close()
|
||||||
|
|
||||||
|
t.reporter.close()
|
||||||
|
|
||||||
|
// Closed connection can happen in the end of scenario
|
||||||
|
t.reporter.removeMatchingRecords(
|
||||||
|
gomock.Eq(false),
|
||||||
|
gomock.Eq("Failed to parse imap command"),
|
||||||
|
gomock.Any(), // mocks.NewClosedConnectionMatcher(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
t.reporter.assertEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,12 +23,16 @@ Feature: IMAP create mailbox
|
|||||||
Scenario: Creating folder or label with existing name is not possible
|
Scenario: Creating folder or label with existing name is not possible
|
||||||
When IMAP client "1" creates "Folders/f1"
|
When IMAP client "1" creates "Folders/f1"
|
||||||
Then it fails
|
Then it fails
|
||||||
|
And bridge reports a message with "Failed to create mailbox"
|
||||||
When IMAP client "1" creates "Folders/f2"
|
When IMAP client "1" creates "Folders/f2"
|
||||||
Then it fails
|
Then it fails
|
||||||
|
And bridge reports a message with "Failed to create mailbox"
|
||||||
When IMAP client "1" creates "Labels/l1"
|
When IMAP client "1" creates "Labels/l1"
|
||||||
Then it fails
|
Then it fails
|
||||||
|
And bridge reports a message with "Failed to create mailbox"
|
||||||
When IMAP client "1" creates "Labels/l2"
|
When IMAP client "1" creates "Labels/l2"
|
||||||
Then it fails
|
Then it fails
|
||||||
|
And bridge reports a message with "Failed to create mailbox"
|
||||||
When IMAP client "1" creates "Folders/f3"
|
When IMAP client "1" creates "Folders/f3"
|
||||||
Then it succeeds
|
Then it succeeds
|
||||||
When IMAP client "1" creates "Labels/l3"
|
When IMAP client "1" creates "Labels/l3"
|
||||||
@ -182,4 +186,4 @@ Feature: IMAP create mailbox
|
|||||||
| Folders/f2/f22 |
|
| Folders/f2/f22 |
|
||||||
| Labels |
|
| Labels |
|
||||||
| Labels/l1 |
|
| Labels/l1 |
|
||||||
| Labels/l2 |
|
| Labels/l2 |
|
||||||
|
|||||||
Reference in New Issue
Block a user