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 }