|
|
@@ -15,6 +15,7 @@ import (
|
|
|
"os"
|
|
|
"reflect"
|
|
|
"slices"
|
|
|
+ "strings"
|
|
|
"sync"
|
|
|
"testing"
|
|
|
"time"
|
|
|
@@ -31,6 +32,7 @@ import (
|
|
|
"tailscale.com/health"
|
|
|
"tailscale.com/hostinfo"
|
|
|
"tailscale.com/ipn"
|
|
|
+ "tailscale.com/ipn/ipnauth"
|
|
|
"tailscale.com/ipn/store/mem"
|
|
|
"tailscale.com/net/netcheck"
|
|
|
"tailscale.com/net/netmon"
|
|
|
@@ -3998,3 +4000,541 @@ func TestFillAllowedSuggestions(t *testing.T) {
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+func TestNotificationTargetMatch(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ target notificationTarget
|
|
|
+ actor ipnauth.Actor
|
|
|
+ wantMatch bool
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "AllClients/Nil",
|
|
|
+ target: allClients,
|
|
|
+ actor: nil,
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "AllClients/NoUID/NoCID",
|
|
|
+ target: allClients,
|
|
|
+ actor: &ipnauth.TestActor{},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "AllClients/WithUID/NoCID",
|
|
|
+ target: allClients,
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.NoClientID},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "AllClients/NoUID/WithCID",
|
|
|
+ target: allClients,
|
|
|
+ actor: &ipnauth.TestActor{CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "AllClients/WithUID/WithCID",
|
|
|
+ target: allClients,
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/Nil",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: nil,
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/NoUID/NoCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: &ipnauth.TestActor{},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/NoUID/WithCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: &ipnauth.TestActor{CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/SameUID/NoCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4"},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/DifferentUID/NoCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-5-6-7-8"},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/SameUID/WithCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID/DifferentUID/WithCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-5-6-7-8", CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/Nil",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: nil,
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/NoUID/NoCID",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/NoUID/SameCID",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/NoUID/DifferentCID",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{CID: ipnauth.ClientIDFrom("B")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/WithUID/NoCID",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4"},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/WithUID/SameCID",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByCID/WithUID/DifferentCID",
|
|
|
+ target: notificationTarget{clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.ClientIDFrom("B")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/Nil",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4"},
|
|
|
+ actor: nil,
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/NoUID/NoCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/NoUID/SameCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/NoUID/DifferentCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{CID: ipnauth.ClientIDFrom("B")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/SameUID/NoCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4"},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/SameUID/SameCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/SameUID/DifferentCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-1-2-3-4", CID: ipnauth.ClientIDFrom("B")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/DifferentUID/NoCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-5-6-7-8"},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/DifferentUID/SameCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-5-6-7-8", CID: ipnauth.ClientIDFrom("A")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "FilterByUID+CID/DifferentUID/DifferentCID",
|
|
|
+ target: notificationTarget{userID: "S-1-5-21-1-2-3-4", clientID: ipnauth.ClientIDFrom("A")},
|
|
|
+ actor: &ipnauth.TestActor{UID: "S-1-5-21-5-6-7-8", CID: ipnauth.ClientIDFrom("B")},
|
|
|
+ wantMatch: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ gotMatch := tt.target.match(tt.actor)
|
|
|
+ if gotMatch != tt.wantMatch {
|
|
|
+ t.Errorf("match: got %v; want %v", gotMatch, tt.wantMatch)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type newTestControlFn func(tb testing.TB, opts controlclient.Options) controlclient.Client
|
|
|
+
|
|
|
+func newLocalBackendWithTestControl(t *testing.T, enableLogging bool, newControl newTestControlFn) *LocalBackend {
|
|
|
+ logf := logger.Discard
|
|
|
+ if enableLogging {
|
|
|
+ logf = tstest.WhileTestRunningLogger(t)
|
|
|
+ }
|
|
|
+ sys := new(tsd.System)
|
|
|
+ store := new(mem.Store)
|
|
|
+ sys.Set(store)
|
|
|
+ e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
|
|
+ }
|
|
|
+ t.Cleanup(e.Close)
|
|
|
+ sys.Set(e)
|
|
|
+
|
|
|
+ b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("NewLocalBackend: %v", err)
|
|
|
+ }
|
|
|
+ b.DisablePortMapperForTest()
|
|
|
+
|
|
|
+ b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
|
|
+ return newControl(t, opts), nil
|
|
|
+ })
|
|
|
+ return b
|
|
|
+}
|
|
|
+
|
|
|
+// notificationHandler is any function that can process (e.g., check) a notification.
|
|
|
+// It returns whether the notification has been handled or should be passed to the next handler.
|
|
|
+// The handler may be called from any goroutine, so it must avoid calling functions
|
|
|
+// that are restricted to the goroutine running the test or benchmark function,
|
|
|
+// such as [testing.common.FailNow] and [testing.common.Fatalf].
|
|
|
+type notificationHandler func(testing.TB, ipnauth.Actor, *ipn.Notify) bool
|
|
|
+
|
|
|
+// wantedNotification names a [notificationHandler] that processes a notification
|
|
|
+// the test expects and wants to receive. The name is used to report notifications
|
|
|
+// that haven't been received within the expected timeout.
|
|
|
+type wantedNotification struct {
|
|
|
+ name string
|
|
|
+ cond notificationHandler
|
|
|
+}
|
|
|
+
|
|
|
+// notificationWatcher observes [LocalBackend] notifications as the specified actor,
|
|
|
+// reporting missing but expected notifications using [testing.common.Error],
|
|
|
+// and delegating the handling of unexpected notifications to the [notificationHandler]s.
|
|
|
+type notificationWatcher struct {
|
|
|
+ tb testing.TB
|
|
|
+ lb *LocalBackend
|
|
|
+ actor ipnauth.Actor
|
|
|
+
|
|
|
+ mu sync.Mutex
|
|
|
+ mask ipn.NotifyWatchOpt
|
|
|
+ want []wantedNotification // notifications we want to receive
|
|
|
+ unexpected []notificationHandler // funcs that are called to check any other notifications
|
|
|
+ ctxCancel context.CancelFunc // cancels the outstanding [LocalBackend.WatchNotificationsAs] call
|
|
|
+ got []*ipn.Notify // all notifications, both wanted and unexpected, we've received so far
|
|
|
+ gotWanted []*ipn.Notify // only the expected notifications; holds nil for any notification that hasn't been received
|
|
|
+ gotWantedCh chan struct{} // closed when we have received the last wanted notification
|
|
|
+ doneCh chan struct{} // closed when [LocalBackend.WatchNotificationsAs] returns
|
|
|
+}
|
|
|
+
|
|
|
+func newNotificationWatcher(tb testing.TB, lb *LocalBackend, actor ipnauth.Actor) *notificationWatcher {
|
|
|
+ return ¬ificationWatcher{tb: tb, lb: lb, actor: actor}
|
|
|
+}
|
|
|
+
|
|
|
+func (w *notificationWatcher) watch(mask ipn.NotifyWatchOpt, wanted []wantedNotification, unexpected ...notificationHandler) {
|
|
|
+ w.tb.Helper()
|
|
|
+
|
|
|
+ // Cancel any outstanding [LocalBackend.WatchNotificationsAs] calls.
|
|
|
+ w.mu.Lock()
|
|
|
+ ctxCancel := w.ctxCancel
|
|
|
+ doneCh := w.doneCh
|
|
|
+ w.mu.Unlock()
|
|
|
+ if doneCh != nil {
|
|
|
+ ctxCancel()
|
|
|
+ <-doneCh
|
|
|
+ }
|
|
|
+
|
|
|
+ doneCh = make(chan struct{})
|
|
|
+ gotWantedCh := make(chan struct{})
|
|
|
+ ctx, ctxCancel := context.WithCancel(context.Background())
|
|
|
+ w.tb.Cleanup(func() {
|
|
|
+ ctxCancel()
|
|
|
+ <-doneCh
|
|
|
+ })
|
|
|
+
|
|
|
+ w.mu.Lock()
|
|
|
+ w.mask = mask
|
|
|
+ w.want = wanted
|
|
|
+ w.unexpected = unexpected
|
|
|
+ w.ctxCancel = ctxCancel
|
|
|
+ w.got = nil
|
|
|
+ w.gotWanted = make([]*ipn.Notify, len(wanted))
|
|
|
+ w.gotWantedCh = gotWantedCh
|
|
|
+ w.doneCh = doneCh
|
|
|
+ w.mu.Unlock()
|
|
|
+
|
|
|
+ watchAddedCh := make(chan struct{})
|
|
|
+ go func() {
|
|
|
+ defer close(doneCh)
|
|
|
+ if len(wanted) == 0 {
|
|
|
+ close(gotWantedCh)
|
|
|
+ if len(unexpected) == 0 {
|
|
|
+ close(watchAddedCh)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var nextWantIdx int
|
|
|
+ w.lb.WatchNotificationsAs(ctx, w.actor, w.mask, func() { close(watchAddedCh) }, func(notify *ipn.Notify) (keepGoing bool) {
|
|
|
+ w.tb.Helper()
|
|
|
+
|
|
|
+ w.mu.Lock()
|
|
|
+ defer w.mu.Unlock()
|
|
|
+ w.got = append(w.got, notify)
|
|
|
+
|
|
|
+ wanted := false
|
|
|
+ for i := nextWantIdx; i < len(w.want); i++ {
|
|
|
+ if wanted = w.want[i].cond(w.tb, w.actor, notify); wanted {
|
|
|
+ w.gotWanted[i] = notify
|
|
|
+ nextWantIdx = i + 1
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if wanted && nextWantIdx == len(w.want) {
|
|
|
+ close(w.gotWantedCh)
|
|
|
+ if len(w.unexpected) == 0 {
|
|
|
+ // If we have received the last wanted notification,
|
|
|
+ // and we don't have any handlers for the unexpected notifications,
|
|
|
+ // we can stop the watcher right away.
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if !wanted {
|
|
|
+ // If we've received a notification we didn't expect,
|
|
|
+ // it could either be an unwanted notification caused by a bug
|
|
|
+ // or just a miscellaneous one that's irrelevant for the current test.
|
|
|
+ // Call unexpected notification handlers, if any, to
|
|
|
+ // check and fail the test if necessary.
|
|
|
+ for _, h := range w.unexpected {
|
|
|
+ if h(w.tb, w.actor, notify) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ })
|
|
|
+
|
|
|
+ }()
|
|
|
+ <-watchAddedCh
|
|
|
+}
|
|
|
+
|
|
|
+func (w *notificationWatcher) check() []*ipn.Notify {
|
|
|
+ w.tb.Helper()
|
|
|
+
|
|
|
+ w.mu.Lock()
|
|
|
+ cancel := w.ctxCancel
|
|
|
+ gotWantedCh := w.gotWantedCh
|
|
|
+ checkUnexpected := len(w.unexpected) != 0
|
|
|
+ doneCh := w.doneCh
|
|
|
+ w.mu.Unlock()
|
|
|
+
|
|
|
+ // Wait for up to 10 seconds to receive expected notifications.
|
|
|
+ timeout := 10 * time.Second
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case <-gotWantedCh:
|
|
|
+ if checkUnexpected {
|
|
|
+ gotWantedCh = nil
|
|
|
+ // But do not wait longer than 500ms for unexpected notifications after
|
|
|
+ // the expected notifications have been received.
|
|
|
+ timeout = 500 * time.Millisecond
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ case <-doneCh:
|
|
|
+ // [LocalBackend.WatchNotificationsAs] has already returned, so no further
|
|
|
+ // notifications will be received. There's no reason to wait any longer.
|
|
|
+ case <-time.After(timeout):
|
|
|
+ }
|
|
|
+ cancel()
|
|
|
+ <-doneCh
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ // Report missing notifications, if any, and log all received notifications,
|
|
|
+ // including both expected and unexpected ones.
|
|
|
+ w.mu.Lock()
|
|
|
+ defer w.mu.Unlock()
|
|
|
+ if hasMissing := slices.Contains(w.gotWanted, nil); hasMissing {
|
|
|
+ want := make([]string, len(w.want))
|
|
|
+ got := make([]string, 0, len(w.want))
|
|
|
+ for i, wn := range w.want {
|
|
|
+ want[i] = wn.name
|
|
|
+ if w.gotWanted[i] != nil {
|
|
|
+ got = append(got, wn.name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ w.tb.Errorf("Notifications(%s): got %q; want %q", actorDescriptionForTest(w.actor), strings.Join(got, ", "), strings.Join(want, ", "))
|
|
|
+ for i, n := range w.got {
|
|
|
+ w.tb.Logf("%d. %v", i, n)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return w.gotWanted
|
|
|
+}
|
|
|
+
|
|
|
+func actorDescriptionForTest(actor ipnauth.Actor) string {
|
|
|
+ var parts []string
|
|
|
+ if actor != nil {
|
|
|
+ if name, _ := actor.Username(); name != "" {
|
|
|
+ parts = append(parts, name)
|
|
|
+ }
|
|
|
+ if uid := actor.UserID(); uid != "" {
|
|
|
+ parts = append(parts, string(uid))
|
|
|
+ }
|
|
|
+ if clientID, _ := actor.ClientID(); clientID != ipnauth.NoClientID {
|
|
|
+ parts = append(parts, clientID.String())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return fmt.Sprintf("Actor{%s}", strings.Join(parts, ", "))
|
|
|
+}
|
|
|
+
|
|
|
+func TestLoginNotifications(t *testing.T) {
|
|
|
+ const (
|
|
|
+ enableLogging = true
|
|
|
+ controlURL = "https://localhost:1/"
|
|
|
+ loginURL = "https://localhost:1/1"
|
|
|
+ )
|
|
|
+
|
|
|
+ wantBrowseToURL := wantedNotification{
|
|
|
+ name: "BrowseToURL",
|
|
|
+ cond: func(t testing.TB, actor ipnauth.Actor, n *ipn.Notify) bool {
|
|
|
+ if n.BrowseToURL != nil && *n.BrowseToURL != loginURL {
|
|
|
+ t.Errorf("BrowseToURL (%s): got %q; want %q", actorDescriptionForTest(actor), *n.BrowseToURL, loginURL)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return n.BrowseToURL != nil
|
|
|
+ },
|
|
|
+ }
|
|
|
+ unexpectedBrowseToURL := func(t testing.TB, actor ipnauth.Actor, n *ipn.Notify) bool {
|
|
|
+ if n.BrowseToURL != nil {
|
|
|
+ t.Errorf("Unexpected BrowseToURL(%s): %v", actorDescriptionForTest(actor), n)
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ logInAs ipnauth.Actor
|
|
|
+ urlExpectedBy []ipnauth.Actor
|
|
|
+ urlUnexpectedBy []ipnauth.Actor
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "NoObservers",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A"},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{}, // ensure that it does not panic if no one is watching
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "SingleUser",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A"},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A"}},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "SameUser/TwoSessions/NoCID",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A"},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A"}, &ipnauth.TestActor{UID: "A"}},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "SameUser/TwoSessions/OneWithCID",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A", CID: ipnauth.ClientIDFrom("123")},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A", CID: ipnauth.ClientIDFrom("123")}},
|
|
|
+ urlUnexpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A"}},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "SameUser/TwoSessions/BothWithCID",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A", CID: ipnauth.ClientIDFrom("123")},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A", CID: ipnauth.ClientIDFrom("123")}},
|
|
|
+ urlUnexpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A", CID: ipnauth.ClientIDFrom("456")}},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "DifferentUsers/NoCID",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A"},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A"}},
|
|
|
+ urlUnexpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "B"}},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "DifferentUsers/SameCID",
|
|
|
+ logInAs: &ipnauth.TestActor{UID: "A"},
|
|
|
+ urlExpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "A", CID: ipnauth.ClientIDFrom("123")}},
|
|
|
+ urlUnexpectedBy: []ipnauth.Actor{&ipnauth.TestActor{UID: "B", CID: ipnauth.ClientIDFrom("123")}},
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ lb := newLocalBackendWithTestControl(t, enableLogging, func(tb testing.TB, opts controlclient.Options) controlclient.Client {
|
|
|
+ return newClient(tb, opts)
|
|
|
+ })
|
|
|
+ if _, err := lb.EditPrefs(&ipn.MaskedPrefs{ControlURLSet: true, Prefs: ipn.Prefs{ControlURL: controlURL}}); err != nil {
|
|
|
+ t.Fatalf("(*EditPrefs).Start(): %v", err)
|
|
|
+ }
|
|
|
+ if err := lb.Start(ipn.Options{}); err != nil {
|
|
|
+ t.Fatalf("(*LocalBackend).Start(): %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ sessions := make([]*notificationWatcher, 0, len(tt.urlExpectedBy)+len(tt.urlUnexpectedBy))
|
|
|
+ for _, actor := range tt.urlExpectedBy {
|
|
|
+ session := newNotificationWatcher(t, lb, actor)
|
|
|
+ session.watch(0, []wantedNotification{wantBrowseToURL})
|
|
|
+ sessions = append(sessions, session)
|
|
|
+ }
|
|
|
+ for _, actor := range tt.urlUnexpectedBy {
|
|
|
+ session := newNotificationWatcher(t, lb, actor)
|
|
|
+ session.watch(0, nil, unexpectedBrowseToURL)
|
|
|
+ sessions = append(sessions, session)
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := lb.StartLoginInteractiveAs(context.Background(), tt.logInAs); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ lb.cc.(*mockControl).send(nil, loginURL, false, nil)
|
|
|
+
|
|
|
+ var wg sync.WaitGroup
|
|
|
+ wg.Add(len(sessions))
|
|
|
+ for _, sess := range sessions {
|
|
|
+ go func() { // check all sessions in parallel
|
|
|
+ sess.check()
|
|
|
+ wg.Done()
|
|
|
+ }()
|
|
|
+ }
|
|
|
+ wg.Wait()
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|