Files
netbird/client/internal/updater/manager_mode_test.go
T
Zoltán Papp 38d7b35c8e [client] Reset the updater mode on engine stop
Removing SetDownloadOnly from the engine teardown also dropped the reset of
the forced-update flag. After netbird down the manager kept the last
management directive, so a release published while the client was stopped
could still trigger a forced install that management no longer asked for.
The same window existed between an engine teardown and the next reconnect
attempt, where ResetMode only ran at the start of the attempt.

Reset the mode in stopLocked instead, so the updater stays silent from the
moment the engine goes down until the first network map of the next
connection decides the mode again.
2026-09-23 10:37:02 +02:00

155 lines
5.1 KiB
Go

package updater
import (
"context"
"path"
"testing"
"time"
v "github.com/hashicorp/go-version"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
func Test_UndecidedMode_SuppressesNotification(t *testing.T) {
tmpFile := path.Join(t.TempDir(), "update-test-undecided.json")
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
mockUpdate := &versionUpdateMock{latestVersion: v.Must(v.NewSemver("1.0.1"))}
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = mockUpdate
m.currentVersion = "1.0.0"
m.Start(context.Background())
defer m.Stop()
mockUpdate.onUpdate()
if ver, _ := waitForUpdateEvent(sub, 300*time.Millisecond); ver != "" {
t.Fatalf("undecided mode must not publish, got %q", ver)
}
m.NotifyUI()
if ver, _ := waitForUpdateEvent(sub, 300*time.Millisecond); ver != "" {
t.Fatalf("NotifyUI in undecided mode must not publish, got %q", ver)
}
m.SetDownloadOnly()
ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond)
if ver != "1.0.1" {
t.Fatalf("expected download-only event for 1.0.1, got %q", ver)
}
if enforced {
t.Error("download-only event must not carry enforced metadata")
}
}
func Test_ResetMode_ReturnsToUndecided(t *testing.T) {
tmpFile := path.Join(t.TempDir(), "update-test-reset.json")
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
mockUpdate := &versionUpdateMock{latestVersion: v.Must(v.NewSemver("1.0.1"))}
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = mockUpdate
m.currentVersion = "1.0.0"
m.autoUpdateSupported = func() bool { return true }
m.Start(context.Background())
defer m.Stop()
m.SetVersion("1.0.1", false)
ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond)
if ver != "1.0.1" || !enforced {
t.Fatalf("expected enforced event for 1.0.1, got %q enforced=%v", ver, enforced)
}
m.ResetMode()
mockUpdate.onUpdate()
if ver, _ := waitForUpdateEvent(sub, 300*time.Millisecond); ver != "" {
t.Fatalf("reset mode must not publish on fetch, got %q", ver)
}
m.NotifyUI()
if ver, _ := waitForUpdateEvent(sub, 300*time.Millisecond); ver != "" {
t.Fatalf("NotifyUI after reset must not publish, got %q", ver)
}
if err := m.Install(context.Background()); err == nil {
t.Fatal("Install after reset must fail without a pending version")
}
m.SetVersion("1.0.1", false)
ver, enforced = waitForUpdateEvent(sub, 500*time.Millisecond)
if ver != "1.0.1" || !enforced {
t.Fatalf("expected enforced event again after reset, got %q enforced=%v", ver, enforced)
}
}
func Test_SetDownloadOnly_ClearsPendingVersion(t *testing.T) {
tmpFile := path.Join(t.TempDir(), "update-test-pending.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.autoUpdateSupported = func() bool { return true }
m.Start(context.Background())
defer m.Stop()
m.SetVersion("1.0.1", false)
if ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond); ver != "1.0.1" || !enforced {
t.Fatalf("expected enforced event for 1.0.1, got %q enforced=%v", ver, enforced)
}
m.SetDownloadOnly()
if ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond); ver != "1.0.1" || enforced {
t.Fatalf("expected download-only event for 1.0.1, got %q enforced=%v", ver, enforced)
}
if err := m.Install(context.Background()); err == nil {
t.Fatal("Install in download-only mode must not install the staged managed version")
}
}
func Test_ResetMode_DropsForceUpdate(t *testing.T) {
tmpFile := path.Join(t.TempDir(), "update-test-force-reset.json")
recorder := peer.NewRecorder("")
sub := recorder.SubscribeToEvents()
defer recorder.UnsubscribeFromEvents(sub)
mockUpdate := &versionUpdateMock{}
m := NewManager(recorder, statemanager.New(tmpFile))
m.update = mockUpdate
m.currentVersion = "1.0.0"
m.autoUpdateSupported = func() bool { return true }
m.Start(context.Background())
defer m.Stop()
// Management enforces "latest" before the fetcher has reported any version,
// so nothing can be installed while the engine is still up.
m.SetVersion(latestVersion, true)
if event := waitForAnyEvent(sub, 300*time.Millisecond); event != nil {
t.Fatalf("no event expected before the latest version is known, got %v", event)
}
// The engine stop resets the mode. A release published afterwards must not
// trigger the stale forced install or any notification.
m.ResetMode()
mockUpdate.setLatestVersion(v.Must(v.NewSemver("1.0.1")))
mockUpdate.onUpdate()
if event := waitForAnyEvent(sub, 300*time.Millisecond); event != nil {
t.Fatalf("stale force directive must stay silent after reset, got %v", event)
}
m.SetVersion("1.0.1", false)
ver, enforced := waitForUpdateEvent(sub, 500*time.Millisecond)
if ver != "1.0.1" || !enforced {
t.Fatalf("expected enforced event after a fresh directive, got %q enforced=%v", ver, enforced)
}
}