// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package setting import ( "slices" "strings" "testing" "tailscale.com/types/lazy" "tailscale.com/types/ptr" "tailscale.com/util/syspolicy/internal" ) func TestSettingDefinition(t *testing.T) { tests := []struct { name string setting *Definition osOverride string wantKey Key wantScope Scope wantType Type wantIsSupported bool wantSupportedPlatforms PlatformList wantString string }{ { name: "Nil", setting: nil, wantKey: "", wantScope: 0, wantType: InvalidValue, wantIsSupported: false, wantString: "(nil)", }, { name: "Device/Invalid", setting: NewDefinition("TestDevicePolicySetting", DeviceSetting, InvalidValue), wantKey: "TestDevicePolicySetting", wantScope: DeviceSetting, wantType: InvalidValue, wantIsSupported: true, wantString: `Device("TestDevicePolicySetting", Invalid)`, }, { name: "Device/Integer", setting: NewDefinition("TestDevicePolicySetting", DeviceSetting, IntegerValue), wantKey: "TestDevicePolicySetting", wantScope: DeviceSetting, wantType: IntegerValue, wantIsSupported: true, wantString: `Device("TestDevicePolicySetting", Integer)`, }, { name: "Profile/String", setting: NewDefinition("TestProfilePolicySetting", ProfileSetting, StringValue), wantKey: "TestProfilePolicySetting", wantScope: ProfileSetting, wantType: StringValue, wantIsSupported: true, wantString: `Profile("TestProfilePolicySetting", String)`, }, { name: "Device/StringList", setting: NewDefinition("AllowedSuggestedExitNodes", DeviceSetting, StringListValue), wantKey: "AllowedSuggestedExitNodes", wantScope: DeviceSetting, wantType: StringListValue, wantIsSupported: true, wantString: `Device("AllowedSuggestedExitNodes", StringList)`, }, { name: "Device/PreferenceOption", setting: NewDefinition("AdvertiseExitNode", DeviceSetting, PreferenceOptionValue), wantKey: "AdvertiseExitNode", wantScope: DeviceSetting, wantType: PreferenceOptionValue, wantIsSupported: true, wantString: `Device("AdvertiseExitNode", PreferenceOption)`, }, { name: "User/Boolean", setting: NewDefinition("TestUserPolicySetting", UserSetting, BooleanValue), wantKey: "TestUserPolicySetting", wantScope: UserSetting, wantType: BooleanValue, wantIsSupported: true, wantString: `User("TestUserPolicySetting", Boolean)`, }, { name: "User/Visibility", setting: NewDefinition("AdminConsole", UserSetting, VisibilityValue), wantKey: "AdminConsole", wantScope: UserSetting, wantType: VisibilityValue, wantIsSupported: true, wantString: `User("AdminConsole", Visibility)`, }, { name: "User/Duration", setting: NewDefinition("KeyExpirationNotice", UserSetting, DurationValue), wantKey: "KeyExpirationNotice", wantScope: UserSetting, wantType: DurationValue, wantIsSupported: true, wantString: `User("KeyExpirationNotice", Duration)`, }, { name: "SupportedSetting", setting: NewDefinition("DesktopPolicySetting", DeviceSetting, StringValue, "macos", "windows"), osOverride: "windows", wantKey: "DesktopPolicySetting", wantScope: DeviceSetting, wantType: StringValue, wantIsSupported: true, wantSupportedPlatforms: PlatformList{"macos", "windows"}, wantString: `Device("DesktopPolicySetting", String)`, }, { name: "UnsupportedSetting", setting: NewDefinition("AndroidPolicySetting", DeviceSetting, StringValue, "android"), osOverride: "macos", wantKey: "AndroidPolicySetting", wantScope: DeviceSetting, wantType: StringValue, wantIsSupported: false, wantSupportedPlatforms: PlatformList{"android"}, wantString: `Device("AndroidPolicySetting", String)`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.osOverride != "" { internal.OSForTesting.SetForTest(t, tt.osOverride, nil) } if !tt.setting.Equal(tt.setting) { t.Errorf("the setting should be equal to itself") } if tt.setting != nil && !tt.setting.Equal(ptr.To(*tt.setting)) { t.Errorf("the setting should be equal to its shallow copy") } if gotKey := tt.setting.Key(); gotKey != tt.wantKey { t.Errorf("Key: got %q, want %q", gotKey, tt.wantKey) } if gotScope := tt.setting.Scope(); gotScope != tt.wantScope { t.Errorf("Scope: got %v, want %v", gotScope, tt.wantScope) } if gotType := tt.setting.Type(); gotType != tt.wantType { t.Errorf("Type: got %v, want %v", gotType, tt.wantType) } if gotIsSupported := tt.setting.IsSupported(); gotIsSupported != tt.wantIsSupported { t.Errorf("IsSupported: got %v, want %v", gotIsSupported, tt.wantIsSupported) } if gotSupportedPlatforms := tt.setting.SupportedPlatforms(); !slices.Equal(gotSupportedPlatforms, tt.wantSupportedPlatforms) { t.Errorf("SupportedPlatforms: got %v, want %v", gotSupportedPlatforms, tt.wantSupportedPlatforms) } if gotString := tt.setting.String(); gotString != tt.wantString { t.Errorf("String: got %v, want %v", gotString, tt.wantString) } }) } } func TestRegisterSettingDefinition(t *testing.T) { const testPolicySettingKey Key = "TestPolicySetting" tests := []struct { name string key Key wantEq *Definition wantErr error }{ { name: "GetRegistered", key: "TestPolicySetting", wantEq: NewDefinition(testPolicySettingKey, DeviceSetting, StringValue), }, { name: "GetNonRegistered", key: "OtherPolicySetting", wantEq: nil, wantErr: ErrNoSuchKey, }, } resetSettingDefinitions(t) Register(testPolicySettingKey, DeviceSetting, StringValue) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, gotErr := DefinitionOf(tt.key) if gotErr != tt.wantErr { t.Errorf("gotErr %v, wantErr %v", gotErr, tt.wantErr) } if !got.Equal(tt.wantEq) { t.Errorf("got %v, want %v", got, tt.wantEq) } }) } } func TestRegisterAfterUsePanics(t *testing.T) { resetSettingDefinitions(t) Register("TestPolicySetting", DeviceSetting, StringValue) DefinitionOf("TestPolicySetting") func() { defer func() { if gotPanic, wantPanic := recover(), "policy definitions are already in use"; gotPanic != wantPanic { t.Errorf("gotPanic: %q, wantPanic: %q", gotPanic, wantPanic) } }() Register("TestPolicySetting", DeviceSetting, StringValue) }() } func TestRegisterDuplicateSettings(t *testing.T) { tests := []struct { name string settings []*Definition wantEq *Definition wantErrStr string }{ { name: "NoConflict/Exact", settings: []*Definition{ NewDefinition("TestPolicySetting", DeviceSetting, StringValue), NewDefinition("TestPolicySetting", DeviceSetting, StringValue), }, wantEq: NewDefinition("TestPolicySetting", DeviceSetting, StringValue), }, { name: "NoConflict/MergeOS-First", settings: []*Definition{ NewDefinition("TestPolicySetting", DeviceSetting, StringValue, "android", "macos"), NewDefinition("TestPolicySetting", DeviceSetting, StringValue), // all platforms }, wantEq: NewDefinition("TestPolicySetting", DeviceSetting, StringValue), // all platforms }, { name: "NoConflict/MergeOS-Second", settings: []*Definition{ NewDefinition("TestPolicySetting", DeviceSetting, StringValue), // all platforms NewDefinition("TestPolicySetting", DeviceSetting, StringValue, "android", "macos"), }, wantEq: NewDefinition("TestPolicySetting", DeviceSetting, StringValue), // all platforms }, { name: "NoConflict/MergeOS-Both", settings: []*Definition{ NewDefinition("TestPolicySetting", DeviceSetting, StringValue, "macos"), NewDefinition("TestPolicySetting", DeviceSetting, StringValue, "windows"), }, wantEq: NewDefinition("TestPolicySetting", DeviceSetting, StringValue, "macos", "windows"), }, { name: "Conflict/Scope", settings: []*Definition{ NewDefinition("TestPolicySetting", DeviceSetting, StringValue), NewDefinition("TestPolicySetting", UserSetting, StringValue), }, wantEq: nil, wantErrStr: `duplicate policy definition: "TestPolicySetting"`, }, { name: "Conflict/Type", settings: []*Definition{ NewDefinition("TestPolicySetting", UserSetting, StringValue), NewDefinition("TestPolicySetting", UserSetting, IntegerValue), }, wantEq: nil, wantErrStr: `duplicate policy definition: "TestPolicySetting"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resetSettingDefinitions(t) for _, s := range tt.settings { Register(s.Key(), s.Scope(), s.Type(), s.SupportedPlatforms()...) } got, err := DefinitionOf("TestPolicySetting") var gotErrStr string if err != nil { gotErrStr = err.Error() } if gotErrStr != tt.wantErrStr { t.Fatalf("ErrStr: got %q, want %q", gotErrStr, tt.wantErrStr) } if !got.Equal(tt.wantEq) { t.Errorf("Definition got %v, want %v", got, tt.wantEq) } if !slices.Equal(got.SupportedPlatforms(), tt.wantEq.SupportedPlatforms()) { t.Errorf("SupportedPlatforms got %v, want %v", got.SupportedPlatforms(), tt.wantEq.SupportedPlatforms()) } }) } } func TestListSettingDefinitions(t *testing.T) { definitions := []*Definition{ NewDefinition("TestDevicePolicySetting", DeviceSetting, IntegerValue), NewDefinition("TestProfilePolicySetting", ProfileSetting, StringValue), NewDefinition("TestUserPolicySetting", UserSetting, BooleanValue), NewDefinition("TestStringListPolicySetting", DeviceSetting, StringListValue), } if err := SetDefinitionsForTest(t, definitions...); err != nil { t.Fatalf("SetDefinitionsForTest failed: %v", err) } cmp := func(l, r *Definition) int { return strings.Compare(string(l.Key()), string(r.Key())) } want := append([]*Definition{}, definitions...) slices.SortFunc(want, cmp) got, err := Definitions() if err != nil { t.Fatalf("Definitions failed: %v", err) } slices.SortFunc(got, cmp) if !slices.Equal(got, want) { t.Errorf("got %v, want %v", got, want) } } func resetSettingDefinitions(t *testing.T) { t.Cleanup(func() { definitionsMu.Lock() definitionsList = nil definitions = lazy.SyncValue[DefinitionMap]{} definitionsUsed = false definitionsMu.Unlock() }) definitionsMu.Lock() definitionsList = nil definitions = lazy.SyncValue[DefinitionMap]{} definitionsUsed = false definitionsMu.Unlock() }