mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
feat(GODT-2552): Add functional test.
This commit is contained in:
committed by
Romain Le Jeune
parent
67b5e7f96a
commit
d88bee68c6
@ -268,6 +268,8 @@ func run(c *cli.Context) error {
|
||||
logrus.Warn("The vault is corrupt and has been wiped")
|
||||
b.PushError(bridge.ErrVaultCorrupt)
|
||||
}
|
||||
// Start telemetry heartbeat process
|
||||
b.StartHeartbeat(b)
|
||||
|
||||
// Run the frontend.
|
||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||
|
||||
@ -44,7 +44,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@ -307,7 +306,6 @@ func newBridge(
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||
bridge.heartbeat = telemetry.NewHeartbeat(bridge, 1143, 1025, gluonCacheDir, keychain.DefaultHelper)
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
@ -429,8 +427,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
})
|
||||
})
|
||||
|
||||
// init telemetry
|
||||
bridge.initHeartbeat()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -76,7 +77,9 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
|
||||
return bridge.vault.SetLastHeartbeatSent(timestamp)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) initHeartbeat() {
|
||||
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
|
||||
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
|
||||
|
||||
safe.RLock(func() {
|
||||
var splitMode = false
|
||||
for _, user := range bridge.users {
|
||||
@ -102,8 +105,10 @@ func (bridge *Bridge) initHeartbeat() {
|
||||
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
|
||||
if val, err := bridge.GetKeychainApp(); err != nil {
|
||||
bridge.heartbeat.SetKeyChainPref(val)
|
||||
} else {
|
||||
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
|
||||
}
|
||||
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
|
||||
|
||||
bridge.heartbeat.StartSending()
|
||||
bridge.heartbeat.TrySending()
|
||||
}
|
||||
|
||||
@ -571,6 +571,9 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
}, bridge.usersLock)
|
||||
|
||||
// As we need at least one user to send heartbeat, try to send it.
|
||||
bridge.heartbeat.TrySending()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -148,7 +148,7 @@ func (heartbeat *Heartbeat) SetPrevVersion(val string) {
|
||||
heartbeat.metrics.Dimensions.PrevVersion = val
|
||||
}
|
||||
|
||||
func (heartbeat *Heartbeat) StartSending() {
|
||||
func (heartbeat *Heartbeat) TrySending() {
|
||||
if heartbeat.manager.IsTelemetryAvailable() {
|
||||
lastSent := heartbeat.manager.GetLastHeartbeatSent()
|
||||
now := time.Now()
|
||||
|
||||
@ -57,7 +57,7 @@ func TestHeartbeat_default_heartbeat(t *testing.T) {
|
||||
mock.EXPECT().SendHeartbeat(&data).Return(true)
|
||||
mock.EXPECT().SetLastHeartbeatSent(gomock.Any()).Return(nil)
|
||||
|
||||
hb.StartSending()
|
||||
hb.TrySending()
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ func TestHeartbeat_already_sent_heartbeat(t *testing.T) {
|
||||
mock.EXPECT().IsTelemetryAvailable().Return(true)
|
||||
mock.EXPECT().GetLastHeartbeatSent().Return(time.Now().Truncate(24 * time.Hour))
|
||||
|
||||
hb.StartSending()
|
||||
hb.TrySending()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -231,6 +231,12 @@ func TestFeatures(testingT *testing.T) {
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends RSET$`, s.smtpClientSendsReset)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends the following message from "([^"]*)" to "([^"]*)":$`, s.smtpClientSendsTheFollowingMessageFromTo)
|
||||
ctx.Step(`^SMTP client "([^"]*)" logs out$`, s.smtpClientLogsOut)
|
||||
|
||||
// ==== TELEMETRY ====
|
||||
ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat)
|
||||
ctx.Step(`^bridge needs to send heartbeat$`, s.bridgeNeedsToSendHeartbeat)
|
||||
ctx.Step(`^bridge do not need to send heartbeat$`, s.bridgeDoNotNeedToSendHeartbeat)
|
||||
ctx.Step(`^heartbeat is not whitelisted$`, s.heartbeatIsNotwhitelisted)
|
||||
},
|
||||
Options: &godog.Options{
|
||||
Format: "pretty",
|
||||
|
||||
@ -174,6 +174,8 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
|
||||
}
|
||||
|
||||
t.bridge = bridge
|
||||
t.heartbeat.setBridge(bridge)
|
||||
bridge.StartHeartbeat(t.heartbeat)
|
||||
|
||||
return t.events.collectFrom(eventCh), nil
|
||||
}
|
||||
|
||||
72
tests/ctx_heartbeat_test.go
Normal file
72
tests/ctx_heartbeat_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type heartbeatRecorder struct {
|
||||
heartbeat telemetry.HeartbeatData
|
||||
bridge *bridge.Bridge
|
||||
reject bool
|
||||
assert *assert.Assertions
|
||||
}
|
||||
|
||||
func newHeartbeatRecorder(tb testing.TB) *heartbeatRecorder {
|
||||
return &heartbeatRecorder{
|
||||
heartbeat: telemetry.HeartbeatData{},
|
||||
bridge: nil,
|
||||
reject: false,
|
||||
assert: assert.New(tb),
|
||||
}
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) setBridge(bridge *bridge.Bridge) {
|
||||
hb.bridge = bridge
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) GetLastHeartbeatSent() time.Time {
|
||||
if hb.bridge == nil {
|
||||
return time.Now()
|
||||
}
|
||||
return hb.bridge.GetLastHeartbeatSent()
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) IsTelemetryAvailable() bool {
|
||||
if hb.bridge == nil {
|
||||
return false
|
||||
}
|
||||
return hb.bridge.IsTelemetryAvailable()
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) SendHeartbeat(metrics *telemetry.HeartbeatData) bool {
|
||||
if hb.bridge == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(hb.bridge.GetUserIDs()) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if hb.reject {
|
||||
return false
|
||||
}
|
||||
hb.heartbeat = *metrics
|
||||
return true
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) SetLastHeartbeatSent(timestamp time.Time) error {
|
||||
if hb.bridge == nil {
|
||||
return errors.New("no bridge initialized")
|
||||
}
|
||||
return hb.bridge.SetLastHeartbeatSent(timestamp)
|
||||
}
|
||||
|
||||
func (hb *heartbeatRecorder) rejectSend() {
|
||||
hb.reject = true
|
||||
}
|
||||
@ -131,6 +131,7 @@ type testCtx struct {
|
||||
mocks *bridge.Mocks
|
||||
events *eventCollector
|
||||
reporter *reportRecorder
|
||||
heartbeat *heartbeatRecorder
|
||||
|
||||
// bridge holds the bridge app under test.
|
||||
bridge *bridge.Bridge
|
||||
@ -189,6 +190,7 @@ func newTestCtx(tb testing.TB) *testCtx {
|
||||
mocks: bridge.NewMocks(tb, defaultVersion, defaultVersion),
|
||||
events: newEventCollector(),
|
||||
reporter: newReportRecorder(tb),
|
||||
heartbeat: newHeartbeatRecorder(tb),
|
||||
|
||||
userByID: make(map[string]*testUser),
|
||||
userUUIDByName: make(map[string]string),
|
||||
|
||||
47
tests/features/bridge/heartbeat.feature
Normal file
47
tests/features/bridge/heartbeat.feature
Normal file
@ -0,0 +1,47 @@
|
||||
Feature: Send Telemetry Heartbeat
|
||||
Background:
|
||||
Given there exists an account with username "[user:user1]" and password "password"
|
||||
And bridge starts
|
||||
|
||||
|
||||
Scenario: Send at first start - one user
|
||||
Then bridge telemetry feature is enabled
|
||||
And bridge needs to send heartbeat
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And user "[user:user1]" finishes syncing
|
||||
Then bridge eventually sends the following heartbeat:
|
||||
"""
|
||||
{
|
||||
"MeasurementGroup": "bridge.any.usage",
|
||||
"Event": "bridge_heartbeat",
|
||||
"Values": {
|
||||
"nb_account": 1
|
||||
},
|
||||
"Dimensions": {
|
||||
"auto_update": "on",
|
||||
"auto_start": "on",
|
||||
"beta": "off",
|
||||
"doh": "off",
|
||||
"split_mode": "off",
|
||||
"show_all_mail": "on",
|
||||
"imap_connection_mode": "starttls",
|
||||
"smtp_connection_mode": "starttls",
|
||||
"imap_port": "default",
|
||||
"smtp_port": "default",
|
||||
"cache_location": "default",
|
||||
"keychain_pref": "default",
|
||||
"prev_version": "0.0.0",
|
||||
"rollout": 42
|
||||
}
|
||||
}
|
||||
"""
|
||||
And bridge do not need to send heartbeat
|
||||
|
||||
Scenario: GroupMeasurement rejected by API
|
||||
Given heartbeat is not whitelisted
|
||||
Then bridge telemetry feature is enabled
|
||||
And bridge needs to send heartbeat
|
||||
When the user logs in with username "[user:user1]" and password "password"
|
||||
And user "[user:user1]" finishes syncing
|
||||
Then bridge needs to send heartbeat
|
||||
|
||||
70
tests/heartbeat_test.go
Normal file
70
tests/heartbeat_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *scenario) bridgeEventuallySendsTheFollowingHeartbeat(text *godog.DocString) error {
|
||||
return eventually(func() error {
|
||||
err := s.bridgeSendsTheFollowingHeartbeat(text)
|
||||
logrus.WithError(err).Trace("Matching eventually")
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *scenario) bridgeSendsTheFollowingHeartbeat(text *godog.DocString) error {
|
||||
var wantHeartbeat telemetry.HeartbeatData
|
||||
err := json.Unmarshal([]byte(text.Content), &wantHeartbeat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return matchHeartbeat(s.t.heartbeat.heartbeat, wantHeartbeat)
|
||||
}
|
||||
|
||||
func (s *scenario) bridgeNeedsToSendHeartbeat() error {
|
||||
last := s.t.heartbeat.GetLastHeartbeatSent()
|
||||
if !isAnotherDay(last, time.Now()) {
|
||||
return fmt.Errorf("heartbeat already sent at %s", last)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) bridgeDoNotNeedToSendHeartbeat() error {
|
||||
last := s.t.heartbeat.GetLastHeartbeatSent()
|
||||
if isAnotherDay(last, time.Now()) {
|
||||
return fmt.Errorf("heartbeat needs to be sent - last %s", last)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) heartbeatIsNotwhitelisted() error {
|
||||
s.t.heartbeat.rejectSend()
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchHeartbeat(have, want telemetry.HeartbeatData) error {
|
||||
if have == (telemetry.HeartbeatData{}) {
|
||||
return errors.New("no heartbeat send (yet)")
|
||||
}
|
||||
|
||||
// Ignore rollout number
|
||||
want.Dimensions.Rollout = have.Dimensions.Rollout
|
||||
|
||||
if have != want {
|
||||
return fmt.Errorf("missing heartbeat: have %#v, want %#v", have, want)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAnotherDay(last, now time.Time) bool {
|
||||
return now.Year() > last.Year() || (now.Year() == last.Year() && now.YearDay() > last.YearDay())
|
||||
}
|
||||
Reference in New Issue
Block a user