mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-18 12:49:07 +02:00
360 lines
11 KiB
Go
360 lines
11 KiB
Go
//go:build !android && !ios && !freebsd && !js
|
|
|
|
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
func newTestWindowManager() *WindowManager {
|
|
return &WindowManager{
|
|
creating: map[string]bool{},
|
|
pendingOps: map[string][]windowOp{},
|
|
pendingClose: map[string]windowCloser{},
|
|
}
|
|
}
|
|
|
|
func waitDone(t *testing.T, done <-chan struct{}, msg string) {
|
|
t.Helper()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal(msg)
|
|
}
|
|
}
|
|
|
|
func TestWithWindowReusesExistingWindow(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
existing := &application.WebviewWindow{}
|
|
slot := existing
|
|
factoryCalls := 0
|
|
var got *application.WebviewWindow
|
|
created := true
|
|
s.withWindow(windowMain, &slot, func() *application.WebviewWindow {
|
|
factoryCalls++
|
|
return &application.WebviewWindow{}
|
|
}, func(w *application.WebviewWindow, c bool) {
|
|
got, created = w, c
|
|
})
|
|
require.Equal(t, 0, factoryCalls)
|
|
require.Same(t, existing, got)
|
|
require.False(t, created)
|
|
}
|
|
|
|
func TestWithWindowNilFactoryWithoutWindowSkipsOp(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
opCalls := 0
|
|
s.withWindow(windowMain, &slot, nil, func(*application.WebviewWindow, bool) {
|
|
opCalls++
|
|
})
|
|
require.Equal(t, 0, opCalls)
|
|
require.Nil(t, slot)
|
|
}
|
|
|
|
func TestWithWindowReentrantCallDuringCreationIsQueued(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
factoryCalls := 0
|
|
var order []string
|
|
var factory func() *application.WebviewWindow
|
|
factory = func() *application.WebviewWindow {
|
|
factoryCalls++
|
|
// Simulates the Windows message pump re-entering the tray click handler
|
|
// while WebView2 is still initialising the window being created.
|
|
s.withWindow(windowMain, &slot, factory, func(_ *application.WebviewWindow, created bool) {
|
|
order = append(order, fmt.Sprintf("reentrant:%v", created))
|
|
})
|
|
return &application.WebviewWindow{}
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
s.withWindow(windowMain, &slot, factory, func(_ *application.WebviewWindow, created bool) {
|
|
order = append(order, fmt.Sprintf("outer:%v", created))
|
|
})
|
|
}()
|
|
waitDone(t, done, "withWindow deadlocked on a re-entrant call during creation")
|
|
|
|
require.Equal(t, 1, factoryCalls)
|
|
require.Equal(t, []string{"outer:true", "reentrant:false"}, order)
|
|
require.NotNil(t, slot)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingOps)
|
|
}
|
|
|
|
func TestWithWindowConcurrentCallersShareOneCreation(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
factoryEntered := make(chan struct{})
|
|
release := make(chan struct{})
|
|
var factoryCalls, opCalls atomic.Int32
|
|
factory := func() *application.WebviewWindow {
|
|
factoryCalls.Add(1)
|
|
close(factoryEntered)
|
|
<-release
|
|
return &application.WebviewWindow{}
|
|
}
|
|
op := func(*application.WebviewWindow, bool) { opCalls.Add(1) }
|
|
|
|
first := make(chan struct{})
|
|
go func() {
|
|
defer close(first)
|
|
s.withWindow(windowSettings, &slot, factory, op)
|
|
}()
|
|
<-factoryEntered
|
|
|
|
second := make(chan struct{})
|
|
go func() {
|
|
defer close(second)
|
|
s.withWindow(windowSettings, &slot, factory, op)
|
|
}()
|
|
waitDone(t, second, "second caller blocked while the window was being created")
|
|
require.Equal(t, int32(0), opCalls.Load())
|
|
|
|
close(release)
|
|
waitDone(t, first, "creator did not finish")
|
|
|
|
require.Equal(t, int32(1), factoryCalls.Load())
|
|
require.Equal(t, int32(2), opCalls.Load())
|
|
require.NotNil(t, slot)
|
|
}
|
|
|
|
func TestWithWindowOpsQueuedDuringCreationRunInArrivalOrder(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
var order []string
|
|
record := func(label string) windowOp {
|
|
return func(_ *application.WebviewWindow, created bool) {
|
|
order = append(order, fmt.Sprintf("%s:%v", label, created))
|
|
}
|
|
}
|
|
var factory func() *application.WebviewWindow
|
|
factory = func() *application.WebviewWindow {
|
|
s.withWindow(windowMain, &slot, factory, func(w *application.WebviewWindow, created bool) {
|
|
record("a")(w, created)
|
|
// Arrives while the creator is still draining the queue: it must not
|
|
// jump ahead of "b" through the existing-window fast path.
|
|
s.withWindow(windowMain, &slot, factory, record("c"))
|
|
})
|
|
s.withWindow(windowMain, &slot, factory, record("b"))
|
|
return &application.WebviewWindow{}
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
s.withWindow(windowMain, &slot, factory, record("outer"))
|
|
}()
|
|
waitDone(t, done, "withWindow deadlocked while draining queued operations")
|
|
|
|
require.Equal(t, []string{"outer:true", "a:false", "b:false", "c:false"}, order)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingOps)
|
|
}
|
|
|
|
func TestWithWindowFactoryPanicReleasesCreation(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
func() {
|
|
defer func() { require.NotNil(t, recover()) }()
|
|
s.withWindow(windowMain, &slot, func() *application.WebviewWindow {
|
|
panic("factory failed")
|
|
}, func(*application.WebviewWindow, bool) {})
|
|
}()
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingOps)
|
|
require.Nil(t, slot)
|
|
|
|
created := false
|
|
s.withWindow(windowMain, &slot, func() *application.WebviewWindow {
|
|
return &application.WebviewWindow{}
|
|
}, func(_ *application.WebviewWindow, c bool) {
|
|
created = c
|
|
})
|
|
require.True(t, created)
|
|
require.NotNil(t, slot)
|
|
}
|
|
|
|
func TestWithWindowNilFromFactoryReleasesCreation(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
opCalls := 0
|
|
s.withWindow(windowMain, &slot, func() *application.WebviewWindow {
|
|
return nil
|
|
}, func(*application.WebviewWindow, bool) {
|
|
opCalls++
|
|
})
|
|
require.Equal(t, 0, opCalls)
|
|
require.Empty(t, s.creating)
|
|
require.Nil(t, slot)
|
|
}
|
|
|
|
func TestCloseWindowDuringCreationDefersCloseAndSkipsOps(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
created := &application.WebviewWindow{}
|
|
opCalls, closeCalls := 0, 0
|
|
var closed *application.WebviewWindow
|
|
s.withWindow(windowError, &slot, func() *application.WebviewWindow {
|
|
s.closeWindow(windowError, &slot, func(w *application.WebviewWindow) {
|
|
closeCalls++
|
|
closed = w
|
|
})
|
|
require.Equal(t, 0, closeCalls)
|
|
return created
|
|
}, func(*application.WebviewWindow, bool) {
|
|
opCalls++
|
|
})
|
|
require.Equal(t, 0, opCalls)
|
|
require.Equal(t, 1, closeCalls)
|
|
require.Same(t, created, closed)
|
|
require.Nil(t, slot)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingOps)
|
|
require.Empty(t, s.pendingClose)
|
|
|
|
factoryCalls := 0
|
|
reopened := false
|
|
s.withWindow(windowError, &slot, func() *application.WebviewWindow {
|
|
factoryCalls++
|
|
return &application.WebviewWindow{}
|
|
}, func(_ *application.WebviewWindow, c bool) {
|
|
reopened = c
|
|
})
|
|
require.Equal(t, 1, factoryCalls)
|
|
require.True(t, reopened)
|
|
require.NotNil(t, slot)
|
|
}
|
|
|
|
func TestCloseWindowDuringDrainStopsRemainingOps(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
var order []string
|
|
closeCalls := 0
|
|
var factory func() *application.WebviewWindow
|
|
factory = func() *application.WebviewWindow {
|
|
s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) {
|
|
order = append(order, "a")
|
|
s.closeWindow(windowWelcome, &slot, func(*application.WebviewWindow) { closeCalls++ })
|
|
s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) {
|
|
order = append(order, "c")
|
|
})
|
|
})
|
|
s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) {
|
|
order = append(order, "b")
|
|
})
|
|
return &application.WebviewWindow{}
|
|
}
|
|
s.withWindow(windowWelcome, &slot, factory, func(*application.WebviewWindow, bool) {
|
|
order = append(order, "outer")
|
|
})
|
|
|
|
// "b" was queued before the close and "c" after it; a close supersedes both
|
|
// rather than showing a window that is about to be destroyed.
|
|
require.Equal(t, []string{"outer", "a"}, order)
|
|
require.Equal(t, 1, closeCalls)
|
|
require.Nil(t, slot)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingOps)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
func TestCloseWindowWithoutWindowSkipsCloser(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
calls := 0
|
|
s.closeWindow(windowBrowserLogin, &slot, func(*application.WebviewWindow) { calls++ })
|
|
require.Equal(t, 0, calls)
|
|
require.Nil(t, slot)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
func TestCloseWindowWithExistingWindowRunsCloser(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
existing := &application.WebviewWindow{}
|
|
slot := existing
|
|
var got *application.WebviewWindow
|
|
s.closeWindow(windowError, &slot, func(w *application.WebviewWindow) { got = w })
|
|
require.Same(t, existing, got)
|
|
require.Nil(t, slot)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
func TestWithWindowNilFromFactoryDropsPendingClose(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
closeCalls := 0
|
|
s.withWindow(windowError, &slot, func() *application.WebviewWindow {
|
|
s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { closeCalls++ })
|
|
return nil
|
|
}, func(*application.WebviewWindow, bool) {})
|
|
require.Equal(t, 0, closeCalls)
|
|
require.Nil(t, slot)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
func TestWithWindowFactoryPanicDropsPendingClose(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
closeCalls := 0
|
|
func() {
|
|
defer func() { require.NotNil(t, recover()) }()
|
|
s.withWindow(windowError, &slot, func() *application.WebviewWindow {
|
|
s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { closeCalls++ })
|
|
panic("factory failed")
|
|
}, func(*application.WebviewWindow, bool) {})
|
|
}()
|
|
require.Equal(t, 0, closeCalls)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
func TestCloseWindowKeepsFirstDeferredCloser(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
var slot *application.WebviewWindow
|
|
var ran []string
|
|
s.withWindow(windowError, &slot, func() *application.WebviewWindow {
|
|
s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { ran = append(ran, "first") })
|
|
s.closeWindow(windowError, &slot, func(*application.WebviewWindow) { ran = append(ran, "second") })
|
|
return &application.WebviewWindow{}
|
|
}, func(*application.WebviewWindow, bool) {})
|
|
require.Equal(t, []string{"first"}, ran)
|
|
require.Nil(t, slot)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
func TestCloseRenewFlowDuringBrowserLoginCreationRestoresHiddenWindows(t *testing.T) {
|
|
s := newTestWindowManager()
|
|
s.withWindow(windowBrowserLogin, &s.browserLogin, func() *application.WebviewWindow {
|
|
s.CloseRenewFlow()
|
|
// Seeded after the call so the deferred closer, not CloseRenewFlow's own
|
|
// immediate restore, is what has to drain it. A nil entry is skipped by
|
|
// restoreHiddenWindows, so no Wails window is needed.
|
|
s.hiddenForLogin = []application.Window{nil}
|
|
return &application.WebviewWindow{}
|
|
}, func(*application.WebviewWindow, bool) {})
|
|
|
|
require.Nil(t, s.browserLogin)
|
|
require.Empty(t, s.hiddenForLogin)
|
|
require.Empty(t, s.creating)
|
|
require.Empty(t, s.pendingClose)
|
|
}
|
|
|
|
// The settings window opens on whichever tab the caller asked for, so a tab that
|
|
// does not read the daemon configuration never mounts the one that does.
|
|
func TestSettingsWindowURLCarriesTab(t *testing.T) {
|
|
require.Equal(t, "/#/settings?tab=profiles", settingsWindowURL("profiles"))
|
|
require.Equal(t, "/#/settings?tab=general", settingsWindowURL("general"))
|
|
require.Equal(t, "/#/settings?tab=a%2Fb+c", settingsWindowURL("a/b c"),
|
|
"a tab name is escaped rather than trusted to be URL-safe")
|
|
}
|