mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-16 15:29:28 +00:00
feature,ipn/ipnlocal: add profileManager.StateChangeHook
We update profileManager to allow registering a single state (profile+prefs) change hook. This is to invert the dependency between the profileManager and the LocalBackend, so that instead of LocalBackend asking profileManager for the state, we can have profileManager call LocalBackend when the state changes. We also update feature.Hook with a new (*feature.Hook).GetOk method to avoid calling both IsSet and Get. Updates tailscale/corp#28014 Updates #12614 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
0cfd643d95
commit
66371f392a
@ -53,6 +53,12 @@ func (h *Hook[Func]) Get() Func {
|
||||
return h.f
|
||||
}
|
||||
|
||||
// GetOk returns the hook function and true if it has been set,
|
||||
// otherwise its zero value and false.
|
||||
func (h *Hook[Func]) GetOk() (f Func, ok bool) {
|
||||
return h.f, h.ok
|
||||
}
|
||||
|
||||
// Hooks is a slice of funcs.
|
||||
//
|
||||
// As opposed to a single Hook, this is meant to be used when
|
||||
|
@ -1675,7 +1675,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
|
||||
// Perform all mutations of prefs based on the netmap here.
|
||||
if prefsChanged {
|
||||
profile := b.pm.CurrentProfile()
|
||||
// Prefs will be written out if stale; this is not safe unless locked or cloned.
|
||||
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
|
||||
MagicDNSName: curNetMap.MagicDNSSuffix(),
|
||||
@ -1683,20 +1682,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
}); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
// Updating profile prefs may have resulted in a change to the current [ipn.LoginProfile],
|
||||
// either because the user completed a login, which populated and persisted their profile
|
||||
// for the first time, or because of an [ipn.NetworkProfile] or [tailcfg.UserProfile] change.
|
||||
// Theoretically, a completed login could also result in a switch to a different existing
|
||||
// profile representing a different node (see tailscale/tailscale#8816).
|
||||
//
|
||||
// Let's check if the current profile has changed, and invoke all registered
|
||||
// [ipnext.ProfileStateChangeCallback] if necessary.
|
||||
if cp := b.pm.CurrentProfile(); *cp.AsStruct() != *profile.AsStruct() {
|
||||
// If the profile ID was empty before SetPrefs, it's a new profile
|
||||
// and the user has just completed a login for the first time.
|
||||
sameNode := profile.ID() == "" || profile.ID() == cp.ID()
|
||||
b.extHost.NotifyProfileChange(profile, prefs.View(), sameNode)
|
||||
}
|
||||
}
|
||||
|
||||
// initTKALocked is dependent on CurrentProfile.ID, which is initialized
|
||||
|
@ -43,6 +43,15 @@ type profileManager struct {
|
||||
currentProfile ipn.LoginProfileView // always Valid (once [newProfileManager] returns).
|
||||
prefs ipn.PrefsView // always Valid (once [newProfileManager] returns).
|
||||
|
||||
// StateChangeHook is an optional hook that is called when the current profile or prefs change,
|
||||
// such as due to a profile switch or a change in the profile's preferences.
|
||||
// It is typically set by the [LocalBackend] to invert the dependency between
|
||||
// the [profileManager] and the [LocalBackend], so that instead of [LocalBackend]
|
||||
// asking [profileManager] for the state, we can have [profileManager] call
|
||||
// [LocalBackend] when the state changes. See also:
|
||||
// https://github.com/tailscale/tailscale/pull/15791#discussion_r2060838160
|
||||
StateChangeHook ipnext.ProfileStateChangeCallback
|
||||
|
||||
// extHost is the bridge between [profileManager] and the registered [ipnext.Extension]s.
|
||||
// It may be nil in tests. A nil pointer is a valid, no-op host.
|
||||
extHost *ExtensionHost
|
||||
@ -166,6 +175,16 @@ func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.
|
||||
// But if updating the default profile fails, we should log it.
|
||||
pm.logf("failed to set %s (%s) as the default profile: %v", profile.Name(), profile.ID(), err)
|
||||
}
|
||||
|
||||
if f := pm.StateChangeHook; f != nil {
|
||||
f(pm.currentProfile, pm.prefs, false)
|
||||
}
|
||||
// Do not call pm.extHost.NotifyProfileChange here; it is invoked in
|
||||
// [LocalBackend.resetForProfileChangeLockedOnEntry] after the netmap reset.
|
||||
// TODO(nickkhyl): Consider moving it here (or into the stateChangeCb handler
|
||||
// in [LocalBackend]) once the profile/node state, including the netmap,
|
||||
// is actually tied to the current profile.
|
||||
|
||||
return profile, true, nil
|
||||
}
|
||||
|
||||
@ -344,11 +363,19 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
|
||||
// [LocalBackend.resetForProfileChangeLockedOnEntry] is not called and certain
|
||||
// node/profile-specific state may not be reset as expected.
|
||||
//
|
||||
// However, LocalBackend notifies [ipnext.Extension]s about the profile change,
|
||||
// However, [profileManager] notifies [ipnext.Extension]s about the profile change,
|
||||
// so features migrated from LocalBackend to external packages should not be affected.
|
||||
//
|
||||
// See tailscale/corp#28014.
|
||||
pm.currentProfile = cp
|
||||
if !cp.Equals(pm.currentProfile) {
|
||||
const sameNode = false // implicit profile switch
|
||||
pm.currentProfile = cp
|
||||
pm.prefs = prefsIn.AsStruct().View()
|
||||
if f := pm.StateChangeHook; f != nil {
|
||||
f(cp, prefsIn, sameNode)
|
||||
}
|
||||
pm.extHost.NotifyProfileChange(cp, prefsIn, sameNode)
|
||||
}
|
||||
cp, err := pm.setProfilePrefs(nil, prefsIn, np)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -410,7 +437,20 @@ func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
|
||||
// Update the current profile view to reflect the changes
|
||||
// if the specified profile is the current profile.
|
||||
if isCurrentProfile {
|
||||
pm.currentProfile = lp.View()
|
||||
// Always set pm.currentProfile to the new profile view for pointer equality.
|
||||
// We check it further down the call stack.
|
||||
lp := lp.View()
|
||||
sameProfileInfo := lp.Equals(pm.currentProfile)
|
||||
pm.currentProfile = lp
|
||||
if !sameProfileInfo {
|
||||
// But only invoke the callbacks if the profile info has actually changed.
|
||||
const sameNode = true // just an info update; still the same node
|
||||
pm.prefs = prefsIn.AsStruct().View() // suppress further callbacks for this change
|
||||
if f := pm.StateChangeHook; f != nil {
|
||||
f(lp, prefsIn, sameNode)
|
||||
}
|
||||
pm.extHost.NotifyProfileChange(lp, prefsIn, sameNode)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty profile.ID indicates that the node info is not available yet,
|
||||
@ -470,7 +510,13 @@ func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileVie
|
||||
// That said, regardless of the cleanup, we might want
|
||||
// to keep the profileManager responsible for invoking
|
||||
// profile- and prefs-related callbacks.
|
||||
pm.extHost.NotifyProfilePrefsChanged(pm.currentProfile, oldPrefs, clonedPrefs)
|
||||
|
||||
if !clonedPrefs.Equals(oldPrefs) {
|
||||
if f := pm.StateChangeHook; f != nil {
|
||||
f(pm.currentProfile, clonedPrefs, true)
|
||||
}
|
||||
pm.extHost.NotifyProfilePrefsChanged(pm.currentProfile, oldPrefs, clonedPrefs)
|
||||
}
|
||||
|
||||
pm.updateHealth()
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@ -609,3 +610,535 @@ func TestDefaultPrefs(t *testing.T) {
|
||||
t.Errorf("defaultPrefs is %s, want %s; defaultPrefs should only modify WantRunning and LoggedOut, all other defaults should be in ipn.NewPrefs.", p2.Pretty(), p1.Pretty())
|
||||
}
|
||||
}
|
||||
|
||||
// mutPrefsFn is a function that mutates the prefs.
|
||||
// Deserialization pre‑populates prefs with default (non‑zero) values.
|
||||
// After saving prefs and reading them back, we may not get exactly what we set.
|
||||
// For this reason, tests apply changes through a helper that mutates
|
||||
// [ipn.NewPrefs] instead of hard‑coding expected values in each case.
|
||||
type mutPrefsFn func(*ipn.Prefs)
|
||||
|
||||
type profileState struct {
|
||||
*ipn.LoginProfile
|
||||
mutPrefs mutPrefsFn
|
||||
}
|
||||
|
||||
func (s *profileState) prefs() ipn.PrefsView {
|
||||
prefs := ipn.NewPrefs() // apply changes to the default prefs
|
||||
s.mutPrefs(prefs)
|
||||
return prefs.View()
|
||||
}
|
||||
|
||||
type profileStateChange struct {
|
||||
*ipn.LoginProfile
|
||||
mutPrefs mutPrefsFn
|
||||
sameNode bool
|
||||
}
|
||||
|
||||
func wantProfileChange(state profileState) profileStateChange {
|
||||
return profileStateChange{
|
||||
LoginProfile: state.LoginProfile,
|
||||
mutPrefs: state.mutPrefs,
|
||||
sameNode: false,
|
||||
}
|
||||
}
|
||||
|
||||
func wantPrefsChange(state profileState) profileStateChange {
|
||||
return profileStateChange{
|
||||
LoginProfile: state.LoginProfile,
|
||||
mutPrefs: state.mutPrefs,
|
||||
sameNode: true,
|
||||
}
|
||||
}
|
||||
|
||||
func makeDefaultPrefs(p *ipn.Prefs) { *p = *defaultPrefs.AsStruct() }
|
||||
|
||||
func makeKnownProfileState(id int, nameSuffix string, uid ipn.WindowsUserID, mutPrefs mutPrefsFn) profileState {
|
||||
lowerNameSuffix := strings.ToLower(nameSuffix)
|
||||
nid := "node-" + tailcfg.StableNodeID(lowerNameSuffix)
|
||||
up := tailcfg.UserProfile{
|
||||
ID: tailcfg.UserID(id),
|
||||
LoginName: fmt.Sprintf("user-%s@example.com", lowerNameSuffix),
|
||||
DisplayName: "User " + nameSuffix,
|
||||
}
|
||||
return profileState{
|
||||
LoginProfile: &ipn.LoginProfile{
|
||||
LocalUserID: uid,
|
||||
Name: up.LoginName,
|
||||
ID: ipn.ProfileID(fmt.Sprintf("%04X", id)),
|
||||
Key: "profile-" + ipn.StateKey(nameSuffix),
|
||||
NodeID: nid,
|
||||
UserProfile: up,
|
||||
},
|
||||
mutPrefs: func(p *ipn.Prefs) {
|
||||
p.Hostname = "Hostname-" + nameSuffix
|
||||
if mutPrefs != nil {
|
||||
mutPrefs(p) // apply any additional changes
|
||||
}
|
||||
p.Persist = &persist.Persist{NodeID: nid, UserProfile: up}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileStateChangeCallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A few well-known profiles to use in tests.
|
||||
emptyProfile := profileState{
|
||||
LoginProfile: &ipn.LoginProfile{},
|
||||
mutPrefs: makeDefaultPrefs,
|
||||
}
|
||||
profile0000 := profileState{
|
||||
LoginProfile: &ipn.LoginProfile{ID: "0000", Key: "profile-0000"},
|
||||
mutPrefs: makeDefaultPrefs,
|
||||
}
|
||||
profileA := makeKnownProfileState(0xA, "A", "", nil)
|
||||
profileB := makeKnownProfileState(0xB, "B", "", nil)
|
||||
profileC := makeKnownProfileState(0xC, "C", "", nil)
|
||||
|
||||
aliceUserID := ipn.WindowsUserID("S-1-5-21-1-2-3-4")
|
||||
aliceEmptyProfile := profileState{
|
||||
LoginProfile: &ipn.LoginProfile{LocalUserID: aliceUserID},
|
||||
mutPrefs: makeDefaultPrefs,
|
||||
}
|
||||
bobUserID := ipn.WindowsUserID("S-1-5-21-3-4-5-6")
|
||||
bobEmptyProfile := profileState{
|
||||
LoginProfile: &ipn.LoginProfile{LocalUserID: bobUserID},
|
||||
mutPrefs: makeDefaultPrefs,
|
||||
}
|
||||
bobKnownProfile := makeKnownProfileState(0xB0B, "Bob", bobUserID, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
initial *profileState // if non-nil, this is the initial profile and prefs to start wit
|
||||
knownProfiles []profileState // known profiles we can switch to
|
||||
action func(*profileManager) // action to take on the profile manager
|
||||
wantChanges []profileStateChange // expected state changes
|
||||
}{
|
||||
{
|
||||
name: "no-changes",
|
||||
action: func(*profileManager) {
|
||||
// do nothing
|
||||
},
|
||||
wantChanges: nil,
|
||||
},
|
||||
{
|
||||
name: "no-initial/new-profile",
|
||||
action: func(pm *profileManager) {
|
||||
// The profile manager is new and started with a new empty profile.
|
||||
// This should not trigger a state change callback.
|
||||
pm.SwitchToNewProfile()
|
||||
},
|
||||
wantChanges: nil,
|
||||
},
|
||||
{
|
||||
name: "no-initial/new-profile-for-user",
|
||||
action: func(pm *profileManager) {
|
||||
// But switching to a new profile for a specific user should trigger
|
||||
// a state change callback.
|
||||
pm.SwitchToNewProfileForUser(aliceUserID)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// We want a new empty profile (owned by the specified user)
|
||||
// and the default prefs.
|
||||
wantProfileChange(aliceEmptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-initial/new-profile",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// And so does switching to a new profile when the initial profile
|
||||
// is non-empty.
|
||||
pm.SwitchToNewProfile()
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// We want a new empty profile and the default prefs.
|
||||
wantProfileChange(emptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-initial/new-profile/twice",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// If we switch to a new profile twice, we should only get one state change.
|
||||
pm.SwitchToNewProfile()
|
||||
pm.SwitchToNewProfile()
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// We want a new empty profile and the default prefs.
|
||||
wantProfileChange(emptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-initial/new-profile-for-user/twice",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// Unless we switch to a new profile for a specific user,
|
||||
// in which case we should get a state change twice.
|
||||
pm.SwitchToNewProfileForUser(aliceUserID)
|
||||
pm.SwitchToNewProfileForUser(aliceUserID) // no change here
|
||||
pm.SwitchToNewProfileForUser(bobUserID)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// Both profiles are empty, but they are owned by different users.
|
||||
wantProfileChange(aliceEmptyProfile),
|
||||
wantProfileChange(bobEmptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-initial/new-profile/twice/with-prefs-change",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// Or unless we switch to a new profile, change the prefs,
|
||||
// then switch to a new profile again. Since the current
|
||||
// profile is not empty after the prefs change, we should
|
||||
// get state changes for all three actions.
|
||||
pm.SwitchToNewProfile()
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
p.WantRunning = true
|
||||
pm.SetPrefs(p.View(), ipn.NetworkProfile{})
|
||||
pm.SwitchToNewProfile()
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(emptyProfile), // new empty profile
|
||||
wantPrefsChange(profileState{ // prefs change, same profile
|
||||
LoginProfile: &ipn.LoginProfile{},
|
||||
mutPrefs: func(p *ipn.Prefs) {
|
||||
*p = *defaultPrefs.AsStruct()
|
||||
p.WantRunning = true
|
||||
},
|
||||
}),
|
||||
wantProfileChange(emptyProfile), // new empty profile again
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-id",
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// Switching to a known profile by ID should trigger a state change callback.
|
||||
pm.SwitchToProfileByID(profileB.ID)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(profileB),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-id/non-existent",
|
||||
knownProfiles: []profileState{profileA, profileC}, // no profileB
|
||||
action: func(pm *profileManager) {
|
||||
// Switching to a non-existent profile should fail and not trigger a state change callback.
|
||||
pm.SwitchToProfileByID(profileB.ID)
|
||||
},
|
||||
wantChanges: []profileStateChange{},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-id/twice-same",
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// But only for the first switch.
|
||||
// The second switch to the same profile should not trigger a state change callback.
|
||||
pm.SwitchToProfileByID(profileB.ID)
|
||||
pm.SwitchToProfileByID(profileB.ID)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(profileB),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-id/many",
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// Same idea, but with multiple switches.
|
||||
pm.SwitchToProfileByID(profileB.ID) // switch to Profile-B
|
||||
pm.SwitchToProfileByID(profileB.ID) // then to Profile-B again (no change)
|
||||
pm.SwitchToProfileByID(profileC.ID) // then to Profile-C (change)
|
||||
pm.SwitchToProfileByID(profileA.ID) // then to Profile-A (change)
|
||||
pm.SwitchToProfileByID(profileB.ID) // then to Profile-B (change)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(profileB),
|
||||
wantProfileChange(profileC),
|
||||
wantProfileChange(profileA),
|
||||
wantProfileChange(profileB),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-view",
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// Switching to a known profile by an [ipn.LoginProfileView]
|
||||
// should also trigger a state change callback.
|
||||
pm.SwitchToProfile(profileB.View())
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(profileB),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-view/empty",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// SwitchToProfile supports switching to an empty profile.
|
||||
emptyProfile := &ipn.LoginProfile{}
|
||||
pm.SwitchToProfile(emptyProfile.View())
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(emptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-view/non-existent",
|
||||
knownProfiles: []profileState{profileA, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// Switching to a an unknown profile by an [ipn.LoginProfileView]
|
||||
// should fail and not trigger a state change callback.
|
||||
pm.SwitchToProfile(profileB.View())
|
||||
},
|
||||
wantChanges: []profileStateChange{},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-view/empty-for-user",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// And switching to an empty profile for a specific user also works.
|
||||
pm.SwitchToProfile(bobEmptyProfile.View())
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(bobEmptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "switch-to-profile/by-view/invalid",
|
||||
initial: &profile0000,
|
||||
action: func(pm *profileManager) {
|
||||
// Switching to an invalid profile should create and switch
|
||||
// to a new empty profile.
|
||||
pm.SwitchToProfile(ipn.LoginProfileView{})
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(emptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete-profile/current",
|
||||
initial: &profileA, // profileA is the current profile
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// Deleting the current profile should switch to a new empty profile.
|
||||
pm.DeleteProfile(profileA.ID)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(emptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete-profile/current-with-user",
|
||||
initial: &bobKnownProfile,
|
||||
knownProfiles: []profileState{profileA, profileB, profileC, bobKnownProfile},
|
||||
action: func(pm *profileManager) {
|
||||
// Similarly, deleting the current profile for a specific user should switch
|
||||
// to a new empty profile for that user (at least while the "current user"
|
||||
// is still a thing on Windows).
|
||||
pm.DeleteProfile(bobKnownProfile.ID)
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(bobEmptyProfile),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete-profile/non-current",
|
||||
initial: &profileA, // profileA is the current profile
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// But deleting a non-current profile should not trigger a state change callback.
|
||||
pm.DeleteProfile(profileB.ID)
|
||||
},
|
||||
wantChanges: []profileStateChange{},
|
||||
},
|
||||
{
|
||||
name: "set-prefs/new-profile",
|
||||
initial: &emptyProfile, // the current profile is empty
|
||||
action: func(pm *profileManager) {
|
||||
// The current profile is new and empty, but we can still set p.
|
||||
// This should trigger a state change callback.
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.Hostname = "New-Hostname"
|
||||
pm.SetPrefs(p.View(), ipn.NetworkProfile{})
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// Still an empty profile, but with new prefs.
|
||||
wantPrefsChange(profileState{
|
||||
LoginProfile: emptyProfile.LoginProfile,
|
||||
mutPrefs: func(p *ipn.Prefs) {
|
||||
*p = *emptyProfile.prefs().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.Hostname = "New-Hostname"
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set-prefs/current-profile",
|
||||
initial: &profileA, // profileA is the current profile
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.Hostname = "New-Hostname"
|
||||
pm.SetPrefs(p.View(), ipn.NetworkProfile{})
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantPrefsChange(profileState{
|
||||
LoginProfile: profileA.LoginProfile, // same profile
|
||||
mutPrefs: func(p *ipn.Prefs) { // but with new prefs
|
||||
*p = *profileA.prefs().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.Hostname = "New-Hostname"
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set-prefs/current-profile/profile-name",
|
||||
initial: &profileA, // profileA is the current profile
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
p.ProfileName = "This is User A"
|
||||
pm.SetPrefs(p.View(), ipn.NetworkProfile{})
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// Still the same profile, but with a new profile name
|
||||
// populated from the prefs. The prefs are also updated.
|
||||
wantPrefsChange(profileState{
|
||||
LoginProfile: func() *ipn.LoginProfile {
|
||||
p := profileA.Clone()
|
||||
p.Name = "This is User A"
|
||||
return p
|
||||
}(),
|
||||
mutPrefs: func(p *ipn.Prefs) {
|
||||
*p = *profileA.prefs().AsStruct()
|
||||
p.ProfileName = "This is User A"
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set-prefs/implicit-switch/from-new",
|
||||
initial: &emptyProfile, // a new, empty profile
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// The user attempted to add a new profile but actually logged in as the same
|
||||
// node/user as profileB. When [LocalBackend.SetControlClientStatus] calls
|
||||
// [profileManager.SetPrefs] with the [persist.Persist] for profileB, we
|
||||
// implicitly switch to that profile instead of creating a duplicate for the
|
||||
// same node/user.
|
||||
//
|
||||
// TODO(nickkhyl): currently, [LocalBackend.SetControlClientStatus] uses the p
|
||||
// of the current profile, not those of the profile we switch to. This is all wrong
|
||||
// and should be fixed. But for now, we just test that the state change callback
|
||||
// is called with the new profile and p.
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
p.Persist = profileB.prefs().Persist().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.LoggedOut = false
|
||||
pm.SetPrefs(p.View(), ipn.NetworkProfile{})
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
// Calling [profileManager.SetPrefs] like this is effectively a profile switch
|
||||
// rather than a prefs change.
|
||||
wantProfileChange(profileState{
|
||||
LoginProfile: profileB.LoginProfile,
|
||||
mutPrefs: func(p *ipn.Prefs) {
|
||||
*p = *emptyProfile.prefs().AsStruct()
|
||||
p.Persist = profileB.prefs().Persist().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.LoggedOut = false
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set-prefs/implicit-switch/from-other",
|
||||
initial: &profileA, // profileA is the current profile
|
||||
knownProfiles: []profileState{profileA, profileB, profileC},
|
||||
action: func(pm *profileManager) {
|
||||
// Same idea, but the current profile is profileA rather than a new empty profile.
|
||||
// Note: this is all wrong. See the comment above and [profileManager.SetPrefs].
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
p.Persist = profileB.prefs().Persist().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.LoggedOut = false
|
||||
pm.SetPrefs(p.View(), ipn.NetworkProfile{})
|
||||
},
|
||||
wantChanges: []profileStateChange{
|
||||
wantProfileChange(profileState{
|
||||
LoginProfile: profileB.LoginProfile,
|
||||
mutPrefs: func(p *ipn.Prefs) {
|
||||
*p = *profileA.prefs().AsStruct()
|
||||
p.Persist = profileB.prefs().Persist().AsStruct()
|
||||
p.WantRunning = true
|
||||
p.LoggedOut = false
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store := new(mem.Store)
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatalf("newProfileManagerWithGOOS: %v", err)
|
||||
}
|
||||
for _, p := range tt.knownProfiles {
|
||||
pm.writePrefsToStore(p.Key, p.prefs())
|
||||
pm.knownProfiles[p.ID] = p.View()
|
||||
}
|
||||
if err := pm.writeKnownProfiles(); err != nil {
|
||||
t.Fatalf("writeKnownProfiles: %v", err)
|
||||
}
|
||||
|
||||
if tt.initial != nil {
|
||||
pm.currentUserID = tt.initial.LocalUserID
|
||||
pm.currentProfile = tt.initial.View()
|
||||
pm.prefs = tt.initial.prefs()
|
||||
}
|
||||
|
||||
type stateChange struct {
|
||||
Profile *ipn.LoginProfile
|
||||
Prefs *ipn.Prefs
|
||||
SameNode bool
|
||||
}
|
||||
wantChanges := make([]stateChange, 0, len(tt.wantChanges))
|
||||
for _, w := range tt.wantChanges {
|
||||
wantPrefs := ipn.NewPrefs()
|
||||
w.mutPrefs(wantPrefs) // apply changes to the default prefs
|
||||
wantChanges = append(wantChanges, stateChange{
|
||||
Profile: w.LoginProfile,
|
||||
Prefs: wantPrefs,
|
||||
SameNode: w.sameNode,
|
||||
})
|
||||
}
|
||||
|
||||
gotChanges := make([]stateChange, 0, len(tt.wantChanges))
|
||||
pm.StateChangeHook = func(profile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
|
||||
gotChanges = append(gotChanges, stateChange{
|
||||
Profile: profile.AsStruct(),
|
||||
Prefs: prefs.AsStruct(),
|
||||
SameNode: sameNode,
|
||||
})
|
||||
}
|
||||
|
||||
tt.action(pm)
|
||||
|
||||
if diff := cmp.Diff(wantChanges, gotChanges, defaultCmpOpts...); diff != "" {
|
||||
t.Errorf("StateChange callbacks: (-want +got): %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user