Signed-off-by: Maisem Ali <maisem@tailscale.com>main
parent
c9d6a9cb4d
commit
4d330bac14
@ -0,0 +1,439 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"math/rand" |
||||
"runtime" |
||||
"time" |
||||
|
||||
"golang.org/x/exp/slices" |
||||
"tailscale.com/ipn" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/util/strs" |
||||
"tailscale.com/version" |
||||
) |
||||
|
||||
// profileManager is a wrapper around a StateStore that manages
|
||||
// multiple profiles and the current profile.
|
||||
type profileManager struct { |
||||
store ipn.StateStore |
||||
logf logger.Logf |
||||
|
||||
currentUserID string // only used on Windows
|
||||
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile |
||||
currentProfile *ipn.LoginProfile |
||||
prefs ipn.PrefsView |
||||
|
||||
// isNewProfile is a sentinel value that indicates that the
|
||||
// current profile is new and has not been saved to disk yet.
|
||||
// It is reset to false after a call to SetPrefs with a filled
|
||||
// in LoginName.
|
||||
isNewProfile bool |
||||
} |
||||
|
||||
// CurrentUser returns the current user ID. It is only non-empty on
|
||||
// Windows where we have a multi-user system.
|
||||
func (pm *profileManager) CurrentUser() string { |
||||
return pm.currentUserID |
||||
} |
||||
|
||||
// SetCurrentUser sets the current user ID. The uid is only non-empty
|
||||
// on Windows where we have a multi-user system.
|
||||
func (pm *profileManager) SetCurrentUser(uid string) error { |
||||
if pm.currentUserID == uid { |
||||
return nil |
||||
} |
||||
cpk := ipn.CurrentProfileKey(uid) |
||||
if b, err := pm.store.ReadState(cpk); err == nil { |
||||
pk := ipn.StateKey(string(b)) |
||||
prefs, err := pm.loadSavedPrefs(pk) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
pm.currentProfile = pm.findProfileByKey(pk) |
||||
pm.prefs = prefs |
||||
pm.isNewProfile = false |
||||
} else if err == ipn.ErrStateNotExist { |
||||
pm.NewProfile() |
||||
} else { |
||||
return err |
||||
} |
||||
pm.currentUserID = uid |
||||
return nil |
||||
} |
||||
|
||||
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile { |
||||
for _, p := range pm.knownProfiles { |
||||
if p.Name == name { |
||||
return p |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile { |
||||
for _, p := range pm.knownProfiles { |
||||
if p.Key == key { |
||||
return p |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (pm *profileManager) setUnattendedModeAsConfigured() error { |
||||
if pm.currentUserID == "" { |
||||
return nil |
||||
} |
||||
|
||||
if pm.prefs.ForceDaemon() { |
||||
return pm.store.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key)) |
||||
} else { |
||||
return pm.store.WriteState(ipn.ServerModeStartKey, nil) |
||||
} |
||||
} |
||||
|
||||
// Reset unloads the current profile, if any.
|
||||
func (pm *profileManager) Reset() { |
||||
pm.currentUserID = "" |
||||
pm.NewProfile() |
||||
} |
||||
|
||||
func init() { |
||||
rand.Seed(time.Now().UnixNano()) |
||||
} |
||||
|
||||
// SetPrefs sets the current profile's prefs to the provided value.
|
||||
// It also saves the prefs to the StateStore. It stores a copy of the
|
||||
// provided prefs, which may be accessed via CurrentPrefs.
|
||||
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error { |
||||
prefs := prefsIn.AsStruct().View() |
||||
ps := prefs.Persist() |
||||
if ps == nil || ps.LoginName == "" { |
||||
return pm.setPrefsLocked(prefs) |
||||
} |
||||
up := ps.UserProfile |
||||
if up.LoginName == "" { |
||||
up.LoginName = ps.LoginName |
||||
} |
||||
if up.DisplayName == "" { |
||||
up.DisplayName = up.LoginName |
||||
} |
||||
cp := pm.currentProfile |
||||
if pm.isNewProfile { |
||||
pm.isNewProfile = false |
||||
cp.ID, cp.Key = newUnusedID(pm.knownProfiles) |
||||
cp.Name = ps.LoginName |
||||
cp.UserProfile = ps.UserProfile |
||||
cp.LocalUserID = pm.currentUserID |
||||
} else { |
||||
cp.UserProfile = ps.UserProfile |
||||
} |
||||
pm.knownProfiles[cp.ID] = cp |
||||
if err := pm.writeKnownProfiles(); err != nil { |
||||
return err |
||||
} |
||||
if err := pm.setAsUserSelectedProfileLocked(); err != nil { |
||||
return err |
||||
} |
||||
if err := pm.setPrefsLocked(prefs); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.ProfileID, ipn.StateKey) { |
||||
var idb [2]byte |
||||
for { |
||||
rand.Read(idb[:]) |
||||
id := ipn.ProfileID(fmt.Sprintf("%x", idb)) |
||||
if _, ok := knownProfiles[id]; ok { |
||||
continue |
||||
} |
||||
return id, ipn.StateKey("profile-" + id) |
||||
} |
||||
} |
||||
|
||||
// setPrefsLocked sets the current profile's prefs to the provided value.
|
||||
// It also saves the prefs to the StateStore, if the current profile
|
||||
// is not new.
|
||||
func (pm *profileManager) setPrefsLocked(clonedPrefs ipn.PrefsView) error { |
||||
pm.prefs = clonedPrefs |
||||
if pm.isNewProfile { |
||||
return nil |
||||
} |
||||
if err := pm.writePrefsToStore(pm.currentProfile.Key, pm.prefs); err != nil { |
||||
return err |
||||
} |
||||
return pm.setUnattendedModeAsConfigured() |
||||
} |
||||
|
||||
func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsView) error { |
||||
if key == "" { |
||||
return nil |
||||
} |
||||
if err := pm.store.WriteState(key, prefs.ToBytes()); err != nil { |
||||
pm.logf("WriteState(%q): %v", key, err) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Profiles returns the list of known profiles.
|
||||
func (pm *profileManager) Profiles() []ipn.LoginProfile { |
||||
var profiles []ipn.LoginProfile |
||||
for _, p := range pm.knownProfiles { |
||||
if p.LocalUserID == pm.currentUserID { |
||||
profiles = append(profiles, *p) |
||||
} |
||||
} |
||||
slices.SortFunc(profiles, func(a, b ipn.LoginProfile) bool { |
||||
return a.Name < b.Name |
||||
}) |
||||
return profiles |
||||
} |
||||
|
||||
// SwitchProfile switches to the profile with the given id.
|
||||
// If the profile is not known, it returns an errProfileNotFound.
|
||||
func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error { |
||||
kp, ok := pm.knownProfiles[id] |
||||
if !ok { |
||||
return errProfileNotFound |
||||
} |
||||
|
||||
if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() { |
||||
return nil |
||||
} |
||||
if kp.LocalUserID != pm.currentUserID { |
||||
return fmt.Errorf("profile %q is not owned by current user", id) |
||||
} |
||||
prefs, err := pm.loadSavedPrefs(kp.Key) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
pm.prefs = prefs |
||||
pm.currentProfile = kp |
||||
pm.isNewProfile = false |
||||
return pm.setAsUserSelectedProfileLocked() |
||||
} |
||||
|
||||
func (pm *profileManager) setAsUserSelectedProfileLocked() error { |
||||
k := ipn.CurrentProfileKey(pm.currentUserID) |
||||
return pm.store.WriteState(k, []byte(pm.currentProfile.Key)) |
||||
} |
||||
|
||||
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) { |
||||
bs, err := pm.store.ReadState(key) |
||||
if err != nil { |
||||
if err == ipn.ErrStateNotExist { |
||||
return emptyPrefs, nil |
||||
} |
||||
return ipn.PrefsView{}, err |
||||
} |
||||
savedPrefs, err := ipn.PrefsFromBytes(bs) |
||||
if err != nil { |
||||
return ipn.PrefsView{}, fmt.Errorf("PrefsFromBytes: %v", err) |
||||
} |
||||
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty()) |
||||
|
||||
// Ignore any old stored preferences for https://login.tailscale.com
|
||||
// as the control server that would override the new default of
|
||||
// controlplane.tailscale.com.
|
||||
if savedPrefs.ControlURL != "" && |
||||
savedPrefs.ControlURL != ipn.DefaultControlURL && |
||||
ipn.IsLoginServerSynonym(savedPrefs.ControlURL) { |
||||
savedPrefs.ControlURL = "" |
||||
} |
||||
return savedPrefs.View(), nil |
||||
} |
||||
|
||||
// CurrentProfile returns the name and ID of the current profile, or "" if the profile
|
||||
// is not named.
|
||||
func (pm *profileManager) CurrentProfile() ipn.LoginProfile { |
||||
return *pm.currentProfile |
||||
} |
||||
|
||||
// errProfileNotFound is returned by methods that accept a ProfileID.
|
||||
var errProfileNotFound = errors.New("profile not found") |
||||
|
||||
// DeleteProfile removes the profile with the given id. It returns
|
||||
// errProfileNotFound if the profile does not exist.
|
||||
// If the profile is the current profile, it is the equivalent of
|
||||
// calling NewProfile() followed by DeleteProfile(id). This is
|
||||
// useful for deleting the last profile. In other cases, it is
|
||||
// recommended to call SwitchProfile() first.
|
||||
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error { |
||||
kp, ok := pm.knownProfiles[id] |
||||
if !ok { |
||||
return errProfileNotFound |
||||
} |
||||
if kp.ID == pm.currentProfile.ID { |
||||
pm.NewProfile() |
||||
} |
||||
if err := pm.store.WriteState(kp.Key, nil); err != nil { |
||||
return err |
||||
} |
||||
delete(pm.knownProfiles, id) |
||||
return pm.writeKnownProfiles() |
||||
} |
||||
|
||||
func (pm *profileManager) writeKnownProfiles() error { |
||||
b, err := json.Marshal(pm.knownProfiles) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return pm.store.WriteState(ipn.KnownProfilesStateKey, b) |
||||
} |
||||
|
||||
// NewProfile creates and switches to a new unnamed profile. The new profile is
|
||||
// not persisted until SetPrefs is called with a logged-in user.
|
||||
func (pm *profileManager) NewProfile() { |
||||
pm.prefs = emptyPrefs |
||||
pm.isNewProfile = true |
||||
pm.currentProfile = &ipn.LoginProfile{} |
||||
} |
||||
|
||||
// emptyPrefs is the default prefs for a new profile.
|
||||
var emptyPrefs = func() ipn.PrefsView { |
||||
prefs := ipn.NewPrefs() |
||||
prefs.WantRunning = false |
||||
return prefs.View() |
||||
}() |
||||
|
||||
// Store returns the StateStore used by the ProfileManager.
|
||||
func (pm *profileManager) Store() ipn.StateStore { |
||||
return pm.store |
||||
} |
||||
|
||||
// CurrentPrefs returns a read-only view of the current prefs.
|
||||
func (pm *profileManager) CurrentPrefs() ipn.PrefsView { |
||||
return pm.prefs |
||||
} |
||||
|
||||
// ReadStartupPrefsForTest reads the startup prefs from disk. It is only used for testing.
|
||||
func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsView, error) { |
||||
pm, err := newProfileManager(store, logf, "") |
||||
if err != nil { |
||||
return ipn.PrefsView{}, err |
||||
} |
||||
return pm.CurrentPrefs(), nil |
||||
} |
||||
|
||||
// newProfileManager creates a new ProfileManager using the provided StateStore.
|
||||
// It also loads the list of known profiles from the StateStore.
|
||||
// If a state key is provided, it will be used to load the current profile.
|
||||
func newProfileManager(store ipn.StateStore, logf logger.Logf, stateKey ipn.StateKey) (*profileManager, error) { |
||||
return newProfileManagerWithGOOS(store, logf, stateKey, runtime.GOOS) |
||||
} |
||||
|
||||
func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) { |
||||
startKey := ipn.CurrentProfileStateKey |
||||
if goos == "windows" { |
||||
// When tailscaled runs on Windows it is not typically run unattended.
|
||||
// So we can't use the profile mechanism to load the profile at startup.
|
||||
startKey = ipn.ServerModeStartKey |
||||
} |
||||
autoStartKey, err := store.ReadState(startKey) |
||||
if err != nil && err != ipn.ErrStateNotExist { |
||||
return "", fmt.Errorf("calling ReadState on state store: %w", err) |
||||
} |
||||
return ipn.StateKey(autoStartKey), nil |
||||
} |
||||
|
||||
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) { |
||||
var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile |
||||
prfB, err := store.ReadState(ipn.KnownProfilesStateKey) |
||||
switch err { |
||||
case nil: |
||||
if err := json.Unmarshal(prfB, &knownProfiles); err != nil { |
||||
return nil, fmt.Errorf("unmarshaling known profiles: %w", err) |
||||
} |
||||
case ipn.ErrStateNotExist: |
||||
knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile) |
||||
default: |
||||
return nil, fmt.Errorf("calling ReadState on state store: %w", err) |
||||
} |
||||
return knownProfiles, nil |
||||
} |
||||
|
||||
func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, stateKey ipn.StateKey, goos string) (*profileManager, error) { |
||||
if stateKey == "" { |
||||
var err error |
||||
stateKey, err = readAutoStartKey(store, goos) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
knownProfiles, err := readKnownProfiles(store) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
pm := &profileManager{ |
||||
store: store, |
||||
knownProfiles: knownProfiles, |
||||
logf: logf, |
||||
} |
||||
|
||||
if stateKey != "" { |
||||
for _, v := range knownProfiles { |
||||
if v.Key == stateKey { |
||||
pm.currentProfile = v |
||||
} |
||||
} |
||||
if pm.currentProfile == nil { |
||||
if suf, ok := strs.CutPrefix(string(stateKey), "user-"); ok { |
||||
pm.currentUserID = suf |
||||
} |
||||
pm.NewProfile() |
||||
} else { |
||||
pm.currentUserID = pm.currentProfile.LocalUserID |
||||
} |
||||
prefs, err := pm.loadSavedPrefs(stateKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if err := pm.setPrefsLocked(prefs); err != nil { |
||||
return nil, err |
||||
} |
||||
} else if len(knownProfiles) == 0 && goos != "windows" { |
||||
// No known profiles, try a migration.
|
||||
if err := pm.migrateFromLegacyPrefs(); err != nil { |
||||
return nil, err |
||||
} |
||||
} else { |
||||
pm.NewProfile() |
||||
} |
||||
|
||||
return pm, nil |
||||
} |
||||
|
||||
func (pm *profileManager) migrateFromLegacyPrefs() error { |
||||
pm.NewProfile() |
||||
k := ipn.LegacyGlobalDaemonStateKey |
||||
switch { |
||||
case runtime.GOOS == "ios": |
||||
k = "ipn-go-bridge" |
||||
case version.IsSandboxedMacOS(): |
||||
k = "ipn-go-bridge" |
||||
case runtime.GOOS == "android": |
||||
k = "ipn-android" |
||||
} |
||||
prefs, err := pm.loadSavedPrefs(k) |
||||
if err != nil { |
||||
return fmt.Errorf("calling ReadState on state store: %w", err) |
||||
} |
||||
pm.logf("migrating %q profile to new format", k) |
||||
if err := pm.SetPrefs(prefs); err != nil { |
||||
return fmt.Errorf("migrating _daemon profile: %w", err) |
||||
} |
||||
// Do not delete the old state key, as we may be downgraded to an
|
||||
// older version that still relies on it.
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,221 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"tailscale.com/ipn" |
||||
"tailscale.com/ipn/store/mem" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/types/persist" |
||||
) |
||||
|
||||
// TestProfileManagement tests creating, loading, and switching profiles.
|
||||
func TestProfileManagement(t *testing.T) { |
||||
store := new(mem.Store) |
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "", "linux") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
wantCurProfile := "" |
||||
wantProfiles := map[string]ipn.PrefsView{ |
||||
"": emptyPrefs, |
||||
} |
||||
checkProfiles := func(t *testing.T) { |
||||
t.Helper() |
||||
prof := pm.CurrentProfile() |
||||
t.Logf("\tCurrentProfile = %q", prof) |
||||
if prof.Name != wantCurProfile { |
||||
t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) |
||||
} |
||||
profiles := pm.Profiles() |
||||
wantLen := len(wantProfiles) |
||||
if _, ok := wantProfiles[""]; ok { |
||||
wantLen-- |
||||
} |
||||
if len(profiles) != wantLen { |
||||
t.Fatalf("Profiles = %v; want %v", profiles, wantProfiles) |
||||
} |
||||
p := pm.CurrentPrefs() |
||||
if !p.Valid() { |
||||
t.Fatalf("CurrentPrefs = %v; want valid", p) |
||||
} |
||||
if !p.Equals(wantProfiles[wantCurProfile]) { |
||||
t.Fatalf("CurrentPrefs = %v; want %v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) |
||||
} |
||||
for _, p := range profiles { |
||||
got, err := pm.loadSavedPrefs(p.Key) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
// Use Hostname as a proxy for all prefs.
|
||||
if got.Hostname() != wantProfiles[p.Name].Hostname() { |
||||
t.Fatalf("Prefs for profile %q = %v; want %v", p, got.Pretty(), wantProfiles[p.Name].Pretty()) |
||||
} |
||||
} |
||||
} |
||||
setPrefs := func(t *testing.T, loginName string) ipn.PrefsView { |
||||
p := pm.CurrentPrefs().AsStruct() |
||||
p.Persist = &persist.Persist{ |
||||
LoginName: loginName, |
||||
} |
||||
if err := pm.SetPrefs(p.View()); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return p.View() |
||||
} |
||||
t.Logf("Check initial state from empty store") |
||||
checkProfiles(t) |
||||
|
||||
{ |
||||
t.Logf("Set prefs for default profile") |
||||
wantProfiles["user@1.example.com"] = setPrefs(t, "user@1.example.com") |
||||
wantCurProfile = "user@1.example.com" |
||||
delete(wantProfiles, "") |
||||
} |
||||
checkProfiles(t) |
||||
|
||||
t.Logf("Create new profile") |
||||
pm.NewProfile() |
||||
wantCurProfile = "" |
||||
wantProfiles[""] = emptyPrefs |
||||
checkProfiles(t) |
||||
|
||||
{ |
||||
t.Logf("Set prefs for test profile") |
||||
wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com") |
||||
wantCurProfile = "user@2.example.com" |
||||
delete(wantProfiles, "") |
||||
} |
||||
checkProfiles(t) |
||||
|
||||
t.Logf("Recreate profile manager from store") |
||||
// Recreate the profile manager to ensure that it can load the profiles
|
||||
// from the store at startup.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "linux") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
checkProfiles(t) |
||||
|
||||
t.Logf("Delete default profile") |
||||
if err := pm.DeleteProfile(pm.findProfileByName("user@1.example.com").ID); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
delete(wantProfiles, "user@1.example.com") |
||||
checkProfiles(t) |
||||
|
||||
t.Logf("Recreate profile manager from store after deleting default profile") |
||||
// Recreate the profile manager to ensure that it can load the profiles
|
||||
// from the store at startup.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "linux") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
checkProfiles(t) |
||||
} |
||||
|
||||
// TestProfileManagementWindows tests going into and out of Unattended mode on
|
||||
// Windows.
|
||||
func TestProfileManagementWindows(t *testing.T) { |
||||
store := new(mem.Store) |
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "", "windows") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
wantCurProfile := "" |
||||
wantProfiles := map[string]ipn.PrefsView{ |
||||
"": emptyPrefs, |
||||
} |
||||
checkProfiles := func(t *testing.T) { |
||||
t.Helper() |
||||
prof := pm.CurrentProfile() |
||||
t.Logf("\tCurrentProfile = %q", prof) |
||||
if prof.Name != wantCurProfile { |
||||
t.Fatalf("CurrentProfile = %q; want %q", prof, wantCurProfile) |
||||
} |
||||
if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) { |
||||
t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty()) |
||||
} |
||||
} |
||||
setPrefs := func(t *testing.T, loginName string, forceDaemon bool) ipn.PrefsView { |
||||
p := pm.CurrentPrefs().AsStruct() |
||||
p.ForceDaemon = forceDaemon |
||||
p.Persist = &persist.Persist{ |
||||
LoginName: loginName, |
||||
} |
||||
if err := pm.SetPrefs(p.View()); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return p.View() |
||||
} |
||||
t.Logf("Check initial state from empty store") |
||||
checkProfiles(t) |
||||
|
||||
{ |
||||
t.Logf("Set user1 as logged in user") |
||||
if err := pm.SetCurrentUser("user1"); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
checkProfiles(t) |
||||
t.Logf("Save prefs for user1") |
||||
wantProfiles["default"] = setPrefs(t, "default", false) |
||||
wantCurProfile = "default" |
||||
} |
||||
checkProfiles(t) |
||||
|
||||
{ |
||||
t.Logf("Create new profile") |
||||
pm.NewProfile() |
||||
wantCurProfile = "" |
||||
wantProfiles[""] = emptyPrefs |
||||
checkProfiles(t) |
||||
|
||||
t.Logf("Save as test profile") |
||||
wantProfiles["test"] = setPrefs(t, "test", false) |
||||
wantCurProfile = "test" |
||||
checkProfiles(t) |
||||
} |
||||
|
||||
t.Logf("Recreate profile manager from store, should reset prefs") |
||||
// Recreate the profile manager to ensure that it can load the profiles
|
||||
// from the store at startup.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "windows") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
wantCurProfile = "" |
||||
wantProfiles[""] = emptyPrefs |
||||
checkProfiles(t) |
||||
|
||||
{ |
||||
t.Logf("Set user1 as current user") |
||||
if err := pm.SetCurrentUser("user1"); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
wantCurProfile = "test" |
||||
} |
||||
checkProfiles(t) |
||||
{ |
||||
t.Logf("set unattended mode") |
||||
wantProfiles["test"] = setPrefs(t, "test", true) |
||||
} |
||||
if pm.CurrentUser() != "user1" { |
||||
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUser(), "user1") |
||||
} |
||||
|
||||
// Recreate the profile manager to ensure that it starts with test profile.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "", "windows") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
checkProfiles(t) |
||||
if pm.CurrentUser() != "user1" { |
||||
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUser(), "user1") |
||||
} |
||||
} |
||||
Loading…
Reference in new issue