// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package rsop import ( "errors" "slices" "sort" "strconv" "sync" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "tailscale.com/tstest" "tailscale.com/util/syspolicy/setting" "tailscale.com/util/syspolicy/source" ) func TestGetEffectivePolicyNoSource(t *testing.T) { tests := []struct { name string scope setting.PolicyScope }{ { name: "DevicePolicy", scope: setting.DeviceScope, }, { name: "CurrentProfilePolicy", scope: setting.CurrentProfileScope, }, { name: "CurrentUserPolicy", scope: setting.CurrentUserScope, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var policy *Policy t.Cleanup(func() { if policy != nil { policy.Close() <-policy.Done() } }) // Make sure we don't create any goroutines. // We intentionally call ResourceCheck after t.Cleanup, so that when the test exits, // the resource check runs before the test cleanup closes the policy. // This helps to report any unexpectedly created goroutines. // The goal is to ensure that using the syspolicy package, and particularly // the rsop sub-package, is not wasteful and does not create unnecessary goroutines // on platforms without registered policy sources. tstest.ResourceCheck(t) policy, err := PolicyFor(tt.scope) if err != nil { t.Fatalf("Failed to get effective policy for %v: %v", tt.scope, err) } if got := policy.Get(); got.Len() != 0 { t.Errorf("Snapshot: got %v; want empty", got) } if got, err := policy.Reload(); err != nil { t.Errorf("Reload failed: %v", err) } else if got.Len() != 0 { t.Errorf("Snapshot: got %v; want empty", got) } }) } } func TestRegisterSourceAndGetEffectivePolicy(t *testing.T) { type sourceConfig struct { name string scope setting.PolicyScope settingKey setting.Key settingValue string wantEffective bool } tests := []struct { name string scope setting.PolicyScope initialSources []sourceConfig additionalSources []sourceConfig wantSnapshot *setting.Snapshot }{ { name: "DevicePolicy/NoSources", scope: setting.DeviceScope, wantSnapshot: setting.NewSnapshot(nil, setting.DeviceScope), }, { name: "UserScope/NoSources", scope: setting.CurrentUserScope, wantSnapshot: setting.NewSnapshot(nil, setting.CurrentUserScope), }, { name: "DevicePolicy/OneInitialSource", scope: setting.DeviceScope, initialSources: []sourceConfig{ { name: "TestSourceA", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueA", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)), }, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)), }, { name: "DevicePolicy/OneAdditionalSource", scope: setting.DeviceScope, additionalSources: []sourceConfig{ { name: "TestSourceA", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueA", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)), }, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)), }, { name: "DevicePolicy/ManyInitialSources/NoConflicts", scope: setting.DeviceScope, initialSources: []sourceConfig{ { name: "TestSourceA", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueA", wantEffective: true, }, { name: "TestSourceB", scope: setting.DeviceScope, settingKey: "TestKeyB", settingValue: "TestValueB", wantEffective: true, }, { name: "TestSourceC", scope: setting.DeviceScope, settingKey: "TestKeyC", settingValue: "TestValueC", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("TestValueA", nil, setting.NewNamedOrigin("TestSourceA", setting.DeviceScope)), "TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)), "TestKeyC": setting.RawItemWith("TestValueC", nil, setting.NewNamedOrigin("TestSourceC", setting.DeviceScope)), }, setting.DeviceScope), }, { name: "DevicePolicy/ManyInitialSources/Conflicts", scope: setting.DeviceScope, initialSources: []sourceConfig{ { name: "TestSourceA", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueA", wantEffective: true, }, { name: "TestSourceB", scope: setting.DeviceScope, settingKey: "TestKeyB", settingValue: "TestValueB", wantEffective: true, }, { name: "TestSourceC", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueC", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("TestValueC", nil, setting.NewNamedOrigin("TestSourceC", setting.DeviceScope)), "TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)), }, setting.DeviceScope), }, { name: "DevicePolicy/MixedSources/Conflicts", scope: setting.DeviceScope, initialSources: []sourceConfig{ { name: "TestSourceA", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueA", wantEffective: true, }, { name: "TestSourceB", scope: setting.DeviceScope, settingKey: "TestKeyB", settingValue: "TestValueB", wantEffective: true, }, { name: "TestSourceC", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueC", wantEffective: true, }, }, additionalSources: []sourceConfig{ { name: "TestSourceD", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueD", wantEffective: true, }, { name: "TestSourceE", scope: setting.DeviceScope, settingKey: "TestKeyC", settingValue: "TestValueE", wantEffective: true, }, { name: "TestSourceF", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "TestValueF", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("TestValueF", nil, setting.NewNamedOrigin("TestSourceF", setting.DeviceScope)), "TestKeyB": setting.RawItemWith("TestValueB", nil, setting.NewNamedOrigin("TestSourceB", setting.DeviceScope)), "TestKeyC": setting.RawItemWith("TestValueE", nil, setting.NewNamedOrigin("TestSourceE", setting.DeviceScope)), }, setting.DeviceScope), }, { name: "UserScope/Init-DeviceSource", scope: setting.CurrentUserScope, initialSources: []sourceConfig{ { name: "TestSourceDevice", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "DeviceValue", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)), }, setting.CurrentUserScope, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)), }, { name: "UserScope/Init-DeviceSource/Add-UserSource", scope: setting.CurrentUserScope, initialSources: []sourceConfig{ { name: "TestSourceDevice", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "DeviceValue", wantEffective: true, }, }, additionalSources: []sourceConfig{ { name: "TestSourceUser", scope: setting.CurrentUserScope, settingKey: "TestKeyB", settingValue: "UserValue", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)), "TestKeyB": setting.RawItemWith("UserValue", nil, setting.NewNamedOrigin("TestSourceUser", setting.CurrentUserScope)), }, setting.CurrentUserScope), }, { name: "UserScope/Init-DeviceSource/Add-UserSource-and-ProfileSource", scope: setting.CurrentUserScope, initialSources: []sourceConfig{ { name: "TestSourceDevice", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "DeviceValue", wantEffective: true, }, }, additionalSources: []sourceConfig{ { name: "TestSourceProfile", scope: setting.CurrentProfileScope, settingKey: "TestKeyB", settingValue: "ProfileValue", wantEffective: true, }, { name: "TestSourceUser", scope: setting.CurrentUserScope, settingKey: "TestKeyB", settingValue: "UserValue", wantEffective: true, }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)), "TestKeyB": setting.RawItemWith("ProfileValue", nil, setting.NewNamedOrigin("TestSourceProfile", setting.CurrentProfileScope)), }, setting.CurrentUserScope), }, { name: "DevicePolicy/User-Source-does-not-apply", scope: setting.DeviceScope, initialSources: []sourceConfig{ { name: "TestSourceDevice", scope: setting.DeviceScope, settingKey: "TestKeyA", settingValue: "DeviceValue", wantEffective: true, }, }, additionalSources: []sourceConfig{ { name: "TestSourceUser", scope: setting.CurrentUserScope, settingKey: "TestKeyA", settingValue: "UserValue", wantEffective: false, // Registering a user source should have no impact on the device policy. }, }, wantSnapshot: setting.NewSnapshot(map[setting.Key]setting.RawItem{ "TestKeyA": setting.RawItemWith("DeviceValue", nil, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)), }, setting.NewNamedOrigin("TestSourceDevice", setting.DeviceScope)), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Register all settings that we use in this test. var definitions []*setting.Definition for _, source := range slices.Concat(tt.initialSources, tt.additionalSources) { definitions = append(definitions, setting.NewDefinition(source.settingKey, tt.scope.Kind(), setting.StringValue)) } if err := setting.SetDefinitionsForTest(t, definitions...); err != nil { t.Fatalf("SetDefinitionsForTest failed: %v", err) } // Add the initial policy sources. var wantSources []*source.Source for _, s := range tt.initialSources { store := source.NewTestStoreOf(t, source.TestSettingOf(s.settingKey, s.settingValue)) source := source.NewSource(s.name, s.scope, store) if err := registerSource(source); err != nil { t.Fatalf("Failed to register policy source: %v", source) } if s.wantEffective { wantSources = append(wantSources, source) } t.Cleanup(func() { unregisterSource(source) }) } // Retrieve the effective policy. policy, err := policyForTest(t, tt.scope) if err != nil { t.Fatalf("Failed to get effective policy for %v: %v", tt.scope, err) } checkPolicySources(t, policy, wantSources) // Add additional setting sources. for _, s := range tt.additionalSources { store := source.NewTestStoreOf(t, source.TestSettingOf(s.settingKey, s.settingValue)) source := source.NewSource(s.name, s.scope, store) if err := registerSource(source); err != nil { t.Fatalf("Failed to register additional policy source: %v", source) } if s.wantEffective { wantSources = append(wantSources, source) } t.Cleanup(func() { unregisterSource(source) }) } checkPolicySources(t, policy, wantSources) // Verify the final effective settings snapshots. if got := policy.Get(); !got.Equal(tt.wantSnapshot) { t.Errorf("Snapshot: got %v; want %v", got, tt.wantSnapshot) } }) } } func TestPolicyFor(t *testing.T) { tests := []struct { name string scopeA, scopeB setting.PolicyScope closePolicy bool // indicates whether to close policyA before retrieving policyB wantSame bool // specifies whether policyA and policyB should reference the same [Policy] instance }{ { name: "Device/Device", scopeA: setting.DeviceScope, scopeB: setting.DeviceScope, wantSame: true, }, { name: "Device/CurrentProfile", scopeA: setting.DeviceScope, scopeB: setting.CurrentProfileScope, wantSame: false, }, { name: "Device/CurrentUser", scopeA: setting.DeviceScope, scopeB: setting.CurrentUserScope, wantSame: false, }, { name: "CurrentProfile/CurrentProfile", scopeA: setting.CurrentProfileScope, scopeB: setting.CurrentProfileScope, wantSame: true, }, { name: "CurrentProfile/CurrentUser", scopeA: setting.CurrentProfileScope, scopeB: setting.CurrentUserScope, wantSame: false, }, { name: "CurrentUser/CurrentUser", scopeA: setting.CurrentUserScope, scopeB: setting.CurrentUserScope, wantSame: true, }, { name: "UserA/UserA", scopeA: setting.UserScopeOf("UserA"), scopeB: setting.UserScopeOf("UserA"), wantSame: true, }, { name: "UserA/UserB", scopeA: setting.UserScopeOf("UserA"), scopeB: setting.UserScopeOf("UserB"), wantSame: false, }, { name: "New-after-close", scopeA: setting.DeviceScope, scopeB: setting.DeviceScope, closePolicy: true, wantSame: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { policyA, err := policyForTest(t, tt.scopeA) if err != nil { t.Fatalf("Failed to get effective policy for %v: %v", tt.scopeA, err) } if tt.closePolicy { policyA.Close() } policyB, err := policyForTest(t, tt.scopeB) if err != nil { t.Fatalf("Failed to get effective policy for %v: %v", tt.scopeB, err) } if gotSame := policyA == policyB; gotSame != tt.wantSame { t.Fatalf("Got same: %v; want same %v", gotSame, tt.wantSame) } }) } } func TestPolicyChangeHasChanged(t *testing.T) { tests := []struct { name string old, new map[setting.Key]setting.RawItem wantChanged []setting.Key wantUnchanged []setting.Key }{ { name: "String-Settings", old: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf("Old"), "UnchangedSetting": setting.RawItemOf("Value"), }, new: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf("New"), "UnchangedSetting": setting.RawItemOf("Value"), }, wantChanged: []setting.Key{"ChangedSetting"}, wantUnchanged: []setting.Key{"UnchangedSetting"}, }, { name: "UInt64-Settings", old: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf(uint64(0)), "UnchangedSetting": setting.RawItemOf(uint64(42)), }, new: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf(uint64(1)), "UnchangedSetting": setting.RawItemOf(uint64(42)), }, wantChanged: []setting.Key{"ChangedSetting"}, wantUnchanged: []setting.Key{"UnchangedSetting"}, }, { name: "StringSlice-Settings", old: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf([]string{"Chicago"}), "UnchangedSetting": setting.RawItemOf([]string{"String1", "String2"}), }, new: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf([]string{"New York"}), "UnchangedSetting": setting.RawItemOf([]string{"String1", "String2"}), }, wantChanged: []setting.Key{"ChangedSetting"}, wantUnchanged: []setting.Key{"UnchangedSetting"}, }, { name: "Int8-Settings", // We don't have actual int8 settings, but this should still work. old: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf(int8(0)), "UnchangedSetting": setting.RawItemOf(int8(42)), }, new: map[setting.Key]setting.RawItem{ "ChangedSetting": setting.RawItemOf(int8(1)), "UnchangedSetting": setting.RawItemOf(int8(42)), }, wantChanged: []setting.Key{"ChangedSetting"}, wantUnchanged: []setting.Key{"UnchangedSetting"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { old := setting.NewSnapshot(tt.old) new := setting.NewSnapshot(tt.new) change := PolicyChange{Change[*setting.Snapshot]{old, new}} for _, wantChanged := range tt.wantChanged { if !change.HasChanged(wantChanged) { t.Errorf("%q changed: got false; want true", wantChanged) } } for _, wantUnchanged := range tt.wantUnchanged { if change.HasChanged(wantUnchanged) { t.Errorf("%q unchanged: got true; want false", wantUnchanged) } } }) } } func TestChangePolicySetting(t *testing.T) { setForTest(t, &policyReloadMinDelay, 100*time.Millisecond) setForTest(t, &policyReloadMaxDelay, 500*time.Millisecond) // Register policy settings used in this test. settingA := setting.NewDefinition("TestSettingA", setting.DeviceSetting, setting.StringValue) settingB := setting.NewDefinition("TestSettingB", setting.DeviceSetting, setting.StringValue) if err := setting.SetDefinitionsForTest(t, settingA, settingB); err != nil { t.Fatalf("SetDefinitionsForTest failed: %v", err) } // Register a test policy store and create a effective policy that reads the policy settings from it. store := source.NewTestStoreOf[string](t) if _, err := RegisterStoreForTest(t, "TestSource", setting.DeviceScope, store); err != nil { t.Fatalf("Failed to register policy store: %v", err) } policy, err := policyForTest(t, setting.DeviceScope) if err != nil { t.Fatalf("Failed to get effective policy: %v", err) } // The policy setting is not configured yet. if _, ok := policy.Get().GetSetting(settingA.Key()); ok { t.Fatalf("Policy setting %q unexpectedly exists", settingA.Key()) } // Subscribe to the policy change callback... policyChanged := make(chan *PolicyChange) unregister := policy.RegisterChangeCallback(func(pc *PolicyChange) { policyChanged <- pc }) t.Cleanup(unregister) // ...make the change, and measure the time between initiating the change // and receiving the callback. start := time.Now() const wantValueA = "TestValueA" store.SetStrings(source.TestSettingOf(settingA.Key(), wantValueA)) change := <-policyChanged gotDelay := time.Since(start) // Ensure there is at least a [policyReloadMinDelay] delay between // a change and the policy reload along with the callback invocation. // This prevents reloading policy settings too frequently // when multiple settings change within a short period of time. if gotDelay < policyReloadMinDelay { t.Errorf("Delay: got %v; want >= %v", gotDelay, policyReloadMinDelay) } // Verify that the [PolicyChange] passed to the policy change callback // contains the correct information regarding the policy setting changes. if !change.HasChanged(settingA.Key()) { t.Errorf("Policy setting %q has not changed", settingA.Key()) } if change.HasChanged(settingB.Key()) { t.Errorf("Policy setting %q was unexpectedly changed", settingB.Key()) } if _, ok := change.Old().GetSetting(settingA.Key()); ok { t.Fatalf("Policy setting %q unexpectedly exists", settingA.Key()) } if gotValue := change.New().Get(settingA.Key()); gotValue != wantValueA { t.Errorf("Policy setting %q: got %q; want %q", settingA.Key(), gotValue, wantValueA) } // And also verify that the current (most recent) [setting.Snapshot] // includes the change we just made. if gotValue := policy.Get().Get(settingA.Key()); gotValue != wantValueA { t.Errorf("Policy setting %q: got %q; want %q", settingA.Key(), gotValue, wantValueA) } // Now, let's change another policy setting value N times. const N = 10 wantValueB := strconv.Itoa(N) start = time.Now() for i := range N { store.SetStrings(source.TestSettingOf(settingB.Key(), strconv.Itoa(i+1))) } // The callback should be invoked only once, even though the policy setting // has changed N times. change = <-policyChanged gotDelay = time.Since(start) gotCallbacks := 1 drain: for { select { case <-policyChanged: gotCallbacks++ case <-time.After(policyReloadMaxDelay): break drain } } if wantCallbacks := 1; gotCallbacks > wantCallbacks { t.Errorf("Callbacks: got %d; want %d", gotCallbacks, wantCallbacks) } // Additionally, the policy change callback should be received no sooner // than [policyReloadMinDelay] and no later than [policyReloadMaxDelay]. if gotDelay < policyReloadMinDelay || gotDelay > policyReloadMaxDelay { t.Errorf("Delay: got %v; want >= %v && <= %v", gotDelay, policyReloadMinDelay, policyReloadMaxDelay) } // Verify that the [PolicyChange] received via the callback // contains the final policy setting value. if !change.HasChanged(settingB.Key()) { t.Errorf("Policy setting %q has not changed", settingB.Key()) } if change.HasChanged(settingA.Key()) { t.Errorf("Policy setting %q was unexpectedly changed", settingA.Key()) } if _, ok := change.Old().GetSetting(settingB.Key()); ok { t.Fatalf("Policy setting %q unexpectedly exists", settingB.Key()) } if gotValue := change.New().Get(settingB.Key()); gotValue != wantValueB { t.Errorf("Policy setting %q: got %q; want %q", settingB.Key(), gotValue, wantValueB) } // Lastly, if a policy store issues a change notification, but the effective policy // remains unchanged, the [Policy] should ignore it without invoking the change callbacks. store.NotifyPolicyChanged() select { case <-policyChanged: t.Fatal("Unexpected policy changed notification") case <-time.After(policyReloadMaxDelay): } } func TestClosePolicySource(t *testing.T) { testSetting := setting.NewDefinition("TestSetting", setting.DeviceSetting, setting.StringValue) if err := setting.SetDefinitionsForTest(t, testSetting); err != nil { t.Fatalf("SetDefinitionsForTest failed: %v", err) } wantSettingValue := "TestValue" store := source.NewTestStoreOf(t, source.TestSettingOf(testSetting.Key(), wantSettingValue)) if _, err := RegisterStoreForTest(t, "TestSource", setting.DeviceScope, store); err != nil { t.Fatalf("Failed to register policy store: %v", err) } policy, err := policyForTest(t, setting.DeviceScope) if err != nil { t.Fatalf("Failed to get effective policy: %v", err) } initialSnapshot, err := policy.Reload() if err != nil { t.Fatalf("Failed to reload policy: %v", err) } if gotSettingValue, err := initialSnapshot.GetErr(testSetting.Key()); err != nil { t.Fatalf("Failed to get %q setting value: %v", testSetting.Key(), err) } else if gotSettingValue != wantSettingValue { t.Fatalf("Setting %q: got %q; want %q", testSetting.Key(), gotSettingValue, wantSettingValue) } store.Close() // Closing a policy source abruptly without removing it first should invalidate and close the policy. <-policy.Done() if policy.IsValid() { t.Fatal("The policy was not properly closed") } // The resulting policy snapshot should remain valid and unchanged. finalSnapshot := policy.Get() if !finalSnapshot.Equal(initialSnapshot) { t.Fatal("Policy snapshot has changed") } if gotSettingValue, err := finalSnapshot.GetErr(testSetting.Key()); err != nil { t.Fatalf("Failed to get final %q setting value: %v", testSetting.Key(), err) } else if gotSettingValue != wantSettingValue { t.Fatalf("Setting %q: got %q; want %q", testSetting.Key(), gotSettingValue, wantSettingValue) } // However, any further requests to reload the policy should fail. if _, err := policy.Reload(); err == nil || !errors.Is(err, ErrPolicyClosed) { t.Fatalf("Reload: gotErr: %v; wantErr: %v", err, ErrPolicyClosed) } } func TestRemovePolicySource(t *testing.T) { // Register policy settings used in this test. settingA := setting.NewDefinition("TestSettingA", setting.DeviceSetting, setting.StringValue) settingB := setting.NewDefinition("TestSettingB", setting.DeviceSetting, setting.StringValue) if err := setting.SetDefinitionsForTest(t, settingA, settingB); err != nil { t.Fatalf("SetDefinitionsForTest failed: %v", err) } // Register two policy stores. storeA := source.NewTestStoreOf(t, source.TestSettingOf(settingA.Key(), "A")) storeRegA, err := RegisterStoreForTest(t, "TestSourceA", setting.DeviceScope, storeA) if err != nil { t.Fatalf("Failed to register policy store A: %v", err) } storeB := source.NewTestStoreOf(t, source.TestSettingOf(settingB.Key(), "B")) storeRegB, err := RegisterStoreForTest(t, "TestSourceB", setting.DeviceScope, storeB) if err != nil { t.Fatalf("Failed to register policy store A: %v", err) } // Create a effective [Policy] that reads policy settings from the two stores. policy, err := policyForTest(t, setting.DeviceScope) if err != nil { t.Fatalf("Failed to get effective policy: %v", err) } // Verify that the [Policy] uses both stores and includes policy settings from each. if gotSources, wantSources := len(policy.sources), 2; gotSources != wantSources { t.Fatalf("Policy Sources: got %v; want %v", gotSources, wantSources) } if got, want := policy.Get().Get(settingA.Key()), "A"; got != want { t.Fatalf("Setting %q: got %q; want %q", settingA.Key(), got, want) } if got, want := policy.Get().Get(settingB.Key()), "B"; got != want { t.Fatalf("Setting %q: got %q; want %q", settingB.Key(), got, want) } // Unregister Store A and verify that the effective policy remains valid. // It should no longer use the removed store or include any policy settings from it. if err := storeRegA.Unregister(); err != nil { t.Fatalf("Failed to unregister Store A: %v", err) } if !policy.IsValid() { t.Fatalf("Policy was unexpectedly closed") } if gotSources, wantSources := len(policy.sources), 1; gotSources != wantSources { t.Fatalf("Policy Sources: got %v; want %v", gotSources, wantSources) } if got, want := policy.Get().Get(settingA.Key()), any(nil); got != want { t.Fatalf("Setting %q: got %q; want %q", settingA.Key(), got, want) } if got, want := policy.Get().Get(settingB.Key()), "B"; got != want { t.Fatalf("Setting %q: got %q; want %q", settingB.Key(), got, want) } // Unregister Store B and verify that the effective policy is still valid. // However, it should be empty since there are no associated policy sources. if err := storeRegB.Unregister(); err != nil { t.Fatalf("Failed to unregister Store B: %v", err) } if !policy.IsValid() { t.Fatalf("Policy was unexpectedly closed") } if gotSources, wantSources := len(policy.sources), 0; gotSources != wantSources { t.Fatalf("Policy Sources: got %v; want %v", gotSources, wantSources) } if got := policy.Get(); got.Len() != 0 { t.Fatalf("Settings: got %v; want {Empty}", got) } } func TestReplacePolicySource(t *testing.T) { setForTest(t, &policyReloadMinDelay, 100*time.Millisecond) setForTest(t, &policyReloadMaxDelay, 500*time.Millisecond) // Register policy settings used in this test. testSetting := setting.NewDefinition("TestSettingA", setting.DeviceSetting, setting.StringValue) if err := setting.SetDefinitionsForTest(t, testSetting); err != nil { t.Fatalf("SetDefinitionsForTest failed: %v", err) } // Create two policy stores. initialStore := source.NewTestStoreOf(t, source.TestSettingOf(testSetting.Key(), "InitialValue")) newStore := source.NewTestStoreOf(t, source.TestSettingOf(testSetting.Key(), "NewValue")) unchangedStore := source.NewTestStoreOf(t, source.TestSettingOf(testSetting.Key(), "NewValue")) // Register the initial store and create a effective [Policy] that reads policy settings from it. reg, err := RegisterStoreForTest(t, "TestStore", setting.DeviceScope, initialStore) if err != nil { t.Fatalf("Failed to register the initial store: %v", err) } policy, err := policyForTest(t, setting.DeviceScope) if err != nil { t.Fatalf("Failed to get effective policy: %v", err) } // Verify that the test setting has its initial value. if got, want := policy.Get().Get(testSetting.Key()), "InitialValue"; got != want { t.Fatalf("Setting %q: got %q; want %q", testSetting.Key(), got, want) } // Subscribe to the policy change callback. policyChanged := make(chan *PolicyChange, 1) unregister := policy.RegisterChangeCallback(func(pc *PolicyChange) { policyChanged <- pc }) t.Cleanup(unregister) // Now, let's replace the initial store with the new store. reg, err = reg.ReplaceStore(newStore) if err != nil { t.Fatalf("Failed to replace the policy store: %v", err) } t.Cleanup(func() { reg.Unregister() }) // We should receive a policy change notification as the setting value has changed. <-policyChanged // Verify that the test setting has the new value. if got, want := policy.Get().Get(testSetting.Key()), "NewValue"; got != want { t.Fatalf("Setting %q: got %q; want %q", testSetting.Key(), got, want) } // Replacing a policy store with an identical one containing the same // values for the same settings should not be considered a policy change. reg, err = reg.ReplaceStore(unchangedStore) if err != nil { t.Fatalf("Failed to replace the policy store: %v", err) } t.Cleanup(func() { reg.Unregister() }) select { case <-policyChanged: t.Fatal("Unexpected policy changed notification") default: <-time.After(policyReloadMaxDelay) } } func TestAddClosedPolicySource(t *testing.T) { store := source.NewTestStoreOf[string](t) if _, err := RegisterStoreForTest(t, "TestSource", setting.DeviceScope, store); err != nil { t.Fatalf("Failed to register policy store: %v", err) } store.Close() _, err := policyForTest(t, setting.DeviceScope) if err == nil || !errors.Is(err, source.ErrStoreClosed) { t.Fatalf("got: %v; want: %v", err, source.ErrStoreClosed) } } func TestClosePolicyMoreThanOnce(t *testing.T) { tests := []struct { name string numSources int }{ { name: "NoSources", numSources: 0, }, { name: "OneSource", numSources: 1, }, { name: "ManySources", numSources: 10, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for i := range tt.numSources { store := source.NewTestStoreOf[string](t) if _, err := RegisterStoreForTest(t, "TestSource #"+strconv.Itoa(i), setting.DeviceScope, store); err != nil { t.Fatalf("Failed to register policy store: %v", err) } } policy, err := policyForTest(t, setting.DeviceScope) if err != nil { t.Fatalf("failed to get effective policy: %v", err) } const N = 10000 var wg sync.WaitGroup for range N { wg.Add(1) go func() { wg.Done() policy.Close() <-policy.Done() }() } wg.Wait() }) } } func checkPolicySources(tb testing.TB, gotPolicy *Policy, wantSources []*source.Source) { tb.Helper() sort.SliceStable(wantSources, func(i, j int) bool { return wantSources[i].Compare(wantSources[j]) < 0 }) gotSources := make([]*source.Source, len(gotPolicy.sources)) for i := range gotPolicy.sources { gotSources[i] = gotPolicy.sources[i].Source } type sourceSummary struct{ Name, Scope string } toSourceSummary := cmp.Transformer("source", func(s *source.Source) sourceSummary { return sourceSummary{s.Name(), s.Scope().String()} }) if diff := cmp.Diff(wantSources, gotSources, toSourceSummary, cmpopts.EquateEmpty()); diff != "" { tb.Errorf("Policy Sources mismatch: %v", diff) } } // policyForTest is like [PolicyFor], but it deletes the policy // when tb and all its subtests complete. func policyForTest(tb testing.TB, target setting.PolicyScope) (*Policy, error) { tb.Helper() policy, err := PolicyFor(target) if err != nil { return nil, err } tb.Cleanup(func() { policy.Close() <-policy.Done() deletePolicy(policy) }) return policy, nil } func setForTest[T any](tb testing.TB, target *T, newValue T) { oldValue := *target tb.Cleanup(func() { *target = oldValue }) *target = newValue }