mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
292 lines
13 KiB
Go
292 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/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")
|
||
|
}
|
||
|
}
|