mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
Auto-update logic moved out of the UI into a dedicated updatemanager.Manager service that runs in the connection layer. The UI no longer polls or checks for updates independently. The update manager supports three modes driven by the management server's auto-update policy: No policy set by mgm: checks GitHub for the latest version and notifies the user (previous behavior, now centralized) mgm enforces update: the "About" menu triggers installation directly instead of just downloading the file — user still initiates the action mgm forces update: installation proceeds automatically without user interaction updateManager lifecycle is now owned by daemon, giving the daemon server direct control via a new TriggerUpdate RPC Introduces EngineServices struct to group external service dependencies passed to NewEngine, reducing its argument count from 11 to 4
228 lines
7.2 KiB
Go
228 lines
7.2 KiB
Go
//go:build windows || darwin
|
|
|
|
package updater
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
v "github.com/hashicorp/go-version"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/peer"
|
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
|
cProto "github.com/netbirdio/netbird/client/proto"
|
|
)
|
|
|
|
func Test_LatestVersion(t *testing.T) {
|
|
testMatrix := []struct {
|
|
name string
|
|
daemonVersion string
|
|
initialLatestVersion *v.Version
|
|
latestVersion *v.Version
|
|
shouldUpdateInit bool
|
|
shouldUpdateLater bool
|
|
}{
|
|
{
|
|
name: "Should notify again when a newer version arrives even within 5 minutes",
|
|
daemonVersion: "1.0.0",
|
|
initialLatestVersion: v.Must(v.NewSemver("1.0.1")),
|
|
latestVersion: v.Must(v.NewSemver("1.0.2")),
|
|
shouldUpdateInit: true,
|
|
shouldUpdateLater: true,
|
|
},
|
|
{
|
|
name: "Shouldn't update initially, but should update as soon as latest version is fetched",
|
|
daemonVersion: "1.0.0",
|
|
initialLatestVersion: nil,
|
|
latestVersion: v.Must(v.NewSemver("1.0.1")),
|
|
shouldUpdateInit: false,
|
|
shouldUpdateLater: true,
|
|
},
|
|
}
|
|
|
|
for idx, c := range testMatrix {
|
|
mockUpdate := &versionUpdateMock{latestVersion: c.initialLatestVersion}
|
|
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
|
|
recorder := peer.NewRecorder("")
|
|
sub := recorder.SubscribeToEvents()
|
|
defer recorder.UnsubscribeFromEvents(sub)
|
|
|
|
m := NewManager(recorder, statemanager.New(tmpFile))
|
|
m.update = mockUpdate
|
|
m.currentVersion = c.daemonVersion
|
|
m.autoUpdateSupported = func() bool { return true }
|
|
m.Start(context.Background())
|
|
m.SetVersion("latest", false)
|
|
|
|
ver, _ := waitForUpdateEvent(sub, 500*time.Millisecond)
|
|
triggeredInit := ver != ""
|
|
if triggeredInit != c.shouldUpdateInit {
|
|
t.Errorf("%s: Initial update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdateInit, triggeredInit)
|
|
}
|
|
if triggeredInit && c.initialLatestVersion != nil && ver != c.initialLatestVersion.String() {
|
|
t.Errorf("%s: Initial update version mismatch, expected %v, got %v", c.name, c.initialLatestVersion.String(), ver)
|
|
}
|
|
|
|
mockUpdate.latestVersion = c.latestVersion
|
|
mockUpdate.onUpdate()
|
|
|
|
ver, _ = waitForUpdateEvent(sub, 500*time.Millisecond)
|
|
triggeredLater := ver != ""
|
|
if triggeredLater != c.shouldUpdateLater {
|
|
t.Errorf("%s: Later update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdateLater, triggeredLater)
|
|
}
|
|
if triggeredLater && c.latestVersion != nil && ver != c.latestVersion.String() {
|
|
t.Errorf("%s: Later update version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), ver)
|
|
}
|
|
|
|
m.Stop()
|
|
}
|
|
}
|
|
|
|
func Test_HandleUpdate(t *testing.T) {
|
|
testMatrix := []struct {
|
|
name string
|
|
daemonVersion string
|
|
latestVersion *v.Version
|
|
expectedVersion string
|
|
shouldUpdate bool
|
|
}{
|
|
{
|
|
name: "Install to a specific version should update regardless of if latestVersion is available yet",
|
|
daemonVersion: "0.55.0",
|
|
latestVersion: nil,
|
|
expectedVersion: "0.56.0",
|
|
shouldUpdate: true,
|
|
},
|
|
{
|
|
name: "Install to specific version should not update if version matches",
|
|
daemonVersion: "0.55.0",
|
|
latestVersion: nil,
|
|
expectedVersion: "0.55.0",
|
|
shouldUpdate: false,
|
|
},
|
|
{
|
|
name: "Install to specific version should not update if current version is newer",
|
|
daemonVersion: "0.55.0",
|
|
latestVersion: nil,
|
|
expectedVersion: "0.54.0",
|
|
shouldUpdate: false,
|
|
},
|
|
{
|
|
name: "Install to latest version should update if latest is newer",
|
|
daemonVersion: "0.55.0",
|
|
latestVersion: v.Must(v.NewSemver("0.56.0")),
|
|
expectedVersion: "latest",
|
|
shouldUpdate: true,
|
|
},
|
|
{
|
|
name: "Install to latest version should not update if latest == current",
|
|
daemonVersion: "0.56.0",
|
|
latestVersion: v.Must(v.NewSemver("0.56.0")),
|
|
expectedVersion: "latest",
|
|
shouldUpdate: false,
|
|
},
|
|
{
|
|
name: "Should not update if daemon version is invalid",
|
|
daemonVersion: "development",
|
|
latestVersion: v.Must(v.NewSemver("1.0.0")),
|
|
expectedVersion: "latest",
|
|
shouldUpdate: false,
|
|
},
|
|
{
|
|
name: "Should not update if expecting latest and latest version is unavailable",
|
|
daemonVersion: "0.55.0",
|
|
latestVersion: nil,
|
|
expectedVersion: "latest",
|
|
shouldUpdate: false,
|
|
},
|
|
{
|
|
name: "Should not update if expected version is invalid",
|
|
daemonVersion: "0.55.0",
|
|
latestVersion: nil,
|
|
expectedVersion: "development",
|
|
shouldUpdate: false,
|
|
},
|
|
}
|
|
for idx, c := range testMatrix {
|
|
tmpFile := path.Join(t.TempDir(), fmt.Sprintf("update-test-%d.json", idx))
|
|
recorder := peer.NewRecorder("")
|
|
sub := recorder.SubscribeToEvents()
|
|
defer recorder.UnsubscribeFromEvents(sub)
|
|
|
|
m := NewManager(recorder, statemanager.New(tmpFile))
|
|
m.update = &versionUpdateMock{latestVersion: c.latestVersion}
|
|
m.currentVersion = c.daemonVersion
|
|
m.autoUpdateSupported = func() bool { return true }
|
|
m.Start(context.Background())
|
|
m.SetVersion(c.expectedVersion, false)
|
|
|
|
ver, _ := waitForUpdateEvent(sub, 500*time.Millisecond)
|
|
updateTriggered := ver != ""
|
|
|
|
if updateTriggered {
|
|
if c.expectedVersion == "latest" && c.latestVersion != nil && ver != c.latestVersion.String() {
|
|
t.Errorf("%s: Version mismatch, expected %v, got %v", c.name, c.latestVersion.String(), ver)
|
|
} else if c.expectedVersion != "latest" && c.expectedVersion != "development" && ver != c.expectedVersion {
|
|
t.Errorf("%s: Version mismatch, expected %v, got %v", c.name, c.expectedVersion, ver)
|
|
}
|
|
}
|
|
|
|
if updateTriggered != c.shouldUpdate {
|
|
t.Errorf("%s: Update trigger mismatch, expected %v, got %v", c.name, c.shouldUpdate, updateTriggered)
|
|
}
|
|
m.Stop()
|
|
}
|
|
}
|
|
|
|
func Test_EnforcedMetadata(t *testing.T) {
|
|
// Mode 1 (downloadOnly): no enforced metadata
|
|
tmpFile := path.Join(t.TempDir(), "update-test-mode1.json")
|
|
recorder := peer.NewRecorder("")
|
|
sub := recorder.SubscribeToEvents()
|
|
defer recorder.UnsubscribeFromEvents(sub)
|
|
|
|
m := NewManager(recorder, statemanager.New(tmpFile))
|
|
m.update = &versionUpdateMock{latestVersion: v.Must(v.NewSemver("1.0.1"))}
|
|
m.currentVersion = "1.0.0"
|
|
m.Start(context.Background())
|
|
m.SetDownloadOnly()
|
|
|
|
ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond)
|
|
if ver == "" {
|
|
t.Fatal("Mode 1: expected new_version_available event")
|
|
}
|
|
if enforced {
|
|
t.Error("Mode 1: expected no enforced metadata")
|
|
}
|
|
m.Stop()
|
|
|
|
// Mode 2 (enforced, forceUpdate=false): enforced metadata present, no auto-install
|
|
tmpFile2 := path.Join(t.TempDir(), "update-test-mode2.json")
|
|
recorder2 := peer.NewRecorder("")
|
|
sub2 := recorder2.SubscribeToEvents()
|
|
defer recorder2.UnsubscribeFromEvents(sub2)
|
|
|
|
m2 := NewManager(recorder2, statemanager.New(tmpFile2))
|
|
m2.update = &versionUpdateMock{latestVersion: nil}
|
|
m2.currentVersion = "1.0.0"
|
|
m2.autoUpdateSupported = func() bool { return true }
|
|
m2.Start(context.Background())
|
|
m2.SetVersion("1.0.1", false)
|
|
|
|
ver, enforced2 := waitForUpdateEvent(sub2, 500*time.Millisecond)
|
|
if ver == "" {
|
|
t.Fatal("Mode 2: expected new_version_available event")
|
|
}
|
|
if !enforced2 {
|
|
t.Error("Mode 2: expected enforced metadata")
|
|
}
|
|
m2.Stop()
|
|
}
|
|
|
|
// ensure the proto import is used
|
|
var _ = cProto.SystemEvent_INFO
|