tailscale/util/syspolicy/source/policy_reader_test.go

292 lines
13 KiB
Go
Raw Normal View History

// 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/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[setting.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[setting.Key]setting.RawItem{
"DurationValue": setting.RawItemWith(must.Get(time.ParseDuration("2h30m")), nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
"PreferenceOptionValue": setting.RawItemWith(setting.AlwaysByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
"VisibilityValue": setting.RawItemWith(setting.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[setting.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(setting.ShowChoiceByPolicy, nil, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
"VisibilityValue": setting.RawItemWith(setting.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[setting.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")
}
}