diff --git a/tests/bdd_test.go b/tests/bdd_test.go index c333c693..f28877ad 100644 --- a/tests/bdd_test.go +++ b/tests/bdd_test.go @@ -195,6 +195,7 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with address "([^"]*)"$`, s.imapClientCannotAuthenticateWithAddress) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect username$`, s.imapClientCannotAuthenticateWithIncorrectUsername) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect password$`, s.imapClientCannotAuthenticateWithIncorrectPassword) + ctx.Step(`^IMAP client "([^"]*)" closes$`, s.imapClientCloses) ctx.Step(`^IMAP client "([^"]*)" announces its ID with name "([^"]*)" and version "([^"]*)"$`, s.imapClientAnnouncesItsIDWithNameAndVersion) ctx.Step(`^IMAP client "([^"]*)" creates "([^"]*)"$`, s.imapClientCreatesMailbox) ctx.Step(`^IMAP client "([^"]*)" deletes "([^"]*)"$`, s.imapClientDeletesMailbox) @@ -256,7 +257,11 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^heartbeat is not whitelisted`, s.heartbeatIsNotwhitelisted) ctx.Step(`^config status file exist for user "([^"]*)"$`, s.configStatusFileExistForUser) ctx.Step(`^config status is pending for user "([^"]*)"$`, s.configStatusIsPendingForUser) + ctx.Step(`^config status is pending with failure for user "([^"]*)"$`, s.configStatusIsPendingWithFailureForUser) ctx.Step(`^config status succeed for user "([^"]*)"$`, s.configStatusSucceedForUser) + ctx.Step(`^config status event "([^"]*)" is eventually send (\d+) time`, s.configStatusEventIsEventuallySendXTime) + ctx.Step(`^config status event "([^"]*)" is not send more than (\d+) time`, s.configStatusEventIsNotSendMoreThanXTime) + ctx.Step(`^force config status progress to be sent for user"([^"]*)"$`, s.forceConfigStatusProgressToBeSentForUser) }, Options: &godog.Options{ Format: "pretty", diff --git a/tests/config_status_test.go b/tests/config_status_test.go index ec541a9b..254c2a1f 100644 --- a/tests/config_status_test.go +++ b/tests/config_status_test.go @@ -22,8 +22,12 @@ import ( "fmt" "os" "path/filepath" + "time" + "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/go-proton-api/server" "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" + "github.com/sirupsen/logrus" ) func (s *scenario) configStatusFileExistForUser(username string) error { @@ -38,14 +42,11 @@ func (s *scenario) configStatusFileExistForUser(username string) error { } func (s *scenario) configStatusIsPendingForUser(username string) error { - configStatusFile, err := getConfigStatusFile(s.t, username) - if err != nil { - return err - } - data, err := loadConfigStatusFile(configStatusFile) + data, err := loadConfigStatusFile(s.t, username) if err != nil { return err } + if data.DataV1.PendingSince.IsZero() { return fmt.Errorf("expected ConfigStatus pending but got success instead") } @@ -53,15 +54,28 @@ func (s *scenario) configStatusIsPendingForUser(username string) error { return nil } +func (s *scenario) configStatusIsPendingWithFailureForUser(username string) error { + data, err := loadConfigStatusFile(s.t, username) + if err != nil { + return err + } + + if data.DataV1.PendingSince.IsZero() { + return fmt.Errorf("expected ConfigStatus pending but got success instead") + } + if data.DataV1.FailureDetails == "" { + return fmt.Errorf("expected ConfigStatus pending with failure but got no failure instead") + } + + return nil +} + func (s *scenario) configStatusSucceedForUser(username string) error { - configStatusFile, err := getConfigStatusFile(s.t, username) - if err != nil { - return err - } - data, err := loadConfigStatusFile(configStatusFile) + data, err := loadConfigStatusFile(s.t, username) if err != nil { return err } + if !data.DataV1.PendingSince.IsZero() { return fmt.Errorf("expected ConfigStatus success but got pending since %s", data.DataV1.PendingSince) } @@ -69,6 +83,58 @@ func (s *scenario) configStatusSucceedForUser(username string) error { return nil } +func (s *scenario) configStatusEventIsEventuallySendXTime(event string, number int) error { + return eventually(func() error { + err := s.checkEventSentForUser(event, number) + logrus.WithError(err).Trace("Matching eventually") + return err + }) +} + +func (s *scenario) configStatusEventIsNotSendMoreThanXTime(event string, number int) error { + if err := eventually(func() error { + err := s.checkEventSentForUser(event, number+1) + logrus.WithError(err).Trace("Matching eventually") + return err + }); err == nil { + return fmt.Errorf("expected %s to be sent %d but catch %d", event, number, number+1) + } + return nil +} + +func (s *scenario) forceConfigStatusProgressToBeSentForUser(username string) error { + configStatusFile, err := getConfigStatusFile(s.t, username) + if err != nil { + return err + } + + data, err := loadConfigStatusFile(s.t, username) + if err != nil { + return err + } + data.DataV1.PendingSince = time.Now().AddDate(0, 0, -2) + data.DataV1.LastProgress = time.Now().AddDate(0, 0, -1) + + f, err := os.Create(configStatusFile) + if err != nil { + return err + } + defer f.Close() + + return json.NewEncoder(f).Encode(data) +} + +func (s *scenario) checkEventSentForUser(event string, number int) error { + calls, err := getLastTelemetryEventSent(s.t, event) + if err != nil { + return err + } + if len(calls) != number { + return fmt.Errorf("expected %s to be sent %d but catch %d", event, number, len(calls)) + } + return nil +} + func getConfigStatusFile(t *testCtx, username string) (string, error) { userID := t.getUserByName(username).getUserID() statsDir, err := t.locator.ProvideStatsPath() @@ -78,13 +144,19 @@ func getConfigStatusFile(t *testCtx, username string) (string, error) { return filepath.Join(statsDir, userID+".json"), nil } -func loadConfigStatusFile(filepath string) (configstatus.ConfigurationStatusData, error) { +func loadConfigStatusFile(t *testCtx, username string) (configstatus.ConfigurationStatusData, error) { data := configstatus.ConfigurationStatusData{} - if _, err := os.Stat(filepath); err != nil { + + configStatusFile, err := getConfigStatusFile(t, username) + if err != nil { return data, err } - f, err := os.Open(filepath) + if _, err := os.Stat(configStatusFile); err != nil { + return data, err + } + + f, err := os.Open(configStatusFile) if err != nil { return data, err } @@ -93,3 +165,23 @@ func loadConfigStatusFile(filepath string) (configstatus.ConfigurationStatusData err = json.NewDecoder(f).Decode(&data) return data, err } + +func getLastTelemetryEventSent(t *testCtx, event string) ([]server.Call, error) { + var matches []server.Call + + calls, err := t.getAllCalls("POST", "/data/v1/stats") + if err != nil { + return matches, err + } + + for _, call := range calls { + var req proton.SendStatsReq + if err := json.Unmarshal(call.RequestBody, &req); err != nil { + continue + } + if req.Event == event { + matches = append(matches, call) + } + } + return matches, err +} diff --git a/tests/ctx_test.go b/tests/ctx_test.go index f667c5f1..7020ebd9 100644 --- a/tests/ctx_test.go +++ b/tests/ctx_test.go @@ -360,21 +360,32 @@ func (t *testCtx) getDraftID(username string, draftIndex int) (string, error) { } func (t *testCtx) getLastCall(method, pathExp string) (server.Call, error) { + matches, err := t.getAllCalls(method, pathExp) + if err != nil { + return server.Call{}, err + } + if len(matches) > 0 { + return matches[len(matches)-1], nil + } + return server.Call{}, fmt.Errorf("no call with method %q and path %q was made", method, pathExp) +} + +func (t *testCtx) getAllCalls(method, pathExp string) ([]server.Call, error) { t.callsLock.RLock() defer t.callsLock.RUnlock() root, err := url.Parse(t.api.GetHostURL()) if err != nil { - return server.Call{}, err + return []server.Call{}, err } if matches := xslices.Filter(xslices.Join(t.calls...), func(call server.Call) bool { return call.Method == method && regexp.MustCompile("^"+pathExp+"$").MatchString(strings.TrimPrefix(call.URL.Path, root.Path)) }); len(matches) > 0 { - return matches[len(matches)-1], nil + return matches, nil } - return server.Call{}, fmt.Errorf("no call with method %q and path %q was made", method, pathExp) + return []server.Call{}, fmt.Errorf("no call with method %q and path %q was made", method, pathExp) } func (t *testCtx) getLastCallExcludingHTTPOverride(method, pathExp string) (server.Call, error) { diff --git a/tests/features/bridge/config_status.feature b/tests/features/bridge/config_status.feature index 364f5074..064b3d8b 100644 --- a/tests/features/bridge/config_status.feature +++ b/tests/features/bridge/config_status.feature @@ -5,12 +5,14 @@ Feature: Configuration Status Telemetry When bridge starts Then it succeeds + Scenario: Init config status on user addition Then bridge telemetry feature is enabled When the user logs in with username "[user:user]" and password "password" Then config status file exist for user "[user:user]" And config status is pending for user "[user:user]" + Scenario: Config Status Success on IMAP Then bridge telemetry feature is enabled When the user logs in with username "[user:user]" and password "password" @@ -18,6 +20,8 @@ Feature: Configuration Status Telemetry And config status is pending for user "[user:user]" When user "[user:user]" connects and authenticates IMAP client "1" Then config status succeed for user "[user:user]" + And config status event "bridge_config_success" is eventually send 1 time + Scenario: Config Status Success on SMTP Then bridge telemetry feature is enabled @@ -25,4 +29,52 @@ Feature: Configuration Status Telemetry Then config status file exist for user "[user:user]" And config status is pending for user "[user:user]" When user "[user:user]" connects and authenticates SMTP client "1" - Then config status succeed for user "[user:user]" \ No newline at end of file + Then config status succeed for user "[user:user]" + And config status event "bridge_config_success" is eventually send 1 time + + + @long-black + Scenario: Config Status Success send only once + Then bridge telemetry feature is enabled + When the user logs in with username "[user:user]" and password "password" + Then config status file exist for user "[user:user]" + And config status is pending for user "[user:user]" + When user "[user:user]" connects and authenticates IMAP client "1" + Then config status succeed for user "[user:user]" + And config status event "bridge_config_success" is eventually send 1 time + When user "[user:user]" connects and authenticates IMAP client "2" + Then config status event "bridge_config_success" is not send more than 1 time + + + Scenario: Config Status Abort + Then bridge telemetry feature is enabled + When the user logs in with username "[user:user]" and password "password" + And user "[user:user]" finishes syncing + Then config status file exist for user "[user:user]" + And config status is pending for user "[user:user]" + When user "[user:user]" is deleted + Then config status event "bridge_config_abort" is eventually send 1 time + + + Scenario: Config Status Recovery from deauth + Then bridge telemetry feature is enabled + When the user logs in with username "[user:user]" and password "password" + And user "[user:user]" connects and authenticates IMAP client "1" + Then config status succeed for user "[user:user]" + When the auth of user "[user:user]" is revoked + Then bridge sends a deauth event for user "[user:user]" + Then config status is pending with failure for user "[user:user]" + When the user logs in with username "[user:user]" and password "password" + And user "[user:user]" connects and authenticates IMAP client "1" + Then config status succeed for user "[user:user]" + And config status event "bridge_config_recovery" is eventually send 1 time + + + Scenario: Config Status Progress + Then bridge telemetry feature is enabled + When the user logs in with username "[user:user]" and password "password" + And config status is pending for user "[user:user]" + And bridge stops + And force config status progress to be sent for user"[user:user]" + And bridge starts + Then config status event "bridge_config_progress" is eventually send 1 time \ No newline at end of file diff --git a/tests/imap_test.go b/tests/imap_test.go index b8d6ceb9..b11e7813 100644 --- a/tests/imap_test.go +++ b/tests/imap_test.go @@ -125,6 +125,15 @@ func (s *scenario) imapClientCannotAuthenticateWithIncorrectPassword(clientID st return nil } +func (s *scenario) imapClientCloses(clientID string) error { + _, client := s.t.getIMAPClient(clientID) + if err := client.Logout(); err != nil { + return err + } + delete(s.t.imapClients, clientID) + return nil +} + func (s *scenario) imapClientAnnouncesItsIDWithNameAndVersion(clientID, name, version string) error { _, client := s.t.getIMAPClient(clientID)