mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01: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")
 | |
| 	}
 | |
| }
 |