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