github.com/crowdsecurity/crowdsec@v1.6.1/pkg/fflag/features_test.go (about)

     1  package fflag_test
     2  
     3  import (
     4  	"os"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/sirupsen/logrus"
     9  	logtest "github.com/sirupsen/logrus/hooks/test"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/crowdsecurity/go-cs-lib/cstest"
    13  
    14  	"github.com/crowdsecurity/crowdsec/pkg/fflag"
    15  )
    16  
    17  func TestRegisterFeature(t *testing.T) {
    18  	tests := []struct {
    19  		name        string
    20  		feature     fflag.Feature
    21  		expectedErr string
    22  	}{
    23  		{
    24  			name: "a plain feature",
    25  			feature: fflag.Feature{
    26  				Name: "plain",
    27  			},
    28  		},
    29  		{
    30  			name: "capitalized feature name",
    31  			feature: fflag.Feature{
    32  				Name: "Plain",
    33  			},
    34  			expectedErr: "feature flag 'Plain': name is not lowercase",
    35  		},
    36  		{
    37  			name: "empty feature name",
    38  			feature: fflag.Feature{
    39  				Name: "",
    40  			},
    41  			expectedErr: "feature flag '': name is empty",
    42  		},
    43  		{
    44  			name: "invalid feature name",
    45  			feature: fflag.Feature{
    46  				Name: "meh!",
    47  			},
    48  			expectedErr: "feature flag 'meh!': invalid name (allowed a-z, 0-9, _, .)",
    49  		},
    50  	}
    51  
    52  	for _, tc := range tests {
    53  		tc := tc
    54  
    55  		t.Run("", func(t *testing.T) {
    56  			fr := fflag.FeatureRegister{EnvPrefix: "FFLAG_TEST_"}
    57  			err := fr.RegisterFeature(&tc.feature)
    58  			cstest.RequireErrorContains(t, err, tc.expectedErr)
    59  		})
    60  	}
    61  }
    62  
    63  func setUp(t *testing.T) fflag.FeatureRegister {
    64  	t.Helper()
    65  
    66  	fr := fflag.FeatureRegister{EnvPrefix: "FFLAG_TEST_"}
    67  
    68  	err := fr.RegisterFeature(&fflag.Feature{Name: "experimental1"})
    69  	require.NoError(t, err)
    70  
    71  	err = fr.RegisterFeature(&fflag.Feature{
    72  		Name:        "some_feature",
    73  		Description: "A feature that does something, with a description",
    74  	})
    75  	require.NoError(t, err)
    76  
    77  	err = fr.RegisterFeature(&fflag.Feature{
    78  		Name:           "new_standard",
    79  		State:          fflag.DeprecatedState,
    80  		Description:    "This implements the new standard T34.256w",
    81  		DeprecationMsg: "In 2.0 we'll do T34.256w by default",
    82  	})
    83  	require.NoError(t, err)
    84  
    85  	err = fr.RegisterFeature(&fflag.Feature{
    86  		Name:           "was_adopted",
    87  		State:          fflag.RetiredState,
    88  		Description:    "This implements a new tricket",
    89  		DeprecationMsg: "The trinket was implemented in 1.5",
    90  	})
    91  	require.NoError(t, err)
    92  
    93  	return fr
    94  }
    95  
    96  func TestGetFeature(t *testing.T) {
    97  	tests := []struct {
    98  		name        string
    99  		feature     string
   100  		expectedErr string
   101  	}{
   102  		{
   103  			name:    "just a feature",
   104  			feature: "experimental1",
   105  		}, {
   106  			name:        "feature that does not exist",
   107  			feature:     "will_never_exist",
   108  			expectedErr: "unknown feature",
   109  		},
   110  	}
   111  
   112  	fr := setUp(t)
   113  
   114  	for _, tc := range tests {
   115  		tc := tc
   116  		t.Run(tc.name, func(t *testing.T) {
   117  			_, err := fr.GetFeature(tc.feature)
   118  			cstest.RequireErrorMessage(t, err, tc.expectedErr)
   119  			if tc.expectedErr != "" {
   120  				return
   121  			}
   122  		})
   123  	}
   124  }
   125  
   126  func TestIsEnabled(t *testing.T) {
   127  	tests := []struct {
   128  		name     string
   129  		feature  string
   130  		enable   bool
   131  		expected bool
   132  	}{
   133  		{
   134  			name:     "feature that was not enabled",
   135  			feature:  "experimental1",
   136  			expected: false,
   137  		}, {
   138  			name:     "feature that was enabled",
   139  			feature:  "experimental1",
   140  			enable:   true,
   141  			expected: true,
   142  		},
   143  	}
   144  
   145  	fr := setUp(t)
   146  
   147  	for _, tc := range tests {
   148  		tc := tc
   149  		t.Run(tc.name, func(t *testing.T) {
   150  			feat, err := fr.GetFeature(tc.feature)
   151  			require.NoError(t, err)
   152  
   153  			err = feat.Set(tc.enable)
   154  			require.NoError(t, err)
   155  
   156  			require.Equal(t, tc.expected, feat.IsEnabled())
   157  		})
   158  	}
   159  }
   160  
   161  func TestFeatureSet(t *testing.T) {
   162  	tests := []struct {
   163  		name           string // test description
   164  		feature        string // feature name
   165  		value          bool   // value for SetFeature
   166  		expected       bool   // expected value from IsEnabled
   167  		expectedSetErr string // error expected from SetFeature
   168  		expectedGetErr string // error expected from GetFeature
   169  	}{
   170  		{
   171  			name:     "enable a feature to try something new",
   172  			feature:  "experimental1",
   173  			value:    true,
   174  			expected: true,
   175  		}, {
   176  			// not useful in practice, unlikely to happen
   177  			name:     "disable the feature that was enabled",
   178  			feature:  "experimental1",
   179  			value:    false,
   180  			expected: false,
   181  		}, {
   182  			name:           "enable a feature that will be retired in v2",
   183  			feature:        "new_standard",
   184  			value:          true,
   185  			expected:       true,
   186  			expectedSetErr: "the flag is deprecated",
   187  		}, {
   188  			name:           "enable a feature that was retired in v1.5",
   189  			feature:        "was_adopted",
   190  			value:          true,
   191  			expected:       false,
   192  			expectedSetErr: "the flag is retired",
   193  		}, {
   194  			name:           "enable a feature that does not exist",
   195  			feature:        "will_never_exist",
   196  			value:          true,
   197  			expectedSetErr: "unknown feature",
   198  			expectedGetErr: "unknown feature",
   199  		},
   200  	}
   201  
   202  	// the tests are not indepedent because we don't instantiate a feature
   203  	// map for each one, but it simplified the code
   204  	fr := setUp(t)
   205  
   206  	for _, tc := range tests {
   207  		tc := tc
   208  		t.Run(tc.name, func(t *testing.T) {
   209  			feat, err := fr.GetFeature(tc.feature)
   210  			cstest.RequireErrorMessage(t, err, tc.expectedGetErr)
   211  			if tc.expectedGetErr != "" {
   212  				return
   213  			}
   214  
   215  			err = feat.Set(tc.value)
   216  			cstest.RequireErrorMessage(t, err, tc.expectedSetErr)
   217  			require.Equal(t, tc.expected, feat.IsEnabled())
   218  		})
   219  	}
   220  }
   221  
   222  func TestSetFromEnv(t *testing.T) {
   223  	tests := []struct {
   224  		name   string
   225  		envvar string
   226  		value  string
   227  		// expected bool
   228  		expectedLog []string
   229  		expectedErr string
   230  	}{
   231  		{
   232  			name:   "variable that does not start with FFLAG_TEST_",
   233  			envvar: "PATH",
   234  			value:  "/bin:/usr/bin/:/usr/local/bin",
   235  			// silently ignored
   236  		}, {
   237  			name:        "enable a feature flag",
   238  			envvar:      "FFLAG_TEST_EXPERIMENTAL1",
   239  			value:       "true",
   240  			expectedLog: []string{"Feature flag: experimental1=true (from envvar)"},
   241  		}, {
   242  			name:        "invalid value (not true or false)",
   243  			envvar:      "FFLAG_TEST_EXPERIMENTAL1",
   244  			value:       "maybe",
   245  			expectedLog: []string{"Ignored envvar FFLAG_TEST_EXPERIMENTAL1=maybe: invalid value (must be 'true' or 'false')"},
   246  		}, {
   247  			name:        "feature flag that is unknown",
   248  			envvar:      "FFLAG_TEST_WILL_NEVER_EXIST",
   249  			value:       "true",
   250  			expectedLog: []string{"Ignored envvar 'FFLAG_TEST_WILL_NEVER_EXIST': unknown feature"},
   251  		}, {
   252  			name:   "enable a feature flag with a description",
   253  			envvar: "FFLAG_TEST_SOME_FEATURE",
   254  			value:  "true",
   255  			expectedLog: []string{
   256  				"Feature flag: some_feature=true (from envvar). A feature that does something, with a description",
   257  			},
   258  		}, {
   259  			name:   "enable a deprecated feature",
   260  			envvar: "FFLAG_TEST_NEW_STANDARD",
   261  			value:  "true",
   262  			expectedLog: []string{
   263  				"Envvar 'FFLAG_TEST_NEW_STANDARD': the flag is deprecated. In 2.0 we'll do T34.256w by default",
   264  				"Feature flag: new_standard=true (from envvar). This implements the new standard T34.256w",
   265  			},
   266  		}, {
   267  			name:   "enable a feature that was retired in v1.5",
   268  			envvar: "FFLAG_TEST_WAS_ADOPTED",
   269  			value:  "true",
   270  			expectedLog: []string{
   271  				"Ignored envvar 'FFLAG_TEST_WAS_ADOPTED': the flag is retired. " +
   272  					"The trinket was implemented in 1.5",
   273  			},
   274  		}, {
   275  			// this could happen in theory, but only if environment variables
   276  			// are parsed after configuration files, which is not a good idea
   277  			// because they are more useful asap
   278  			name:   "disable a feature flag already set",
   279  			envvar: "FFLAG_TEST_EXPERIMENTAL1",
   280  			value:  "false",
   281  		},
   282  	}
   283  
   284  	fr := setUp(t)
   285  
   286  	for _, tc := range tests {
   287  		tc := tc
   288  		t.Run(tc.name, func(t *testing.T) {
   289  			logger, hook := logtest.NewNullLogger()
   290  			logger.SetLevel(logrus.DebugLevel)
   291  			t.Setenv(tc.envvar, tc.value)
   292  			err := fr.SetFromEnv(logger)
   293  			cstest.RequireErrorMessage(t, err, tc.expectedErr)
   294  			for _, expectedMessage := range tc.expectedLog {
   295  				cstest.RequireLogContains(t, hook, expectedMessage)
   296  			}
   297  		})
   298  	}
   299  }
   300  
   301  func TestSetFromYaml(t *testing.T) {
   302  	tests := []struct {
   303  		name        string
   304  		yml         string
   305  		expectedLog []string
   306  		expectedErr string
   307  	}{
   308  		{
   309  			name: "empty file",
   310  			yml:  "",
   311  			// no error
   312  		}, {
   313  			name:        "invalid yaml",
   314  			yml:         "bad! content, bad!",
   315  			expectedErr: "failed to parse feature flags: [1:1] string was used where sequence is expected\n    >  1 | bad! content, bad!\n           ^",
   316  		}, {
   317  			name:        "invalid feature flag name",
   318  			yml:         "- not_a_feature",
   319  			expectedLog: []string{"Ignored feature flag 'not_a_feature': unknown feature"},
   320  		}, {
   321  			name:        "invalid value (must be a list)",
   322  			yml:         "experimental1: true",
   323  			expectedErr: "failed to parse feature flags: [1:14] value was used where sequence is expected\n    >  1 | experimental1: true\n                        ^",
   324  		}, {
   325  			name:        "enable a feature flag",
   326  			yml:         "- experimental1",
   327  			expectedLog: []string{"Feature flag: experimental1=true (from config file)"},
   328  		}, {
   329  			name: "enable a deprecated feature",
   330  			yml:  "- new_standard",
   331  			expectedLog: []string{
   332  				"Feature 'new_standard': the flag is deprecated. In 2.0 we'll do T34.256w by default",
   333  				"Feature flag: new_standard=true (from config file). This implements the new standard T34.256w",
   334  			},
   335  		}, {
   336  			name: "enable a retired feature",
   337  			yml:  "- was_adopted",
   338  			expectedLog: []string{
   339  				"Ignored feature flag 'was_adopted': the flag is retired. The trinket was implemented in 1.5",
   340  			},
   341  		},
   342  	}
   343  
   344  	fr := setUp(t)
   345  
   346  	for _, tc := range tests {
   347  		tc := tc
   348  		t.Run(tc.name, func(t *testing.T) {
   349  			logger, hook := logtest.NewNullLogger()
   350  			logger.SetLevel(logrus.DebugLevel)
   351  			err := fr.SetFromYaml(strings.NewReader(tc.yml), logger)
   352  			cstest.RequireErrorMessage(t, err, tc.expectedErr)
   353  			for _, expectedMessage := range tc.expectedLog {
   354  				cstest.RequireLogContains(t, hook, expectedMessage)
   355  			}
   356  		})
   357  	}
   358  }
   359  
   360  func TestSetFromYamlFile(t *testing.T) {
   361  	tmpfile, err := os.CreateTemp("", "test")
   362  	require.NoError(t, err)
   363  
   364  	defer os.Remove(tmpfile.Name())
   365  
   366  	// write the config file
   367  	_, err = tmpfile.WriteString("- experimental1")
   368  	require.NoError(t, err)
   369  	require.NoError(t, tmpfile.Close())
   370  
   371  	fr := setUp(t)
   372  	logger, hook := logtest.NewNullLogger()
   373  	logger.SetLevel(logrus.DebugLevel)
   374  
   375  	err = fr.SetFromYamlFile(tmpfile.Name(), logger)
   376  	require.NoError(t, err)
   377  
   378  	cstest.RequireLogContains(t, hook, "Feature flag: experimental1=true (from config file)")
   379  }
   380  
   381  func TestGetEnabledFeatures(t *testing.T) {
   382  	fr := setUp(t)
   383  
   384  	feat1, err := fr.GetFeature("new_standard")
   385  	require.NoError(t, err)
   386  	feat1.Set(true)
   387  
   388  	feat2, err := fr.GetFeature("experimental1")
   389  	require.NoError(t, err)
   390  	feat2.Set(true)
   391  
   392  	expected := []string{
   393  		"experimental1",
   394  		"new_standard",
   395  	}
   396  
   397  	require.Equal(t, expected, fr.GetEnabledFeatures())
   398  }