@ -9,57 +9,15 @@ import (
"testing"
"time"
"tailscale.com/types/logger"
"tailscale.com/util/syspolicy/internal/loggerx"
"tailscale.com/util/syspolicy/internal/metrics"
"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 . T
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 ) {
th . t . Errorf ( "ReadString(%q) want %q" , key , th . key )
}
th . calls ++
return th . s , th . err
}
func ( th * testHandler ) ReadUInt64 ( key string ) ( uint64 , error ) {
if key != string ( th . key ) {
th . t . Errorf ( "ReadUint64(%q) want %q" , key , th . key )
}
th . calls ++
return th . u64 , th . err
}
func ( th * testHandler ) ReadBoolean ( key string ) ( bool , error ) {
if key != string ( th . key ) {
th . t . Errorf ( "ReadBool(%q) want %q" , key , th . key )
}
th . calls ++
return th . b , th . err
}
func ( th * testHandler ) ReadStringArray ( key string ) ( [ ] string , error ) {
if key != string ( th . key ) {
th . t . Errorf ( "ReadStringArray(%q) want %q" , key , th . key )
}
th . calls ++
return th . sArr , th . err
}
func TestGetString ( t * testing . T ) {
tests := [ ] struct {
name string
@ -69,23 +27,28 @@ func TestGetString(t *testing.T) {
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 : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantError : nil ,
} ,
{
name : "read non-existing value, non-blank default" ,
key : EnableServerMode ,
handlerError : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
defaultValue : "test" ,
wantValue : "test" ,
wantError : nil ,
@ -95,24 +58,43 @@ func TestGetString(t *testing.T) {
key : NetworkDevicesVisibility ,
handlerError : someOtherError ,
wantError : someOtherError ,
wantMetrics : [ ] metrics . TestState {
{ Name : "$os_syspolicy_errors" , Value : 1 } ,
{ Name : "$os_syspolicy_NetworkDevices_error" , Value : 1 } ,
} ,
} ,
}
RegisterWellKnownSettingsForTest ( t )
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
s : tt . handlerValue ,
err : tt . handlerError ,
} )
h := metrics . NewTestHandler ( t )
metrics . SetHooksForTest ( t , h . AddMetric , h . SetMetric )
s := source . TestSetting [ string ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
value , err := GetString ( tt . key , tt . defaultValue )
if err != tt . wantError {
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-09-04, syspolicy only reports metrics
// on Windows and Android.
wantMetrics = nil
}
h . MustEqual ( wantMetrics ... )
} )
}
}
@ -129,7 +111,7 @@ func TestGetUint64(t *testing.T) {
} {
{
name : "read existing value" ,
key : KeyExpirationNoticeTime ,
key : LogSCMInteractions ,
handlerValue : 1 ,
wantValue : 1 ,
} ,
@ -137,14 +119,14 @@ func TestGetUint64(t *testing.T) {
name : "read non-existing value" ,
key : LogSCMInteractions ,
handlerValue : 0 ,
handlerError : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : 0 ,
} ,
{
name : "read non-existing value, non-zero default" ,
key : LogSCMInteractions ,
defaultValue : 2 ,
handlerError : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : 2 ,
} ,
{
@ -157,14 +139,23 @@ func TestGetUint64(t *testing.T) {
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
u64 : tt . handlerValue ,
err : tt . handlerError ,
} )
// None of the policy settings tested here are integers.
// In fact, we don't have any integer policies as of 2024-10-08.
// 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 )
}
s := source . TestSetting [ uint64 ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
value , err := GetUint64 ( tt . key , tt . defaultValue )
if err != tt . wantError {
if ! errorsMatchForTest ( err , tt . wantError ) {
t . Errorf ( "err=%q, want %q" , err , tt . wantError )
}
if value != tt . wantValue {
@ -183,45 +174,69 @@ func TestGetBoolean(t *testing.T) {
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 : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : false ,
} ,
{
name : "reading value returns other error" ,
key : FlushDNSOnSessionUnlock ,
handlerError : someOtherError ,
wantError : someOtherError ,
wantError : someOtherError , // expect error...
defaultValue : true ,
wantValue : false ,
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 } ,
} ,
} ,
}
RegisterWellKnownSettingsForTest ( t )
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
b : tt . handlerValue ,
err : tt . handlerError ,
} )
h := metrics . NewTestHandler ( t )
metrics . SetHooksForTest ( t , h . AddMetric , h . SetMetric )
s := source . TestSetting [ bool ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
value , err := GetBoolean ( tt . key , tt . defaultValue )
if err != tt . wantError {
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-09-04, syspolicy only reports metrics
// on Windows and Android.
wantMetrics = nil
}
h . MustEqual ( wantMetrics ... )
} )
}
}
@ -234,29 +249,42 @@ func TestGetPreferenceOption(t *testing.T) {
handlerError error
wantValue setting . 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 : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : setting . ShowChoiceByPolicy ,
} ,
{
@ -265,24 +293,43 @@ func TestGetPreferenceOption(t *testing.T) {
handlerError : someOtherError ,
wantValue : setting . ShowChoiceByPolicy ,
wantError : someOtherError ,
wantMetrics : [ ] metrics . TestState {
{ Name : "$os_syspolicy_errors" , Value : 1 } ,
{ Name : "$os_syspolicy_AllowIncomingConnections_error" , Value : 1 } ,
} ,
} ,
}
RegisterWellKnownSettingsForTest ( t )
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
s : tt . handlerValue ,
err : tt . handlerError ,
} )
h := metrics . NewTestHandler ( t )
metrics . SetHooksForTest ( t , h . AddMetric , h . SetMetric )
s := source . TestSetting [ string ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
option , err := GetPreferenceOption ( tt . key )
if err != tt . wantError {
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-09-04, syspolicy only reports metrics
// on Windows and Android.
wantMetrics = nil
}
h . MustEqual ( wantMetrics ... )
} )
}
}
@ -295,24 +342,33 @@ func TestGetVisibility(t *testing.T) {
handlerError error
wantValue setting . 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 : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : setting . VisibleByPolicy ,
} ,
{
@ -322,24 +378,43 @@ func TestGetVisibility(t *testing.T) {
handlerError : someOtherError ,
wantValue : setting . VisibleByPolicy ,
wantError : someOtherError ,
wantMetrics : [ ] metrics . TestState {
{ Name : "$os_syspolicy_errors" , Value : 1 } ,
{ Name : "$os_syspolicy_AdminConsole_error" , Value : 1 } ,
} ,
} ,
}
RegisterWellKnownSettingsForTest ( t )
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
s : tt . handlerValue ,
err : tt . handlerError ,
} )
h := metrics . NewTestHandler ( t )
metrics . SetHooksForTest ( t , h . AddMetric , h . SetMetric )
s := source . TestSetting [ string ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
visibility , err := GetVisibility ( tt . key )
if err != tt . wantError {
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-09-04, syspolicy only reports metrics
// on Windows and Android.
wantMetrics = nil
}
h . MustEqual ( wantMetrics ... )
} )
}
}
@ -353,6 +428,7 @@ func TestGetDuration(t *testing.T) {
defaultValue time . Duration
wantValue time . Duration
wantError error
wantMetrics [ ] metrics . TestState
} {
{
name : "read existing value" ,
@ -360,25 +436,34 @@ func TestGetDuration(t *testing.T) {
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 : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : 24 * time . Hour ,
defaultValue : 24 * time . Hour ,
} ,
{
name : "read non-existing value different default" ,
key : KeyExpirationNoticeTime ,
handlerError : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantValue : 0 * time . Second ,
defaultValue : 0 * time . Second ,
} ,
@ -389,24 +474,43 @@ func TestGetDuration(t *testing.T) {
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 } ,
} ,
} ,
}
RegisterWellKnownSettingsForTest ( t )
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
s : tt . handlerValue ,
err : tt . handlerError ,
} )
h := metrics . NewTestHandler ( t )
metrics . SetHooksForTest ( t , h . AddMetric , h . SetMetric )
s := source . TestSetting [ string ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
duration , err := GetDuration ( tt . key , tt . defaultValue )
if err != tt . wantError {
if ! errorsMatchForTest ( err , 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-09-04, syspolicy only reports metrics
// on Windows and Android.
wantMetrics = nil
}
h . MustEqual ( wantMetrics ... )
} )
}
}
@ -420,23 +524,28 @@ func TestGetStringArray(t *testing.T) {
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 : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
wantError : nil ,
} ,
{
name : "read non-existing value, non nil default" ,
key : AllowedSuggestedExitNodes ,
handlerError : ErrNo SuchKey ,
handlerError : ErrNo tConfigured ,
defaultValue : [ ] string { "foo" , "bar" } ,
wantValue : [ ] string { "foo" , "bar" } ,
wantError : nil ,
@ -446,28 +555,68 @@ func TestGetStringArray(t *testing.T) {
key : AllowedSuggestedExitNodes ,
handlerError : someOtherError ,
wantError : someOtherError ,
wantMetrics : [ ] metrics . TestState {
{ Name : "$os_syspolicy_errors" , Value : 1 } ,
{ Name : "$os_syspolicy_AllowedSuggestedExitNodes_error" , Value : 1 } ,
} ,
} ,
}
RegisterWellKnownSettingsForTest ( t )
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
SetHandlerForTest ( t , & testHandler {
t : t ,
key : tt . key ,
sArr : tt . handlerValue ,
err : tt . handlerError ,
} )
h := metrics . NewTestHandler ( t )
metrics . SetHooksForTest ( t , h . AddMetric , h . SetMetric )
s := source . TestSetting [ [ ] string ] {
Key : tt . key ,
Value : tt . handlerValue ,
Error : tt . handlerError ,
}
registerSingleSettingStoreForTest ( t , s )
value , err := GetStringArray ( tt . key , tt . defaultValue )
if err != tt . wantError {
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-09-04, syspolicy only reports metrics
// on Windows and Android.
wantMetrics = nil
}
h . MustEqual ( wantMetrics ... )
} )
}
}
func registerSingleSettingStoreForTest [ T source . TestValueType ] ( tb TB , s source . TestSetting [ T ] ) {
policyStore := source . NewTestStoreOf ( tb , s )
MustRegisterStoreForTest ( tb , "TestStore" , setting . DeviceScope , policyStore )
}
func BenchmarkGetString ( b * testing . B ) {
loggerx . SetForTest ( b , logger . Discard , logger . Discard )
RegisterWellKnownSettingsForTest ( b )
wantControlURL := "https://login.tailscale.com"
registerSingleSettingStoreForTest ( b , source . TestSettingOf ( ControlURL , wantControlURL ) )
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
@ -499,3 +648,13 @@ func TestSelectControlURL(t *testing.T) {
}
}
}
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 ( )
}