forked from Silverfish/proton-bridge
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6105f32c75 | |||
| f5bc6ad1f0 | |||
| e8a95e26f6 | |||
| ebe54ca92e | |||
| ff7e45f395 | |||
| 79c63f5785 | |||
| 3ca9e625f5 | |||
| 5b874657cb | |||
| bfe67f3005 | |||
| 99e6f00aaa |
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,6 +7,7 @@
|
||||
*~
|
||||
.idea
|
||||
.vscode
|
||||
.vs
|
||||
|
||||
# Test files
|
||||
godog.test
|
||||
@ -35,6 +36,8 @@ cmd/Import-Export/deploy
|
||||
proton-bridge
|
||||
cmd/Desktop-Bridge/*.exe
|
||||
cmd/launcher/*.exe
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Jetbrains (CLion, Golang) cmake build dirs
|
||||
cmake-build-*/
|
||||
|
||||
13
Changelog.md
13
Changelog.md
@ -3,6 +3,19 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Dragon Bridge 3.14.0
|
||||
|
||||
### Changed
|
||||
* BRIDGE-207: Failure to download or verify an update now fails silently.
|
||||
* BRIDGE-204: Removed redundant Sentry events.
|
||||
* BRIDGE-150: Observability service modification.
|
||||
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-106: Fixed import of multipart-related messages.
|
||||
* BRIDGE-108: Fixed GetInitials when empty username is passed.
|
||||
|
||||
|
||||
## Colorado Bridge 3.13.0
|
||||
|
||||
### Added
|
||||
|
||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.13.0+git
|
||||
BRIDGE_APP_VERSION?=3.14.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
|
||||
6
go.mod
6
go.mod
@ -7,9 +7,9 @@ toolchain go1.21.9
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
@ -122,7 +122,7 @@ require (
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
|
||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
|
||||
|
||||
18
go.sum
18
go.sum
@ -29,6 +29,12 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961 h1:kCaz78X7OKETvK6AGHeyggHKxDBcqX7EWHf7spJ+D3g=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5 h1:LzaUpUj6M2PEBArFCkaimViNpGXDgwHVrdhvYwHLoJQ=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
@ -36,6 +42,14 @@ github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8 h1:+eE7FGX+4Hu8RZaRmSebrDVXyLuowKSaO7ZhQ6ca4+E=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542 h1:5DqSycYnKfUdHiu0yOdiYW5R2hVxoE0Mk4PLSYwqGyg=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb h1:uOKp93u6JFYlBoJJvOhzmHZURcvWmXiqhihGWtT3HtY=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
|
||||
@ -56,6 +70,10 @@ github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1 h1:gATl
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2 h1:yx0iejqB5c21HIN5jn9IsbyzUns0dPUUaGfyUHF3TmQ=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc h1:SWVPwO1M2jCI1bJHBji/JVU01FpWP/6nzh8NBIjo+Fg=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 h1:a+3dOyIxJEslN5HxyICM8flY9lnCyJupXNcv6fUaivA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
|
||||
@ -267,6 +267,8 @@ func newBridge(
|
||||
|
||||
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
||||
|
||||
observabilityService := observability.NewService(ctx, panicHandler)
|
||||
|
||||
bridge := &Bridge{
|
||||
vault: vault,
|
||||
|
||||
@ -306,11 +308,11 @@ func newBridge(
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
syncService: syncservice.NewService(reporter, panicHandler),
|
||||
syncService: syncservice.NewService(panicHandler, observabilityService),
|
||||
|
||||
unleashService: unleashService,
|
||||
|
||||
observabilityService: observability.NewService(ctx, panicHandler),
|
||||
observabilityService: observabilityService,
|
||||
|
||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||
}
|
||||
@ -342,7 +344,7 @@ func newBridge(
|
||||
|
||||
bridge.unleashService.Run()
|
||||
|
||||
bridge.observabilityService.Run()
|
||||
bridge.observabilityService.Run(bridge)
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
@ -710,5 +712,13 @@ func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
||||
bridge.observabilityService.AddMetric(metric)
|
||||
bridge.observabilityService.AddMetrics(metric)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
|
||||
bridge.observabilityService.ModifyHeartbeatInterval(duration)
|
||||
}
|
||||
|
||||
49
internal/bridge/mocks/observability_mocks.go
Normal file
49
internal/bridge/mocks/observability_mocks.go
Normal file
@ -0,0 +1,49 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
type MockObservabilitySender struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockObservabilitySenderRecorder
|
||||
}
|
||||
|
||||
type MockObservabilitySenderRecorder struct {
|
||||
mock *MockObservabilitySender
|
||||
}
|
||||
|
||||
func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySender {
|
||||
mock := &MockObservabilitySender{ctrl: ctrl}
|
||||
mock.recorder = &MockObservabilitySenderRecorder{mock: mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
||||
|
||||
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
||||
}
|
||||
|
||||
func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddMetrics", metrics)
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
||||
"AddDistinctMetrics",
|
||||
reflect.TypeOf((*MockObservabilitySender)(nil).AddDistinctMetrics),
|
||||
errType)
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.ObservabilityMetric) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
||||
}
|
||||
@ -95,3 +95,70 @@ func TestBridge_Observability(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Observability_Heartbeat(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
throttlePeriod := time.Millisecond * 300
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
|
||||
|
||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 350)
|
||||
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 350)
|
||||
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Observability_UserMetric(t *testing.T) {
|
||||
testMetric := proton.ObservabilityMetric{
|
||||
Name: "test1",
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userMetricPeriod := time.Millisecond * 200
|
||||
heartbeatPeriod := time.Second * 10
|
||||
throttlePeriod := time.Millisecond * 100
|
||||
observability.ModifyUserMetricInterval(userMetricPeriod)
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
|
||||
|
||||
time.Sleep(throttlePeriod)
|
||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
|
||||
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
|
||||
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// Two metric updates should be sent again.
|
||||
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// Only a single one should be sent.
|
||||
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -29,13 +29,12 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Report(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -56,12 +55,6 @@ func TestBridge_Report(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, conn.Close()) }()
|
||||
|
||||
// Sending garbage to the IMAP port should cause the bridge to report it.
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
||||
gomock.Eq("Failed to parse IMAP command"),
|
||||
gomock.Any(),
|
||||
).Return(nil)
|
||||
|
||||
// Read lines from the IMAP port.
|
||||
lineCh := liner.New(conn).Lines(func() error { return nil })
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
@ -115,6 +116,17 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, updater.ErrDownloadVerify):
|
||||
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
|
||||
// and we fail silently.
|
||||
log.WithError(err).Error("The update could not be installed, but we will fail silently")
|
||||
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||
"Cannot download or verify update",
|
||||
reporter.Context{"error": err},
|
||||
); reporterErr != nil {
|
||||
log.WithError(reporterErr).Error("Failed to report update error")
|
||||
}
|
||||
|
||||
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||
log.Info("The update was already installed")
|
||||
|
||||
|
||||
@ -571,7 +571,6 @@ func (bridge *Bridge) addUserWithVault(
|
||||
isNew,
|
||||
bridge.notificationStore,
|
||||
bridge.unleashService.GetFlagValue,
|
||||
bridge.observabilityService.AddMetric,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
|
||||
@ -38,9 +38,6 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
|
||||
case events.UserLoadedCheckResync:
|
||||
user.VerifyResyncAndExecute()
|
||||
|
||||
case events.UncategorizedEventError:
|
||||
bridge.handleUncategorizedErrorEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,12 +64,3 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
|
||||
user.OnBadEvent(ctx)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
|
||||
"error_type": internal.ErrCauseType(event.Error),
|
||||
"error": event.Error,
|
||||
}); rerr != nil {
|
||||
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for
|
||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
||||
QString const orphanInstanceException = "An orphan instance of bridge is already running. Please terminate it and relaunch the application.";
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
@ -317,7 +318,7 @@ int main(int argc, char *argv[]) {
|
||||
QString bridgeExe;
|
||||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
throw Exception(orphanInstanceException,
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
@ -413,13 +414,18 @@ int main(int argc, char *argv[]) {
|
||||
lock.unlock();
|
||||
return result;
|
||||
} catch (Exception const &e) {
|
||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||
QString message = e.qwhat();
|
||||
if (e.showSupportLink()) {
|
||||
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
|
||||
}
|
||||
QMessageBox::critical(nullptr, "Error", message);
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||
|
||||
if (e.qwhat() != orphanInstanceException) {
|
||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :"
|
||||
<< e.detailedWhat() << "\n";
|
||||
}
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,11 @@ var (
|
||||
// getInitials based on webapp implementation:
|
||||
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
|
||||
func getInitials(fullName string) string {
|
||||
fullName = strings.TrimSpace(fullName)
|
||||
if fullName == "" {
|
||||
return "?"
|
||||
}
|
||||
|
||||
words := strings.Split(
|
||||
reMultiSpaces.ReplaceAllString(fullName, " "),
|
||||
" ",
|
||||
|
||||
45
internal/frontend/grpc/utils_test.go
Normal file
45
internal/frontend/grpc/utils_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2024 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 grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetInitials(t *testing.T) {
|
||||
require.Equal(t, "?", getInitials(""))
|
||||
require.Equal(t, "T", getInitials(" test"))
|
||||
require.Equal(t, "T", getInitials("test "))
|
||||
require.Equal(t, "T", getInitials(" test "))
|
||||
require.Equal(t, "JD", getInitials(" John Doe "))
|
||||
require.Equal(t, "J", getInitials(" JohnDoe@proton.me "))
|
||||
require.Equal(t, "JD", getInitials("\t\r\n John Doe \t\r\n "))
|
||||
|
||||
require.Equal(t, "T", getInitials("TestTestman"))
|
||||
require.Equal(t, "TT", getInitials("Test Testman"))
|
||||
require.Equal(t, "J", getInitials("JamesJoyce"))
|
||||
require.Equal(t, "J", getInitials("JamesJoyceJeremy"))
|
||||
require.Equal(t, "J", getInitials("james.joyce"))
|
||||
require.Equal(t, "JJ", getInitials("James Joyce"))
|
||||
require.Equal(t, "JM", getInitials("James Joyce Mahabharata"))
|
||||
require.Equal(t, "JL", getInitials("James Joyce Jeremy Lin"))
|
||||
require.Equal(t, "JM", getInitials("Jean Michel"))
|
||||
require.Equal(t, "GC", getInitials("George Michael Carrie"))
|
||||
}
|
||||
@ -31,7 +31,6 @@ import (
|
||||
"github.com/ProtonMail/gluon/connector"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/rfc5322"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
@ -690,8 +689,6 @@ func (s *Connector) importMessage(
|
||||
|
||||
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
|
||||
|
||||
s.reportGODT3185(isDraft, addr.Email, p, s.addressMode == usertypes.AddressModeCombined)
|
||||
|
||||
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
||||
primaryKey, errKey := addrKR.FirstKey()
|
||||
if errKey != nil {
|
||||
@ -877,80 +874,3 @@ func stripPlusAlias(a string) string {
|
||||
func equalAddresses(a, b string) bool {
|
||||
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
|
||||
}
|
||||
|
||||
func (s *Connector) reportGODT3185(isDraft bool, defaultAddr string, p *parser.Parser, isCombinedMode bool) {
|
||||
reportAction := "draft"
|
||||
if !isDraft {
|
||||
reportAction = "import"
|
||||
}
|
||||
|
||||
reportMode := "combined"
|
||||
if !isCombinedMode {
|
||||
reportMode = "split"
|
||||
}
|
||||
|
||||
senderAddr := ""
|
||||
if p != nil && p.Root() != nil && p.Root().Header.Len() != 0 {
|
||||
addrField := p.Root().Header.Get("From")
|
||||
if addrField == "" {
|
||||
addrField = p.Root().Header.Get("Sender")
|
||||
}
|
||||
if addrField != "" {
|
||||
sender, err := rfc5322.ParseAddressList(addrField)
|
||||
if err == nil && len(sender) > 0 {
|
||||
senderAddr = sender[0].Address
|
||||
} else {
|
||||
s.log.WithError(err).Warn("Invalid sender address in reporter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if equalAddresses(defaultAddr, senderAddr) {
|
||||
return
|
||||
}
|
||||
|
||||
isDisabled := false
|
||||
isUserAddress := false
|
||||
for _, a := range s.identityState.GetAddresses() {
|
||||
if !equalAddresses(a.Email, senderAddr) {
|
||||
continue
|
||||
}
|
||||
|
||||
isUserAddress = true
|
||||
isDisabled = !bool(a.Send) || (a.Status != proton.AddressStatusEnabled)
|
||||
break
|
||||
}
|
||||
|
||||
if !isUserAddress && senderAddr != "" {
|
||||
return
|
||||
}
|
||||
|
||||
reportResult := "using sender address"
|
||||
|
||||
if !isCombinedMode {
|
||||
reportResult = "error address not match"
|
||||
}
|
||||
|
||||
reportAddress := ""
|
||||
if senderAddr == "" {
|
||||
reportAddress = " invalid"
|
||||
reportResult = "error import/draft"
|
||||
}
|
||||
|
||||
if isDisabled {
|
||||
reportAddress = " disabled"
|
||||
if isDraft {
|
||||
reportResult = "error draft"
|
||||
}
|
||||
}
|
||||
|
||||
report := fmt.Sprintf(
|
||||
"GODT-3185: %s with non-default%s address in %s mode: %s",
|
||||
reportAction, reportAddress, reportMode, reportResult,
|
||||
)
|
||||
|
||||
s.log.Warn(report)
|
||||
if s.reporter != nil {
|
||||
_ = s.reporter.ReportMessage(report)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2024 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 evtloopmsgevents
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const (
|
||||
messageEventErrorCaseSchemaName = "bridge_event_loop_message_event_failures_total"
|
||||
messageEventErrorCaseSchemaVersion = 1
|
||||
)
|
||||
|
||||
func generateMessageEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: messageEventErrorCaseSchemaName,
|
||||
Version: messageEventErrorCaseSchemaVersion,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"eventType": eventType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateMessageEventFailureCreateMessageMetric() proton.ObservabilityMetric {
|
||||
return generateMessageEventFailureObservabilityMetric("createMessageEvent")
|
||||
}
|
||||
|
||||
func GenerateMessageEventFailureDeleteMessageMetric() proton.ObservabilityMetric {
|
||||
return generateMessageEventFailureObservabilityMetric("deleteMessageEvent")
|
||||
}
|
||||
|
||||
func GenerateMessageEventFailureUpdateMetric() proton.ObservabilityMetric {
|
||||
return generateMessageEventFailureObservabilityMetric("updateEvent")
|
||||
}
|
||||
|
||||
func GenerateMessageEventFailedToBuildMessage() proton.ObservabilityMetric {
|
||||
return generateMessageEventFailureObservabilityMetric("failedToBuildMessage")
|
||||
}
|
||||
|
||||
func GenerateMessageEventFailedToBuildDraft() proton.ObservabilityMetric {
|
||||
return generateMessageEventFailureObservabilityMetric("failedToBuildDraft")
|
||||
}
|
||||
|
||||
func GenerateMessageEventUpdateChannelDoesNotExist() proton.ObservabilityMetric {
|
||||
return generateMessageEventFailureObservabilityMetric("messageUpdateChannelDoesNotExist")
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024 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 syncmsgevents
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const (
|
||||
syncEventErrorCaseSchemaName = "bridge_sync_message_event_failures_total"
|
||||
syncEventErrorCaseSchemaVersion = 1
|
||||
)
|
||||
|
||||
func generateSyncEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: syncEventErrorCaseSchemaName,
|
||||
Version: syncEventErrorCaseSchemaVersion,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"eventType": eventType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateSyncFailureCreateMessageEventMetric() proton.ObservabilityMetric {
|
||||
return generateSyncEventFailureObservabilityMetric("createMessageEvent")
|
||||
}
|
||||
|
||||
func GenerateSyncFailureDeleteMessageEventMetric() proton.ObservabilityMetric {
|
||||
return generateSyncEventFailureObservabilityMetric("deleteMessageEvent")
|
||||
}
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||
@ -96,6 +97,8 @@ type Service struct {
|
||||
syncConfigPath string
|
||||
lastHandledEventID string
|
||||
isSyncing atomic.Bool
|
||||
|
||||
observabilitySender observability.Sender
|
||||
}
|
||||
|
||||
func NewService(
|
||||
@ -116,6 +119,7 @@ func NewService(
|
||||
syncConfigDir string,
|
||||
maxSyncMemory uint64,
|
||||
showAllMail bool,
|
||||
observabilitySender observability.Sender,
|
||||
) *Service {
|
||||
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
||||
|
||||
@ -160,6 +164,8 @@ func NewService(
|
||||
syncMessageBuilder: syncMessageBuilder,
|
||||
syncReporter: syncReporter,
|
||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||
|
||||
observabilitySender: observabilitySender,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -26,11 +26,11 @@ import (
|
||||
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
@ -46,7 +46,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
||||
case proton.EventCreate:
|
||||
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
|
||||
if err != nil {
|
||||
reportError(s.reporter, s.log, "Failed to apply create message event", err)
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureCreateMessageMetric())
|
||||
return fmt.Errorf("failed to handle create message event: %w", err)
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
||||
event,
|
||||
)
|
||||
if err != nil {
|
||||
reportError(s.reporter, s.log, "Failed to apply update draft message event", err)
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
|
||||
return fmt.Errorf("failed to handle update draft event: %w", err)
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
||||
event.Message,
|
||||
)
|
||||
if err != nil {
|
||||
reportError(s.reporter, s.log, "Failed to apply update message event", err)
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
|
||||
return fmt.Errorf("failed to handle update message event: %w", err)
|
||||
}
|
||||
|
||||
@ -113,6 +113,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
||||
)
|
||||
|
||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureDeleteMessageMetric())
|
||||
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
||||
}
|
||||
}
|
||||
@ -158,8 +159,7 @@ func onMessageCreated(
|
||||
s.log.WithError(err).Error("Failed to add failed message ID to vault")
|
||||
}
|
||||
|
||||
reportErrorAndMessageID(s.reporter, s.log, "Failed to build message (event create)", res.err, res.messageID)
|
||||
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildMessage())
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -221,8 +221,7 @@ func onMessageUpdateDraftOrSent(ctx context.Context, s *Service, event proton.Me
|
||||
s.log.WithError(err).Error("Failed to add failed message ID to vault")
|
||||
}
|
||||
|
||||
reportErrorAndMessageID(s.reporter, s.log, "Failed to build draft message (event update)", res.err, res.messageID)
|
||||
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildDraft())
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -299,24 +298,6 @@ func onMessageDeleted(ctx context.Context, s *Service, event proton.MessageEvent
|
||||
return updates
|
||||
}
|
||||
|
||||
func reportError(r reporter.Reporter, entry *logrus.Entry, title string, err error) {
|
||||
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{})
|
||||
}
|
||||
|
||||
func reportErrorAndMessageID(r reporter.Reporter, entry *logrus.Entry, title string, err error, messgeID string) {
|
||||
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{"messageID": messgeID})
|
||||
}
|
||||
|
||||
func reportErrorNoContextCancel(r reporter.Reporter, entry *logrus.Entry, title string, err error, reportContext reporter.Context) {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
reportContext["error"] = err
|
||||
reportContext["error_type"] = internal.ErrCauseType(err)
|
||||
if rerr := r.ReportMessageWithContext(title, reportContext); rerr != nil {
|
||||
entry.WithError(err).WithField("title", title).Error("Failed to report message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same
|
||||
// event. This rare case can take place if in the same event fetch request there is an update for delete address and
|
||||
// create/update message.
|
||||
@ -341,7 +322,7 @@ func safePublishMessageUpdate(ctx context.Context, s *Service, addressID string,
|
||||
}
|
||||
|
||||
logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID)
|
||||
_ = s.reporter.ReportMessage("Message Update channel does not exist")
|
||||
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventUpdateChannelDoesNotExist())
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@ -23,6 +23,8 @@ import (
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||
)
|
||||
|
||||
@ -55,7 +57,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
reportError(s.service.reporter, s.service.log, "Failed to apply create message event", err)
|
||||
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureCreateMessageEventMetric())
|
||||
return fmt.Errorf("failed to handle create message event: %w", err)
|
||||
}
|
||||
|
||||
@ -71,6 +73,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
|
||||
)
|
||||
|
||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureDeleteMessageEventMetric())
|
||||
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
||||
}
|
||||
default:
|
||||
|
||||
@ -45,14 +45,15 @@ type Service struct {
|
||||
store *Store
|
||||
|
||||
getFlagValueFn unleash.GetFlagValueFn
|
||||
pushObservabilityMetricFn observability.PushObsMetricFn
|
||||
|
||||
observabilitySender observability.Sender
|
||||
}
|
||||
|
||||
const bitfieldRegexPattern = `^\\\d+`
|
||||
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
|
||||
|
||||
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
||||
getFlagFn unleash.GetFlagValueFn, pushMetricFn observability.PushObsMetricFn) *Service {
|
||||
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service {
|
||||
return &Service{
|
||||
userID: userID,
|
||||
|
||||
@ -69,7 +70,7 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
|
||||
store: store,
|
||||
|
||||
getFlagValueFn: getFlagFn,
|
||||
pushObservabilityMetricFn: pushMetricFn,
|
||||
observabilitySender: observabilitySender,
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +111,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
|
||||
s.log.Debug("Handling notification events")
|
||||
|
||||
// Publish observability metrics that we've received notifications
|
||||
s.pushObservabilityMetricFn(GenerateReceivedMetric(len(notificationEvents)))
|
||||
s.observabilitySender.AddMetrics(GenerateReceivedMetric(len(notificationEvents)))
|
||||
|
||||
for _, event := range notificationEvents {
|
||||
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
|
||||
@ -133,7 +134,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
|
||||
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
|
||||
|
||||
// Publish observability metric that we've successfully processed notifications
|
||||
s.pushObservabilityMetricFn(GenerateProcessedMetric(1))
|
||||
s.observabilitySender.AddMetrics(GenerateProcessedMetric(1))
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@ -109,7 +109,7 @@ func (s *Store) readCache() {
|
||||
|
||||
file, err := os.Open(s.cacheFilepath)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to open cache file")
|
||||
s.log.WithError(err).Info("Unable to open cache file")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
47
internal/services/observability/distinction_error_types.go
Normal file
47
internal/services/observability/distinction_error_types.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import "time"
|
||||
|
||||
// DistinctionErrorTypeEnum - maps to the specific error schema for which we
|
||||
// want to send a user update.
|
||||
type DistinctionErrorTypeEnum int
|
||||
|
||||
const (
|
||||
SyncError DistinctionErrorTypeEnum = iota
|
||||
EventLoopError
|
||||
)
|
||||
|
||||
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name.
|
||||
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals
|
||||
SyncError: "bridge_sync_errors_users_total",
|
||||
EventLoopError: "bridge_event_loop_events_errors_users_total",
|
||||
}
|
||||
|
||||
// createLastSentMap - needs to be updated whenever we make changes to the enum.
|
||||
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time {
|
||||
registerTime := time.Now().Add(-updateInterval)
|
||||
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time)
|
||||
|
||||
for errType := SyncError; errType <= EventLoopError; errType++ {
|
||||
lastSentMap[errType] = registerTime
|
||||
}
|
||||
|
||||
return lastSentMap
|
||||
}
|
||||
170
internal/services/observability/distinction_utility.go
Normal file
170
internal/services/observability/distinction_utility.go
Normal file
@ -0,0 +1,170 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
)
|
||||
|
||||
var updateInterval = time.Minute * 5 //nolint:gochecknoglobals
|
||||
|
||||
type observabilitySender interface {
|
||||
addMetricsIfClients(metric ...proton.ObservabilityMetric)
|
||||
}
|
||||
|
||||
// distinctionUtility - used to discern whether X number of events stem from Y number of users.
|
||||
type distinctionUtility struct {
|
||||
ctx context.Context
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
|
||||
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
|
||||
|
||||
observabilitySender observabilitySender
|
||||
settingsGetter settingsGetter
|
||||
|
||||
userPlanUnsafe string
|
||||
userPlanLock sync.Mutex
|
||||
|
||||
heartbeatData heartbeatData
|
||||
heartbeatDataLock sync.Mutex
|
||||
heartbeatTicker *time.Ticker
|
||||
}
|
||||
|
||||
func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler, observabilitySender observabilitySender) *distinctionUtility {
|
||||
distinctionUtility := &distinctionUtility{
|
||||
ctx: ctx,
|
||||
|
||||
panicHandler: panicHandler,
|
||||
|
||||
lastSentMap: createLastSentMap(),
|
||||
|
||||
observabilitySender: observabilitySender,
|
||||
|
||||
userPlanUnsafe: planUnknown,
|
||||
|
||||
heartbeatData: heartbeatData{},
|
||||
heartbeatTicker: time.NewTicker(updateInterval),
|
||||
}
|
||||
|
||||
return distinctionUtility
|
||||
}
|
||||
|
||||
// sendMetricsWithGuard - schedules the metrics to be sent only if there are authenticated clients.
|
||||
func (d *distinctionUtility) sendMetricsWithGuard(metrics ...proton.ObservabilityMetric) {
|
||||
if d.observabilitySender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.observabilitySender.addMetricsIfClients(metrics...)
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
|
||||
d.settingsGetter = getter
|
||||
}
|
||||
|
||||
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
|
||||
// within the last 5 minutes.
|
||||
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool {
|
||||
curTime := time.Now()
|
||||
val, ok := d.lastSentMap[key]
|
||||
if !ok {
|
||||
d.lastSentMap[key] = curTime
|
||||
return true
|
||||
}
|
||||
|
||||
if val.Add(updateInterval).Before(curTime) {
|
||||
d.lastSentMap[key] = curTime
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// generateUserMetric creates the relevant user update metric based on its type
|
||||
// and the relevant settings. In the future this will need to be expanded to support multiple
|
||||
// versions of the metric if we ever decide to change them.
|
||||
func (d *distinctionUtility) generateUserMetric(
|
||||
metricType DistinctionErrorTypeEnum,
|
||||
) proton.ObservabilityMetric {
|
||||
schemaName, ok := errorSchemaMap[metricType]
|
||||
if !ok {
|
||||
return proton.ObservabilityMetric{}
|
||||
}
|
||||
|
||||
return generateUserMetric(schemaName, d.getUserPlanSafe(),
|
||||
d.getEmailClientUserAgent(),
|
||||
getEnabled(d.getProxyAllowed()),
|
||||
getEnabled(d.getBetaAccessEnabled()),
|
||||
)
|
||||
}
|
||||
|
||||
func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: schemaName,
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"plan": plan,
|
||||
"mailClient": mailClient,
|
||||
"dohEnabled": dohEnabled,
|
||||
"betaAccessEnabled": betaAccess,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
|
||||
d.updateHeartbeatData(errType)
|
||||
|
||||
if d.checkAndUpdateLastSentMap(errType) {
|
||||
metrics = append(metrics, d.generateUserMetric(errType))
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getEmailClientUserAgent() string {
|
||||
ua := ""
|
||||
if d.settingsGetter != nil {
|
||||
ua = d.settingsGetter.GetCurrentUserAgent()
|
||||
}
|
||||
|
||||
return matchUserAgent(ua)
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getBetaAccessEnabled() bool {
|
||||
if d.settingsGetter == nil {
|
||||
return false
|
||||
}
|
||||
return d.settingsGetter.GetUpdateChannel() == updater.EarlyChannel
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getProxyAllowed() bool {
|
||||
if d.settingsGetter == nil {
|
||||
return false
|
||||
}
|
||||
return d.settingsGetter.GetProxyAllowed()
|
||||
}
|
||||
119
internal/services/observability/heartbeat.go
Normal file
119
internal/services/observability/heartbeat.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const genericHeartbeatSchemaName = "bridge_generic_user_heartbeat_total"
|
||||
|
||||
type heartbeatData struct {
|
||||
receivedSyncError bool
|
||||
receivedEventLoopError bool
|
||||
receivedOtherError bool
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) resetHeartbeatData() {
|
||||
d.heartbeatData.receivedSyncError = false
|
||||
d.heartbeatData.receivedOtherError = false
|
||||
d.heartbeatData.receivedEventLoopError = false
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) {
|
||||
d.withUpdateHeartbeatDataLock(func() {
|
||||
switch errType {
|
||||
case SyncError:
|
||||
d.heartbeatData.receivedSyncError = true
|
||||
case EventLoopError:
|
||||
d.heartbeatData.receivedEventLoopError = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) runHeartbeat() {
|
||||
go func() {
|
||||
defer async.HandlePanic(d.panicHandler)
|
||||
defer d.heartbeatTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case <-d.heartbeatTicker.C:
|
||||
d.sendHeartbeat()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) withUpdateHeartbeatDataLock(fn func()) {
|
||||
d.heartbeatDataLock.Lock()
|
||||
defer d.heartbeatDataLock.Unlock()
|
||||
fn()
|
||||
}
|
||||
|
||||
// sendHeartbeat - will only send a heartbeat if there is an authenticated client
|
||||
// otherwise we might end up polluting the cache and therefore our metrics.
|
||||
func (d *distinctionUtility) sendHeartbeat() {
|
||||
d.withUpdateHeartbeatDataLock(func() {
|
||||
d.sendMetricsWithGuard(d.generateHeartbeatUserMetric())
|
||||
d.resetHeartbeatData()
|
||||
})
|
||||
}
|
||||
|
||||
func formatBool(value bool) string {
|
||||
return fmt.Sprintf("%t", value)
|
||||
}
|
||||
|
||||
// generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data.
|
||||
func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric {
|
||||
return generateHeartbeatMetric(
|
||||
d.getUserPlanSafe(),
|
||||
d.getEmailClientUserAgent(),
|
||||
getEnabled(d.settingsGetter.GetProxyAllowed()),
|
||||
getEnabled(d.getBetaAccessEnabled()),
|
||||
formatBool(d.heartbeatData.receivedOtherError),
|
||||
formatBool(d.heartbeatData.receivedSyncError),
|
||||
formatBool(d.heartbeatData.receivedEventLoopError),
|
||||
)
|
||||
}
|
||||
|
||||
func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherError, syncError, eventLoopError string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: genericHeartbeatSchemaName,
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"plan": plan,
|
||||
"mailClient": mailClient,
|
||||
"dohEnabled": dohEnabled,
|
||||
"betaAccessEnabled": betaAccess,
|
||||
"receivedOtherError": otherError,
|
||||
"receivedSyncError": syncError,
|
||||
"receivedEventLoopError": eventLoopError,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
121
internal/services/observability/plan_utils.go
Normal file
121
internal/services/observability/plan_utils.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const (
|
||||
planUnknown = "unknown"
|
||||
planOther = "other"
|
||||
planBusiness = "business"
|
||||
planIndividual = "individual"
|
||||
planGroup = "group"
|
||||
)
|
||||
|
||||
var planHierarchy = map[string]int{ //nolint:gochecknoglobals
|
||||
planBusiness: 4,
|
||||
planGroup: 3,
|
||||
planIndividual: 2,
|
||||
planOther: 1,
|
||||
planUnknown: 0,
|
||||
}
|
||||
|
||||
type planGetter interface {
|
||||
GetOrganizationData(ctx context.Context) (proton.OrganizationResponse, error)
|
||||
}
|
||||
|
||||
func isHigherPriority(currentPlan, newPlan string) bool {
|
||||
newRank, ok := planHierarchy[newPlan]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
currentRank, ok2 := planHierarchy[currentPlan]
|
||||
if !ok2 {
|
||||
return true // we don't have a valid plan, might as well replace it
|
||||
}
|
||||
|
||||
return newRank > currentRank
|
||||
}
|
||||
|
||||
func mapUserPlan(planName string) string {
|
||||
if planName == "" {
|
||||
return planUnknown
|
||||
}
|
||||
switch strings.TrimSpace(strings.ToLower(planName)) {
|
||||
case "mail2022":
|
||||
return planIndividual
|
||||
case "bundle2022":
|
||||
return planIndividual
|
||||
case "family2022":
|
||||
return planGroup
|
||||
case "visionary2022":
|
||||
return planGroup
|
||||
case "mailpro2022":
|
||||
return planBusiness
|
||||
case "planbiz2024":
|
||||
return planBusiness
|
||||
case "bundlepro2022":
|
||||
return planBusiness
|
||||
case "bundlepro2024":
|
||||
return planBusiness
|
||||
case "duo2024":
|
||||
return planGroup
|
||||
|
||||
default:
|
||||
return planOther
|
||||
}
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) setUserPlan(planName string) {
|
||||
if planName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
d.userPlanLock.Lock()
|
||||
defer d.userPlanLock.Unlock()
|
||||
|
||||
userPlanMapped := mapUserPlan(planName)
|
||||
if isHigherPriority(d.userPlanUnsafe, userPlanMapped) {
|
||||
d.userPlanUnsafe = userPlanMapped
|
||||
}
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) registerUserPlan(ctx context.Context, getter planGetter, panicHandler async.PanicHandler) {
|
||||
go func() {
|
||||
defer async.HandlePanic(panicHandler)
|
||||
|
||||
orgRes, err := getter.GetOrganizationData(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
d.setUserPlan(orgRes.Organization.PlanName)
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getUserPlanSafe() string {
|
||||
d.userPlanLock.Lock()
|
||||
defer d.userPlanLock.Unlock()
|
||||
return d.userPlanUnsafe
|
||||
}
|
||||
@ -36,13 +36,18 @@ const (
|
||||
maxBatchSize = 1000
|
||||
)
|
||||
|
||||
type PushObsMetricFn func(metric proton.ObservabilityMetric)
|
||||
|
||||
type client struct {
|
||||
isTelemetryEnabled func(context.Context) bool
|
||||
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
||||
}
|
||||
|
||||
// Sender - interface maps to the observability service methods,
|
||||
// so we can easily pass them down to relevant components.
|
||||
type Sender interface {
|
||||
AddMetrics(metrics ...proton.ObservabilityMetric)
|
||||
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@ -62,6 +67,8 @@ type Service struct {
|
||||
|
||||
userClientStore map[string]*client
|
||||
userClientStoreLock sync.Mutex
|
||||
|
||||
distinctionUtility *distinctionUtility
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||
@ -85,11 +92,19 @@ func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||
userClientStore: make(map[string]*client),
|
||||
}
|
||||
|
||||
service.distinctionUtility = newDistinctionUtility(ctx, panicHandler, service)
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// Run starts the observability service goroutine.
|
||||
// The function also sets some utility functions to a helper struct aimed at differentiating the amount of users sending metric updates.
|
||||
func (s *Service) Run(settingsGetter settingsGetter) {
|
||||
s.log.Info("Starting service")
|
||||
|
||||
s.distinctionUtility.setSettingsGetter(settingsGetter)
|
||||
s.distinctionUtility.runHeartbeat()
|
||||
|
||||
go func() {
|
||||
s.start()
|
||||
}()
|
||||
@ -200,7 +215,7 @@ func (s *Service) scheduleDispatch() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
||||
func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
|
||||
s.withMetricStoreLock(func() {
|
||||
metricStoreLength := len(s.metricStore)
|
||||
if metricStoreLength >= maxStorageSize {
|
||||
@ -209,12 +224,32 @@ func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
||||
dropCount := metricStoreLength - maxStorageSize + 1
|
||||
s.metricStore = s.metricStore[dropCount:]
|
||||
}
|
||||
s.metricStore = append(s.metricStore, metric)
|
||||
s.metricStore = append(s.metricStore, metric...)
|
||||
})
|
||||
|
||||
// If the context has been cancelled i.e. the service has been stopped then we should be free to exit.
|
||||
if s.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSignal(s.signalDataArrived)
|
||||
}
|
||||
|
||||
// addMetricsIfClients - will append a metric only if there are authenticated clients
|
||||
// via which we can reach the endpoint.
|
||||
func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) {
|
||||
hasClients := false
|
||||
s.withUserClientStoreLock(func() {
|
||||
hasClients = len(s.userClientStore) > 0
|
||||
})
|
||||
|
||||
if !hasClients {
|
||||
return
|
||||
}
|
||||
|
||||
s.addMetrics(metric...)
|
||||
}
|
||||
|
||||
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
||||
s.log.Info("Registering user client, ID:", userID)
|
||||
|
||||
@ -225,6 +260,8 @@ func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client,
|
||||
}
|
||||
})
|
||||
|
||||
s.distinctionUtility.registerUserPlan(s.ctx, protonClient, s.panicHandler)
|
||||
|
||||
// There may be a case where we already have metric updates stored, so try to flush;
|
||||
s.sendSignal(s.signalDataArrived)
|
||||
}
|
||||
@ -279,3 +316,25 @@ func (s *Service) sendSignal(channel chan struct{}) {
|
||||
func ModifyThrottlePeriod(duration time.Duration) {
|
||||
throttleDuration = duration
|
||||
}
|
||||
|
||||
func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||
s.addMetrics(metrics...)
|
||||
}
|
||||
|
||||
// AddDistinctMetrics - sends an additional metric related to the user, so we can determine
|
||||
// what number of events come from what number of users.
|
||||
// As the binning interval is what allows us to do this we
|
||||
// should not send these if there are no logged-in users at that moment.
|
||||
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
|
||||
s.addMetricsIfClients(metrics...)
|
||||
}
|
||||
|
||||
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
|
||||
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
|
||||
s.distinctionUtility.heartbeatTicker.Reset(duration)
|
||||
}
|
||||
|
||||
func ModifyUserMetricInterval(duration time.Duration) {
|
||||
updateInterval = duration
|
||||
}
|
||||
|
||||
106
internal/services/observability/test_utils.go
Normal file
106
internal/services/observability/test_utils.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric {
|
||||
planValues := []string{
|
||||
planUnknown,
|
||||
planOther,
|
||||
planBusiness,
|
||||
planIndividual,
|
||||
planGroup}
|
||||
mailClientValues := []string{
|
||||
emailAgentAppleMail,
|
||||
emailAgentOutlook,
|
||||
emailAgentThunderbird,
|
||||
emailAgentOther,
|
||||
emailAgentUnknown,
|
||||
}
|
||||
enabledValues := []string{
|
||||
getEnabled(true), getEnabled(false),
|
||||
}
|
||||
|
||||
var metrics []proton.ObservabilityMetric
|
||||
|
||||
for _, schemaName := range errorSchemaMap {
|
||||
for _, plan := range planValues {
|
||||
for _, mailClient := range mailClientValues {
|
||||
for _, dohEnabled := range enabledValues {
|
||||
for _, betaAccess := range enabledValues {
|
||||
metrics = append(metrics, generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
|
||||
planValues := []string{
|
||||
planUnknown,
|
||||
planOther,
|
||||
planBusiness,
|
||||
planIndividual,
|
||||
planGroup}
|
||||
mailClientValues := []string{
|
||||
emailAgentAppleMail,
|
||||
emailAgentOutlook,
|
||||
emailAgentThunderbird,
|
||||
emailAgentOther,
|
||||
emailAgentUnknown,
|
||||
}
|
||||
enabledValues := []string{
|
||||
getEnabled(true), getEnabled(false),
|
||||
}
|
||||
|
||||
trueFalseValues := []string{
|
||||
"true", "false",
|
||||
}
|
||||
|
||||
var metrics []proton.ObservabilityMetric
|
||||
for _, plan := range planValues {
|
||||
for _, mailClient := range mailClientValues {
|
||||
for _, dohEnabled := range enabledValues {
|
||||
for _, betaAccess := range enabledValues {
|
||||
for _, receivedOtherError := range trueFalseValues {
|
||||
for _, receivedSyncError := range trueFalseValues {
|
||||
for _, receivedEventLoopError := range trueFalseValues {
|
||||
metrics = append(metrics,
|
||||
generateHeartbeatMetric(plan,
|
||||
mailClient,
|
||||
dohEnabled,
|
||||
betaAccess,
|
||||
receivedOtherError,
|
||||
receivedSyncError,
|
||||
receivedEventLoopError,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
68
internal/services/observability/utils.go
Normal file
68
internal/services/observability/utils.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
)
|
||||
|
||||
// settingsGetter - interface that maps to bridge object methods such that we
|
||||
// can pass the whole object instead of individual function callbacks.
|
||||
type settingsGetter interface {
|
||||
GetCurrentUserAgent() string
|
||||
GetProxyAllowed() bool
|
||||
GetUpdateChannel() updater.Channel
|
||||
}
|
||||
|
||||
// User agent mapping.
|
||||
const (
|
||||
emailAgentAppleMail = "apple_mail"
|
||||
emailAgentOutlook = "outlook"
|
||||
emailAgentThunderbird = "thunderbird"
|
||||
emailAgentOther = "other"
|
||||
emailAgentUnknown = "unknown"
|
||||
)
|
||||
|
||||
func matchUserAgent(userAgent string) string {
|
||||
if userAgent == "" {
|
||||
return emailAgentUnknown
|
||||
}
|
||||
|
||||
userAgent = strings.ToLower(userAgent)
|
||||
switch {
|
||||
case strings.Contains(userAgent, "outlook"):
|
||||
return emailAgentOutlook
|
||||
case strings.Contains(userAgent, "thunderbird"):
|
||||
return emailAgentThunderbird
|
||||
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "mail"):
|
||||
return emailAgentAppleMail
|
||||
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "notes"):
|
||||
return emailAgentUnknown
|
||||
default:
|
||||
return emailAgentOther
|
||||
}
|
||||
}
|
||||
|
||||
func getEnabled(value bool) string {
|
||||
if !value {
|
||||
return "disabled"
|
||||
}
|
||||
return "enabled"
|
||||
}
|
||||
111
internal/services/observability/utils_test.go
Normal file
111
internal/services/observability/utils_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2024 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 observability
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatchUserAgent(t *testing.T) {
|
||||
type agentParseResult struct {
|
||||
agent string
|
||||
result string
|
||||
}
|
||||
|
||||
testCases := []agentParseResult{
|
||||
{
|
||||
agent: "Microsoft Outlook/16.0.17928.20114 (Windows 11 Version 23H2)",
|
||||
result: emailAgentOutlook,
|
||||
},
|
||||
{
|
||||
agent: "Mailbird/3.0.18.0 (Windows 11 Version 23H2)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Microsoft Outlook/16.0.17830.20166 (Windows 11 Version 23H2)",
|
||||
result: emailAgentOutlook,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Mail/16.0-3776.700.51 (macOS 14.6)",
|
||||
result: emailAgentAppleMail,
|
||||
},
|
||||
{
|
||||
agent: "/ (Windows 11 Version 23H2)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Microsoft Outlook for Mac/16.88.0-BUILDDAY (macOS 14.6)",
|
||||
result: emailAgentOutlook,
|
||||
},
|
||||
{
|
||||
agent: "/ (macOS 14.5)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "/ (Freedesktop SDK 23.08 (Flatpak runtime))",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Mail/16.0-3774.600.62 (macOS 14.5)",
|
||||
result: emailAgentAppleMail,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Notes/4.11-2817 (macOS 14.6)",
|
||||
result: emailAgentUnknown,
|
||||
},
|
||||
{
|
||||
agent: "NoClient/0.0.1 (macOS 14.6)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Thunderbird/115.15.0 (Ubuntu 20.04.6 LTS)",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "Thunderbird/115.14.0 (macOS 14.6)",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "Thunderbird/115.10.2 (Windows 11 Version 23H2)",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Notes/4.9-1965 (macOS Monterey (12.0))",
|
||||
result: emailAgentUnknown,
|
||||
},
|
||||
{
|
||||
agent: " Thunderbird/115.14.0 (macOS 14.6) ",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "",
|
||||
result: emailAgentUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
require.Equal(t, testCase.result, matchUserAgent(testCase.agent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBool(t *testing.T) {
|
||||
require.Equal(t, "false", formatBool(false))
|
||||
require.Equal(t, "true", formatBool(true))
|
||||
}
|
||||
@ -30,7 +30,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/rfc5322"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
@ -197,13 +196,7 @@ func (s *Service) sendWithKey(
|
||||
}
|
||||
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
||||
if err != nil {
|
||||
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
|
||||
"error": err,
|
||||
"references": message.References,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to report error")
|
||||
}
|
||||
|
||||
// Sentry event has been removed; should be replaced with observability - BRIDGE-206.
|
||||
s.log.WithError(err).Warn("Failed to get parent ID")
|
||||
}
|
||||
|
||||
|
||||
@ -160,10 +160,6 @@ func (s *childJob) onError(err error) {
|
||||
s.job.onError(err)
|
||||
}
|
||||
|
||||
func (s *childJob) userID() string {
|
||||
return s.job.userID
|
||||
}
|
||||
|
||||
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
|
||||
numChunks := len(chunks)
|
||||
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2024 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 observabilitymetrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const (
|
||||
errorCaseSchemaName = "bridge_sync_message_build_errors_total"
|
||||
errorCaseSchemaVersion = 1
|
||||
successCaseSchemaName = "bridge_sync_message_build_success_total"
|
||||
successCaseSchemaVersion = 1
|
||||
)
|
||||
|
||||
func generateStageBuildFailureObservabilityMetric(errorType string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: errorCaseSchemaName,
|
||||
Version: errorCaseSchemaVersion,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"errorType": errorType,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateNoUnlockedKeyringMetric() proton.ObservabilityMetric {
|
||||
return generateStageBuildFailureObservabilityMetric("noUnlockedKeyring")
|
||||
}
|
||||
|
||||
func GenerateFailedToBuildMetric() proton.ObservabilityMetric {
|
||||
return generateStageBuildFailureObservabilityMetric("failedToBuild")
|
||||
}
|
||||
|
||||
// GenerateMessageBuiltSuccessMetric - Maybe this is incorrect, I'm not sure how metrics with no labels
|
||||
// should be dealt with. The integration tests will tell us.
|
||||
func GenerateMessageBuiltSuccessMetric() proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: successCaseSchemaName,
|
||||
Version: successCaseSchemaVersion,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
)
|
||||
|
||||
// Service which mediates IMAP syncing in Bridge.
|
||||
@ -36,8 +36,9 @@ type Service struct {
|
||||
group *async.Group
|
||||
}
|
||||
|
||||
func NewService(reporter reporter.Reporter,
|
||||
func NewService(
|
||||
panicHandler async.PanicHandler,
|
||||
observabilitySender observability.Sender,
|
||||
) *Service {
|
||||
limits := newSyncLimits(2 * Gigabyte)
|
||||
|
||||
@ -50,7 +51,7 @@ func NewService(reporter reporter.Reporter,
|
||||
limits: limits,
|
||||
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, panicHandler),
|
||||
downloadStage: NewDownloadStage(downloadCh, buildCh, limits.MaxParallelDownloads, panicHandler),
|
||||
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, reporter),
|
||||
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, observabilitySender),
|
||||
applyStage: NewApplyStage(applyCh),
|
||||
metaCh: metaCh,
|
||||
group: async.NewGroup(context.Background(), panicHandler),
|
||||
|
||||
@ -26,9 +26,10 @@ import (
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/logging"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||
"github.com/bradenaw/juniper/parallel"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -50,8 +51,10 @@ type BuildStage struct {
|
||||
maxBuildMem uint64
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
reporter reporter.Reporter
|
||||
log *logrus.Entry
|
||||
|
||||
// Observability
|
||||
observabilitySender observability.Sender
|
||||
}
|
||||
|
||||
func NewBuildStage(
|
||||
@ -59,7 +62,7 @@ func NewBuildStage(
|
||||
output BuildStageOutput,
|
||||
maxBuildMem uint64,
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
observabilitySender observability.Sender,
|
||||
) *BuildStage {
|
||||
return &BuildStage{
|
||||
input: input,
|
||||
@ -67,7 +70,7 @@ func NewBuildStage(
|
||||
maxBuildMem: maxBuildMem,
|
||||
log: logrus.WithField("sync-stage", "build"),
|
||||
panicHandler: panicHandler,
|
||||
reporter: reporter,
|
||||
observabilitySender: observabilitySender,
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,35 +150,24 @@ func (b *BuildStage) run(ctx context.Context) {
|
||||
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
||||
}
|
||||
|
||||
if err := b.reporter.ReportMessageWithContext("Failed to build message - no unlocked keyring (sync)", reporter.Context{
|
||||
"messageID": msg.ID,
|
||||
"userID": req.userID(),
|
||||
}); err != nil {
|
||||
req.job.log.WithError(err).Error("Failed to report message build error")
|
||||
}
|
||||
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
|
||||
return BuildResult{}, nil
|
||||
}
|
||||
|
||||
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
|
||||
if err != nil {
|
||||
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (syn)")
|
||||
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (sync)")
|
||||
|
||||
if err := req.job.state.AddFailedMessageID(req.getContext(), msg.ID); err != nil {
|
||||
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
||||
}
|
||||
|
||||
if err := b.reporter.ReportMessageWithContext("Failed to build message (sync)", reporter.Context{
|
||||
"messageID": msg.ID,
|
||||
"error": err,
|
||||
"userID": req.userID(),
|
||||
}); err != nil {
|
||||
req.job.log.WithError(err).Error("Failed to report message build error")
|
||||
}
|
||||
|
||||
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateFailedToBuildMetric())
|
||||
// We could sync a placeholder message here, but for now we skip it entirely.
|
||||
return BuildResult{}, nil
|
||||
}
|
||||
|
||||
b.observabilitySender.AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
|
||||
return res, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@ -24,10 +24,11 @@ import (
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -67,7 +68,6 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
||||
|
||||
input := NewChannelConsumerProducer[BuildRequest]()
|
||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||
reporter := mocks.NewMockReporter(mockCtrl)
|
||||
|
||||
labels := getTestLabels()
|
||||
|
||||
@ -105,7 +105,10 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
||||
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(buildResult, nil)
|
||||
tj.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
|
||||
observabilityService := mocks.NewMockObservabilitySender(mockCtrl)
|
||||
observabilityService.EXPECT().AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilityService)
|
||||
|
||||
go func() {
|
||||
stage.run(ctx)
|
||||
@ -125,7 +128,7 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
||||
|
||||
input := NewChannelConsumerProducer[BuildRequest]()
|
||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||
mockObservabilityService := mocks.NewMockObservabilitySender(mockCtrl)
|
||||
|
||||
labels := getTestLabels()
|
||||
|
||||
@ -156,15 +159,12 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
||||
|
||||
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(BuildResult{}, buildError)
|
||||
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
||||
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
|
||||
"userID": "u",
|
||||
"messageID": "MSG",
|
||||
"error": buildError,
|
||||
})).Return(nil)
|
||||
|
||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||
mockObservabilityService.EXPECT().AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockObservabilityService)
|
||||
|
||||
go func() {
|
||||
stage.run(ctx)
|
||||
@ -183,7 +183,6 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
||||
|
||||
input := NewChannelConsumerProducer[BuildRequest]()
|
||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||
|
||||
labels := getTestLabels()
|
||||
|
||||
@ -209,14 +208,13 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
||||
tj.job.end()
|
||||
|
||||
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
||||
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
|
||||
"userID": "u",
|
||||
"messageID": "MSG",
|
||||
})).Return(nil)
|
||||
|
||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||
observabilitySender := mocks.NewMockObservabilitySender(mockCtrl)
|
||||
observabilitySender.EXPECT().AddDistinctMetrics(observability.SyncError)
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilitySender)
|
||||
|
||||
go func() {
|
||||
stage.run(ctx)
|
||||
@ -235,7 +233,6 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
||||
|
||||
input := NewChannelConsumerProducer[BuildRequest]()
|
||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||
|
||||
labels := getTestLabels()
|
||||
|
||||
@ -261,7 +258,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
||||
childJob := tj.job.newChildJob("f", 10)
|
||||
tj.job.end()
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
||||
|
||||
go func() {
|
||||
stage.run(ctx)
|
||||
@ -283,7 +280,6 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
||||
|
||||
input := NewChannelConsumerProducer[BuildRequest]()
|
||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||
|
||||
msg := proton.FullMessage{
|
||||
Message: proton.Message{
|
||||
@ -294,7 +290,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@ -327,7 +323,6 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
|
||||
|
||||
input := NewChannelConsumerProducer[BuildRequest]()
|
||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||
reporter := mocks.NewMockReporter(mockCtrl)
|
||||
|
||||
labels := getTestLabels()
|
||||
|
||||
@ -340,7 +335,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
|
||||
childJob := tj.job.newChildJob("f", 10)
|
||||
tj.job.end()
|
||||
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
|
||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
||||
|
||||
go func() {
|
||||
stage.run(ctx)
|
||||
|
||||
@ -111,7 +111,7 @@ func (s *Service) readCacheFile() {
|
||||
|
||||
file, err := os.Open(s.cacheFilepath)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to open cache file")
|
||||
s.log.WithError(err).Info("Unable to open cache file")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
||||
update.Package+".sig",
|
||||
)
|
||||
if err != nil {
|
||||
return ErrDownloadVerify
|
||||
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
|
||||
}
|
||||
|
||||
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
|
||||
|
||||
@ -115,7 +115,6 @@ func New(
|
||||
isNew bool,
|
||||
notificationStore *notifications.Store,
|
||||
getFlagValFn unleash.GetFlagValueFn,
|
||||
pushObservabilityMetric observability.PushObsMetricFn,
|
||||
) (*User, error) {
|
||||
user, err := newImpl(
|
||||
ctx,
|
||||
@ -137,7 +136,6 @@ func New(
|
||||
isNew,
|
||||
notificationStore,
|
||||
getFlagValFn,
|
||||
pushObservabilityMetric,
|
||||
)
|
||||
if err != nil {
|
||||
// Cleanup any pending resources on error
|
||||
@ -172,7 +170,6 @@ func newImpl(
|
||||
isNew bool,
|
||||
notificationStore *notifications.Store,
|
||||
getFlagValueFn unleash.GetFlagValueFn,
|
||||
pushObservabilityMetric observability.PushObsMetricFn,
|
||||
) (*User, error) {
|
||||
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
||||
|
||||
@ -288,9 +285,10 @@ func newImpl(
|
||||
syncConfigDir,
|
||||
user.maxSyncMemory,
|
||||
showAllMail,
|
||||
observabilityService,
|
||||
)
|
||||
|
||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, pushObservabilityMetric)
|
||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService)
|
||||
|
||||
// Check for status_progress when triggered.
|
||||
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
|
||||
|
||||
@ -175,7 +175,6 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
||||
func(_ string) bool {
|
||||
return false
|
||||
},
|
||||
func(_ proton.ObservabilityMetric) {},
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
defer user.Close()
|
||||
|
||||
@ -19,6 +19,7 @@ package vault
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"golang.org/x/exp/slices"
|
||||
@ -37,6 +38,10 @@ func (user *User) Username() string {
|
||||
return user.vault.getUser(user.userID).Username
|
||||
}
|
||||
|
||||
func (user *User) usernameUnsafe() string {
|
||||
return user.vault.getUserUnsafe(user.userID).Username
|
||||
}
|
||||
|
||||
// PrimaryEmail returns the user's primary email address.
|
||||
func (user *User) PrimaryEmail() string {
|
||||
return user.vault.getUser(user.userID).PrimaryEmail
|
||||
@ -242,3 +247,15 @@ func (user *User) SetShouldSync(shouldResync bool) error {
|
||||
func (user *User) GetShouldResync() bool {
|
||||
return user.vault.getUser(user.userID).ShouldResync
|
||||
}
|
||||
|
||||
// updateUsernameUnsafe - updates the username of the relevant user, provided that the new username is not empty
|
||||
// and differs from the previous. Writes are not performed if this case is not met.
|
||||
// Should only be called from contexts where the vault mutex is already locked.
|
||||
func (user *User) updateUsernameUnsafe(username string) error {
|
||||
if strings.TrimSpace(username) == "" || user.usernameUnsafe() == username {
|
||||
return nil
|
||||
}
|
||||
return user.vault.modUserUnsafe(user.userID, func(userData *UserData) {
|
||||
userData.Username = username
|
||||
})
|
||||
}
|
||||
|
||||
@ -240,6 +240,10 @@ func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRe
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := user.updateUsernameUnsafe(username); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return user, false, nil
|
||||
}
|
||||
}
|
||||
@ -450,6 +454,22 @@ func (vault *Vault) getUser(userID string) UserData {
|
||||
return users[idx]
|
||||
}
|
||||
|
||||
// getUserUnsafe - fetches the relevant UserData.
|
||||
// Should only be called from contexts in which the vault mutex has been read locked.
|
||||
func (vault *Vault) getUserUnsafe(userID string) UserData {
|
||||
users := vault.getUnsafe().Users
|
||||
|
||||
idx := xslices.IndexFunc(users, func(user UserData) bool {
|
||||
return user.UserID == userID
|
||||
})
|
||||
|
||||
if idx < 0 {
|
||||
panic("Unknown user")
|
||||
}
|
||||
|
||||
return users[idx]
|
||||
}
|
||||
|
||||
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
|
||||
vault.lock.Lock()
|
||||
defer vault.lock.Unlock()
|
||||
|
||||
@ -857,6 +857,23 @@ func getFileReader(filename string) io.Reader {
|
||||
return f
|
||||
}
|
||||
|
||||
func TestParseInvalidOriginalBoundary(t *testing.T) {
|
||||
f := getFileReader("incorrect_boundary_w_invalid_character_tuta.eml")
|
||||
|
||||
p, err := parser.New(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, p.Root().Header.Get("Content-Type") == `multipart/related; boundary="------------1234567890@tutanota"`)
|
||||
|
||||
m, err := ParseWithParser(p, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, strings.HasPrefix(string(m.MIMEBody), "Content-Type: multipart/related;\r\n boundary="))
|
||||
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary="------------1234567890@tutanota"`))
|
||||
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=------------1234567890@tutanota`))
|
||||
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=1234567890@tutanota`))
|
||||
}
|
||||
|
||||
type panicReader struct{}
|
||||
|
||||
func (panicReader) Read(_ []byte) (int, error) {
|
||||
|
||||
13
pkg/message/testdata/incorrect_boundary_w_invalid_character_tuta.eml
vendored
Normal file
13
pkg/message/testdata/incorrect_boundary_w_invalid_character_tuta.eml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
Date: Mon, 01 Jan 2000 00:00:00 +0000 (UTC)
|
||||
From: Daniel at Test <daniel@test.com>
|
||||
Mime-Version: 1.0
|
||||
Subject: Test incorrect original boundary w. invalid character
|
||||
To: david@test.com
|
||||
Content-Type: multipart/related; boundary="------------1234567890@tutanota"
|
||||
|
||||
--------------1234567890@tutanota
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-transfer-encoding: base64
|
||||
|
||||
PGh0bWw+PGgxPkhlbGxvIFdvcmxkITwvaDE+PC9odG1sPg==
|
||||
--------------1234567890@tutanota--
|
||||
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FlaUI.Core" Version="4.0.0" />
|
||||
<PackageReference Include="FlaUI.UIA3" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageReference Include="NUnit" Version="4.2.1" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="NUnit.Framework" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
29
tests/e2e/ui_tests/windows_os/Results/HomeResult.cs
Normal file
29
tests/e2e/ui_tests/windows_os/Results/HomeResult.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using FlaUI.Core.AutomationElements;
|
||||
using FlaUI.Core.Definitions;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests.Results
|
||||
{
|
||||
public class HomeResult : UIActions
|
||||
{
|
||||
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
|
||||
private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
|
||||
private TextBox FreeAccountErrorText => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
|
||||
private TextBox SignedOutAccount => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
|
||||
public HomeResult CheckIfLoggedIn()
|
||||
{
|
||||
Assert.That(SignOutButton.IsAvailable, Is.True);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HomeResult CheckIfFreeAccountErrorIsDisplayed(string ErrorText)
|
||||
{
|
||||
Assert.That(FreeAccountErrorText.Name == ErrorText, Is.True);
|
||||
return this;
|
||||
}
|
||||
public HomeResult CheckIfAccountIsSignedOut()
|
||||
{
|
||||
Assert.That(SignedOutAccount.IsAvailable, Is.True);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
tests/e2e/ui_tests/windows_os/TestSession.cs
Normal file
43
tests/e2e/ui_tests/windows_os/TestSession.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using FlaUI.Core.AutomationElements;
|
||||
using FlaUI.Core;
|
||||
using FlaUI.UIA3;
|
||||
using ProtonMailBridge.UI.Tests.TestsHelper;
|
||||
using FlaUI.Core.Input;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests
|
||||
{
|
||||
public class TestSession
|
||||
{
|
||||
|
||||
public static Application App;
|
||||
protected static Application Service;
|
||||
protected static Window Window;
|
||||
|
||||
protected static void ClientCleanup()
|
||||
{
|
||||
App.Kill();
|
||||
App.Dispose();
|
||||
// Give some time to properly exit the app
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
|
||||
public static void LaunchApp()
|
||||
{
|
||||
string appExecutable = TestData.AppExecutable;
|
||||
Application.Launch(appExecutable);
|
||||
Wait.UntilInputIsProcessed(TestData.FiveSecondsTimeout);
|
||||
App = Application.Attach("bridge-gui.exe");
|
||||
|
||||
try
|
||||
{
|
||||
Window = App.GetMainWindow(new UIA3Automation(), TestData.ThirtySecondsTimeout);
|
||||
}
|
||||
catch (System.TimeoutException)
|
||||
{
|
||||
Assert.Fail("Failed to get window of application!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs
Normal file
51
tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using NUnit.Framework;
|
||||
using ProtonMailBridge.UI.Tests.TestsHelper;
|
||||
using ProtonMailBridge.UI.Tests.Windows;
|
||||
using ProtonMailBridge.UI.Tests.Results;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class LoginLogoutTests : TestSession
|
||||
{
|
||||
private readonly LoginWindow _loginWindow = new();
|
||||
private readonly HomeWindow _mainWindow = new();
|
||||
private readonly HomeResult _homeResult = new();
|
||||
private readonly string FreeAccountErrorText = "Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.";
|
||||
|
||||
[Test]
|
||||
public void LoginAsPaidUser()
|
||||
{
|
||||
_loginWindow.SignIn(TestUserData.GetPaidUser());
|
||||
_homeResult.CheckIfLoggedIn();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoginAsFreeUser()
|
||||
{
|
||||
_loginWindow.SignIn(TestUserData.GetFreeUser());
|
||||
_homeResult.CheckIfFreeAccountErrorIsDisplayed(FreeAccountErrorText);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuccessfullLogout()
|
||||
{
|
||||
_loginWindow.SignIn(TestUserData.GetPaidUser());
|
||||
_mainWindow.SignOutAccount();
|
||||
_homeResult.CheckIfAccountIsSignedOut();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void TestInitialize()
|
||||
{
|
||||
LaunchApp();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TestCleanup()
|
||||
{
|
||||
_mainWindow.RemoveAccount();
|
||||
ClientCleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs
Normal file
16
tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests.TestsHelper
|
||||
{
|
||||
public static class TestData
|
||||
{
|
||||
public static TimeSpan FiveSecondsTimeout => TimeSpan.FromSeconds(5);
|
||||
public static TimeSpan TenSecondsTimeout => TimeSpan.FromSeconds(10);
|
||||
public static TimeSpan ThirtySecondsTimeout => TimeSpan.FromSeconds(30);
|
||||
public static TimeSpan OneMinuteTimeout => TimeSpan.FromSeconds(60);
|
||||
public static TimeSpan RetryInterval => TimeSpan.FromMilliseconds(1000);
|
||||
public static string AppExecutable => "C:\\Program Files\\Proton AG\\Proton Mail Bridge\\bridge-gui.exe";
|
||||
}
|
||||
}
|
||||
58
tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs
Normal file
58
tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests.TestsHelper
|
||||
{
|
||||
public class TestUserData
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
|
||||
public TestUserData(string username, string password)
|
||||
{
|
||||
Username = username;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
public static TestUserData GetFreeUser()
|
||||
{
|
||||
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_FREE_USER");
|
||||
return new TestUserData(username, password);
|
||||
}
|
||||
|
||||
public static TestUserData GetPaidUser()
|
||||
{
|
||||
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_PAID_USER");
|
||||
return new TestUserData(username, password);
|
||||
}
|
||||
|
||||
public static TestUserData GetIncorrectCredentialsUser()
|
||||
{
|
||||
return new TestUserData("IncorrectUsername", "IncorrectPass");
|
||||
}
|
||||
|
||||
private static (string, string) GetusernameAndPassword(string userType)
|
||||
{
|
||||
// Get the environment variable for the user and check if missing
|
||||
// When changing or adding an environment variable, you must restart Visual Studio
|
||||
// if you have it open while doing this
|
||||
string? str = Environment.GetEnvironmentVariable(userType);
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
throw new Exception($"Missing environment variable: {userType}");
|
||||
}
|
||||
|
||||
// Check if the environment variable contains only one ':'
|
||||
// The ':' character must be between the username/email and password
|
||||
string ch = ":";
|
||||
if ((str.IndexOf(ch) != str.LastIndexOf(ch)) | (str.IndexOf(ch) == -1))
|
||||
{
|
||||
throw new Exception(
|
||||
$"Environment variable {str} must contain one ':' and it must be between username and password!"
|
||||
);
|
||||
}
|
||||
|
||||
string[] split = str.Split(':');
|
||||
return (split[0], split[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
tests/e2e/ui_tests/windows_os/UIActions.cs
Normal file
14
tests/e2e/ui_tests/windows_os/UIActions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using FlaUI.Core.AutomationElements;
|
||||
using FlaUI.Core.Definitions;
|
||||
using FlaUI.Core.Input;
|
||||
using FlaUI.Core.Tools;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests
|
||||
{
|
||||
public class UIActions : TestSession
|
||||
{
|
||||
public AutomationElement AccountView => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane));
|
||||
}
|
||||
}
|
||||
34
tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs
Normal file
34
tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using FlaUI.Core.AutomationElements;
|
||||
using FlaUI.Core.Definitions;
|
||||
using System;
|
||||
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests.Windows
|
||||
{
|
||||
public class HomeWindow : UIActions
|
||||
{
|
||||
private AutomationElement[] AccountViewButtons => AccountView.FindAllChildren(cf => cf.ByControlType(ControlType.Button));
|
||||
private Button RemoveAccountButton => AccountViewButtons[1].AsButton();
|
||||
private AutomationElement RemoveAccountConfirmModal => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
|
||||
private Button ConfirmRemoveAccountButton => RemoveAccountConfirmModal.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Remove this account"))).AsButton();
|
||||
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
|
||||
public HomeWindow RemoveAccount()
|
||||
{
|
||||
try
|
||||
{
|
||||
RemoveAccountButton.Click();
|
||||
ConfirmRemoveAccountButton.Click();
|
||||
}
|
||||
catch (System.NullReferenceException)
|
||||
{
|
||||
ClientCleanup();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public HomeWindow SignOutAccount()
|
||||
{
|
||||
SignOutButton.Click();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs
Normal file
49
tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using FlaUI.Core.AutomationElements;
|
||||
using FlaUI.Core.Input;
|
||||
using FlaUI.Core.Definitions;
|
||||
using ProtonMailBridge.UI.Tests.TestsHelper;
|
||||
|
||||
namespace ProtonMailBridge.UI.Tests.Windows
|
||||
{
|
||||
public class LoginWindow : UIActions
|
||||
{
|
||||
private AutomationElement[] InputFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Edit));
|
||||
private TextBox UsernameInput => InputFields[0].AsTextBox();
|
||||
private TextBox PasswordInput => InputFields[1].AsTextBox();
|
||||
private Button SignInButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign in"))).AsButton();
|
||||
private Button StartSetupButton => Window.FindFirstDescendant(cf => cf.ByName("Start setup")).AsButton();
|
||||
private Button SetUpLater => Window.FindFirstDescendant(cf => cf.ByName("Setup later")).AsButton();
|
||||
|
||||
public LoginWindow SignIn(TestUserData user)
|
||||
{
|
||||
ClickStartSetupButton();
|
||||
EnterCredentials(user);
|
||||
Wait.UntilInputIsProcessed(TestData.TenSecondsTimeout);
|
||||
SetUpLater?.Click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoginWindow SignIn(string username, string password)
|
||||
{
|
||||
TestUserData user = new TestUserData(username, password);
|
||||
SignIn(user);
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoginWindow ClickStartSetupButton()
|
||||
{
|
||||
StartSetupButton?.Click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoginWindow EnterCredentials(TestUserData user)
|
||||
{
|
||||
UsernameInput.Text = user.Username;
|
||||
PasswordInput.Text = user.Password;
|
||||
SignInButton.Click();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
tests/e2e/ui_tests/windows_os/app.config
Normal file
35
tests/e2e/ui_tests/windows_os/app.config
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Diagnostics.PerformanceCounter" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.1" newVersion="8.0.0.1"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/></startup></configuration>
|
||||
31
tests/e2e/ui_tests/windows_os/ui_tests.sln
Normal file
31
tests/e2e/ui_tests/windows_os/ui_tests.sln
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35208.52
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtonMailBridge.UI.Tests", "ProtonMailBridge.UI.Tests.csproj", "{027E5266-E353-4095-AF24-B3ED240EACAA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.Build.0 = Debug|x64
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.ActiveCfg = Release|x64
|
||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {817DD45A-EA2C-4F16-A680-5810DADCE4E7}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -57,7 +57,6 @@ Feature: IMAP create messages
|
||||
And IMAP client "1" eventually sees the following messages in "All Mail":
|
||||
| from | to | subject | body |
|
||||
| [alias:alias]@[domain] | john.doe@email.com | foo | bar |
|
||||
And bridge reports a message with "GODT-3185: import with non-default address in combined mode: using sender address"
|
||||
|
||||
Scenario: Imports an unrelated message to inbox
|
||||
When IMAP client "1" appends the following messages to "INBOX":
|
||||
|
||||
@ -164,7 +164,6 @@ Feature: IMAP Draft messages
|
||||
And IMAP client "1" eventually sees the following messages in "Drafts":
|
||||
| to | subject | body |
|
||||
| someone@example.com | Draft without From | This is a Draft without From in header |
|
||||
And bridge reports a message with "GODT-3185: draft with non-default invalid address in combined mode: error import/draft"
|
||||
|
||||
@regression
|
||||
Scenario: Only one draft in Drafts and All Mail after editing it locally multiple times
|
||||
|
||||
@ -651,3 +651,83 @@ Feature: IMAP import messages
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Message import multipart/related with invalid boundary character
|
||||
When IMAP client "1" appends the following message to "INBOX":
|
||||
"""
|
||||
From: Bridge Test <bridgetest@pm.test>
|
||||
Date: 01 Jan 1980 00:00:00 +0000
|
||||
To: Internal Bridge <bridgetest@example.com>
|
||||
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||
Subject: Message with invalid boundary
|
||||
Content-Type: multipart/related; boundary="------------123456789@tutanota"
|
||||
|
||||
--------------123456789@tutanota
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-transfer-encoding: base64
|
||||
|
||||
PGRpdiBjbGFzcz0iIj4KPHAgY2xhc3M9IiI+PGEgbmFtZT0iX0hsazE5MDA1NjM2IiByZWw9Im5vb3
|
||||
BlbmVyIG5vcmVmZXJyZXIiIHRhcmdldD0iX2JsYW5rIj48c3BhbiBzdHlsZT0ibXNvLWZhcmVhc3Qt
|
||||
|
||||
--------------123456789@tutanota
|
||||
Content-Type: image/png;
|
||||
name==?UTF-8?B?MC5wbmc=?=
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename=image1.png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAPq3pUWHRSYXcgcHJvZmlsZSB0eXBlIG
|
||||
V4aWYAAHjarZlrliOpkoT/s4pZAuCAw3J4njM7mOXP54SUlZmV1bd7plNVEVIoAhx/mJsht//nv4/7
|
||||
|
||||
--------------123456789@tutanota
|
||||
Content-Type: image/png;
|
||||
name==?UTF-8?B?Mi5wbmc=?=
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment;
|
||||
filename=img2.png
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAR+HpUWHRSYXcgcHJvZmlsZSB0eXBlIG
|
||||
V4aWYAAHjarZprdhs5DoX/cxWzBD4Bcjl8njM7mOXPB5bsOI49SU+3nViKLFWxgIv7YMXt//z7uH/x
|
||||
|
||||
--------------123456789@tutanota--
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
||||
"""
|
||||
{
|
||||
"from": "Bridge Test <bridgetest@pm.test>",
|
||||
"date": "01 Jan 80 00:00 +0000",
|
||||
"to": "Internal Bridge <bridgetest@example.com>",
|
||||
"subject": "Message with invalid boundary",
|
||||
"content": {
|
||||
"content-type": "multipart/mixed",
|
||||
"sections":[
|
||||
{
|
||||
"content-type": "multipart/related",
|
||||
"sections": [
|
||||
{
|
||||
"content-type": "text/html",
|
||||
"transfer-encoding": "base64",
|
||||
"body-is": "PGRpdiBjbGFzcz0iIj4KPHAgY2xhc3M9IiI+PGEgbmFtZT0iX0hsazE5MDA1NjM2IiByZWw9Im5v\r\nb3BlbmVyIG5vcmVmZXJyZXIiIHRhcmdldD0iX2JsYW5rIj48c3BhbiBzdHlsZT0ibXNvLWZhcmVh\r\nc3Qt"
|
||||
},
|
||||
{
|
||||
"content-type": "image/png",
|
||||
"transfer-encoding": "base64",
|
||||
"content-disposition": "attachment",
|
||||
"content-disposition-filename": "image1.png",
|
||||
"body-is": "iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAPq3pUWHRSYXcgcHJvZmlsZSB0eXBl\r\nIGV4aWYAAHjarZlrliOpkoT/s4pZAuCAw3J4njM7mOXP54SUlZmV1bd7plNVEVIoAhx/mJsht//n\r\nv4/7"
|
||||
},
|
||||
{
|
||||
"content-type": "image/png",
|
||||
"transfer-encoding": "base64",
|
||||
"content-disposition": "attachment",
|
||||
"content-disposition-filename": "img2.png",
|
||||
"body-is": "iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAR+HpUWHRSYXcgcHJvZmlsZSB0eXBl\r\nIGV4aWYAAHjarZprdhs5DoX/cxWzBD4Bcjl8njM7mOXPB5bsOI49SU+3nViKLFWxgIv7YMXt//z7\r\nuH/x"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
38
tests/features/observability/all_metrics.feature
Normal file
38
tests/features/observability/all_metrics.feature
Normal file
@ -0,0 +1,38 @@
|
||||
Feature: Bridge send remote notification observability metrics
|
||||
Background:
|
||||
Given there exists an account with username "[user:user1]" and password "password"
|
||||
Then it succeeds
|
||||
When bridge starts
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Test all possible heartbeat metrics
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And the user with username "[user:user1]" sends all possible observability heartbeat metrics
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Test all possible user discrimination metrics
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And the user with username "[user:user1]" sends all possible user distinction metrics
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Test all possible sync message event failure observability metrics
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And the user with username "[user:user1]" sends all possible sync message event failure observability metrics
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Test all possible event loop message events observability metrics
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And the user with username "[user:user1]" sends all possible event loop message events observability metrics
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Test all possible sync message building failure observability metrics
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And the user with username "[user:user1]" sends all possible sync message building failure observability metrics
|
||||
Then it succeeds
|
||||
|
||||
Scenario: Test all possible sync message building success observability metrics
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And the user with username "[user:user1]" sends all possible sync message building success observability metrics
|
||||
Then it succeeds
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
111
tests/observability_test.go
Normal file
111
tests/observability_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2024 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 tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||
)
|
||||
|
||||
// userHeartbeatPermutationsObservability - corresponds to bridge_generic_user_heartbeat_total_v1.schema.json.
|
||||
func (s *scenario) userHeartbeatPermutationsObservability(username string) error {
|
||||
metrics := observability.GenerateAllHeartbeatMetricPermutations()
|
||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||
batch := proton.ObservabilityBatch{Metrics: metrics}
|
||||
return c.SendObservabilityBatch(ctx, batch)
|
||||
})
|
||||
}
|
||||
|
||||
// userDistinctionMetricsPermutationsObservability - corresponds to:
|
||||
// bridge_sync_errors_users_total_v1.schema.json
|
||||
// bridge_event_loop_events_errors_users_total_v1.schema.json.
|
||||
func (s *scenario) userDistinctionMetricsPermutationsObservability(username string) error {
|
||||
batch := proton.ObservabilityBatch{
|
||||
Metrics: observability.GenerateAllUsedDistinctionMetricPermutations()}
|
||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||
err := c.SendObservabilityBatch(ctx, batch)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// syncFailureMessageEventsObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
|
||||
func (s *scenario) syncFailureMessageEventsObservability(username string) error {
|
||||
batch := proton.ObservabilityBatch{
|
||||
Metrics: []proton.ObservabilityMetric{
|
||||
syncmsgevents.GenerateSyncFailureCreateMessageEventMetric(),
|
||||
syncmsgevents.GenerateSyncFailureDeleteMessageEventMetric(),
|
||||
},
|
||||
}
|
||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||
err := c.SendObservabilityBatch(ctx, batch)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// eventLoopFailureMessageEventsObservability - corresponds to bridge_event_loop_message_event_failures_total_v1.schema.json.
|
||||
func (s *scenario) eventLoopFailureMessageEventsObservability(username string) error {
|
||||
batch := proton.ObservabilityBatch{
|
||||
Metrics: []proton.ObservabilityMetric{
|
||||
evtloopmsgevents.GenerateMessageEventFailedToBuildDraft(),
|
||||
evtloopmsgevents.GenerateMessageEventFailedToBuildMessage(),
|
||||
evtloopmsgevents.GenerateMessageEventFailureCreateMessageMetric(),
|
||||
evtloopmsgevents.GenerateMessageEventFailureDeleteMessageMetric(),
|
||||
evtloopmsgevents.GenerateMessageEventFailureUpdateMetric(),
|
||||
evtloopmsgevents.GenerateMessageEventUpdateChannelDoesNotExist(),
|
||||
},
|
||||
}
|
||||
|
||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||
err := c.SendObservabilityBatch(ctx, batch)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// syncFailureMessageBuiltObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
|
||||
func (s *scenario) syncFailureMessageBuiltObservability(username string) error {
|
||||
batch := proton.ObservabilityBatch{
|
||||
Metrics: []proton.ObservabilityMetric{
|
||||
observabilitymetrics.GenerateNoUnlockedKeyringMetric(),
|
||||
observabilitymetrics.GenerateFailedToBuildMetric(),
|
||||
},
|
||||
}
|
||||
|
||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||
err := c.SendObservabilityBatch(ctx, batch)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// syncSuccessMessageBuiltObservability - corresponds to bridge_sync_message_build_success_total_v1.schema.json.
|
||||
func (s *scenario) syncSuccessMessageBuiltObservability(username string) error {
|
||||
batch := proton.ObservabilityBatch{
|
||||
Metrics: []proton.ObservabilityMetric{
|
||||
observabilitymetrics.GenerateMessageBuiltSuccessMetric(),
|
||||
},
|
||||
}
|
||||
|
||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||
err := c.SendObservabilityBatch(ctx, batch)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@ -218,6 +218,12 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
||||
ctx.Step(`^the contact "([^"]*)" of user "([^"]*)" has public key from file "([^"]*)"$`, s.contactOfUserHasPubKeyFromFile)
|
||||
|
||||
// ==== OBSERVABILITY METRICS ====
|
||||
ctx.Step(`^the user with username "([^"]*)" sends the following remote notification observability metric "([^"]*)"`,
|
||||
ctx.Step(`^the user with username "([^"]*)" sends the following remote notification observability metric "([^"]*)"$`,
|
||||
s.userRemoteNotificationMetricTest)
|
||||
ctx.Step(`^the user with username "([^"]*)" sends all possible observability heartbeat metrics$`, s.userHeartbeatPermutationsObservability)
|
||||
ctx.Step(`^the user with username "([^"]*)" sends all possible user distinction metrics$`, s.userDistinctionMetricsPermutationsObservability)
|
||||
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message event failure observability metrics$`, s.syncFailureMessageEventsObservability)
|
||||
ctx.Step(`^the user with username "([^"]*)" sends all possible event loop message events observability metrics$`, s.eventLoopFailureMessageEventsObservability)
|
||||
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building failure observability metrics$`, s.syncFailureMessageBuiltObservability)
|
||||
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building success observability metrics$`, s.syncSuccessMessageBuiltObservability)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user