mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-18 20:51:45 +00:00

This updates the syspolicy package to support multiple policy sources in the three policy scopes: user, profile, and device, and provides a merged resultant policy. A policy source is a syspolicy/source.Store that has a name and provides access to policy settings for a given scope. It can be registered with syspolicy/rsop.RegisterStore. Policy sources and policy stores can be either platform-specific or platform-agnostic. On Windows, we have the Registry-based, platform-specific policy store implemented as syspolicy/source.PlatformPolicyStore. This store provides access to the Group Policy and MDM policy settings stored in the Registry. On other platforms, we currently provide a wrapper that converts a syspolicy.Handler into a syspolicy/source.Store. However, we should update them in follow-up PRs. An example of a platform-agnostic policy store would be a policy deployed from the control, a local policy config file, or even environment variables. We maintain the current, most recent version of the resultant policy for each scope in an rsop.Policy. This is done by reading and merging the policy settings from the registered stores the first time the resultant policy is requested, then re-reading and re-merging them if a store implements the source.Changeable interface and reports a policy change. Policy change notifications are debounced to avoid re-reading policy settings multiple times if there are several changes within a short period. The rsop.Policy can notify clients if the resultant policy has changed. However, we do not currently expose this via the syspolicy package and plan to do so differently along with a struct-based policy hierarchy in the next PR. To facilitate this, all policy settings should be registered with the setting.Register function. The syspolicy package does this automatically for all policy settings defined in policy_keys.go. The new functionality is available through the existing syspolicy.Read* set of functions. However, we plan to expose it via a struct-based policy hierarchy, along with policy change notifications that other subsystems can use, in the next PR. We also plan to send the resultant policy back from tailscaled to the clients via the LocalAPI. This is primarily a foundational PR to facilitate future changes, but the immediate observable changes on Windows include: - The service will use the current policy setting values instead of those read at OS boot time. - The GUI has access to policy settings configured on a per-user basis. On Android: - We now report policy setting usage via clientmetrics. Updates #12687 Signed-off-by: Nick Khyl <nickk@tailscale.com>
696 lines
19 KiB
Go
696 lines
19 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package syspolicy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/syspolicy/internal/loggerx"
|
|
"tailscale.com/util/syspolicy/internal/metrics"
|
|
"tailscale.com/util/syspolicy/rsop"
|
|
"tailscale.com/util/syspolicy/setting"
|
|
"tailscale.com/util/syspolicy/source"
|
|
)
|
|
|
|
// testHandler encompasses all data types returned when testing any of the syspolicy
|
|
// methods that involve getting a policy value.
|
|
// For keys and the corresponding values, check policy_keys.go.
|
|
type testHandler struct {
|
|
t testing.TB
|
|
key Key
|
|
s string
|
|
u64 uint64
|
|
b bool
|
|
sArr []string
|
|
err error
|
|
calls int // used for testing reads from cache vs. handler
|
|
}
|
|
|
|
var someOtherError = errors.New("error other than not found")
|
|
|
|
func (th *testHandler) ReadString(key string) (string, error) {
|
|
if key != string(th.key) {
|
|
// The syspolicy package now reads and caches all registered policy settings.
|
|
// Therefore, it is expected to call the handler requesting all policies
|
|
// rather than just the specific ones we asked for.
|
|
return "", ErrNotConfigured
|
|
}
|
|
th.calls++
|
|
return th.s, th.err
|
|
}
|
|
|
|
func (th *testHandler) ReadUInt64(key string) (uint64, error) {
|
|
if key != string(th.key) {
|
|
// The syspolicy package now reads and caches all registered policy settings.
|
|
// Therefore, it is expected to call the handler requesting all policies
|
|
// rather than just the specific ones we asked for.
|
|
return 0, ErrNotConfigured
|
|
}
|
|
th.calls++
|
|
return th.u64, th.err
|
|
}
|
|
|
|
func (th *testHandler) ReadBoolean(key string) (bool, error) {
|
|
if key != string(th.key) {
|
|
// The syspolicy package now reads and caches all registered policy settings.
|
|
// Therefore, it is expected to call the handler requesting all policies
|
|
// rather than just the specific ones we asked for.
|
|
return false, ErrNotConfigured
|
|
}
|
|
th.calls++
|
|
return th.b, th.err
|
|
}
|
|
|
|
func (th *testHandler) ReadStringArray(key string) ([]string, error) {
|
|
if key != string(th.key) {
|
|
// The syspolicy package now reads and caches all registered policy settings.
|
|
// Therefore, it is expected to call the handler requesting all policies
|
|
// rather than just the specific ones we asked for.
|
|
return nil, ErrNotConfigured
|
|
}
|
|
th.calls++
|
|
return th.sArr, th.err
|
|
}
|
|
|
|
func TestGetString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue string
|
|
handlerError error
|
|
defaultValue string
|
|
wantValue string
|
|
wantError error
|
|
wantMetrics []metrics.TestState
|
|
}{
|
|
{
|
|
name: "read existing value",
|
|
key: AdminConsoleVisibility,
|
|
handlerValue: "hide",
|
|
wantValue: "hide",
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AdminConsole", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: EnableServerMode,
|
|
handlerError: ErrNotConfigured,
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "read non-existing value, non-blank default",
|
|
key: EnableServerMode,
|
|
handlerError: ErrNotConfigured,
|
|
defaultValue: "test",
|
|
wantValue: "test",
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "reading value returns other error",
|
|
key: NetworkDevicesVisibility,
|
|
handlerError: someOtherError,
|
|
wantError: someOtherError,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_NetworkDevices_error", Value: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := metrics.NewTestHandler(t)
|
|
metrics.SetHooksForTest(t, h.AddMetric, h.SetMetric)
|
|
SetHandlerForTest(t, &testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
s: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
})
|
|
value, err := GetString(tt.key, tt.defaultValue)
|
|
if !errorsMatchForTest(err, tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if value != tt.wantValue {
|
|
t.Errorf("value=%v, want %v", value, tt.wantValue)
|
|
}
|
|
wantMetrics := tt.wantMetrics
|
|
if !metrics.ShouldReport() {
|
|
// Check that metrics are not reported on platforms
|
|
// where they shouldn't be reported.
|
|
// As of 2024-08-02, syspolicy only reports metrics
|
|
// on Windows and Android.
|
|
wantMetrics = nil
|
|
}
|
|
h.MustEqual(wantMetrics...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetUint64(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue uint64
|
|
handlerError error
|
|
defaultValue uint64
|
|
wantValue uint64
|
|
wantError error
|
|
}{
|
|
{
|
|
name: "read existing value",
|
|
key: LogSCMInteractions,
|
|
handlerValue: 1,
|
|
wantValue: 1,
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: LogSCMInteractions,
|
|
handlerValue: 0,
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: 0,
|
|
},
|
|
{
|
|
name: "read non-existing value, non-zero default",
|
|
key: LogSCMInteractions,
|
|
defaultValue: 2,
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: 2,
|
|
},
|
|
{
|
|
name: "reading value returns other error",
|
|
key: FlushDNSOnSessionUnlock,
|
|
handlerError: someOtherError,
|
|
wantError: someOtherError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// None of the policy settings tested here are integers.
|
|
// In fact, we don't have any integer policies as of 2024-07-29.
|
|
// However, we can register each of them as an integer policy setting
|
|
// for the duration of the test, providing us with something to test against.
|
|
if err := setting.SetDefinitionsForTest(t, setting.NewDefinition(tt.key, setting.DeviceSetting, setting.IntegerValue)); err != nil {
|
|
t.Fatalf("SetDefinitionsForTest failed: %v", err)
|
|
}
|
|
rsop.RegisterStoreForTest(t, tt.name, setting.DeviceScope, WrapHandler(&testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
u64: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
}))
|
|
value, err := GetUint64(tt.key, tt.defaultValue)
|
|
if !errorsMatchForTest(err, tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if value != tt.wantValue {
|
|
t.Errorf("value=%v, want %v", value, tt.wantValue)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetBoolean(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue bool
|
|
handlerError error
|
|
defaultValue bool
|
|
wantValue bool
|
|
wantError error
|
|
wantMetrics []metrics.TestState
|
|
}{
|
|
{
|
|
name: "read existing value",
|
|
key: FlushDNSOnSessionUnlock,
|
|
handlerValue: true,
|
|
wantValue: true,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_FlushDNSOnSessionUnlock", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: LogSCMInteractions,
|
|
handlerValue: false,
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: false,
|
|
},
|
|
{
|
|
name: "reading value returns other error",
|
|
key: FlushDNSOnSessionUnlock,
|
|
handlerError: someOtherError,
|
|
wantError: someOtherError, // expect error...
|
|
defaultValue: true,
|
|
wantValue: true, // ...AND default value if the handler fails.
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_FlushDNSOnSessionUnlock_error", Value: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := metrics.NewTestHandler(t)
|
|
metrics.SetHooksForTest(t, h.AddMetric, h.SetMetric)
|
|
SetHandlerForTest(t, &testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
b: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
})
|
|
value, err := GetBoolean(tt.key, tt.defaultValue)
|
|
if !errorsMatchForTest(err, tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if value != tt.wantValue {
|
|
t.Errorf("value=%v, want %v", value, tt.wantValue)
|
|
}
|
|
wantMetrics := tt.wantMetrics
|
|
if !metrics.ShouldReport() {
|
|
// Check that metrics are not reported on platforms
|
|
// where they shouldn't be reported.
|
|
// As of 2024-08-02, syspolicy only reports metrics
|
|
// on Windows and Android.
|
|
wantMetrics = nil
|
|
}
|
|
h.MustEqual(wantMetrics...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetPreferenceOption(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue string
|
|
handlerError error
|
|
wantValue PreferenceOption
|
|
wantError error
|
|
wantMetrics []metrics.TestState
|
|
}{
|
|
{
|
|
name: "always by policy",
|
|
key: EnableIncomingConnections,
|
|
handlerValue: "always",
|
|
wantValue: setting.AlwaysByPolicy,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AllowIncomingConnections", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "never by policy",
|
|
key: EnableIncomingConnections,
|
|
handlerValue: "never",
|
|
wantValue: setting.NeverByPolicy,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AllowIncomingConnections", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "use default",
|
|
key: EnableIncomingConnections,
|
|
handlerValue: "",
|
|
wantValue: setting.ShowChoiceByPolicy,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AllowIncomingConnections", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: EnableIncomingConnections,
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: setting.ShowChoiceByPolicy,
|
|
},
|
|
{
|
|
name: "other error is returned",
|
|
key: EnableIncomingConnections,
|
|
handlerError: someOtherError,
|
|
wantValue: setting.ShowChoiceByPolicy,
|
|
wantError: someOtherError,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_AllowIncomingConnections_error", Value: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := metrics.NewTestHandler(t)
|
|
metrics.SetHooksForTest(t, h.AddMetric, h.SetMetric)
|
|
SetHandlerForTest(t, &testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
s: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
})
|
|
option, err := GetPreferenceOption(tt.key)
|
|
if !errorsMatchForTest(err, tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if option != tt.wantValue {
|
|
t.Errorf("option=%v, want %v", option, tt.wantValue)
|
|
}
|
|
wantMetrics := tt.wantMetrics
|
|
if !metrics.ShouldReport() {
|
|
// Check that metrics are not reported on platforms
|
|
// where they shouldn't be reported.
|
|
// As of 2024-08-02, syspolicy only reports metrics
|
|
// on Windows and Android.
|
|
wantMetrics = nil
|
|
}
|
|
h.MustEqual(wantMetrics...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetVisibility(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue string
|
|
handlerError error
|
|
wantValue Visibility
|
|
wantError error
|
|
wantMetrics []metrics.TestState
|
|
}{
|
|
{
|
|
name: "hidden by policy",
|
|
key: AdminConsoleVisibility,
|
|
handlerValue: "hide",
|
|
wantValue: setting.HiddenByPolicy,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AdminConsole", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "visibility default",
|
|
key: AdminConsoleVisibility,
|
|
handlerValue: "show",
|
|
wantValue: setting.VisibleByPolicy,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AdminConsole", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: AdminConsoleVisibility,
|
|
handlerValue: "show",
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: setting.VisibleByPolicy,
|
|
},
|
|
{
|
|
name: "other error is returned",
|
|
key: AdminConsoleVisibility,
|
|
handlerValue: "show",
|
|
handlerError: someOtherError,
|
|
wantValue: setting.VisibleByPolicy,
|
|
wantError: someOtherError,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_AdminConsole_error", Value: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := metrics.NewTestHandler(t)
|
|
metrics.SetHooksForTest(t, h.AddMetric, h.SetMetric)
|
|
SetHandlerForTest(t, &testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
s: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
})
|
|
visibility, err := GetVisibility(tt.key)
|
|
if !errorsMatchForTest(err, tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if visibility != tt.wantValue {
|
|
t.Errorf("visibility=%v, want %v", visibility, tt.wantValue)
|
|
}
|
|
wantMetrics := tt.wantMetrics
|
|
if !metrics.ShouldReport() {
|
|
// Check that metrics are not reported on platforms
|
|
// where they shouldn't be reported.
|
|
// As of 2024-08-02, syspolicy only reports metrics
|
|
// on Windows and Android.
|
|
wantMetrics = nil
|
|
}
|
|
h.MustEqual(wantMetrics...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetDuration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue string
|
|
handlerError error
|
|
defaultValue time.Duration
|
|
wantValue time.Duration
|
|
wantError error
|
|
wantMetrics []metrics.TestState
|
|
}{
|
|
{
|
|
name: "read existing value",
|
|
key: KeyExpirationNoticeTime,
|
|
handlerValue: "2h",
|
|
wantValue: 2 * time.Hour,
|
|
defaultValue: 24 * time.Hour,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_KeyExpirationNotice", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "invalid duration value",
|
|
key: KeyExpirationNoticeTime,
|
|
handlerValue: "-20",
|
|
wantValue: 24 * time.Hour,
|
|
wantError: errors.New(`time: missing unit in duration "-20"`),
|
|
defaultValue: 24 * time.Hour,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_KeyExpirationNotice_error", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: KeyExpirationNoticeTime,
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: 24 * time.Hour,
|
|
defaultValue: 24 * time.Hour,
|
|
},
|
|
{
|
|
name: "read non-existing value different default",
|
|
key: KeyExpirationNoticeTime,
|
|
handlerError: ErrNotConfigured,
|
|
wantValue: 0 * time.Second,
|
|
defaultValue: 0 * time.Second,
|
|
},
|
|
{
|
|
name: "other error is returned",
|
|
key: KeyExpirationNoticeTime,
|
|
handlerError: someOtherError,
|
|
wantValue: 24 * time.Hour,
|
|
wantError: someOtherError,
|
|
defaultValue: 24 * time.Hour,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_KeyExpirationNotice_error", Value: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := metrics.NewTestHandler(t)
|
|
metrics.SetHooksForTest(t, h.AddMetric, h.SetMetric)
|
|
SetHandlerForTest(t, &testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
s: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
})
|
|
duration, err := GetDuration(tt.key, tt.defaultValue)
|
|
if fmt.Sprint(err) != fmt.Sprint(tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if duration != tt.wantValue {
|
|
t.Errorf("duration=%v, want %v", duration, tt.wantValue)
|
|
}
|
|
wantMetrics := tt.wantMetrics
|
|
if !metrics.ShouldReport() {
|
|
// Check that metrics are not reported on platforms
|
|
// where they shouldn't be reported.
|
|
// As of 2024-08-02, syspolicy only reports metrics
|
|
// on Windows and Android.
|
|
wantMetrics = nil
|
|
}
|
|
h.MustEqual(wantMetrics...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetStringArray(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key Key
|
|
handlerValue []string
|
|
handlerError error
|
|
defaultValue []string
|
|
wantValue []string
|
|
wantError error
|
|
wantMetrics []metrics.TestState
|
|
}{
|
|
{
|
|
name: "read existing value",
|
|
key: AllowedSuggestedExitNodes,
|
|
handlerValue: []string{"foo", "bar"},
|
|
wantValue: []string{"foo", "bar"},
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_any", Value: 1},
|
|
{Name: "$os_syspolicy_AllowedSuggestedExitNodes", Value: 1},
|
|
},
|
|
},
|
|
{
|
|
name: "read non-existing value",
|
|
key: AllowedSuggestedExitNodes,
|
|
handlerError: ErrNotConfigured,
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "read non-existing value, non nil default",
|
|
key: AllowedSuggestedExitNodes,
|
|
handlerError: ErrNotConfigured,
|
|
defaultValue: []string{"foo", "bar"},
|
|
wantValue: []string{"foo", "bar"},
|
|
wantError: nil,
|
|
},
|
|
{
|
|
name: "reading value returns other error",
|
|
key: AllowedSuggestedExitNodes,
|
|
handlerError: someOtherError,
|
|
wantError: someOtherError,
|
|
wantMetrics: []metrics.TestState{
|
|
{Name: "$os_syspolicy_errors", Value: 1},
|
|
{Name: "$os_syspolicy_AllowedSuggestedExitNodes_error", Value: 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := metrics.NewTestHandler(t)
|
|
metrics.SetHooksForTest(t, h.AddMetric, h.SetMetric)
|
|
SetHandlerForTest(t, &testHandler{
|
|
t: t,
|
|
key: tt.key,
|
|
sArr: tt.handlerValue,
|
|
err: tt.handlerError,
|
|
})
|
|
value, err := GetStringArray(tt.key, tt.defaultValue)
|
|
if !errorsMatchForTest(err, tt.wantError) {
|
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
|
}
|
|
if !slices.Equal(tt.wantValue, value) {
|
|
t.Errorf("value=%v, want %v", value, tt.wantValue)
|
|
}
|
|
wantMetrics := tt.wantMetrics
|
|
if !metrics.ShouldReport() {
|
|
// Check that metrics are not reported on platforms
|
|
// where they shouldn't be reported.
|
|
// As of 2024-08-02, syspolicy only reports metrics
|
|
// on Windows and Android.
|
|
wantMetrics = nil
|
|
}
|
|
h.MustEqual(wantMetrics...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkGetString(b *testing.B) {
|
|
loggerx.SetForTest(b, logger.Discard, logger.Discard)
|
|
setWellKnownSettingsForTest(b)
|
|
|
|
store := source.NewTestStore(b)
|
|
wantControlURL := "https://login.tailscale.com"
|
|
store.SetStrings(source.TestSetting[string]{Key: ControlURL, Value: wantControlURL})
|
|
|
|
_, err := rsop.RegisterStoreForTest(b, "Test Store", setting.DeviceScope, store)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
gotControlURL, _ := GetString(ControlURL, "https://controlplane.tailscale.com")
|
|
if gotControlURL != wantControlURL {
|
|
b.Fatalf("got %v; want %v", gotControlURL, wantControlURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSelectControlURL(t *testing.T) {
|
|
tests := []struct {
|
|
reg, disk, want string
|
|
}{
|
|
// Modern default case.
|
|
{"", "", "https://controlplane.tailscale.com"},
|
|
|
|
// For a user who installed prior to Dec 2020, with
|
|
// stuff in their registry.
|
|
{"https://login.tailscale.com", "", "https://login.tailscale.com"},
|
|
|
|
// Ignore pre-Dec'20 LoginURL from installer if prefs
|
|
// prefs overridden manually to an on-prem control
|
|
// server.
|
|
{"https://login.tailscale.com", "http://on-prem", "http://on-prem"},
|
|
|
|
// Something unknown explicitly set in the registry always wins.
|
|
{"http://explicit-reg", "", "http://explicit-reg"},
|
|
{"http://explicit-reg", "http://on-prem", "http://explicit-reg"},
|
|
{"http://explicit-reg", "https://login.tailscale.com", "http://explicit-reg"},
|
|
{"http://explicit-reg", "https://controlplane.tailscale.com", "http://explicit-reg"},
|
|
|
|
// If nothing in the registry, disk wins.
|
|
{"", "http://on-prem", "http://on-prem"},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := SelectControlURL(tt.reg, tt.disk); got != tt.want {
|
|
t.Errorf("(reg %q, disk %q) = %q; want %q", tt.reg, tt.disk, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func errorsMatchForTest(got, want error) bool {
|
|
if got == nil && want == nil {
|
|
return true
|
|
}
|
|
if got == nil || want == nil {
|
|
return false
|
|
}
|
|
return errors.Is(got, want) || got.Error() == want.Error()
|
|
}
|