tailscale/util/syspolicy/source/policy_reader_test.go
Brad Fitzpatrick 2434bc69fc util/syspolicy/{setting,ptype}: move PreferenceOption and Visibility to new leaf package
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>
2025-09-02 10:28:35 -07:00

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")
}
}