diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go index 579e8d1c..dceb618f 100644 --- a/internal/bridge/settings.go +++ b/internal/bridge/settings.go @@ -248,6 +248,10 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error { return nil } +func (bridge *Bridge) GetCurrentVersion() *semver.Version { + return bridge.curVersion +} + func (bridge *Bridge) GetLastVersion() *semver.Version { return bridge.vault.GetLastVersion() } diff --git a/internal/frontend/grpc/config.go b/internal/frontend/grpc/config.go index a6c79675..a16a5f4f 100644 --- a/internal/frontend/grpc/config.go +++ b/internal/frontend/grpc/config.go @@ -22,15 +22,15 @@ import ( "os" ) -// config is a structure containing the service configuration data that are exchanged by the gRPC server and client. -type config struct { +// Config is a structure containing the service configuration data that are exchanged by the gRPC server and client. +type Config struct { Port int `json:"port"` Cert string `json:"cert"` Token string `json:"token"` } // save saves a gRPC service configuration to file. -func (s *config) save(path string) error { +func (s *Config) save(path string) error { // Another process may be waiting for this file to be available. In order to prevent this process to open // the file while we are writing in it, we write it with a temp file name, then rename it. tempPath := path + "_" @@ -41,7 +41,7 @@ func (s *config) save(path string) error { return os.Rename(tempPath, path) } -func (s *config) _save(path string) error { +func (s *Config) _save(path string) error { f, err := os.Create(path) //nolint:errcheck,gosec if err != nil { return err @@ -53,7 +53,7 @@ func (s *config) _save(path string) error { } // load loads a gRPC service configuration from file. -func (s *config) load(path string) error { +func (s *Config) load(path string) error { f, err := os.Open(path) //nolint:errcheck,gosec if err != nil { return err diff --git a/internal/frontend/grpc/config_test.go b/internal/frontend/grpc/config_test.go index f54e6f88..4ebfb96e 100644 --- a/internal/frontend/grpc/config_test.go +++ b/internal/frontend/grpc/config_test.go @@ -32,7 +32,7 @@ const ( ) func TestConfig(t *testing.T) { - conf1 := config{ + conf1 := Config{ Port: dummyPort, Cert: dummyCert, Token: dummyToken, @@ -43,7 +43,7 @@ func TestConfig(t *testing.T) { tempFilePath := filepath.Join(tempDir, tempFileName) require.NoError(t, conf1.save(tempFilePath)) - conf2 := config{} + conf2 := Config{} require.NoError(t, conf2.load(tempFilePath)) require.Equal(t, conf1, conf2) diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index 7773c1a0..ee63a542 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -31,12 +31,9 @@ import ( "github.com/ProtonMail/proton-bridge/v2/internal/bridge" "github.com/ProtonMail/proton-bridge/v2/internal/certs" - "github.com/ProtonMail/proton-bridge/v2/internal/crash" "github.com/ProtonMail/proton-bridge/v2/internal/events" - "github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/ProtonMail/proton-bridge/v2/internal/safe" "github.com/ProtonMail/proton-bridge/v2/internal/updater" - "github.com/ProtonMail/proton-bridge/v2/pkg/restarter" "github.com/google/uuid" "github.com/sirupsen/logrus" "gitlab.protontech.ch/go/liteapi" @@ -64,8 +61,8 @@ type Service struct { // nolint:structcheck eventQueue []*StreamEvent eventQueueMutex sync.Mutex - panicHandler *crash.Handler - restarter *restarter.Restarter + panicHandler CrashHandler + restarter Restarter bridge *bridge.Bridge eventCh <-chan events.Event @@ -91,9 +88,9 @@ type Service struct { // nolint:structcheck // // nolint:funlen func NewService( - panicHandler *crash.Handler, - restarter *restarter.Restarter, - locations *locations.Locations, + panicHandler CrashHandler, + restarter Restarter, + locations Locator, bridge *bridge.Bridge, eventCh <-chan events.Event, showOnStartup bool, @@ -394,13 +391,13 @@ func newTLSConfig() (*tls.Config, []byte, error) { }, certPEM, nil } -func saveGRPCServerConfigFile(locations *locations.Locations, listener net.Listener, token string, certPEM []byte) (string, error) { +func saveGRPCServerConfigFile(locations Locator, listener net.Listener, token string, certPEM []byte) (string, error) { address, ok := listener.Addr().(*net.TCPAddr) if !ok { return "", fmt.Errorf("could not retrieve gRPC service listener address") } - sc := config{ + sc := Config{ Port: address.Port, Cert: string(certPEM), Token: token, diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index e2486f20..a4d7f04a 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -48,7 +48,7 @@ func (s *Service) CheckTokens(ctx context.Context, clientConfigPath *wrapperspb. path := clientConfigPath.Value logEntry := s.log.WithField("path", path) - var clientConfig config + var clientConfig Config if err := clientConfig.load(path); err != nil { logEntry.WithError(err).Error("Could not read gRPC client config file") @@ -234,7 +234,7 @@ func (s *Service) TriggerReset(ctx context.Context, _ *emptypb.Empty) (*emptypb. func (s *Service) Version(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("Version") - return wrapperspb.String(constants.Version), nil + return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil } func (s *Service) LogsPath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { diff --git a/internal/frontend/grpc/types.go b/internal/frontend/grpc/types.go new file mode 100644 index 00000000..d9e894a3 --- /dev/null +++ b/internal/frontend/grpc/types.go @@ -0,0 +1,32 @@ +// Copyright (c) 2022 Proton AG +// +// This file is part of Proton Mail Bridge.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 . + +package grpc + +type CrashHandler interface { + HandlePanic() +} + +type Restarter interface { + Set(restart, crash bool) + AddFlags(flags ...string) + Override(exe string) +} + +type Locator interface { + ProvideSettingsPath() (string, error) +} diff --git a/tests/bdd_test.go b/tests/bdd_test.go index 2ef9afdf..7c96936d 100644 --- a/tests/bdd_test.go +++ b/tests/bdd_test.go @@ -132,6 +132,9 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^bridge sends an update not available event$`, s.bridgeSendsAnUpdateNotAvailableEvent) ctx.Step(`^bridge sends a forced update event$`, s.bridgeSendsAForcedUpdateEvent) + // ==== FRONTEND ==== + ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion) + // ==== USER ==== ctx.Step(`^the user logs in with username "([^"]*)" and password "([^"]*)"$`, s.userLogsInWithUsernameAndPassword) ctx.Step(`^user "([^"]*)" logs out$`, s.userLogsOut) diff --git a/tests/collector_test.go b/tests/collector_test.go new file mode 100644 index 00000000..2bbe842b --- /dev/null +++ b/tests/collector_test.go @@ -0,0 +1,117 @@ +// Copyright (c) 2022 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package tests + +import ( + "fmt" + "reflect" + "sync" + "time" + + "github.com/ProtonMail/gluon/queue" + "github.com/ProtonMail/proton-bridge/v2/internal/events" +) + +type eventCollector struct { + events map[reflect.Type]*queue.QueuedChannel[events.Event] + fwdCh []*queue.QueuedChannel[events.Event] + lock sync.Mutex + wg sync.WaitGroup +} + +func newEventCollector() *eventCollector { + return &eventCollector{ + events: make(map[reflect.Type]*queue.QueuedChannel[events.Event]), + } +} + +func (c *eventCollector) collectFrom(eventCh <-chan events.Event) <-chan events.Event { + c.lock.Lock() + defer c.lock.Unlock() + + fwdCh := queue.NewQueuedChannel[events.Event](0, 0) + + c.fwdCh = append(c.fwdCh, fwdCh) + + c.wg.Add(1) + + go func() { + defer fwdCh.CloseAndDiscardQueued() + defer c.wg.Done() + + for event := range eventCh { + c.push(event) + } + }() + + return fwdCh.GetChannel() +} + +func awaitType[T events.Event](c *eventCollector, ofType T, timeout time.Duration) (T, bool) { + if event := c.await(ofType, timeout); event == nil { + return *new(T), false //nolint:gocritic + } else if event, ok := event.(T); !ok { + panic(fmt.Errorf("unexpected event type %T", event)) + } else { + return event, true + } +} + +func (c *eventCollector) await(ofType events.Event, timeout time.Duration) events.Event { + select { + case event := <-c.getEventCh(ofType): + return event + + case <-time.After(timeout): + return nil + } +} + +func (c *eventCollector) push(event events.Event) { + c.lock.Lock() + defer c.lock.Unlock() + + if _, ok := c.events[reflect.TypeOf(event)]; !ok { + c.events[reflect.TypeOf(event)] = queue.NewQueuedChannel[events.Event](0, 0) + } + + c.events[reflect.TypeOf(event)].Enqueue(event) + + for _, eventCh := range c.fwdCh { + eventCh.Enqueue(event) + } +} + +func (c *eventCollector) getEventCh(ofType events.Event) <-chan events.Event { + c.lock.Lock() + defer c.lock.Unlock() + + if _, ok := c.events[reflect.TypeOf(ofType)]; !ok { + c.events[reflect.TypeOf(ofType)] = queue.NewQueuedChannel[events.Event](0, 0) + } + + return c.events[reflect.TypeOf(ofType)].GetChannel() +} + +func (c *eventCollector) close() { + c.wg.Wait() + + for _, eventCh := range c.events { + eventCh.CloseAndDiscardQueued() + } +} diff --git a/tests/ctx_bridge_test.go b/tests/ctx_bridge_test.go index 4ce5e2a5..b27cf105 100644 --- a/tests/ctx_bridge_test.go +++ b/tests/ctx_bridge_test.go @@ -20,64 +20,129 @@ package tests import ( "context" "crypto/tls" + "crypto/x509" + "encoding/json" "fmt" "net/http/cookiejar" "os" + "path/filepath" + "runtime" "time" - "github.com/sirupsen/logrus" - + "github.com/ProtonMail/gluon/queue" "github.com/ProtonMail/proton-bridge/v2/internal/bridge" + "github.com/ProtonMail/proton-bridge/v2/internal/constants" "github.com/ProtonMail/proton-bridge/v2/internal/cookies" "github.com/ProtonMail/proton-bridge/v2/internal/events" + frontend "github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc" "github.com/ProtonMail/proton-bridge/v2/internal/useragent" "github.com/ProtonMail/proton-bridge/v2/internal/vault" + "github.com/sirupsen/logrus" "gitlab.protontech.ch/go/liteapi" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/emptypb" ) func (t *testCtx) startBridge() error { + logrus.Info("Starting bridge") + + eventCh, err := t.initBridge() + if err != nil { + return fmt.Errorf("could not create bridge: %w", err) + } + + logrus.Info("Starting frontend service") + + if err := t.initFrontendService(eventCh); err != nil { + return fmt.Errorf("could not create frontend service: %w", err) + } + + logrus.Info("Starting frontend client") + + if err := t.initFrontendClient(); err != nil { + return fmt.Errorf("could not create frontend client: %w", err) + } + + t.events.await(events.AllUsersLoaded{}, 30*time.Second) + + return nil +} + +func (t *testCtx) stopBridge() error { + if err := t.closeFrontendService(context.Background()); err != nil { + return fmt.Errorf("could not close frontend: %w", err) + } + + if err := t.closeFrontendClient(); err != nil { + return fmt.Errorf("could not close frontend client: %w", err) + } + + if err := t.closeBridge(context.Background()); err != nil { + return fmt.Errorf("could not close bridge: %w", err) + } + + return nil +} + +func (t *testCtx) initBridge() (<-chan events.Event, error) { + if t.bridge != nil { + return nil, fmt.Errorf("bridge is already started") + } + // Bridge will enable the proxy by default at startup. t.mocks.ProxyCtl.EXPECT().AllowProxy() // Get the path to the vault. vaultDir, err := t.locator.ProvideSettingsPath() if err != nil { - return err + return nil, fmt.Errorf("could not get vault dir: %w", err) } // Get the default gluon path. gluonDir, err := t.locator.ProvideGluonPath() if err != nil { - return err + return nil, fmt.Errorf("could not get gluon dir: %w", err) } // Create the vault. vault, corrupt, err := vault.New(vaultDir, gluonDir, t.storeKey) if err != nil { - return err + return nil, fmt.Errorf("could not create vault: %w", err) } else if corrupt { - return fmt.Errorf("vault is corrupt") + return nil, fmt.Errorf("vault is corrupt") } // Create the underlying cookie jar. jar, err := cookiejar.New(nil) if err != nil { - return err + return nil, fmt.Errorf("could not create cookie jar: %w", err) } // Create the persisting cookie jar. persister, err := cookies.NewCookieJar(jar, vault) if err != nil { - return err + return nil, fmt.Errorf("could not create cookie persister: %w", err) } - var logIMAP bool + var ( + logIMAP bool + logSMTP bool + ) if len(os.Getenv("FEATURE_TEST_LOG_IMAP")) != 0 { - logrus.SetLevel(logrus.TraceLevel) logIMAP = true } + if len(os.Getenv("FEATURE_TEST_LOG_SMTP")) != 0 { + logSMTP = true + } + + if logIMAP || logSMTP { + logrus.SetLevel(logrus.TraceLevel) + } + // Create the bridge. bridge, eventCh, err := bridge.New( // App stuff @@ -98,31 +163,178 @@ func (t *testCtx) startBridge() error { // Logging stuff logIMAP, logIMAP, - false, + logSMTP, ) if err != nil { - return err + return nil, fmt.Errorf("could not create bridge: %w", err) } - t.events.collectFrom(eventCh) - - // Wait for the users to be loaded. - t.events.await(events.AllUsersLoaded{}, 30*time.Second) - - // Save the bridge to the context. t.bridge = bridge - return nil + return t.events.collectFrom(eventCh), nil } -func (t *testCtx) stopBridge() error { +func (t *testCtx) closeBridge(ctx context.Context) error { if t.bridge == nil { - return fmt.Errorf("bridge is not running") + return fmt.Errorf("bridge is not started") } - t.bridge.Close(context.Background()) - - t.bridge = nil + t.bridge.Close(ctx) return nil } + +func (t *testCtx) initFrontendService(eventCh <-chan events.Event) error { + if t.service != nil { + return fmt.Errorf("frontend service is already started") + } + + // When starting the frontend, we might enable autostart on bridge if it isn't already. + t.mocks.Autostarter.EXPECT().Enable().AnyTimes() + + service, err := frontend.NewService( + new(mockCrashHandler), + new(mockRestarter), + t.locator, + t.bridge, + eventCh, + true, + ) + if err != nil { + return fmt.Errorf("could not create service: %w", err) + } + + logrus.Info("Frontend service started") + + t.service = service + + t.serviceWG.Add(1) + + go func() { + defer t.serviceWG.Done() + + if err := service.Loop(); err != nil { + panic(err) + } + }() + + return nil +} + +func (t *testCtx) closeFrontendService(ctx context.Context) error { + if t.service == nil { + return fmt.Errorf("frontend service is not started") + } + + if _, err := t.client.Quit(ctx, &emptypb.Empty{}); err != nil { + return fmt.Errorf("could not quit frontend: %w", err) + } + + t.serviceWG.Wait() + + logrus.Info("Frontend service stopped") + + t.service = nil + + return nil +} + +func (t *testCtx) initFrontendClient() error { + if t.client != nil { + return fmt.Errorf("frontend client is already started") + } + + settings, err := t.locator.ProvideSettingsPath() + if err != nil { + return fmt.Errorf("could not get settings path: %w", err) + } + + b, err := os.ReadFile(filepath.Join(settings, "grpcServerConfig.json")) + if err != nil { + return fmt.Errorf("could not read grpcServerConfig.json: %w", err) + } + + var cfg frontend.Config + + if err := json.Unmarshal(b, &cfg); err != nil { + return fmt.Errorf("could not unmarshal grpcServerConfig.json: %w", err) + } + + cp := x509.NewCertPool() + + if !cp.AppendCertsFromPEM([]byte(cfg.Cert)) { + return fmt.Errorf("failed to append certificates to pool") + } + + conn, err := grpc.DialContext( + context.Background(), + fmt.Sprintf("%v:%d", constants.Host, cfg.Port), + grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{RootCAs: cp})), + grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return invoker(metadata.AppendToOutgoingContext(ctx, "server-token", cfg.Token), method, req, reply, cc, opts...) + }), + ) + if err != nil { + return fmt.Errorf("could not dial grpc server: %w", err) + } + + client := frontend.NewBridgeClient(conn) + + stream, err := client.RunEventStream(context.Background(), &frontend.EventStreamRequest{ClientPlatform: runtime.GOOS}) + if err != nil { + return fmt.Errorf("could not start event stream: %w", err) + } + + eventCh := queue.NewQueuedChannel[*frontend.StreamEvent](0, 0) + + go func() { + defer eventCh.CloseAndDiscardQueued() + + for { + event, err := stream.Recv() + if err != nil { + return + } + + eventCh.Enqueue(event) + } + }() + + logrus.Info("Frontend client started") + + t.client = client + t.clientConn = conn + t.clientEventCh = eventCh + + return nil +} + +func (t *testCtx) closeFrontendClient() error { + if t.client == nil { + return fmt.Errorf("frontend client is not started") + } + + if err := t.clientConn.Close(); err != nil { + return fmt.Errorf("could not close frontend client connection: %w", err) + } + + logrus.Info("Frontend client stopped") + + t.client = nil + t.clientConn = nil + t.clientEventCh = nil + + return nil +} + +type mockCrashHandler struct{} + +func (m *mockCrashHandler) HandlePanic() {} + +type mockRestarter struct{} + +func (m *mockRestarter) Set(restart, crash bool) {} + +func (m *mockRestarter) AddFlags(flags ...string) {} + +func (m *mockRestarter) Override(exe string) {} diff --git a/tests/ctx_test.go b/tests/ctx_test.go index 20086daf..bea4bfba 100644 --- a/tests/ctx_test.go +++ b/tests/ctx_test.go @@ -21,16 +21,14 @@ import ( "context" "fmt" "net/smtp" - "reflect" "regexp" "sync" "testing" - "time" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gluon/queue" "github.com/ProtonMail/proton-bridge/v2/internal/bridge" - "github.com/ProtonMail/proton-bridge/v2/internal/events" + frontend "github.com/ProtonMail/proton-bridge/v2/internal/frontend/grpc" "github.com/ProtonMail/proton-bridge/v2/internal/locations" "github.com/bradenaw/juniper/xslices" "github.com/emersion/go-imap/client" @@ -38,6 +36,7 @@ import ( "gitlab.protontech.ch/go/liteapi" "gitlab.protontech.ch/go/liteapi/server" "golang.org/x/exp/maps" + "google.golang.org/grpc" ) var defaultVersion = semver.MustParse("1.0.0") @@ -56,6 +55,15 @@ type testCtx struct { // bridge holds the bridge app under test. bridge *bridge.Bridge + // service holds the gRPC frontend service under test. + service *frontend.Service + serviceWG sync.WaitGroup + + // client holds the gRPC frontend client under test. + client frontend.BridgeClient + clientConn *grpc.ClientConn + clientEventCh *queue.QueuedChannel[*frontend.StreamEvent] + // These maps hold expected userIDByName, their primary addresses and bridge passwords. userIDByName map[string]string userAddrByEmail map[string]map[string]string @@ -295,85 +303,25 @@ func (t *testCtx) close(ctx context.Context) { } } + if t.service != nil { + if err := t.closeFrontendService(ctx); err != nil { + logrus.WithError(err).Error("Failed to close frontend service") + } + } + + if t.client != nil { + if err := t.closeFrontendClient(); err != nil { + logrus.WithError(err).Error("Failed to close frontend client") + } + } + if t.bridge != nil { - t.bridge.Close(ctx) + if err := t.closeBridge(ctx); err != nil { + logrus.WithError(err).Error("Failed to close bridge") + } } t.api.Close() t.events.close() } - -type eventCollector struct { - events map[reflect.Type]*queue.QueuedChannel[events.Event] - lock sync.RWMutex - wg sync.WaitGroup -} - -func newEventCollector() *eventCollector { - return &eventCollector{ - events: make(map[reflect.Type]*queue.QueuedChannel[events.Event]), - } -} - -func (c *eventCollector) collectFrom(eventCh <-chan events.Event) { - c.wg.Add(1) - - go func() { - defer c.wg.Done() - - for event := range eventCh { - c.push(event) - } - }() -} - -func awaitType[T events.Event](c *eventCollector, ofType T, timeout time.Duration) (T, bool) { - if event := c.await(ofType, timeout); event == nil { - return *new(T), false //nolint:gocritic - } else if event, ok := event.(T); !ok { - panic(fmt.Errorf("unexpected event type %T", event)) - } else { - return event, true - } -} - -func (c *eventCollector) await(ofType events.Event, timeout time.Duration) events.Event { - select { - case event := <-c.getEventCh(ofType): - return event - - case <-time.After(timeout): - return nil - } -} - -func (c *eventCollector) push(event events.Event) { - c.lock.Lock() - defer c.lock.Unlock() - - if _, ok := c.events[reflect.TypeOf(event)]; !ok { - c.events[reflect.TypeOf(event)] = queue.NewQueuedChannel[events.Event](0, 0) - } - - c.events[reflect.TypeOf(event)].Enqueue(event) -} - -func (c *eventCollector) getEventCh(ofType events.Event) <-chan events.Event { - c.lock.Lock() - defer c.lock.Unlock() - - if _, ok := c.events[reflect.TypeOf(ofType)]; !ok { - c.events[reflect.TypeOf(ofType)] = queue.NewQueuedChannel[events.Event](0, 0) - } - - return c.events[reflect.TypeOf(ofType)].GetChannel() -} - -func (c *eventCollector) close() { - c.wg.Wait() - - for _, eventCh := range c.events { - eventCh.CloseAndDiscardQueued() - } -} diff --git a/tests/features/frontend/frontend.feature b/tests/features/frontend/frontend.feature new file mode 100644 index 00000000..14b94726 --- /dev/null +++ b/tests/features/frontend/frontend.feature @@ -0,0 +1,5 @@ +Feature: Frontend events + Scenario: Frontend starts and stops + Given bridge is version "2.3.0" and the latest available version is "2.3.0" reachable from "2.3.0" + When bridge starts + Then frontend sees that bridge is version "2.3.0" diff --git a/tests/frontend_test.go b/tests/frontend_test.go new file mode 100644 index 00000000..12c7e8d2 --- /dev/null +++ b/tests/frontend_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package tests + +import ( + "context" + "fmt" + + "google.golang.org/protobuf/types/known/emptypb" +) + +func (s *scenario) frontendSeesThatBridgeIsVersion(version string) error { + res, err := s.t.client.Version(context.Background(), &emptypb.Empty{}) + if err != nil { + return err + } + + if version != res.GetValue() { + return fmt.Errorf("expected version %s, got %s", version, res.GetValue()) + } + + return nil +}