feat(GODT-2552): Add functional test.

This commit is contained in:
Romain LE JEUNE
2023-04-20 21:14:11 +02:00
committed by Romain Le Jeune
parent 67b5e7f96a
commit d88bee68c6
12 changed files with 232 additions and 27 deletions

View File

@ -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",

View File

@ -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
}

View 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
}

View File

@ -122,15 +122,16 @@ func newTestAddr(addrID, email string) *testAddr {
type testCtx struct {
// These are the objects supporting the test.
dir string
api API
netCtl *proton.NetCtl
locator *locations.Locations
storeKey []byte
version *semver.Version
mocks *bridge.Mocks
events *eventCollector
reporter *reportRecorder
dir string
api API
netCtl *proton.NetCtl
locator *locations.Locations
storeKey []byte
version *semver.Version
mocks *bridge.Mocks
events *eventCollector
reporter *reportRecorder
heartbeat *heartbeatRecorder
// bridge holds the bridge app under test.
bridge *bridge.Bridge
@ -180,15 +181,16 @@ func newTestCtx(tb testing.TB) *testCtx {
dir := tb.TempDir()
t := &testCtx{
dir: dir,
api: newTestAPI(),
netCtl: proton.NewNetCtl(),
locator: locations.New(bridge.NewTestLocationsProvider(dir), "config-name"),
storeKey: []byte("super-secret-store-key"),
version: defaultVersion,
mocks: bridge.NewMocks(tb, defaultVersion, defaultVersion),
events: newEventCollector(),
reporter: newReportRecorder(tb),
dir: dir,
api: newTestAPI(),
netCtl: proton.NewNetCtl(),
locator: locations.New(bridge.NewTestLocationsProvider(dir), "config-name"),
storeKey: []byte("super-secret-store-key"),
version: defaultVersion,
mocks: bridge.NewMocks(tb, defaultVersion, defaultVersion),
events: newEventCollector(),
reporter: newReportRecorder(tb),
heartbeat: newHeartbeatRecorder(tb),
userByID: make(map[string]*testUser),
userUUIDByName: make(map[string]string),

View 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
View 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())
}