mirror of
https://github.com/tailscale/tailscale.git
synced 2025-09-21 13:41:46 +02:00
Step 3 in the series. See earlier cc532efc2000 and d05e6dc09e. This step moves some types into a new leaf "ptype" package out of the big "settings" package. The policyclient.Client will later get new methods to return those things (as well as Duration and Uint64, which weren't done at the time of the earlier prototype). Updates #16998 Updates #12614 Change-Id: I4d72d8079de3b5351ed602eaa72863372bd474a2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
294 lines
13 KiB
Go
294 lines
13 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package source
|
|
|
|
import (
|
|
"cmp"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/util/must"
|
|
"tailscale.com/util/syspolicy/pkey"
|
|
"tailscale.com/util/syspolicy/ptype"
|
|
"tailscale.com/util/syspolicy/setting"
|
|
)
|
|
|
|
func TestReaderLifecycle(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
origin *setting.Origin
|
|
definitions []*setting.Definition
|
|
wantReads []TestExpectedReads
|
|
initStrings []TestSetting[string]
|
|
initUInt64s []TestSetting[uint64]
|
|
initWant *setting.Snapshot
|
|
addStrings []TestSetting[string]
|
|
addStringLists []TestSetting[[]string]
|
|
newWant *setting.Snapshot
|
|
}{
|
|
{
|
|
name: "read-all-settings-once",
|
|
origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("StringValue", setting.DeviceSetting, setting.StringValue),
|
|
setting.NewDefinition("IntegerValue", setting.DeviceSetting, setting.IntegerValue),
|
|
setting.NewDefinition("BooleanValue", setting.DeviceSetting, setting.BooleanValue),
|
|
setting.NewDefinition("StringListValue", setting.DeviceSetting, setting.StringListValue),
|
|
setting.NewDefinition("DurationValue", setting.DeviceSetting, setting.DurationValue),
|
|
setting.NewDefinition("PreferenceOptionValue", setting.DeviceSetting, setting.PreferenceOptionValue),
|
|
setting.NewDefinition("VisibilityValue", setting.DeviceSetting, setting.VisibilityValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
{Key: "StringValue", Type: setting.StringValue, NumTimes: 1},
|
|
{Key: "IntegerValue", Type: setting.IntegerValue, NumTimes: 1},
|
|
{Key: "BooleanValue", Type: setting.BooleanValue, NumTimes: 1},
|
|
{Key: "StringListValue", Type: setting.StringListValue, NumTimes: 1},
|
|
{Key: "DurationValue", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
|
|
{Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
|
|
{Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
|
|
},
|
|
initWant: setting.NewSnapshot(nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
},
|
|
{
|
|
name: "re-read-all-settings-when-the-policy-changes",
|
|
origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("StringValue", setting.DeviceSetting, setting.StringValue),
|
|
setting.NewDefinition("IntegerValue", setting.DeviceSetting, setting.IntegerValue),
|
|
setting.NewDefinition("BooleanValue", setting.DeviceSetting, setting.BooleanValue),
|
|
setting.NewDefinition("StringListValue", setting.DeviceSetting, setting.StringListValue),
|
|
setting.NewDefinition("DurationValue", setting.DeviceSetting, setting.DurationValue),
|
|
setting.NewDefinition("PreferenceOptionValue", setting.DeviceSetting, setting.PreferenceOptionValue),
|
|
setting.NewDefinition("VisibilityValue", setting.DeviceSetting, setting.VisibilityValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
{Key: "StringValue", Type: setting.StringValue, NumTimes: 1},
|
|
{Key: "IntegerValue", Type: setting.IntegerValue, NumTimes: 1},
|
|
{Key: "BooleanValue", Type: setting.BooleanValue, NumTimes: 1},
|
|
{Key: "StringListValue", Type: setting.StringListValue, NumTimes: 1},
|
|
{Key: "DurationValue", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
|
|
{Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
|
|
{Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
|
|
},
|
|
initWant: setting.NewSnapshot(nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
addStrings: []TestSetting[string]{TestSettingOf("StringValue", "S1")},
|
|
addStringLists: []TestSetting[[]string]{TestSettingOf("StringListValue", []string{"S1", "S2", "S3"})},
|
|
newWant: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
|
|
"StringValue": setting.RawItemWith("S1", nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
"StringListValue": setting.RawItemWith([]string{"S1", "S2", "S3"}, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
}, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
},
|
|
{
|
|
name: "read-settings-if-in-scope/device",
|
|
origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("DeviceSetting", setting.DeviceSetting, setting.StringValue),
|
|
setting.NewDefinition("ProfileSetting", setting.ProfileSetting, setting.IntegerValue),
|
|
setting.NewDefinition("UserSetting", setting.UserSetting, setting.BooleanValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
{Key: "DeviceSetting", Type: setting.StringValue, NumTimes: 1},
|
|
{Key: "ProfileSetting", Type: setting.IntegerValue, NumTimes: 1},
|
|
{Key: "UserSetting", Type: setting.BooleanValue, NumTimes: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read-settings-if-in-scope/profile",
|
|
origin: setting.NewNamedOrigin("Test", setting.CurrentProfileScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("DeviceSetting", setting.DeviceSetting, setting.StringValue),
|
|
setting.NewDefinition("ProfileSetting", setting.ProfileSetting, setting.IntegerValue),
|
|
setting.NewDefinition("UserSetting", setting.UserSetting, setting.BooleanValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
// Device settings cannot be configured at the profile scope and should not be read.
|
|
{Key: "ProfileSetting", Type: setting.IntegerValue, NumTimes: 1},
|
|
{Key: "UserSetting", Type: setting.BooleanValue, NumTimes: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read-settings-if-in-scope/user",
|
|
origin: setting.NewNamedOrigin("Test", setting.CurrentUserScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("DeviceSetting", setting.DeviceSetting, setting.StringValue),
|
|
setting.NewDefinition("ProfileSetting", setting.ProfileSetting, setting.IntegerValue),
|
|
setting.NewDefinition("UserSetting", setting.UserSetting, setting.BooleanValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
// Device and profile settings cannot be configured at the profile scope and should not be read.
|
|
{Key: "UserSetting", Type: setting.BooleanValue, NumTimes: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read-stringy-settings",
|
|
origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("DurationValue", setting.DeviceSetting, setting.DurationValue),
|
|
setting.NewDefinition("PreferenceOptionValue", setting.DeviceSetting, setting.PreferenceOptionValue),
|
|
setting.NewDefinition("VisibilityValue", setting.DeviceSetting, setting.VisibilityValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
{Key: "DurationValue", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
|
|
{Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
|
|
{Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
|
|
},
|
|
initStrings: []TestSetting[string]{
|
|
TestSettingOf("DurationValue", "2h30m"),
|
|
TestSettingOf("PreferenceOptionValue", "always"),
|
|
TestSettingOf("VisibilityValue", "show"),
|
|
},
|
|
initWant: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
|
|
"DurationValue": setting.RawItemWith(must.Get(time.ParseDuration("2h30m")), nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
"PreferenceOptionValue": setting.RawItemWith(ptype.AlwaysByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
"VisibilityValue": setting.RawItemWith(ptype.VisibleByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
}, setting.NewNamedOrigin("Test", setting.DeviceScope)),
|
|
},
|
|
{
|
|
name: "read-erroneous-stringy-settings",
|
|
origin: setting.NewNamedOrigin("Test", setting.CurrentUserScope),
|
|
definitions: []*setting.Definition{
|
|
setting.NewDefinition("DurationValue1", setting.UserSetting, setting.DurationValue),
|
|
setting.NewDefinition("DurationValue2", setting.UserSetting, setting.DurationValue),
|
|
setting.NewDefinition("PreferenceOptionValue", setting.UserSetting, setting.PreferenceOptionValue),
|
|
setting.NewDefinition("VisibilityValue", setting.UserSetting, setting.VisibilityValue),
|
|
},
|
|
wantReads: []TestExpectedReads{
|
|
{Key: "DurationValue1", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
|
|
{Key: "DurationValue2", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
|
|
{Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
|
|
{Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
|
|
},
|
|
initStrings: []TestSetting[string]{
|
|
TestSettingOf("DurationValue1", "soon"),
|
|
TestSettingWithError[string]("DurationValue2", setting.NewErrorText("bang!")),
|
|
TestSettingOf("PreferenceOptionValue", "sometimes"),
|
|
},
|
|
initUInt64s: []TestSetting[uint64]{
|
|
TestSettingOf[uint64]("VisibilityValue", 42), // type mismatch
|
|
},
|
|
initWant: setting.NewSnapshot(map[pkey.Key]setting.RawItem{
|
|
"DurationValue1": setting.RawItemWith(nil, setting.NewErrorText("time: invalid duration \"soon\""), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
|
|
"DurationValue2": setting.RawItemWith(nil, setting.NewErrorText("bang!"), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
|
|
"PreferenceOptionValue": setting.RawItemWith(ptype.ShowChoiceByPolicy, nil, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
|
|
"VisibilityValue": setting.RawItemWith(ptype.VisibleByPolicy, setting.NewErrorText("type mismatch in ReadString: got uint64"), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
|
|
}, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setting.SetDefinitionsForTest(t, tt.definitions...)
|
|
store := NewTestStore(t)
|
|
store.SetStrings(tt.initStrings...)
|
|
store.SetUInt64s(tt.initUInt64s...)
|
|
|
|
reader, err := newReader(store, tt.origin)
|
|
if err != nil {
|
|
t.Fatalf("newReader failed: %v", err)
|
|
}
|
|
|
|
if got := reader.GetSettings(); tt.initWant != nil && !got.Equal(tt.initWant) {
|
|
t.Errorf("Settings do not match: got %v, want %v", got, tt.initWant)
|
|
}
|
|
if tt.wantReads != nil {
|
|
store.ReadsMustEqual(tt.wantReads...)
|
|
}
|
|
|
|
// Should not result in new reads as there were no changes.
|
|
N := 100
|
|
for range N {
|
|
reader.GetSettings()
|
|
}
|
|
if tt.wantReads != nil {
|
|
store.ReadsMustEqual(tt.wantReads...)
|
|
}
|
|
store.ResetCounters()
|
|
|
|
got, err := reader.ReadSettings()
|
|
if err != nil {
|
|
t.Fatalf("ReadSettings failed: %v", err)
|
|
}
|
|
|
|
if tt.initWant != nil && !got.Equal(tt.initWant) {
|
|
t.Errorf("Settings do not match: got %v, want %v", got, tt.initWant)
|
|
}
|
|
|
|
if tt.wantReads != nil {
|
|
store.ReadsMustEqual(tt.wantReads...)
|
|
}
|
|
store.ResetCounters()
|
|
|
|
if len(tt.addStrings) != 0 || len(tt.addStringLists) != 0 {
|
|
store.SetStrings(tt.addStrings...)
|
|
store.SetStringLists(tt.addStringLists...)
|
|
|
|
// As the settings have changed, GetSettings needs to re-read them.
|
|
if got, want := reader.GetSettings(), cmp.Or(tt.newWant, tt.initWant); !got.Equal(want) {
|
|
t.Errorf("New Settings do not match: got %v, want %v", got, want)
|
|
}
|
|
if tt.wantReads != nil {
|
|
store.ReadsMustEqual(tt.wantReads...)
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-reader.Done():
|
|
t.Fatalf("the reader is closed")
|
|
default:
|
|
}
|
|
|
|
store.Close()
|
|
|
|
<-reader.Done()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReadingSession(t *testing.T) {
|
|
setting.SetDefinitionsForTest(t, setting.NewDefinition("StringValue", setting.DeviceSetting, setting.StringValue))
|
|
store := NewTestStore(t)
|
|
|
|
origin := setting.NewOrigin(setting.DeviceScope)
|
|
reader, err := newReader(store, origin)
|
|
if err != nil {
|
|
t.Fatalf("newReader failed: %v", err)
|
|
}
|
|
session, err := reader.OpenSession()
|
|
if err != nil {
|
|
t.Fatalf("failed to open a reading session: %v", err)
|
|
}
|
|
t.Cleanup(session.Close)
|
|
|
|
if got, want := session.GetSettings(), setting.NewSnapshot(nil, origin); !got.Equal(want) {
|
|
t.Errorf("Settings do not match: got %v, want %v", got, want)
|
|
}
|
|
|
|
select {
|
|
case _, ok := <-session.PolicyChanged():
|
|
if ok {
|
|
t.Fatalf("the policy changed notification was sent prematurely")
|
|
} else {
|
|
t.Fatalf("the session was closed prematurely")
|
|
}
|
|
default:
|
|
}
|
|
|
|
store.SetStrings(TestSettingOf("StringValue", "S1"))
|
|
_, ok := <-session.PolicyChanged()
|
|
if !ok {
|
|
t.Fatalf("the session was closed prematurely")
|
|
}
|
|
|
|
want := setting.NewSnapshot(map[pkey.Key]setting.RawItem{
|
|
"StringValue": setting.RawItemWith("S1", nil, origin),
|
|
}, origin)
|
|
if got := session.GetSettings(); !got.Equal(want) {
|
|
t.Errorf("Settings do not match: got %v, want %v", got, want)
|
|
}
|
|
|
|
store.Close()
|
|
if _, ok = <-session.PolicyChanged(); ok {
|
|
t.Fatalf("the session must be closed")
|
|
}
|
|
}
|