mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
5 Commits
b230f2ece6
...
4e6236611a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e6236611a | |||
| 0800aeea50 | |||
| f4ddf43ac7 | |||
| da0f51ce5f | |||
| d711d9f562 |
26
Changelog.md
26
Changelog.md
@ -3,6 +3,32 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Helix Bridge 3.18.0
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-309: Revised update logic and structure.
|
||||||
|
* BRIDGE-154: Added access token to expiry refresh request.
|
||||||
|
|
||||||
|
|
||||||
|
## Grunwald Bridge 3.17.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* BRIDGE-271: Report version file check failure to Sentry.
|
||||||
|
* BRIDGE-247: Test: Automate Bridge 0% update rollout.
|
||||||
|
* BRIDGE-248: Test: Additional Bridge UI e2e automation tests.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-73: Update goopenpgp.
|
||||||
|
* BRIDGE-287: Update x/net and x/crypto dependencies.
|
||||||
|
* BRIDGE-303: Update govulncheck to latest release.
|
||||||
|
* BRIDGE-226: Bump Go version to 1.23.4.
|
||||||
|
* BRIDGE-288: Extension to synchronization update handler, observability tweaks and gluon update.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-291: Use correct field for user plan type.
|
||||||
|
* BRIDGE-143: Add missing QML component attribute, cut/paste disabled on read-only text areas.
|
||||||
|
|
||||||
|
|
||||||
## Flavien Bridge 3.16.0
|
## Flavien Bridge 3.16.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.16.0+git
|
BRIDGE_APP_VERSION?=3.18.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -9,7 +9,7 @@ require (
|
|||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2
|
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -47,6 +47,8 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek
|
|||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3 h1:YYnLBVcg7WrEbYVmF1PBr4AEQlob9rCphsMHAmF4CAo=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3 h1:YYnLBVcg7WrEbYVmF1PBr4AEQlob9rCphsMHAmF4CAo=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c h1:dxnbB+ov77BDj1LC35fKZ14hLoTpU6OTpZySwxarVx0=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
|
|||||||
@ -55,6 +55,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -81,8 +82,9 @@ type Bridge struct {
|
|||||||
imapEventCh chan imapEvents.Event
|
imapEventCh chan imapEvents.Event
|
||||||
|
|
||||||
// updater is the bridge's updater.
|
// updater is the bridge's updater.
|
||||||
updater Updater
|
updater Updater
|
||||||
installCh chan installJob
|
installChLegacy chan installJobLegacy
|
||||||
|
installCh chan installJob
|
||||||
|
|
||||||
// heartbeat is the telemetry heartbeat for metrics.
|
// heartbeat is the telemetry heartbeat for metrics.
|
||||||
heartbeat *heartBeatState
|
heartbeat *heartBeatState
|
||||||
@ -149,6 +151,9 @@ type Bridge struct {
|
|||||||
|
|
||||||
// notificationStore is used for notification deduplication
|
// notificationStore is used for notification deduplication
|
||||||
notificationStore *notifications.Store
|
notificationStore *notifications.Store
|
||||||
|
|
||||||
|
// getHostVersion primarily used for testing the update logic - it should return an OS version
|
||||||
|
getHostVersion func(host types.Host) string
|
||||||
}
|
}
|
||||||
|
|
||||||
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
||||||
@ -283,8 +288,9 @@ func newBridge(
|
|||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
imapEventCh: imapEventCh,
|
imapEventCh: imapEventCh,
|
||||||
|
|
||||||
updater: updater,
|
updater: updater,
|
||||||
installCh: make(chan installJob),
|
installChLegacy: make(chan installJobLegacy),
|
||||||
|
installCh: make(chan installJob),
|
||||||
|
|
||||||
curVersion: curVersion,
|
curVersion: curVersion,
|
||||||
newVersion: curVersion,
|
newVersion: curVersion,
|
||||||
@ -316,6 +322,8 @@ func newBridge(
|
|||||||
observabilityService: observabilityService,
|
observabilityService: observabilityService,
|
||||||
|
|
||||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||||
|
|
||||||
|
getHostVersion: func(host types.Host) string { return host.Info().OS.Version },
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
||||||
@ -436,8 +444,17 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
// Check for updates when triggered.
|
// Check for updates when triggered.
|
||||||
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
|
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
|
||||||
logPkg.Info("Checking for updates")
|
logPkg.Info("Checking for updates")
|
||||||
|
var versionLegacy updater.VersionInfoLegacy
|
||||||
|
var version updater.VersionInfo
|
||||||
|
var err error
|
||||||
|
|
||||||
|
useOldUpdateLogic := bridge.GetFeatureFlagValue(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
if useOldUpdateLogic {
|
||||||
|
versionLegacy, err = bridge.updater.GetVersionInfoLegacy(ctx, bridge.api, bridge.vault.GetUpdateChannel())
|
||||||
|
} else {
|
||||||
|
version, err = bridge.updater.GetVersionInfo(ctx, bridge.api)
|
||||||
|
}
|
||||||
|
|
||||||
version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.publish(events.UpdateCheckFailed{Error: err})
|
bridge.publish(events.UpdateCheckFailed{Error: err})
|
||||||
if errors.Is(err, updater.ErrVersionFileDownloadOrVerify) {
|
if errors.Is(err, updater.ErrVersionFileDownloadOrVerify) {
|
||||||
@ -450,12 +467,23 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bridge.handleUpdate(version)
|
if useOldUpdateLogic {
|
||||||
|
bridge.handleUpdateLegacy(versionLegacy)
|
||||||
|
} else {
|
||||||
|
bridge.handleUpdate(version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
defer bridge.goUpdate()
|
defer bridge.goUpdate()
|
||||||
|
|
||||||
// Install updates when available.
|
// Install updates when available - based on old update logic
|
||||||
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
|
async.RangeContext(ctx, bridge.installChLegacy, func(job installJobLegacy) {
|
||||||
|
bridge.installUpdateLegacy(ctx, job)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Install updates when available - based on new update logic
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
|
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
|
||||||
bridge.installUpdate(ctx, job)
|
bridge.installUpdate(ctx, job)
|
||||||
@ -740,3 +768,19 @@ func (bridge *Bridge) ReportMessageWithContext(message string, messageCtx report
|
|||||||
func (bridge *Bridge) GetUsers() map[string]*user.User {
|
func (bridge *Bridge) GetUsers() map[string]*user.User {
|
||||||
return bridge.users
|
return bridge.users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCurrentVersionTest - sets the current version of bridge; should only be used for tests.
|
||||||
|
func (bridge *Bridge) SetCurrentVersionTest(version *semver.Version) {
|
||||||
|
bridge.curVersion = version
|
||||||
|
bridge.newVersion = version
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHostVersionGetterTest - sets the OS version helper func; only used for testing.
|
||||||
|
func (bridge *Bridge) SetHostVersionGetterTest(fn func(host types.Host) string) {
|
||||||
|
bridge.getHostVersion = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRolloutPercentageTest - sets the rollout percentage; should only be used for testing.
|
||||||
|
func (bridge *Bridge) SetRolloutPercentageTest(rollout float64) error {
|
||||||
|
return bridge.vault.SetUpdateRollout(rollout)
|
||||||
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
@ -383,9 +384,14 @@ func TestBridge_Cookies(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_CheckUpdate(t *testing.T) {
|
func TestBridge_CheckUpdate_Legacy(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Disable autoupdate for this test.
|
// Disable autoupdate for this test.
|
||||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||||
|
|
||||||
@ -400,7 +406,7 @@ func TestBridge_CheckUpdate(t *testing.T) {
|
|||||||
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
|
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
|
||||||
|
|
||||||
// Simulate a new version being available.
|
// Simulate a new version being available.
|
||||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
|
||||||
|
|
||||||
// Get a stream of update available events.
|
// Get a stream of update available events.
|
||||||
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||||
@ -411,7 +417,7 @@ func TestBridge_CheckUpdate(t *testing.T) {
|
|||||||
|
|
||||||
// We should receive an event indicating that an update is available.
|
// We should receive an event indicating that an update is available.
|
||||||
require.Equal(t, events.UpdateAvailable{
|
require.Equal(t, events.UpdateAvailable{
|
||||||
Version: updater.VersionInfo{
|
VersionLegacy: updater.VersionInfoLegacy{
|
||||||
Version: v2_4_0,
|
Version: v2_4_0,
|
||||||
MinAuto: v2_3_0,
|
MinAuto: v2_3_0,
|
||||||
RolloutProportion: 1.0,
|
RolloutProportion: 1.0,
|
||||||
@ -423,25 +429,30 @@ func TestBridge_CheckUpdate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_AutoUpdate(t *testing.T) {
|
func TestBridge_AutoUpdate_Legacy(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Enable autoupdate for this test.
|
// Enable autoupdate for this test.
|
||||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
require.NoError(t, b.SetAutoUpdate(true))
|
||||||
|
|
||||||
// Get a stream of update events.
|
// Get a stream of update events.
|
||||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
updateCh, done := b.GetEvents(events.UpdateInstalled{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
// Simulate a new version being available.
|
// Simulate a new version being available.
|
||||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
|
||||||
|
|
||||||
// Check for updates.
|
// Check for updates.
|
||||||
bridge.CheckForUpdates()
|
b.CheckForUpdates()
|
||||||
|
|
||||||
// We should receive an event indicating that the update was silently installed.
|
// We should receive an event indicating that the update was silently installed.
|
||||||
require.Equal(t, events.UpdateInstalled{
|
require.Equal(t, events.UpdateInstalled{
|
||||||
Version: updater.VersionInfo{
|
VersionLegacy: updater.VersionInfoLegacy{
|
||||||
Version: v2_4_0,
|
Version: v2_4_0,
|
||||||
MinAuto: v2_3_0,
|
MinAuto: v2_3_0,
|
||||||
RolloutProportion: 1.0,
|
RolloutProportion: 1.0,
|
||||||
@ -452,9 +463,14 @@ func TestBridge_AutoUpdate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ManualUpdate(t *testing.T) {
|
func TestBridge_ManualUpdate_Legacy(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Disable autoupdate for this test.
|
// Disable autoupdate for this test.
|
||||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||||
|
|
||||||
@ -463,14 +479,14 @@ func TestBridge_ManualUpdate(t *testing.T) {
|
|||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
// Simulate a new version being available, but it's too new for us.
|
// Simulate a new version being available, but it's too new for us.
|
||||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0)
|
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_4_0)
|
||||||
|
|
||||||
// Check for updates.
|
// Check for updates.
|
||||||
bridge.CheckForUpdates()
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
// We should receive an event indicating an update is available, but we can't install it.
|
// We should receive an event indicating an update is available, but we can't install it.
|
||||||
require.Equal(t, events.UpdateAvailable{
|
require.Equal(t, events.UpdateAvailable{
|
||||||
Version: updater.VersionInfo{
|
VersionLegacy: updater.VersionInfoLegacy{
|
||||||
Version: v2_4_0,
|
Version: v2_4_0,
|
||||||
MinAuto: v2_4_0,
|
MinAuto: v2_4_0,
|
||||||
RolloutProportion: 1.0,
|
RolloutProportion: 1.0,
|
||||||
@ -484,7 +500,12 @@ func TestBridge_ManualUpdate(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_ForceUpdate(t *testing.T) {
|
func TestBridge_ForceUpdate(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Get a stream of update events.
|
// Get a stream of update events.
|
||||||
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|||||||
@ -119,13 +119,14 @@ func (provider *TestLocationsProvider) UserCache() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TestUpdater struct {
|
type TestUpdater struct {
|
||||||
latest updater.VersionInfo
|
latest updater.VersionInfoLegacy
|
||||||
lock sync.RWMutex
|
releases updater.VersionInfo
|
||||||
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
||||||
return &TestUpdater{
|
return &TestUpdater{
|
||||||
latest: updater.VersionInfo{
|
latest: updater.VersionInfoLegacy{
|
||||||
Version: version,
|
Version: version,
|
||||||
MinAuto: minAuto,
|
MinAuto: minAuto,
|
||||||
|
|
||||||
@ -134,11 +135,11 @@ func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) {
|
func (testUpdater *TestUpdater) SetLatestVersionLegacy(version, minAuto *semver.Version) {
|
||||||
testUpdater.lock.Lock()
|
testUpdater.lock.Lock()
|
||||||
defer testUpdater.lock.Unlock()
|
defer testUpdater.lock.Unlock()
|
||||||
|
|
||||||
testUpdater.latest = updater.VersionInfo{
|
testUpdater.latest = updater.VersionInfoLegacy{
|
||||||
Version: version,
|
Version: version,
|
||||||
MinAuto: minAuto,
|
MinAuto: minAuto,
|
||||||
|
|
||||||
@ -146,17 +147,35 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) {
|
func (testUpdater *TestUpdater) GetVersionInfoLegacy(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfoLegacy, error) {
|
||||||
testUpdater.lock.RLock()
|
testUpdater.lock.RLock()
|
||||||
defer testUpdater.lock.RUnlock()
|
defer testUpdater.lock.RUnlock()
|
||||||
|
|
||||||
return testUpdater.latest, nil
|
return testUpdater.latest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
func (testUpdater *TestUpdater) InstallUpdateLegacy(_ context.Context, _ updater.Downloader, _ updater.VersionInfoLegacy) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) SetLatestVersion(releases updater.VersionInfo) {
|
||||||
|
testUpdater.lock.Lock()
|
||||||
|
defer testUpdater.lock.Unlock()
|
||||||
|
|
||||||
|
testUpdater.releases = releases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader) (updater.VersionInfo, error) {
|
||||||
|
testUpdater.lock.RLock()
|
||||||
|
defer testUpdater.lock.RUnlock()
|
||||||
|
|
||||||
|
return testUpdater.releases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.Release) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -52,7 +52,9 @@ type Autostarter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Updater interface {
|
type Updater interface {
|
||||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
GetVersionInfoLegacy(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfoLegacy, error)
|
||||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
InstallUpdateLegacy(context.Context, updater.Downloader, updater.VersionInfoLegacy) error
|
||||||
RemoveOldUpdates() error
|
RemoveOldUpdates() error
|
||||||
|
GetVersionInfo(context.Context, updater.Downloader) (updater.VersionInfo, error)
|
||||||
|
InstallUpdate(context.Context, updater.Downloader, updater.Release) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,22 +21,168 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
|
"github.com/elastic/go-sysinfo"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) CheckForUpdates() {
|
func (bridge *Bridge) CheckForUpdates() {
|
||||||
bridge.goUpdate()
|
bridge.goUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
func (bridge *Bridge) InstallUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||||
bridge.installCh <- installJob{version: version, silent: false}
|
bridge.installChLegacy <- installJobLegacy{version: version, silent: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) InstallUpdate(release updater.Release) {
|
||||||
|
bridge.installCh <- installJob{Release: release, Silent: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||||
|
updateChannel := bridge.vault.GetUpdateChannel()
|
||||||
|
updateRollout := bridge.vault.GetUpdateRollout()
|
||||||
|
autoUpdateEnabled := bridge.vault.GetAutoUpdate()
|
||||||
|
|
||||||
|
checkSystemVersion := true
|
||||||
|
hostInfo, err := sysinfo.Host()
|
||||||
|
// If we're unable to get host system information we skip the update's minimum/maximum OS version checks
|
||||||
|
if err != nil {
|
||||||
|
checkSystemVersion = false
|
||||||
|
logrus.WithError(err).Error("Failed to obtain host system info while handling updates")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Failed to obtain host system info while handling updates",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
logrus.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(version.Releases) > 0 {
|
||||||
|
// Update latest is only used to update the release notes and landing page URL
|
||||||
|
bridge.publish(events.UpdateLatest{Release: version.Releases[0]})
|
||||||
|
}
|
||||||
|
|
||||||
|
// minAutoUpdateEvent - used to determine the highest compatible update that satisfies the Minimum Bridge version
|
||||||
|
minAutoUpdateEvent := events.UpdateAvailable{
|
||||||
|
Release: updater.Release{Version: &semver.Version{}},
|
||||||
|
Compatible: false,
|
||||||
|
Silent: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that the version file is always created in descending order
|
||||||
|
// where newer versions are prepended to the top of the releases
|
||||||
|
// The logic for checking update eligibility is as follows:
|
||||||
|
// 1. Check release channel.
|
||||||
|
// 2. Check whether release version is greater.
|
||||||
|
// 3. Check if rollout is larger.
|
||||||
|
// 4. Check OS Version restrictions (provided that restrictions are provided, and we can extract the OS version).
|
||||||
|
// 5. Check Minimum Compatible Bridge Version.
|
||||||
|
// 6. Check if an update package is provided.
|
||||||
|
// 7. Check auto-update.
|
||||||
|
for _, release := range version.Releases {
|
||||||
|
log := logrus.WithFields(logrus.Fields{
|
||||||
|
"current": bridge.curVersion,
|
||||||
|
"channel": updateChannel,
|
||||||
|
"update_version": release.Version,
|
||||||
|
"update_channel": release.ReleaseCategory,
|
||||||
|
"update_min_auto": release.MinAuto,
|
||||||
|
"update_rollout": release.RolloutProportion,
|
||||||
|
"update_min_os_version": release.SystemVersion.Minimum,
|
||||||
|
"update_max_os_version": release.SystemVersion.Maximum,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Debug("Checking update release")
|
||||||
|
|
||||||
|
if !release.ReleaseCategory.UpdateEligible(updateChannel) {
|
||||||
|
log.Debug("Update does not satisfy update channel requirement")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !release.Version.GreaterThan(bridge.curVersion) {
|
||||||
|
log.Debug("Update version is not greater than current version")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.RolloutProportion < updateRollout {
|
||||||
|
log.Debug("Update has not been rolled out yet")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkSystemVersion {
|
||||||
|
shouldContinue, err := release.SystemVersion.IsHostVersionEligible(log, hostInfo, bridge.getHostVersion)
|
||||||
|
if err != nil && shouldContinue {
|
||||||
|
log.WithError(err).Error(
|
||||||
|
"Failed to verify host system version compatibility during release check." +
|
||||||
|
"Error is non-fatal continuing with checks",
|
||||||
|
)
|
||||||
|
} else if err != nil {
|
||||||
|
log.WithError(err).Error("Failed to verify host system version compatibility during update check")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldContinue {
|
||||||
|
log.Debug("Host version does not satisfy system requirements for update")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.MinAuto != nil && bridge.curVersion.LessThan(release.MinAuto) {
|
||||||
|
log.Debug("Update is available but is incompatible with this Bridge version")
|
||||||
|
if release.Version.GreaterThan(minAutoUpdateEvent.Release.Version) {
|
||||||
|
minAutoUpdateEvent.Release = release
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a provided installer package
|
||||||
|
if found := slices.IndexFunc(release.File, func(file updater.File) bool {
|
||||||
|
return file.Identifier == updater.PackageIdentifier
|
||||||
|
}); found == -1 {
|
||||||
|
log.Error("Update is available but does not contain update package")
|
||||||
|
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Available update does not contain update package",
|
||||||
|
reporter.Context{"update_version": release.Version},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoUpdateEnabled {
|
||||||
|
log.Info("An update is available but auto-update is disabled")
|
||||||
|
bridge.publish(events.UpdateAvailable{
|
||||||
|
Release: release,
|
||||||
|
Compatible: true,
|
||||||
|
Silent: false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've gotten to this point that means an automatic update is available and we should install it
|
||||||
|
safe.RLock(func() {
|
||||||
|
bridge.installCh <- installJob{Release: release, Silent: true}
|
||||||
|
}, bridge.newVersionLock)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a release with a minAuto requirement that we satisfy (alongside all other checks)
|
||||||
|
// then notify the user that a manual update is needed
|
||||||
|
if !minAutoUpdateEvent.Release.Version.Equal(&semver.Version{}) {
|
||||||
|
bridge.publish(minAutoUpdateEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateNotAvailable{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||||
log := logrus.WithFields(logrus.Fields{
|
log := logrus.WithFields(logrus.Fields{
|
||||||
"version": version.Version,
|
"version": version.Version,
|
||||||
"current": bridge.curVersion,
|
"current": bridge.curVersion,
|
||||||
@ -44,7 +190,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bridge.publish(events.UpdateLatest{
|
bridge.publish(events.UpdateLatest{
|
||||||
Version: version,
|
VersionLegacy: version,
|
||||||
})
|
})
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@ -62,33 +208,33 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
|||||||
log.Info("An update is available but is incompatible with this version")
|
log.Info("An update is available but is incompatible with this version")
|
||||||
|
|
||||||
bridge.publish(events.UpdateAvailable{
|
bridge.publish(events.UpdateAvailable{
|
||||||
Version: version,
|
VersionLegacy: version,
|
||||||
Compatible: false,
|
Compatible: false,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
case !bridge.vault.GetAutoUpdate():
|
case !bridge.vault.GetAutoUpdate():
|
||||||
log.Info("An update is available but auto-update is disabled")
|
log.Info("An update is available but auto-update is disabled")
|
||||||
|
|
||||||
bridge.publish(events.UpdateAvailable{
|
bridge.publish(events.UpdateAvailable{
|
||||||
Version: version,
|
VersionLegacy: version,
|
||||||
Compatible: true,
|
Compatible: true,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
bridge.installCh <- installJob{version: version, silent: true}
|
bridge.installChLegacy <- installJobLegacy{version: version, silent: true}
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type installJob struct {
|
type installJobLegacy struct {
|
||||||
version updater.VersionInfo
|
version updater.VersionInfoLegacy
|
||||||
silent bool
|
silent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
func (bridge *Bridge) installUpdateLegacy(ctx context.Context, job installJobLegacy) {
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
log := logrus.WithFields(logrus.Fields{
|
log := logrus.WithFields(logrus.Fields{
|
||||||
"version": job.version.Version,
|
"version": job.version.Version,
|
||||||
@ -103,17 +249,12 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
log.WithField("silent", job.silent).Info("An update is available")
|
log.WithField("silent", job.silent).Info("An update is available")
|
||||||
|
|
||||||
bridge.publish(events.UpdateAvailable{
|
bridge.publish(events.UpdateAvailable{
|
||||||
Version: job.version,
|
VersionLegacy: job.version,
|
||||||
Compatible: true,
|
Compatible: true,
|
||||||
Silent: job.silent,
|
Silent: job.silent,
|
||||||
})
|
})
|
||||||
|
|
||||||
bridge.publish(events.UpdateInstalling{
|
err := bridge.updater.InstallUpdateLegacy(ctx, bridge.api, job.version)
|
||||||
Version: job.version,
|
|
||||||
Silent: job.silent,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, updater.ErrDownloadVerify):
|
case errors.Is(err, updater.ErrDownloadVerify):
|
||||||
@ -134,8 +275,79 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
log.WithError(err).Error("The update could not be installed")
|
log.WithError(err).Error("The update could not be installed")
|
||||||
|
|
||||||
bridge.publish(events.UpdateFailed{
|
bridge.publish(events.UpdateFailed{
|
||||||
Version: job.version,
|
VersionLegacy: job.version,
|
||||||
Silent: job.silent,
|
Silent: job.silent,
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Info("The update was installed successfully")
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateInstalled{
|
||||||
|
VersionLegacy: job.version,
|
||||||
|
Silent: job.silent,
|
||||||
|
})
|
||||||
|
|
||||||
|
bridge.newVersion = job.version.Version
|
||||||
|
}
|
||||||
|
}, bridge.newVersionLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
type installJob struct {
|
||||||
|
Release updater.Release
|
||||||
|
Silent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||||
|
safe.Lock(func() {
|
||||||
|
log := logrus.WithFields(logrus.Fields{
|
||||||
|
"version": job.Release.Version,
|
||||||
|
"current": bridge.curVersion,
|
||||||
|
"channel": bridge.vault.GetUpdateChannel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !job.Release.Version.GreaterThan(bridge.newVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("silent", job.Silent).Info("An update is available")
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateAvailable{
|
||||||
|
Release: job.Release,
|
||||||
|
Compatible: true,
|
||||||
|
Silent: job.Silent,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.Release)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, updater.ErrReleaseUpdatePackageMissing):
|
||||||
|
log.WithError(err).Error("The update could not be installed but we will fail silently")
|
||||||
|
if reporterErr := bridge.reporter.ReportExceptionWithContext(
|
||||||
|
"Cannot download update, update package is missing",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
case errors.Is(err, updater.ErrDownloadVerify):
|
||||||
|
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
|
||||||
|
// and we fail silently.
|
||||||
|
log.WithError(err).Error("The update could not be installed, but we will fail silently")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Cannot download or verify update",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
|
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||||
|
log.Info("The update was already installed")
|
||||||
|
|
||||||
|
case err != nil:
|
||||||
|
log.WithError(err).Error("The update could not be installed")
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateFailed{
|
||||||
|
Release: job.Release,
|
||||||
|
Silent: job.Silent,
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -143,11 +355,11 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
log.Info("The update was installed successfully")
|
log.Info("The update was installed successfully")
|
||||||
|
|
||||||
bridge.publish(events.UpdateInstalled{
|
bridge.publish(events.UpdateInstalled{
|
||||||
Version: job.version,
|
Release: job.Release,
|
||||||
Silent: job.silent,
|
Silent: job.Silent,
|
||||||
})
|
})
|
||||||
|
|
||||||
bridge.newVersion = job.version.Version
|
bridge.newVersion = job.Release.Version
|
||||||
}
|
}
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
|
|||||||
700
internal/bridge/updates_test.go
Normal file
700
internal/bridge/updates_test.go
Normal file
@ -0,0 +1,700 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
bridgePkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: we always assume the highest version is always the first in the release json array
|
||||||
|
|
||||||
|
func Test_Update_BetaEligible(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
err := bridge.SetUpdateChannel(updater.EarlyChannel)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.2"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
expectedRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case update := <-updateCh:
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, update)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for update")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_Stable(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
err := bridge.SetUpdateChannel(updater.StableChannel)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.3"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.4"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_CurrentReleaseNewest(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
err := bridge.SetUpdateChannel(updater.StableChannel)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.1.5"))
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.3"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.4"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_NotRolledOutYet(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.EarlyChannel))
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.0.0"))
|
||||||
|
require.NoError(t, bridge.SetRolloutPercentageTest(1.0))
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.5"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 0.5,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.4"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 0.5,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_CheckOSVersion_NoUpdate(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
// Override the OS version check
|
||||||
|
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
|
||||||
|
return "10.0.0"
|
||||||
|
})
|
||||||
|
|
||||||
|
updateNotAvailableCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
updateCh, updateChDone := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer updateChDone()
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.4.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "12.0.0",
|
||||||
|
Maximum: "13.0.0",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
expectedRelease,
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "10.1.0",
|
||||||
|
Maximum: "11.5",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateNotAvailableCh)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_CheckOSVersion_HasUpdate(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
// Override the OS version check
|
||||||
|
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
|
||||||
|
return "10.0.0"
|
||||||
|
})
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "10.0.0",
|
||||||
|
Maximum: "10.1.12",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedUpdateReleaseWindowsLinux := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.4.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "12.0.0",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
expectedUpdateReleaseWindowsLinux,
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "11.0.0",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedUpdateRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedUpdateReleaseWindowsLinux,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_UpdateFromMinVer_UpdateAvailable(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: currentBridgeVersion,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.1"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.1"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.0"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedUpdateRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual -
|
||||||
|
// if we have an update, but we don't satisfy minVersion, a manual update to the highest possible version should be performed.
|
||||||
|
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.1"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.1"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.0"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.1.6"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateAvailable{
|
||||||
|
Release: expectedUpdateRelease,
|
||||||
|
Silent: false,
|
||||||
|
Compatible: false,
|
||||||
|
}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch - only Beta updates are available
|
||||||
|
// nor do we satisfy the minVersion, we can't do anything in this case.
|
||||||
|
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.1"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.1"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.0"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.1.6"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -24,14 +24,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UpdateLatest is published when the latest version of bridge is known.
|
// UpdateLatest is published when the latest version of bridge is known.
|
||||||
|
// It is only used for updating the release notes and landing page URLs.
|
||||||
type UpdateLatest struct {
|
type UpdateLatest struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
Version updater.VersionInfo
|
// VersionLegacy - holds Update version information; corresponding to the old update structure and logic;
|
||||||
|
VersionLegacy updater.VersionInfoLegacy
|
||||||
|
|
||||||
|
// Release - holds Release version data; part of the new update logic as of BRIDGE-309.
|
||||||
|
Release updater.Release
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event UpdateLatest) GetLatestVersion() string {
|
||||||
|
var latestVersion string
|
||||||
|
if !event.VersionLegacy.IsEmpty() {
|
||||||
|
latestVersion = event.VersionLegacy.Version.String()
|
||||||
|
} else if !event.Release.IsEmpty() {
|
||||||
|
latestVersion = event.Release.Version.String()
|
||||||
|
}
|
||||||
|
return latestVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UpdateLatest) String() string {
|
func (event UpdateLatest) String() string {
|
||||||
return fmt.Sprintf("UpdateLatest: Version: %s", event.Version.Version)
|
if !event.VersionLegacy.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateLatest: Version: %s", event.VersionLegacy.Version)
|
||||||
|
}
|
||||||
|
if !event.Release.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateLatest: Version: %s", event.Release.Version)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAvailable is published when an update is available.
|
// UpdateAvailable is published when an update is available.
|
||||||
@ -40,7 +61,11 @@ func (event UpdateLatest) String() string {
|
|||||||
type UpdateAvailable struct {
|
type UpdateAvailable struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
Version updater.VersionInfo
|
// VersionLegacy - holds Update version information; corresponding to the old update structure and logic;
|
||||||
|
VersionLegacy updater.VersionInfoLegacy
|
||||||
|
|
||||||
|
// Release - holds Release version data; part of the new update logic as of BRIDGE-309.
|
||||||
|
Release updater.Release
|
||||||
|
|
||||||
// Compatible is true if the update can be installed automatically.
|
// Compatible is true if the update can be installed automatically.
|
||||||
Compatible bool
|
Compatible bool
|
||||||
@ -49,8 +74,23 @@ type UpdateAvailable struct {
|
|||||||
Silent bool
|
Silent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (event UpdateAvailable) GetLatestVersion() string {
|
||||||
|
var latestVersion string
|
||||||
|
if !event.VersionLegacy.IsEmpty() {
|
||||||
|
latestVersion = event.VersionLegacy.Version.String()
|
||||||
|
} else if !event.Release.IsEmpty() {
|
||||||
|
latestVersion = event.Release.Version.String()
|
||||||
|
}
|
||||||
|
return latestVersion
|
||||||
|
}
|
||||||
|
|
||||||
func (event UpdateAvailable) String() string {
|
func (event UpdateAvailable) String() string {
|
||||||
return fmt.Sprintf("UpdateAvailable: Version %s, Compatible: %t, Silent: %t", event.Version.Version, event.Compatible, event.Silent)
|
if !event.Release.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateAvailable: Version %s, Compatible: %t, Silent: %t", event.Release.Version, event.Compatible, event.Silent)
|
||||||
|
} else if !event.VersionLegacy.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateAvailable: Version %s, Compatible: %t, Silent: %t", event.VersionLegacy.Version, event.Compatible, event.Silent)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateNotAvailable is published when no update is available.
|
// UpdateNotAvailable is published when no update is available.
|
||||||
@ -62,45 +102,70 @@ func (event UpdateNotAvailable) String() string {
|
|||||||
return "UpdateNotAvailable"
|
return "UpdateNotAvailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateInstalling is published when bridge begins installing an update.
|
|
||||||
type UpdateInstalling struct {
|
|
||||||
eventBase
|
|
||||||
|
|
||||||
Version updater.VersionInfo
|
|
||||||
|
|
||||||
Silent bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (event UpdateInstalling) String() string {
|
|
||||||
return fmt.Sprintf("UpdateInstalling: Version %s, Silent: %t", event.Version.Version, event.Silent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateInstalled is published when an update has been installed.
|
// UpdateInstalled is published when an update has been installed.
|
||||||
type UpdateInstalled struct {
|
type UpdateInstalled struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
Version updater.VersionInfo
|
// VersionLegacy - holds Update version information; corresponding to the old update structure and logic;
|
||||||
|
VersionLegacy updater.VersionInfoLegacy
|
||||||
|
|
||||||
|
// Release - holds Release version data; part of the new update logic as of BRIDGE-309.
|
||||||
|
Release updater.Release
|
||||||
|
|
||||||
Silent bool
|
Silent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (event UpdateInstalled) GetLatestVersion() string {
|
||||||
|
var latestVersion string
|
||||||
|
if !event.VersionLegacy.IsEmpty() {
|
||||||
|
latestVersion = event.VersionLegacy.Version.String()
|
||||||
|
} else if !event.Release.IsEmpty() {
|
||||||
|
latestVersion = event.Release.Version.String()
|
||||||
|
}
|
||||||
|
return latestVersion
|
||||||
|
}
|
||||||
|
|
||||||
func (event UpdateInstalled) String() string {
|
func (event UpdateInstalled) String() string {
|
||||||
return fmt.Sprintf("UpdateInstalled: Version %s, Silent: %t", event.Version.Version, event.Silent)
|
if !event.Release.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateInstalled: Version %s, Silent: %t", event.Release.Version, event.Silent)
|
||||||
|
} else if !event.VersionLegacy.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateInstalled: Version %s, Silent: %t", event.VersionLegacy.Version, event.Silent)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFailed is published when an update fails to be installed.
|
// UpdateFailed is published when an update fails to be installed.
|
||||||
type UpdateFailed struct {
|
type UpdateFailed struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
Version updater.VersionInfo
|
// VersionLegacy - holds Update version information; corresponding to the old update structure and logic;
|
||||||
|
VersionLegacy updater.VersionInfoLegacy
|
||||||
|
|
||||||
|
// Release - holds Release version data; part of the new update logic as of BRIDGE-309.
|
||||||
|
Release updater.Release
|
||||||
|
|
||||||
Silent bool
|
Silent bool
|
||||||
|
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (event UpdateFailed) GetLatestVersion() string {
|
||||||
|
var latestVersion string
|
||||||
|
if !event.VersionLegacy.IsEmpty() {
|
||||||
|
latestVersion = event.VersionLegacy.Version.String()
|
||||||
|
} else if !event.Release.IsEmpty() {
|
||||||
|
latestVersion = event.Release.Version.String()
|
||||||
|
}
|
||||||
|
return latestVersion
|
||||||
|
}
|
||||||
|
|
||||||
func (event UpdateFailed) String() string {
|
func (event UpdateFailed) String() string {
|
||||||
return fmt.Sprintf("UpdateFailed: Version %s, Silent: %t, Error: %s", event.Version.Version, event.Silent, event.Error)
|
if !event.Release.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateFailed: Version %s, Silent: %t, Error: %s", event.Release.Version, event.Silent, event.Error)
|
||||||
|
} else if !event.VersionLegacy.IsEmpty() {
|
||||||
|
return fmt.Sprintf("UpdateFailed: Version %s, Silent: %t, Error: %s", event.VersionLegacy.Version, event.Silent, event.Error)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateForced is published when the bridge version is too old and must be updated.
|
// UpdateForced is published when the bridge version is too old and must be updated.
|
||||||
|
|||||||
@ -482,16 +482,16 @@ func (f *frontendCLI) watchEvents(eventCh <-chan events.Event) { // nolint:gocyc
|
|||||||
|
|
||||||
case events.UpdateAvailable:
|
case events.UpdateAvailable:
|
||||||
if !event.Compatible {
|
if !event.Compatible {
|
||||||
f.Printf("A new version (%v) is available but it cannot be installed automatically.\n", event.Version.Version)
|
f.Printf("A new version (%v) is available but it cannot be installed automatically.\n", event.GetLatestVersion())
|
||||||
} else if !event.Silent {
|
} else if !event.Silent {
|
||||||
f.Printf("A new version (%v) is available.\n", event.Version.Version)
|
f.Printf("A new version (%v) is available.\n", event.GetLatestVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
case events.UpdateInstalled:
|
case events.UpdateInstalled:
|
||||||
f.Printf("A new version (%v) was installed.\n", event.Version.Version)
|
f.Printf("A new version (%v) was installed.\n", event.GetLatestVersion())
|
||||||
|
|
||||||
case events.UpdateFailed:
|
case events.UpdateFailed:
|
||||||
f.Printf("A new version (%v) failed to be installed (%v).\n", event.Version.Version, event.Error)
|
f.Printf("A new version (%v) failed to be installed (%v).\n", event.GetLatestVersion(), event.Error)
|
||||||
|
|
||||||
case events.UpdateForced:
|
case events.UpdateForced:
|
||||||
f.notifyNeedUpgrade()
|
f.notifyNeedUpgrade()
|
||||||
|
|||||||
@ -78,11 +78,13 @@ type Service struct { // nolint:structcheck
|
|||||||
eventCh <-chan events.Event
|
eventCh <-chan events.Event
|
||||||
quitCh <-chan struct{}
|
quitCh <-chan struct{}
|
||||||
|
|
||||||
latest updater.VersionInfo
|
latestLegacy updater.VersionInfoLegacy
|
||||||
latestLock safe.RWMutex
|
latest updater.Release
|
||||||
|
latestLock safe.RWMutex
|
||||||
|
|
||||||
target updater.VersionInfo
|
targetLegacy updater.VersionInfoLegacy
|
||||||
targetLock safe.RWMutex
|
target updater.Release
|
||||||
|
targetLock safe.RWMutex
|
||||||
|
|
||||||
authClient *proton.Client
|
authClient *proton.Client
|
||||||
auth proton.Auth
|
auth proton.Auth
|
||||||
@ -168,11 +170,13 @@ func NewService(
|
|||||||
eventCh: eventCh,
|
eventCh: eventCh,
|
||||||
quitCh: quitCh,
|
quitCh: quitCh,
|
||||||
|
|
||||||
latest: updater.VersionInfo{},
|
latestLegacy: updater.VersionInfoLegacy{},
|
||||||
latestLock: safe.NewRWMutex(),
|
latest: updater.Release{},
|
||||||
|
latestLock: safe.NewRWMutex(),
|
||||||
|
|
||||||
target: updater.VersionInfo{},
|
targetLegacy: updater.VersionInfoLegacy{},
|
||||||
targetLock: safe.NewRWMutex(),
|
target: updater.Release{},
|
||||||
|
targetLock: safe.NewRWMutex(),
|
||||||
|
|
||||||
log: logrus.WithField("pkg", "grpc"),
|
log: logrus.WithField("pkg", "grpc"),
|
||||||
initializing: sync.WaitGroup{},
|
initializing: sync.WaitGroup{},
|
||||||
@ -354,10 +358,11 @@ func (s *Service) watchEvents() {
|
|||||||
|
|
||||||
case events.UpdateLatest:
|
case events.UpdateLatest:
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
s.latest = event.Version
|
s.latestLegacy = event.VersionLegacy
|
||||||
|
s.latest = event.Release
|
||||||
}, s.latestLock)
|
}, s.latestLock)
|
||||||
|
|
||||||
_ = s.SendEvent(NewUpdateVersionChangedEvent())
|
_ = s.SendEvent(NewUpdateVersionChangedEvent()) // This updates the release notes page and landing page.
|
||||||
|
|
||||||
case events.UpdateAvailable:
|
case events.UpdateAvailable:
|
||||||
switch {
|
switch {
|
||||||
@ -366,10 +371,11 @@ func (s *Service) watchEvents() {
|
|||||||
|
|
||||||
case !event.Silent:
|
case !event.Silent:
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
s.target = event.Version
|
s.targetLegacy = event.VersionLegacy
|
||||||
|
s.target = event.Release
|
||||||
}, s.targetLock)
|
}, s.targetLock)
|
||||||
|
|
||||||
_ = s.SendEvent(NewUpdateManualReadyEvent(event.Version.Version.String()))
|
_ = s.SendEvent(NewUpdateManualReadyEvent(event.GetLatestVersion()))
|
||||||
}
|
}
|
||||||
|
|
||||||
case events.UpdateInstalled:
|
case events.UpdateInstalled:
|
||||||
@ -391,8 +397,10 @@ func (s *Service) watchEvents() {
|
|||||||
|
|
||||||
if s.latest.Version != nil {
|
if s.latest.Version != nil {
|
||||||
latest = s.latest.Version.String()
|
latest = s.latest.Version.String()
|
||||||
} else if version, ok := s.checkLatestVersion(); ok {
|
} else if s.latestLegacy.Version != nil {
|
||||||
latest = version.Version.String()
|
latest = s.latestLegacy.Version.String()
|
||||||
|
} else if latestVersion, ok := s.checkLatestVersion(); ok {
|
||||||
|
latest = latestVersion
|
||||||
} else {
|
} else {
|
||||||
latest = "unknown"
|
latest = "unknown"
|
||||||
}
|
}
|
||||||
@ -517,7 +525,7 @@ func (s *Service) triggerReset() {
|
|||||||
s.bridge.FactoryReset(context.Background())
|
s.bridge.FactoryReset(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) checkLatestVersion() (updater.VersionInfo, bool) {
|
func (s *Service) checkLatestVersion() (string, bool) {
|
||||||
updateCh, done := s.bridge.GetEvents(events.UpdateLatest{})
|
updateCh, done := s.bridge.GetEvents(events.UpdateLatest{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -526,14 +534,13 @@ func (s *Service) checkLatestVersion() (updater.VersionInfo, bool) {
|
|||||||
select {
|
select {
|
||||||
case event := <-updateCh:
|
case event := <-updateCh:
|
||||||
if latest, ok := event.(events.UpdateLatest); ok {
|
if latest, ok := event.(events.UpdateLatest); ok {
|
||||||
return latest.Version, true
|
return latest.GetLatestVersion(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
return updater.VersionInfo{}, false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTLSConfig() (*tls.Config, []byte, error) {
|
func newTLSConfig() (*tls.Config, []byte, error) {
|
||||||
|
|||||||
@ -298,7 +298,14 @@ func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wr
|
|||||||
s.latestLock.RUnlock()
|
s.latestLock.RUnlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return wrapperspb.String(s.latest.ReleaseNotesPage), nil
|
var releaseNotesPage string
|
||||||
|
if !s.latestLegacy.IsEmpty() {
|
||||||
|
releaseNotesPage = s.latestLegacy.ReleaseNotesPage
|
||||||
|
} else if !s.latest.IsEmpty() {
|
||||||
|
releaseNotesPage = s.latest.ReleaseNotesPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapperspb.String(releaseNotesPage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
|
||||||
@ -308,7 +315,14 @@ func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapper
|
|||||||
s.latestLock.RUnlock()
|
s.latestLock.RUnlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return wrapperspb.String(s.latest.LandingPage), nil
|
var landingPage string
|
||||||
|
if !s.latestLegacy.IsEmpty() {
|
||||||
|
landingPage = s.latestLegacy.LandingPage
|
||||||
|
} else if !s.latest.IsEmpty() {
|
||||||
|
landingPage = s.latest.LandingPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapperspb.String(landingPage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||||
@ -617,7 +631,11 @@ func (s *Service) InstallUpdate(_ context.Context, _ *emptypb.Empty) (*emptypb.E
|
|||||||
defer async.HandlePanic(s.panicHandler)
|
defer async.HandlePanic(s.panicHandler)
|
||||||
|
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
s.bridge.InstallUpdate(s.target)
|
if !s.targetLegacy.IsEmpty() {
|
||||||
|
s.bridge.InstallUpdateLegacy(s.targetLegacy)
|
||||||
|
} else if !s.target.IsEmpty() {
|
||||||
|
s.bridge.InstallUpdate(s.target)
|
||||||
|
}
|
||||||
}, s.targetLock)
|
}, s.targetLock)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@ -37,9 +37,10 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals
|
|||||||
const filename = "unleash_flags"
|
const filename = "unleash_flags"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
|
EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
|
||||||
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
|
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
|
||||||
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
|
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
|
||||||
|
UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled"
|
||||||
)
|
)
|
||||||
|
|
||||||
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
||||||
|
|||||||
255
internal/updater/types_test.go
Normal file
255
internal/updater/types_test.go
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ReleaseCategory_UpdateEligible(t *testing.T) {
|
||||||
|
// If release is beta only beta users can update
|
||||||
|
require.True(t, EarlyAccessReleaseCategory.UpdateEligible(EarlyChannel))
|
||||||
|
require.False(t, EarlyAccessReleaseCategory.UpdateEligible(StableChannel))
|
||||||
|
|
||||||
|
// If the release is stable and is the newest then both beta and stable users can update
|
||||||
|
require.True(t, StableReleaseCategory.UpdateEligible(EarlyChannel))
|
||||||
|
require.True(t, StableReleaseCategory.UpdateEligible(StableChannel))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ReleaseCategory_JsonUnmarshal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected ReleaseCategory
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "EarlyAccess"}`,
|
||||||
|
expected: EarlyAccessReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "Earlyaccess"}`,
|
||||||
|
expected: EarlyAccessReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "earlyaccess"}`,
|
||||||
|
expected: EarlyAccessReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": " earlyaccess "}`,
|
||||||
|
expected: EarlyAccessReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "Stable"}`,
|
||||||
|
expected: StableReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "Stable "}`,
|
||||||
|
expected: StableReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "stable"}`,
|
||||||
|
expected: StableReleaseCategory,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"ReleaseCategory": "invalid"}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
ReleaseCategory ReleaseCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
err := json.Unmarshal([]byte(test.input), &data)
|
||||||
|
if err != nil && !test.wantErr {
|
||||||
|
t.Errorf("json.Unmarshal() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.wantErr && err == nil {
|
||||||
|
t.Errorf("expected err got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !test.wantErr && data.ReleaseCategory != test.expected {
|
||||||
|
t.Errorf("got %v, want %v", data.ReleaseCategory, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ReleaseCategory_JsonMarshal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input struct {
|
||||||
|
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||||
|
}
|
||||||
|
expectedOutput string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: struct {
|
||||||
|
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||||
|
}{ReleaseCategory: StableReleaseCategory},
|
||||||
|
expectedOutput: `{"ReleaseCategory":"Stable"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: struct {
|
||||||
|
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||||
|
}{ReleaseCategory: EarlyAccessReleaseCategory},
|
||||||
|
expectedOutput: `{"ReleaseCategory":"EarlyAccess"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: struct {
|
||||||
|
ReleaseCategory ReleaseCategory `json:"ReleaseCategory"`
|
||||||
|
}{ReleaseCategory: 4},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
output, err := json.Marshal(test.input)
|
||||||
|
|
||||||
|
if test.wantErr {
|
||||||
|
if err == nil && len(output) == 0 {
|
||||||
|
t.Errorf("expected error or non-empty output for invalid category")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(output) != test.expectedOutput {
|
||||||
|
t.Errorf("json.Marshal() = %v, want %v", string(output), test.expectedOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_FileIdentifier_JsonUnmarshal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected FileIdentifier
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "package"}`,
|
||||||
|
expected: PackageIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "Package"}`,
|
||||||
|
expected: PackageIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "pACKage"}`,
|
||||||
|
expected: PackageIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "pACKage "}`,
|
||||||
|
expected: PackageIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "installer"}`,
|
||||||
|
expected: InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "Installer"}`,
|
||||||
|
expected: InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "iNSTaller "}`,
|
||||||
|
expected: InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"Identifier": "error"}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Identifier FileIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
err := json.Unmarshal([]byte(test.input), &data)
|
||||||
|
if err != nil && !test.wantErr {
|
||||||
|
t.Errorf("json.Unmarshal() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.wantErr && err == nil {
|
||||||
|
t.Errorf("expected err got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !test.wantErr && data.Identifier != test.expected {
|
||||||
|
t.Errorf("got %v, want %v", data.Identifier, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_FileIdentifier_JsonMarshal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input struct {
|
||||||
|
Identifier FileIdentifier
|
||||||
|
}
|
||||||
|
expectedOutput string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: struct {
|
||||||
|
Identifier FileIdentifier
|
||||||
|
}{Identifier: PackageIdentifier},
|
||||||
|
expectedOutput: `{"Identifier":"package"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: struct {
|
||||||
|
Identifier FileIdentifier
|
||||||
|
}{Identifier: InstallerIdentifier},
|
||||||
|
expectedOutput: `{"Identifier":"installer"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: struct {
|
||||||
|
Identifier FileIdentifier
|
||||||
|
}{Identifier: 4},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
output, err := json.Marshal(test.input)
|
||||||
|
|
||||||
|
if test.wantErr {
|
||||||
|
if err == nil && len(output) == 0 {
|
||||||
|
t.Errorf("expected error or non-empty output for invalid identifier")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(output) != test.expectedOutput {
|
||||||
|
t.Errorf("json.Marshal() = %v, want %v", string(output), test.expectedOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
internal/updater/types_version.go
Normal file
135
internal/updater/types_version.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleaseCategory uint8
|
||||||
|
type FileIdentifier uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
EarlyAccessReleaseCategory ReleaseCategory = iota
|
||||||
|
StableReleaseCategory
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PackageIdentifier FileIdentifier = iota
|
||||||
|
InstallerIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
releaseCategoryName = map[uint8]string{ //nolint:gochecknoglobals
|
||||||
|
0: "EarlyAccess",
|
||||||
|
1: "Stable",
|
||||||
|
}
|
||||||
|
releaseCategoryValue = map[string]uint8{ //nolint:gochecknoglobals
|
||||||
|
"earlyaccess": 0,
|
||||||
|
"stable": 1,
|
||||||
|
}
|
||||||
|
fileIdentifierName = map[uint8]string{ //nolint:gochecknoglobals
|
||||||
|
0: "package",
|
||||||
|
1: "installer",
|
||||||
|
}
|
||||||
|
fileIdentifierValue = map[string]uint8{ //nolint:gochecknoglobals
|
||||||
|
"package": 0,
|
||||||
|
"installer": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseFileIdentifier(s string) (FileIdentifier, error) {
|
||||||
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
|
val, ok := fileIdentifierValue[s]
|
||||||
|
if !ok {
|
||||||
|
return FileIdentifier(0), fmt.Errorf("%s is not a valid file identifier", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FileIdentifier(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi FileIdentifier) String() string {
|
||||||
|
return fileIdentifierName[uint8(fi)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi FileIdentifier) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(fi.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FileIdentifier) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var fileIdentifier string
|
||||||
|
if err := json.Unmarshal(data, &fileIdentifier); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedFileIdentifier, err := ParseFileIdentifier(fileIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*fi = parsedFileIdentifier
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseReleaseCategory(s string) (ReleaseCategory, error) {
|
||||||
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
|
val, ok := releaseCategoryValue[s]
|
||||||
|
if !ok {
|
||||||
|
return ReleaseCategory(0), fmt.Errorf("%s is not a valid release category", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReleaseCategory(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc ReleaseCategory) String() string {
|
||||||
|
return releaseCategoryName[uint8(rc)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc ReleaseCategory) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(rc.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *ReleaseCategory) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var releaseCat string
|
||||||
|
if err := json.Unmarshal(data, &releaseCat); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedCat, err := ParseReleaseCategory(releaseCat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*rc = parsedCat
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc ReleaseCategory) UpdateEligible(channel Channel) bool {
|
||||||
|
if channel == StableChannel && rc == StableReleaseCategory {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == EarlyChannel && rc == EarlyAccessReleaseCategory || rc == StableReleaseCategory {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -29,13 +29,17 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const updateFileVersion = 1
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDownloadVerify = errors.New("failed to download or verify the update")
|
ErrDownloadVerify = errors.New("failed to download or verify the update")
|
||||||
ErrInstall = errors.New("failed to install the update")
|
ErrInstall = errors.New("failed to install the update")
|
||||||
ErrUpdateAlreadyInstalled = errors.New("update is already installed")
|
ErrUpdateAlreadyInstalled = errors.New("update is already installed")
|
||||||
ErrVersionFileDownloadOrVerify = errors.New("failed to download or verify the version file")
|
ErrVersionFileDownloadOrVerify = errors.New("failed to download or verify the version file")
|
||||||
|
ErrReleaseUpdatePackageMissing = errors.New("release update package is missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Downloader interface {
|
type Downloader interface {
|
||||||
@ -53,6 +57,7 @@ type Updater struct {
|
|||||||
verifier *crypto.KeyRing
|
verifier *crypto.KeyRing
|
||||||
product string
|
product string
|
||||||
platform string
|
platform string
|
||||||
|
version uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, platform string) *Updater {
|
func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, platform string) *Updater {
|
||||||
@ -62,10 +67,36 @@ func NewUpdater(ver *versioner.Versioner, verifier *crypto.KeyRing, product, pla
|
|||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
product: product,
|
product: product,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
|
version: updateFileVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) GetVersionInfo(ctx context.Context, downloader Downloader, channel Channel) (VersionInfo, error) {
|
func (u *Updater) GetVersionInfoLegacy(ctx context.Context, downloader Downloader, channel Channel) (VersionInfoLegacy, error) {
|
||||||
|
b, err := downloader.DownloadAndVerify(
|
||||||
|
ctx,
|
||||||
|
u.verifier,
|
||||||
|
u.getVersionFileURLLegacy(),
|
||||||
|
u.getVersionFileURLLegacy()+".sig",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return VersionInfoLegacy{}, fmt.Errorf("%w: %w", ErrVersionFileDownloadOrVerify, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionMap VersionMap
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &versionMap); err != nil {
|
||||||
|
return VersionInfoLegacy{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
version, ok := versionMap[channel]
|
||||||
|
if !ok {
|
||||||
|
return VersionInfoLegacy{}, errors.New("no updates available for this channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Updater) GetVersionInfo(ctx context.Context, downloader Downloader) (VersionInfo, error) {
|
||||||
b, err := downloader.DownloadAndVerify(
|
b, err := downloader.DownloadAndVerify(
|
||||||
ctx,
|
ctx,
|
||||||
u.verifier,
|
u.verifier,
|
||||||
@ -76,21 +107,16 @@ func (u *Updater) GetVersionInfo(ctx context.Context, downloader Downloader, cha
|
|||||||
return VersionInfo{}, fmt.Errorf("%w: %w", ErrVersionFileDownloadOrVerify, err)
|
return VersionInfo{}, fmt.Errorf("%w: %w", ErrVersionFileDownloadOrVerify, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var versionMap VersionMap
|
var releases VersionInfo
|
||||||
|
|
||||||
if err := json.Unmarshal(b, &versionMap); err != nil {
|
if err := json.Unmarshal(b, &releases); err != nil {
|
||||||
return VersionInfo{}, err
|
return VersionInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
version, ok := versionMap[channel]
|
return releases, nil
|
||||||
if !ok {
|
|
||||||
return VersionInfo{}, errors.New("no updates available for this channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
return version, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, update VersionInfo) error {
|
func (u *Updater) InstallUpdateLegacy(ctx context.Context, downloader Downloader, update VersionInfoLegacy) error {
|
||||||
if u.installer.IsAlreadyInstalled(update.Version) {
|
if u.installer.IsAlreadyInstalled(update.Version) {
|
||||||
return ErrUpdateAlreadyInstalled
|
return ErrUpdateAlreadyInstalled
|
||||||
}
|
}
|
||||||
@ -113,13 +139,64 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, release Release) error {
|
||||||
|
if u.installer.IsAlreadyInstalled(release.Version) {
|
||||||
|
return ErrUpdateAlreadyInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find update package
|
||||||
|
idx := slices.IndexFunc(release.File, func(file File) bool {
|
||||||
|
return file.Identifier == PackageIdentifier
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx == -1 {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"release_version": release.Version,
|
||||||
|
}).Error("Update release does not contain update package")
|
||||||
|
return ErrReleaseUpdatePackageMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseUpdatePackage := release.File[idx]
|
||||||
|
|
||||||
|
b, err := downloader.DownloadAndVerify(
|
||||||
|
ctx,
|
||||||
|
u.verifier,
|
||||||
|
releaseUpdatePackage.URL,
|
||||||
|
releaseUpdatePackage.URL+".sig",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.installer.InstallUpdate(release.Version, bytes.NewReader(b)); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to install update")
|
||||||
|
return ErrInstall
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *Updater) RemoveOldUpdates() error {
|
func (u *Updater) RemoveOldUpdates() error {
|
||||||
return u.versioner.RemoveOldVersions()
|
return u.versioner.RemoveOldVersions()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getVersionFileURL returns the URL of the version file.
|
// getVersionFileURLLegacy returns the URL of the version file.
|
||||||
// For example:
|
// For example:
|
||||||
// - https://protonmail.com/download/bridge/version_linux.json
|
// - https://protonmail.com/download/bridge/version_linux.json
|
||||||
func (u *Updater) getVersionFileURL() string {
|
func (u *Updater) getVersionFileURLLegacy() string {
|
||||||
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.product, u.platform)
|
return fmt.Sprintf("%v/%v/version_%v.json", Host, u.product, u.platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getVersionFileURL returns the URL of the version file.
|
||||||
|
// For example:
|
||||||
|
// - https://protonmail.com/download/windows/x86/v1/version.json
|
||||||
|
// - https://protonmail.com/download/linux/x86/v1/version.json
|
||||||
|
// - https://protonmail.com/download/darwin/universal/v1/version.json
|
||||||
|
func (u *Updater) getVersionFileURL() string {
|
||||||
|
switch u.platform {
|
||||||
|
case "darwin":
|
||||||
|
return fmt.Sprintf("%v/%v/%v/universal/v%v/version.json", Host, u.product, u.platform, u.version)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v/%v/%v/x86/v%v/version.json", Host, u.product, u.platform, u.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -19,10 +19,36 @@ package updater
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VersionInfo is information about one version of the app.
|
type File struct {
|
||||||
|
URL string `json:"Url"`
|
||||||
|
Sha512CheckSum string `json:"Sha512CheckSum,omitempty"`
|
||||||
|
Identifier FileIdentifier `json:"Identifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
ReleaseCategory ReleaseCategory `json:"CategoryName"`
|
||||||
|
Version *semver.Version
|
||||||
|
SystemVersion versioncompare.SystemVersion `json:"SystemVersion,omitempty"`
|
||||||
|
RolloutProportion float64
|
||||||
|
MinAuto *semver.Version `json:"MinAuto,omitempty"`
|
||||||
|
ReleaseNotesPage string
|
||||||
|
LandingPage string
|
||||||
|
File []File `json:"File"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rel Release) IsEmpty() bool {
|
||||||
|
return rel.Version == nil && len(rel.File) == 0
|
||||||
|
}
|
||||||
|
|
||||||
type VersionInfo struct {
|
type VersionInfo struct {
|
||||||
|
Releases []Release `json:"Releases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionInfoLegacy is information about one version of the app.
|
||||||
|
type VersionInfoLegacy struct {
|
||||||
// Version is the semantic version of the release.
|
// Version is the semantic version of the release.
|
||||||
Version *semver.Version
|
Version *semver.Version
|
||||||
|
|
||||||
@ -46,6 +72,10 @@ type VersionInfo struct {
|
|||||||
RolloutProportion float64
|
RolloutProportion float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (verInfo VersionInfoLegacy) IsEmpty() bool {
|
||||||
|
return verInfo.Version == nil && verInfo.ReleaseNotesPage == ""
|
||||||
|
}
|
||||||
|
|
||||||
// VersionMap represents the structure of the version.json file.
|
// VersionMap represents the structure of the version.json file.
|
||||||
// It looks like this:
|
// It looks like this:
|
||||||
//
|
//
|
||||||
@ -79,4 +109,4 @@ type VersionInfo struct {
|
|||||||
// }
|
// }
|
||||||
// }.
|
// }.
|
||||||
|
|
||||||
type VersionMap map[Channel]VersionInfo
|
type VersionMap map[Channel]VersionInfoLegacy
|
||||||
|
|||||||
205
internal/updater/version_test.go
Normal file
205
internal/updater/version_test.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mockJSONData = `
|
||||||
|
{
|
||||||
|
"Releases": [
|
||||||
|
{
|
||||||
|
"CategoryName": "Stable",
|
||||||
|
"Version": "2.1.0",
|
||||||
|
"ReleaseDate": "2025-01-15T08:00:00Z",
|
||||||
|
"File": [
|
||||||
|
{
|
||||||
|
"Url": "https://downloads.example.com/v2.1.0/MyApp-2.1.0.pkg",
|
||||||
|
"Sha512CheckSum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"Identifier": "package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Url": "https://downloads.example.com/v2.1.0/MyApp-2.1.0.dmg",
|
||||||
|
"Sha512CheckSum": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce",
|
||||||
|
"Identifier": "installer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RolloutProportion": 0.5,
|
||||||
|
"MinAuto": "2.0.0",
|
||||||
|
"Commit": "8f52d45c9f8c31aa391315ea24e40c4a7e0b2c1d",
|
||||||
|
"ReleaseNotesPage": "https://example.com/releases/2.1.0/notes",
|
||||||
|
"LandingPage": "https://example.com/releases/2.1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CategoryName": "EarlyAccess",
|
||||||
|
"Version": "2.2.0-beta.1",
|
||||||
|
"ReleaseDate": "2025-01-20T10:00:00Z",
|
||||||
|
"File": [
|
||||||
|
{
|
||||||
|
"Url": "https://downloads.example.com/beta/v2.2.0-beta.1/MyApp-2.2.0-beta.1.pkg",
|
||||||
|
"Sha512CheckSum": "a9f0e44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"Identifier": "package"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SystemVersion": {
|
||||||
|
"Minimum": "13"
|
||||||
|
},
|
||||||
|
"RolloutProportion": 0.25,
|
||||||
|
"MinAuto": "2.1.0",
|
||||||
|
"Commit": "3e72d45c9f8c31aa391315ea24e40c4a7e0b2c1d",
|
||||||
|
"ReleaseNotesPage": "https://example.com/releases/2.2.0-beta.1/notes",
|
||||||
|
"LandingPage": "https://example.com/releases/2.2.0-beta.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CategoryName": "Stable",
|
||||||
|
"Version": "2.0.0",
|
||||||
|
"ReleaseDate": "2024-12-01T09:00:00Z",
|
||||||
|
"File": [
|
||||||
|
{
|
||||||
|
"Url": "https://downloads.example.com/v2.0.0/MyApp-2.0.0.pkg",
|
||||||
|
"Sha512CheckSum": "b5f0e44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"Identifier": "package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Url": "https://downloads.example.com/v2.0.0/MyApp-2.0.0.dmg",
|
||||||
|
"Sha512CheckSum": "d583e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce",
|
||||||
|
"Identifier": "installer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SystemVersion": {
|
||||||
|
"Maximum": "12.0.0",
|
||||||
|
"Minimum": "1.0.0"
|
||||||
|
},
|
||||||
|
"RolloutProportion": 1.0,
|
||||||
|
"MinAuto": "1.9.0",
|
||||||
|
"Commit": "2a42d45c9f8c31aa391315ea24e40c4a7e0b2c1d",
|
||||||
|
"ReleaseNotesPage": "https://example.com/releases/2.0.0/notes",
|
||||||
|
"LandingPage": "https://example.com/releases/2.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var expectedVersionInfo = VersionInfo{
|
||||||
|
Releases: []Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.0"),
|
||||||
|
RolloutProportion: 0.5,
|
||||||
|
MinAuto: semver.MustParse("2.0.0"),
|
||||||
|
File: []File{
|
||||||
|
{
|
||||||
|
URL: "https://downloads.example.com/v2.1.0/MyApp-2.1.0.pkg",
|
||||||
|
Identifier: PackageIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "https://downloads.example.com/v2.1.0/MyApp-2.1.0.dmg",
|
||||||
|
Identifier: InstallerIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0-beta.1"),
|
||||||
|
RolloutProportion: 0.25,
|
||||||
|
MinAuto: semver.MustParse("2.1.0"),
|
||||||
|
File: []File{
|
||||||
|
{
|
||||||
|
URL: "https://downloads.example.com/beta/v2.2.0-beta.1/MyApp-2.2.0-beta.1.pkg",
|
||||||
|
Identifier: PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SystemVersion: versioncompare.SystemVersion{Minimum: "13"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.0.0"),
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("1.9.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{Maximum: "12.0.0", Minimum: "1.0.0"},
|
||||||
|
File: []File{
|
||||||
|
{
|
||||||
|
URL: "https://downloads.example.com/v2.0.0/MyApp-2.0.0.pkg",
|
||||||
|
Identifier: PackageIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "https://downloads.example.com/v2.0.0/MyApp-2.0.0.dmg",
|
||||||
|
Identifier: InstallerIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Releases_JsonParse(t *testing.T) {
|
||||||
|
var versionInfo VersionInfo
|
||||||
|
if err := json.Unmarshal([]byte(mockJSONData), &versionInfo); err != nil {
|
||||||
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expectedVersionInfo.Releases) != len(versionInfo.Releases) {
|
||||||
|
t.Fatalf("expected %d releases, parsed %d releases", len(expectedVersionInfo.Releases), len(versionInfo.Releases))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, expectedRelease := range expectedVersionInfo.Releases {
|
||||||
|
release := versionInfo.Releases[i]
|
||||||
|
|
||||||
|
if release.ReleaseCategory != expectedRelease.ReleaseCategory {
|
||||||
|
t.Errorf("Release %d: expected category %v, got %v", i, expectedRelease.ReleaseCategory, release.ReleaseCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.Version.String() != expectedRelease.Version.String() {
|
||||||
|
t.Errorf("Release %d: expected version %s, got %s", i, expectedRelease.Version, release.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.RolloutProportion != expectedRelease.RolloutProportion {
|
||||||
|
t.Errorf("Release %d: expected rollout proportion %f, got %f", i, expectedRelease.RolloutProportion, release.RolloutProportion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedRelease.MinAuto != nil && release.MinAuto.String() != expectedRelease.MinAuto.String() {
|
||||||
|
t.Errorf("Release %d: expected min auto %s, got %s", i, expectedRelease.MinAuto, release.MinAuto)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedRelease.SystemVersion.Minimum != release.SystemVersion.Minimum {
|
||||||
|
t.Errorf("Release %d: expected system version minimum %s, got %s", i, expectedRelease.SystemVersion.Minimum, release.SystemVersion.Minimum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedRelease.SystemVersion.Maximum != release.SystemVersion.Maximum {
|
||||||
|
t.Errorf("Release %d: expected system version minimum %s, got %s", i, expectedRelease.SystemVersion.Maximum, release.SystemVersion.Maximum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(release.File) != len(expectedRelease.File) {
|
||||||
|
t.Errorf("Release %d: expected %d files, got %d", i, len(expectedRelease.File), len(release.File))
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, expectedFile := range expectedRelease.File {
|
||||||
|
file := release.File[j]
|
||||||
|
if file.URL != expectedFile.URL {
|
||||||
|
t.Errorf("Release %d, File %d: expected URL %s, got %s", i, j, expectedFile.URL, file.URL)
|
||||||
|
}
|
||||||
|
if file.Identifier != expectedFile.Identifier {
|
||||||
|
t.Errorf("Release %d, File %d: expected Identifier %v, got %v", i, j, expectedFile.Identifier, file.Identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
internal/updater/versioncompare/compare_darwin.go
Normal file
134
internal/updater/versioncompare/compare_darwin.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package versioncompare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sysVer SystemVersion) IsHostVersionEligible(log *logrus.Entry, host types.Host, getHostOSVersion func(host types.Host) string) (bool, error) {
|
||||||
|
if sysVer.Minimum == "" && sysVer.Maximum == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use getHostOSVersion simply for testing; It's passed via Bridge.
|
||||||
|
var hostVersion string
|
||||||
|
if getHostOSVersion == nil {
|
||||||
|
hostVersion = host.Info().OS.Version
|
||||||
|
} else {
|
||||||
|
hostVersion = getHostOSVersion(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking host OS and update system version requirements. Host: %s; Maximum: %s; Minimum: %s",
|
||||||
|
hostVersion, sysVer.Maximum, sysVer.Minimum)
|
||||||
|
|
||||||
|
hostVersionArr := strings.Split(hostVersion, ".")
|
||||||
|
if len(hostVersionArr) == 0 || hostVersion == "" {
|
||||||
|
return true, fmt.Errorf("could not get host version: %v", hostVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostVersionArrInt := make([]int, len(hostVersionArr))
|
||||||
|
for i := 0; i < len(hostVersionArr); i++ {
|
||||||
|
hostNum, err := strconv.Atoi(hostVersionArr[i])
|
||||||
|
if err != nil {
|
||||||
|
// If we receive an alphanumeric version - we should continue with the update and stop checking for
|
||||||
|
// OS version requirements.
|
||||||
|
return true, fmt.Errorf("invalid host version number: %s - %s", hostVersionArr[i], hostVersion)
|
||||||
|
}
|
||||||
|
hostVersionArrInt[i] = hostNum
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysVer.Minimum != "" {
|
||||||
|
pass, err := compareMinimumVersion(hostVersionArrInt, sysVer.Minimum)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pass {
|
||||||
|
return false, fmt.Errorf("host version is below minimum: hostVersion %v - minimumVersion %v", hostVersion, sysVer.Minimum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysVer.Maximum != "" {
|
||||||
|
pass, err := compareMaximumVersion(hostVersionArrInt, sysVer.Maximum)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pass {
|
||||||
|
return false, fmt.Errorf("host version is above maximum version: hostVersion %v - minimumVersion %v", hostVersion, sysVer.Maximum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareMinimumVersion(hostVersionArr []int, minVersion string) (bool, error) {
|
||||||
|
minVersionArr := strings.Split(minVersion, ".")
|
||||||
|
iterationDepth := min(len(hostVersionArr), len(minVersionArr))
|
||||||
|
|
||||||
|
for i := 0; i < iterationDepth; i++ {
|
||||||
|
hostNum := hostVersionArr[i]
|
||||||
|
|
||||||
|
minNum, err := strconv.Atoi(minVersionArr[i])
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid minimum version number: %s - %s", minVersionArr[i], minVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostNum < minNum {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostNum > minNum {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil // minVersion is inclusive
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareMaximumVersion(hostVersionArr []int, maxVersion string) (bool, error) {
|
||||||
|
maxVersionArr := strings.Split(maxVersion, ".")
|
||||||
|
iterationDepth := min(len(maxVersionArr), len(hostVersionArr))
|
||||||
|
|
||||||
|
for i := 0; i < iterationDepth; i++ {
|
||||||
|
hostNum := hostVersionArr[i]
|
||||||
|
|
||||||
|
maxNum, err := strconv.Atoi(maxVersionArr[i])
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid maximum version number: %s - %s", maxVersionArr[i], maxVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostNum > maxNum {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostNum < maxNum {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil // maxVersion is inclusive
|
||||||
|
}
|
||||||
105
internal/updater/versioncompare/compare_darwin_test.go
Normal file
105
internal/updater/versioncompare/compare_darwin_test.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package versioncompare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/elastic/go-sysinfo"
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_IsHost_EligibleDarwin(t *testing.T) {
|
||||||
|
host, err := sysinfo.Host()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testData := []struct {
|
||||||
|
sysVer SystemVersion
|
||||||
|
getHostOsVersionFn func(host types.Host) string
|
||||||
|
shouldContinue bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "9.5", Maximum: "12.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||||
|
shouldContinue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "9.5.5.5", Maximum: "10.1.1.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||||
|
shouldContinue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "10.0.1", Maximum: "12.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||||
|
shouldContinue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "11.0", Maximum: "12.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "10.0" },
|
||||||
|
shouldContinue: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "11.1.0", Maximum: "12.0.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "11.0.0" },
|
||||||
|
shouldContinue: false,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "10.0", Maximum: "12.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "12.0" },
|
||||||
|
shouldContinue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "11.1.0", Maximum: "12.0.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "" },
|
||||||
|
shouldContinue: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{Minimum: "11.1.0", Maximum: "12.0.0"},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "a.b.c" },
|
||||||
|
shouldContinue: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sysVer: SystemVersion{},
|
||||||
|
getHostOsVersionFn: func(_ types.Host) string { return "1.2.3" },
|
||||||
|
shouldContinue: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testData {
|
||||||
|
l := logrus.WithField("test", "test")
|
||||||
|
shouldContinue, err := test.sysVer.IsHostVersionEligible(l, host, test.getHostOsVersionFn)
|
||||||
|
|
||||||
|
if test.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, test.shouldContinue, shouldContinue)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/updater/versioncompare/compare_linux.go
Normal file
31
internal/updater/versioncompare/compare_linux.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package versioncompare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsHostVersionEligible - Checks whether host OS version is eligible for update. Defaults to true on Linux.
|
||||||
|
func (sysVer SystemVersion) IsHostVersionEligible(log *logrus.Entry, _ types.Host, _ func(host types.Host) string) (bool, error) {
|
||||||
|
log.Info("Checking host OS version on Linux. Defaulting to true.")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
31
internal/updater/versioncompare/compare_windows.go
Normal file
31
internal/updater/versioncompare/compare_windows.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package versioncompare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsHostVersionEligible - Checks whether host OS version is eligible for update. Defaults to true on Linux.
|
||||||
|
func (sysVer SystemVersion) IsHostVersionEligible(log *logrus.Entry, _ types.Host, _ func(host types.Host) string) (bool, error) {
|
||||||
|
log.Info("Checking host OS version on Windows. Defaulting to true.")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
29
internal/updater/versioncompare/types.go
Normal file
29
internal/updater/versioncompare/types.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2025 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package versioncompare
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type SystemVersion struct {
|
||||||
|
Minimum string `json:"Minimum,omitempty"`
|
||||||
|
Maximum string `json:"Maximum,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sysVer SystemVersion) String() string {
|
||||||
|
return fmt.Sprintf("SystemVersion: Maximum %s, Minimum %s", sysVer.Maximum, sysVer.Minimum)
|
||||||
|
}
|
||||||
@ -36,6 +36,8 @@ type API interface {
|
|||||||
GetDomain() string
|
GetDomain() string
|
||||||
GetAppVersion() string
|
GetAppVersion() string
|
||||||
|
|
||||||
|
PushFeatureFlag(string)
|
||||||
|
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +63,10 @@ func (api *fakeAPI) GetAppVersion() string {
|
|||||||
return proton.DefaultAppVersion
|
return proton.DefaultAppVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *fakeAPI) PushFeatureFlag(flagName string) {
|
||||||
|
api.Server.PushFeatureFlag(flagName)
|
||||||
|
}
|
||||||
|
|
||||||
type liveAPI struct {
|
type liveAPI struct {
|
||||||
*server.Server
|
*server.Server
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
|
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/cucumber/godog"
|
"github.com/cucumber/godog"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
@ -55,7 +56,7 @@ func (s *scenario) bridgeStops() error {
|
|||||||
|
|
||||||
func (s *scenario) bridgeVersionIsAndTheLatestAvailableVersionIsReachableFrom(current, latest, minAuto string) error {
|
func (s *scenario) bridgeVersionIsAndTheLatestAvailableVersionIsReachableFrom(current, latest, minAuto string) error {
|
||||||
s.t.version = semver.MustParse(current)
|
s.t.version = semver.MustParse(current)
|
||||||
s.t.mocks.Updater.SetLatestVersion(semver.MustParse(latest), semver.MustParse(minAuto))
|
s.t.mocks.Updater.SetLatestVersionLegacy(semver.MustParse(latest), semver.MustParse(minAuto))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,8 +362,8 @@ func (s *scenario) bridgeSendsAnUpdateAvailableEventForVersion(version string) e
|
|||||||
return errors.New("expected update event to be installable")
|
return errors.New("expected update event to be installable")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !event.Version.Version.Equal(semver.MustParse(version)) {
|
if !event.VersionLegacy.Version.Equal(semver.MustParse(version)) {
|
||||||
return fmt.Errorf("expected update event for version %s, got %s", version, event.Version.Version)
|
return fmt.Errorf("expected update event for version %s, got %s", version, event.VersionLegacy.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -378,8 +379,8 @@ func (s *scenario) bridgeSendsAManualUpdateEventForVersion(version string) error
|
|||||||
return errors.New("expected update event to not be installable")
|
return errors.New("expected update event to not be installable")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !event.Version.Version.Equal(semver.MustParse(version)) {
|
if !event.VersionLegacy.Version.Equal(semver.MustParse(version)) {
|
||||||
return fmt.Errorf("expected update event for version %s, got %s", version, event.Version.Version)
|
return fmt.Errorf("expected update event for version %s, got %s", version, event.VersionLegacy.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -391,8 +392,8 @@ func (s *scenario) bridgeSendsAnUpdateInstalledEventForVersion(version string) e
|
|||||||
return errors.New("expected update installed event, got none")
|
return errors.New("expected update installed event, got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !event.Version.Version.Equal(semver.MustParse(version)) {
|
if !event.VersionLegacy.Version.Equal(semver.MustParse(version)) {
|
||||||
return fmt.Errorf("expected update installed event for version %s, got %s", version, event.Version.Version)
|
return fmt.Errorf("expected update installed event for version %s, got %s", version, event.VersionLegacy.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -483,3 +484,25 @@ func (s *scenario) bridgeSMTPPortIs(expectedPort int) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *scenario) bridgeLegacyUpdateKillSwitchEnabled() error {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(5*time.Second, 0)
|
||||||
|
s.t.api.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scenario) bridgeLegacyUpdateEnabled() error {
|
||||||
|
return eventually(func() error {
|
||||||
|
res := s.t.bridge.GetFeatureFlagValue(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
fmt.Println("RES", res)
|
||||||
|
if res != true {
|
||||||
|
return fmt.Errorf("expected the %v kill-switch to be enabled", unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scenario) bridgeChecksForUpdates() error {
|
||||||
|
s.t.bridge.CheckForUpdates()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,23 +1,34 @@
|
|||||||
Feature: Bridge checks for updates
|
Feature: Bridge checks for updates
|
||||||
|
Background:
|
||||||
|
Given the legacy update kill switch is enabled
|
||||||
|
|
||||||
Scenario: Update not available
|
Scenario: Update not available
|
||||||
Given bridge is version "2.3.0" and the latest available version is "2.3.0" reachable from "2.3.0"
|
Given bridge is version "2.3.0" and the latest available version is "2.3.0" reachable from "2.3.0"
|
||||||
When bridge starts
|
When bridge starts
|
||||||
|
And bridge verifies that the legacy update is enabled
|
||||||
|
And bridge checks for updates
|
||||||
Then bridge sends an update not available event
|
Then bridge sends an update not available event
|
||||||
|
|
||||||
Scenario: Update available without automatic updates enabled
|
Scenario: Update available without automatic updates enabled
|
||||||
Given bridge is version "2.3.0" and the latest available version is "2.4.0" reachable from "2.3.0"
|
Given bridge is version "2.3.0" and the latest available version is "2.4.0" reachable from "2.3.0"
|
||||||
And the user has disabled automatic updates
|
And the user has disabled automatic updates
|
||||||
When bridge starts
|
When bridge starts
|
||||||
|
And bridge verifies that the legacy update is enabled
|
||||||
|
And bridge checks for updates
|
||||||
Then bridge sends an update available event for version "2.4.0"
|
Then bridge sends an update available event for version "2.4.0"
|
||||||
|
|
||||||
Scenario: Update available with automatic updates enabled
|
Scenario: Update available with automatic updates enabled
|
||||||
Given bridge is version "2.3.0" and the latest available version is "2.4.0" reachable from "2.3.0"
|
Given bridge is version "2.3.0" and the latest available version is "2.4.0" reachable from "2.3.0"
|
||||||
When bridge starts
|
When bridge starts
|
||||||
|
And bridge verifies that the legacy update is enabled
|
||||||
|
And bridge checks for updates
|
||||||
Then bridge sends an update installed event for version "2.4.0"
|
Then bridge sends an update installed event for version "2.4.0"
|
||||||
|
|
||||||
Scenario: Manual update available with automatic updates enabled
|
Scenario: Manual update available with automatic updates enabled
|
||||||
Given bridge is version "2.3.0" and the latest available version is "2.4.0" reachable from "2.4.0"
|
Given bridge is version "2.3.0" and the latest available version is "2.4.0" reachable from "2.4.0"
|
||||||
When bridge starts
|
When bridge starts
|
||||||
|
And bridge verifies that the legacy update is enabled
|
||||||
|
And bridge checks for updates
|
||||||
Then bridge sends a manual update event for version "2.4.0"
|
Then bridge sends a manual update event for version "2.4.0"
|
||||||
|
|
||||||
Scenario: Update is required to continue using bridge
|
Scenario: Update is required to continue using bridge
|
||||||
@ -99,6 +99,9 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
|||||||
ctx.Step(`^bridge reports a message with "([^"]*)"$`, s.bridgeReportsMessage)
|
ctx.Step(`^bridge reports a message with "([^"]*)"$`, s.bridgeReportsMessage)
|
||||||
ctx.Step(`^bridge telemetry feature is enabled$`, s.bridgeTelemetryFeatureEnabled)
|
ctx.Step(`^bridge telemetry feature is enabled$`, s.bridgeTelemetryFeatureEnabled)
|
||||||
ctx.Step(`^bridge telemetry feature is disabled$`, s.bridgeTelemetryFeatureDisabled)
|
ctx.Step(`^bridge telemetry feature is disabled$`, s.bridgeTelemetryFeatureDisabled)
|
||||||
|
ctx.Step(`^the legacy update kill switch is enabled$`, s.bridgeLegacyUpdateKillSwitchEnabled)
|
||||||
|
ctx.Step(`^bridge verifies that the legacy update is enabled$`, s.bridgeLegacyUpdateEnabled)
|
||||||
|
ctx.Step(`^bridge checks for updates$`, s.bridgeChecksForUpdates)
|
||||||
|
|
||||||
// ==== FRONTEND ====
|
// ==== FRONTEND ====
|
||||||
ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion)
|
ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion)
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// Minor comment on vault editor DELETE ME.
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type versionInfo struct {
|
type versionInfo struct {
|
||||||
updater.VersionInfo
|
updater.VersionInfoLegacy
|
||||||
|
|
||||||
Commit string
|
Commit string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user