//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