mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-09 17:52:57 +01:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
1188 lines
35 KiB
Go
1188 lines
35 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package ipnlocal
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os/user"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/google/go-cmp/cmp"
|
||
"github.com/google/go-cmp/cmp/cmpopts"
|
||
_ "tailscale.com/clientupdate" // for feature registration side effects
|
||
"tailscale.com/feature"
|
||
"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/eventbus/eventbustest"
|
||
"tailscale.com/util/must"
|
||
)
|
||
|
||
func TestProfileCurrentUserSwitch(t *testing.T) {
|
||
store := new(mem.Store)
|
||
|
||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, health.NewTracker(eventbustest.NewBus(t)), "linux")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
id := 0
|
||
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
||
id++
|
||
t.Helper()
|
||
pm.SwitchToNewProfile()
|
||
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, health.NewTracker(eventbustest.NewBus(t)), "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, health.NewTracker(eventbustest.NewBus(t)), "linux")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
id := 0
|
||
newProfile := func(t *testing.T, loginName string) ipn.PrefsView {
|
||
id++
|
||
t.Helper()
|
||
pm.SwitchToNewProfile()
|
||
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),
|
||
},
|
||
AttestationKey: nil,
|
||
}
|
||
}
|
||
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.SwitchToNewProfile()
|
||
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, health.NewTracker(eventbustest.NewBus(t)), "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, health.NewTracker(eventbustest.NewBus(t)), "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.SwitchToNewProfile()
|
||
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, health.NewTracker(eventbustest.NewBus(t)), "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, health.NewTracker(eventbustest.NewBus(t)), "linux")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
checkProfiles(t)
|
||
|
||
t.Logf("Create new profile - 2")
|
||
pm.SwitchToNewProfile()
|
||
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 !feature.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, health.NewTracker(eventbustest.NewBus(t)), "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, health.NewTracker(eventbustest.NewBus(t)), "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.SwitchToNewProfile()
|
||
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, health.NewTracker(eventbustest.NewBus(t)), "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, health.NewTracker(eventbustest.NewBus(t)), "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())
|
||
}
|
||
}
|
||
|
||
// 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, health.NewTracker(eventbustest.NewBus(t)), "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, prefView ipn.PrefsView, sameNode bool) {
|
||
prefs := prefView.AsStruct()
|
||
prefs.Sync = prefs.Sync.Normalized()
|
||
gotChanges = append(gotChanges, stateChange{
|
||
Profile: profile.AsStruct(),
|
||
Prefs: prefs,
|
||
SameNode: sameNode,
|
||
})
|
||
}
|
||
|
||
tt.action(pm)
|
||
|
||
if diff := cmp.Diff(wantChanges, gotChanges, defaultCmpOpts...); diff != "" {
|
||
t.Errorf("StateChange callbacks: (-want +got): %v", diff)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestProfileBadAttestationKey(t *testing.T) {
|
||
store := new(mem.Store)
|
||
pm, err := newProfileManagerWithGOOS(store, t.Logf, health.NewTracker(eventbustest.NewBus(t)), "linux")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fk := new(failingHardwareAttestationKey)
|
||
pm.newEmptyHardwareAttestationKey = func() (key.HardwareAttestationKey, error) {
|
||
return fk, nil
|
||
}
|
||
sk := ipn.StateKey(t.Name())
|
||
if err := pm.store.WriteState(sk, []byte(`{"Config": {"AttestationKey": {}}}`)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
prefs, err := pm.loadSavedPrefs(sk)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ak := prefs.Persist().AsStruct().AttestationKey
|
||
if _, ok := ak.(noopAttestationKey); !ok {
|
||
t.Errorf("loaded attestation key of type %T, want noopAttestationKey", ak)
|
||
}
|
||
if !fk.unmarshalCalled {
|
||
t.Error("UnmarshalJSON was not called on failingHardwareAttestationKey")
|
||
}
|
||
}
|
||
|
||
type failingHardwareAttestationKey struct {
|
||
noopAttestationKey
|
||
unmarshalCalled bool
|
||
}
|
||
|
||
func (k *failingHardwareAttestationKey) UnmarshalJSON([]byte) error {
|
||
k.unmarshalCalled = true
|
||
return errors.New("failed to unmarshal attestation key!")
|
||
}
|