mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
![Nick Khyl](/assets/img/avatar_default.png)
Currently, profileManager filters profiles based on their creator/owner and the "current user"'s UID. This causes DefaultUserProfileID(uid) to work incorrectly when the UID doesn't match the current user. While we plan to remove the concept of the "current user" completely, we're not there yet. In this PR, we fix DefaultUserProfileID by updating profileManager to allow checking profile access for a given UID and modifying helper methods to accept UID as a parameter when returning matching profiles. Updates #14823 Signed-off-by: Nick Khyl <nickk@tailscale.com>
612 lines
16 KiB
Go
612 lines
16 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"fmt"
|
|
"os/user"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"tailscale.com/clientupdate"
|
|
"tailscale.com/health"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/store/mem"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/persist"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
func TestProfileCurrentUserSwitch(t *testing.T) {
|
|
store := new(mem.Store)
|
|
|
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id := 0
|
|
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
|
id++
|
|
t.Helper()
|
|
pm.NewProfile()
|
|
p := pm.CurrentPrefs().AsStruct()
|
|
p.Persist = &persist.Persist{
|
|
NodeID: tailcfg.StableNodeID(fmt.Sprint(id)),
|
|
PrivateNodeKey: key.NewNode(),
|
|
UserProfile: tailcfg.UserProfile{
|
|
ID: tailcfg.UserID(id),
|
|
LoginName: loginName,
|
|
},
|
|
}
|
|
if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p.View()
|
|
}
|
|
|
|
pm.SetCurrentUserID("user1")
|
|
newProfile(t, "user1")
|
|
cp := pm.currentProfile
|
|
pm.DeleteProfile(cp.ID())
|
|
if !pm.currentProfile.Valid() {
|
|
t.Fatal("currentProfile is nil")
|
|
} else if pm.currentProfile.ID() != "" {
|
|
t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID())
|
|
}
|
|
if !pm.CurrentPrefs().Equals(defaultPrefs) {
|
|
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
|
|
}
|
|
|
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pm.SetCurrentUserID("user1")
|
|
if !pm.currentProfile.Valid() {
|
|
t.Fatal("currentProfile is nil")
|
|
} else if pm.currentProfile.ID() != "" {
|
|
t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID())
|
|
}
|
|
if !pm.CurrentPrefs().Equals(defaultPrefs) {
|
|
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
|
|
}
|
|
}
|
|
|
|
func TestProfileList(t *testing.T) {
|
|
store := new(mem.Store)
|
|
|
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id := 0
|
|
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
|
id++
|
|
t.Helper()
|
|
pm.NewProfile()
|
|
p := pm.CurrentPrefs().AsStruct()
|
|
p.Persist = &persist.Persist{
|
|
NodeID: tailcfg.StableNodeID(fmt.Sprint(id)),
|
|
PrivateNodeKey: key.NewNode(),
|
|
UserProfile: tailcfg.UserProfile{
|
|
ID: tailcfg.UserID(id),
|
|
LoginName: loginName,
|
|
},
|
|
}
|
|
if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p.View()
|
|
}
|
|
checkProfiles := func(t *testing.T, want ...string) {
|
|
t.Helper()
|
|
got := pm.Profiles()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d profiles, want %d", len(got), len(want))
|
|
}
|
|
for i, w := range want {
|
|
if got[i].Name() != w {
|
|
t.Errorf("got profile %d name %q, want %q", i, got[i].Name(), w)
|
|
}
|
|
}
|
|
}
|
|
|
|
pm.SetCurrentUserID("user1")
|
|
newProfile(t, "alice")
|
|
newProfile(t, "bob")
|
|
checkProfiles(t, "alice", "bob")
|
|
|
|
pm.SetCurrentUserID("user2")
|
|
checkProfiles(t)
|
|
newProfile(t, "carol")
|
|
carol := pm.currentProfile
|
|
checkProfiles(t, "carol")
|
|
|
|
pm.SetCurrentUserID("user1")
|
|
checkProfiles(t, "alice", "bob")
|
|
if lp := pm.findProfileByKey("user1", carol.Key()); lp.Valid() {
|
|
t.Fatalf("found profile for user2 in user1's profile list")
|
|
}
|
|
if lp := pm.findProfileByName("user1", carol.Name()); lp.Valid() {
|
|
t.Fatalf("found profile for user2 in user1's profile list")
|
|
}
|
|
|
|
pm.SetCurrentUserID("user2")
|
|
checkProfiles(t, "carol")
|
|
}
|
|
|
|
func TestProfileDupe(t *testing.T) {
|
|
newPersist := func(user, node int) *persist.Persist {
|
|
return &persist.Persist{
|
|
NodeID: tailcfg.StableNodeID(fmt.Sprintf("node%d", node)),
|
|
UserProfile: tailcfg.UserProfile{
|
|
ID: tailcfg.UserID(user),
|
|
LoginName: fmt.Sprintf("user%d@example.com", user),
|
|
},
|
|
}
|
|
}
|
|
user1Node1 := newPersist(1, 1)
|
|
user1Node2 := newPersist(1, 2)
|
|
user2Node1 := newPersist(2, 1)
|
|
user2Node2 := newPersist(2, 2)
|
|
user3Node3 := newPersist(3, 3)
|
|
|
|
reauth := func(pm *profileManager, p *persist.Persist) {
|
|
prefs := ipn.NewPrefs()
|
|
prefs.Persist = p
|
|
must.Do(pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}))
|
|
}
|
|
login := func(pm *profileManager, p *persist.Persist) {
|
|
pm.NewProfile()
|
|
reauth(pm, p)
|
|
}
|
|
|
|
type step struct {
|
|
fn func(pm *profileManager, p *persist.Persist)
|
|
p *persist.Persist
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
steps []step
|
|
profs []*persist.Persist
|
|
}{
|
|
{
|
|
name: "reauth-new-node",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{reauth, user3Node3},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user3Node3,
|
|
},
|
|
},
|
|
{
|
|
name: "reauth-same-node",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{reauth, user1Node1},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user1Node1,
|
|
},
|
|
},
|
|
{
|
|
name: "reauth-other-profile",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user2Node2},
|
|
{reauth, user1Node1},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user1Node1,
|
|
user2Node2,
|
|
},
|
|
},
|
|
{
|
|
name: "reauth-replace-user",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user3Node3},
|
|
{reauth, user2Node1},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user2Node1,
|
|
user3Node3,
|
|
},
|
|
},
|
|
{
|
|
name: "reauth-replace-node",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user3Node3},
|
|
{reauth, user1Node2},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user1Node2,
|
|
user3Node3,
|
|
},
|
|
},
|
|
{
|
|
name: "login-same-node",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user3Node3}, // random other profile
|
|
{login, user1Node1},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user1Node1,
|
|
user3Node3,
|
|
},
|
|
},
|
|
{
|
|
name: "login-replace-user",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user3Node3}, // random other profile
|
|
{login, user2Node1},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user2Node1,
|
|
user3Node3,
|
|
},
|
|
},
|
|
{
|
|
name: "login-replace-node",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user3Node3}, // random other profile
|
|
{login, user1Node2},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user1Node2,
|
|
user3Node3,
|
|
},
|
|
},
|
|
{
|
|
name: "login-new-node",
|
|
steps: []step{
|
|
{login, user1Node1},
|
|
{login, user2Node2},
|
|
},
|
|
profs: []*persist.Persist{
|
|
user1Node1,
|
|
user2Node2,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
store := new(mem.Store)
|
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range tc.steps {
|
|
s.fn(pm, s.p)
|
|
}
|
|
profs := pm.Profiles()
|
|
var got []*persist.Persist
|
|
for _, p := range profs {
|
|
prefs, err := pm.loadSavedPrefs(p.Key())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = append(got, prefs.Persist().AsStruct())
|
|
}
|
|
d := cmp.Diff(tc.profs, got, cmpopts.SortSlices(func(a, b *persist.Persist) bool {
|
|
if a.NodeID != b.NodeID {
|
|
return a.NodeID < b.NodeID
|
|
}
|
|
return a.UserProfile.ID < b.UserProfile.ID
|
|
}))
|
|
if d != "" {
|
|
t.Fatal(d)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestProfileManagement tests creating, loading, and switching profiles.
|
|
func TestProfileManagement(t *testing.T) {
|
|
store := new(mem.Store)
|
|
|
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantCurProfile := ""
|
|
wantProfiles := map[string]ipn.PrefsView{
|
|
"": defaultPrefs,
|
|
}
|
|
checkProfiles := func(t *testing.T) {
|
|
t.Helper()
|
|
prof := pm.CurrentProfile()
|
|
t.Logf("\tCurrentProfile = %q", prof.Name())
|
|
if prof.Name() != wantCurProfile {
|
|
t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), 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()
|
|
t.Logf("\tCurrentPrefs = %s", p.Pretty())
|
|
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.Equals(wantProfiles[p.Name()]) {
|
|
t.Fatalf("Prefs for profile %q =\n got=%+v\nwant=%v", p.Name(), got.Pretty(), wantProfiles[p.Name()].Pretty())
|
|
}
|
|
}
|
|
}
|
|
logins := make(map[string]tailcfg.UserID)
|
|
nodeIDs := make(map[string]tailcfg.StableNodeID)
|
|
setPrefs := func(t *testing.T, loginName string) ipn.PrefsView {
|
|
t.Helper()
|
|
p := pm.CurrentPrefs().AsStruct()
|
|
uid := logins[loginName]
|
|
if uid.IsZero() {
|
|
uid = tailcfg.UserID(len(logins) + 1)
|
|
logins[loginName] = uid
|
|
}
|
|
nid := nodeIDs[loginName]
|
|
if nid.IsZero() {
|
|
nid = tailcfg.StableNodeID(fmt.Sprint(len(nodeIDs) + 1))
|
|
nodeIDs[loginName] = nid
|
|
}
|
|
p.Persist = &persist.Persist{
|
|
PrivateNodeKey: key.NewNode(),
|
|
UserProfile: tailcfg.UserProfile{
|
|
ID: uid,
|
|
LoginName: loginName,
|
|
},
|
|
NodeID: nid,
|
|
}
|
|
if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); 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[""] = defaultPrefs
|
|
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, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkProfiles(t)
|
|
|
|
t.Logf("Delete default profile")
|
|
if err := pm.DeleteProfile(pm.ProfileIDForName("user@1.example.com")); 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, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkProfiles(t)
|
|
|
|
t.Logf("Create new profile - 2")
|
|
pm.NewProfile()
|
|
wantCurProfile = ""
|
|
wantProfiles[""] = defaultPrefs
|
|
checkProfiles(t)
|
|
|
|
t.Logf("Login with the existing profile")
|
|
wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com")
|
|
delete(wantProfiles, "")
|
|
wantCurProfile = "user@2.example.com"
|
|
checkProfiles(t)
|
|
|
|
t.Logf("Tag the current the profile")
|
|
nodeIDs["tagged-node.2.ts.net"] = nodeIDs["user@2.example.com"]
|
|
wantProfiles["tagged-node.2.ts.net"] = setPrefs(t, "tagged-node.2.ts.net")
|
|
delete(wantProfiles, "user@2.example.com")
|
|
wantCurProfile = "tagged-node.2.ts.net"
|
|
checkProfiles(t)
|
|
|
|
t.Logf("Relogin")
|
|
wantProfiles["user@2.example.com"] = setPrefs(t, "user@2.example.com")
|
|
delete(wantProfiles, "tagged-node.2.ts.net")
|
|
wantCurProfile = "user@2.example.com"
|
|
checkProfiles(t)
|
|
|
|
if !clientupdate.CanAutoUpdate() {
|
|
t.Logf("Save an invalid AutoUpdate pref value")
|
|
prefs := pm.CurrentPrefs().AsStruct()
|
|
prefs.AutoUpdate.Apply.Set(true)
|
|
if err := pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !pm.CurrentPrefs().AutoUpdate().Apply.EqualBool(true) {
|
|
t.Fatal("SetPrefs failed to save auto-update setting")
|
|
}
|
|
// Re-load profiles to trigger migration for invalid auto-update value.
|
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkProfiles(t)
|
|
if pm.CurrentPrefs().AutoUpdate().Apply.EqualBool(true) {
|
|
t.Fatal("invalid auto-update setting persisted after reload")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProfileManagementWindows tests going into and out of Unattended mode on
|
|
// Windows.
|
|
func TestProfileManagementWindows(t *testing.T) {
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uid := ipn.WindowsUserID(u.Uid)
|
|
|
|
store := new(mem.Store)
|
|
|
|
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantCurProfile := ""
|
|
wantProfiles := map[string]ipn.PrefsView{
|
|
"": defaultPrefs,
|
|
}
|
|
checkProfiles := func(t *testing.T) {
|
|
t.Helper()
|
|
prof := pm.CurrentProfile()
|
|
t.Logf("\tCurrentProfile = %q", prof.Name())
|
|
if prof.Name() != wantCurProfile {
|
|
t.Fatalf("CurrentProfile = %q; want %q", prof.Name(), wantCurProfile)
|
|
}
|
|
if p := pm.CurrentPrefs(); !p.Equals(wantProfiles[wantCurProfile]) {
|
|
t.Fatalf("CurrentPrefs = %+v; want %+v", p.Pretty(), wantProfiles[wantCurProfile].Pretty())
|
|
}
|
|
}
|
|
logins := make(map[string]tailcfg.UserID)
|
|
setPrefs := func(t *testing.T, loginName string, forceDaemon bool) ipn.PrefsView {
|
|
id := logins[loginName]
|
|
if id.IsZero() {
|
|
id = tailcfg.UserID(len(logins) + 1)
|
|
logins[loginName] = id
|
|
}
|
|
p := pm.CurrentPrefs().AsStruct()
|
|
p.ForceDaemon = forceDaemon
|
|
p.Persist = &persist.Persist{
|
|
UserProfile: tailcfg.UserProfile{
|
|
ID: id,
|
|
LoginName: loginName,
|
|
},
|
|
NodeID: tailcfg.StableNodeID(strconv.Itoa(int(id))),
|
|
}
|
|
if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); 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")
|
|
pm.SetCurrentUserID(uid)
|
|
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[""] = defaultPrefs
|
|
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, new(health.Tracker), "windows")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantCurProfile = ""
|
|
wantProfiles[""] = defaultPrefs
|
|
checkProfiles(t)
|
|
|
|
{
|
|
t.Logf("Set user1 as current user")
|
|
pm.SetCurrentUserID(uid)
|
|
wantCurProfile = "test"
|
|
}
|
|
checkProfiles(t)
|
|
{
|
|
t.Logf("set unattended mode")
|
|
wantProfiles["test"] = setPrefs(t, "test", true)
|
|
}
|
|
if pm.CurrentUserID() != uid {
|
|
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
|
|
}
|
|
|
|
// Recreate the profile manager to ensure that it starts with test profile.
|
|
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkProfiles(t)
|
|
if pm.CurrentUserID() != uid {
|
|
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
|
|
}
|
|
}
|
|
|
|
// TestDefaultPrefs tests that defaultPrefs is just NewPrefs with
|
|
// LoggedOut=true (the Prefs we use before connecting to control). We shouldn't
|
|
// be putting any defaulting there, and instead put all defaults in NewPrefs.
|
|
func TestDefaultPrefs(t *testing.T) {
|
|
p1 := ipn.NewPrefs()
|
|
p1.LoggedOut = true
|
|
p1.WantRunning = false
|
|
p2 := defaultPrefs
|
|
if !p1.View().Equals(p2) {
|
|
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())
|
|
}
|
|
}
|