For cscli: it should provide a terse output, not nag users with configuration details. Although it's usually important that cscli and crowdsec have the same enabled features, having it list them every time the command is invoked can be too much. For crowdsec: when features are set from the environment, it's too early to log where we should. So we can use log.Debug at activation time, and list them again once logging is configured. - wrap some functions in csconfig for convenience and DRY - for each enabled feature, log.Debug - log all enabled features once as Info (crowdsec) or Debug (cscli) - file does not exist -> log.Trace
398 lines
10 KiB
Go
398 lines
10 KiB
Go
package fflag_test
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
logtest "github.com/sirupsen/logrus/hooks/test"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/cstest"
|
|
"github.com/crowdsecurity/crowdsec/pkg/fflag"
|
|
)
|
|
|
|
func TestRegisterFeature(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
feature fflag.Feature
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "a plain feature",
|
|
feature: fflag.Feature{
|
|
Name: "plain",
|
|
},
|
|
},
|
|
{
|
|
name: "capitalized feature name",
|
|
feature: fflag.Feature{
|
|
Name: "Plain",
|
|
},
|
|
expectedErr: "feature flag 'Plain': name is not lowercase",
|
|
},
|
|
{
|
|
name: "empty feature name",
|
|
feature: fflag.Feature{
|
|
Name: "",
|
|
},
|
|
expectedErr: "feature flag '': name is empty",
|
|
},
|
|
{
|
|
name: "invalid feature name",
|
|
feature: fflag.Feature{
|
|
Name: "meh!",
|
|
},
|
|
expectedErr: "feature flag 'meh!': invalid name (allowed a-z, 0-9, _, .)",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
|
|
t.Run("", func(t *testing.T) {
|
|
fr := fflag.FeatureRegister{EnvPrefix: "FFLAG_TEST_"}
|
|
err := fr.RegisterFeature(&tc.feature)
|
|
cstest.RequireErrorContains(t, err, tc.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func setUp(t *testing.T) fflag.FeatureRegister {
|
|
t.Helper()
|
|
|
|
fr := fflag.FeatureRegister{EnvPrefix: "FFLAG_TEST_"}
|
|
|
|
err := fr.RegisterFeature(&fflag.Feature{Name: "experimental1"})
|
|
require.NoError(t, err)
|
|
|
|
err = fr.RegisterFeature(&fflag.Feature{
|
|
Name: "some_feature",
|
|
Description: "A feature that does something, with a description",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = fr.RegisterFeature(&fflag.Feature{
|
|
Name: "new_standard",
|
|
State: fflag.DeprecatedState,
|
|
Description: "This implements the new standard T34.256w",
|
|
DeprecationMsg: "In 2.0 we'll do T34.256w by default",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = fr.RegisterFeature(&fflag.Feature{
|
|
Name: "was_adopted",
|
|
State: fflag.RetiredState,
|
|
Description: "This implements a new tricket",
|
|
DeprecationMsg: "The trinket was implemented in 1.5",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return fr
|
|
}
|
|
|
|
func TestGetFeature(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
feature string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "just a feature",
|
|
feature: "experimental1",
|
|
}, {
|
|
name: "feature that does not exist",
|
|
feature: "will_never_exist",
|
|
expectedErr: "unknown feature",
|
|
},
|
|
}
|
|
|
|
fr := setUp(t)
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := fr.GetFeature(tc.feature)
|
|
cstest.RequireErrorMessage(t, err, tc.expectedErr)
|
|
if tc.expectedErr != "" {
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsEnabled(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
feature string
|
|
enable bool
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "feature that was not enabled",
|
|
feature: "experimental1",
|
|
expected: false,
|
|
}, {
|
|
name: "feature that was enabled",
|
|
feature: "experimental1",
|
|
enable: true,
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
fr := setUp(t)
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
feat, err := fr.GetFeature(tc.feature)
|
|
require.NoError(t, err)
|
|
|
|
err = feat.Set(tc.enable)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.expected, feat.IsEnabled())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFeatureSet(t *testing.T) {
|
|
tests := []struct {
|
|
name string // test description
|
|
feature string // feature name
|
|
value bool // value for SetFeature
|
|
expected bool // expected value from IsEnabled
|
|
expectedSetErr string // error expected from SetFeature
|
|
expectedGetErr string // error expected from GetFeature
|
|
}{
|
|
{
|
|
name: "enable a feature to try something new",
|
|
feature: "experimental1",
|
|
value: true,
|
|
expected: true,
|
|
}, {
|
|
// not useful in practice, unlikely to happen
|
|
name: "disable the feature that was enabled",
|
|
feature: "experimental1",
|
|
value: false,
|
|
expected: false,
|
|
}, {
|
|
name: "enable a feature that will be retired in v2",
|
|
feature: "new_standard",
|
|
value: true,
|
|
expected: true,
|
|
expectedSetErr: "the flag is deprecated",
|
|
}, {
|
|
name: "enable a feature that was retired in v1.5",
|
|
feature: "was_adopted",
|
|
value: true,
|
|
expected: false,
|
|
expectedSetErr: "the flag is retired",
|
|
}, {
|
|
name: "enable a feature that does not exist",
|
|
feature: "will_never_exist",
|
|
value: true,
|
|
expectedSetErr: "unknown feature",
|
|
expectedGetErr: "unknown feature",
|
|
},
|
|
}
|
|
|
|
// the tests are not indepedent because we don't instantiate a feature
|
|
// map for each one, but it simplified the code
|
|
fr := setUp(t)
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
feat, err := fr.GetFeature(tc.feature)
|
|
cstest.RequireErrorMessage(t, err, tc.expectedGetErr)
|
|
if tc.expectedGetErr != "" {
|
|
return
|
|
}
|
|
|
|
err = feat.Set(tc.value)
|
|
cstest.RequireErrorMessage(t, err, tc.expectedSetErr)
|
|
require.Equal(t, tc.expected, feat.IsEnabled())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetFromEnv(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envvar string
|
|
value string
|
|
// expected bool
|
|
expectedLog []string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "variable that does not start with FFLAG_TEST_",
|
|
envvar: "PATH",
|
|
value: "/bin:/usr/bin/:/usr/local/bin",
|
|
// silently ignored
|
|
}, {
|
|
name: "enable a feature flag",
|
|
envvar: "FFLAG_TEST_EXPERIMENTAL1",
|
|
value: "true",
|
|
expectedLog: []string{"Feature flag: experimental1=true (from envvar)"},
|
|
}, {
|
|
name: "invalid value (not true or false)",
|
|
envvar: "FFLAG_TEST_EXPERIMENTAL1",
|
|
value: "maybe",
|
|
expectedLog: []string{"Ignored envvar FFLAG_TEST_EXPERIMENTAL1=maybe: invalid value (must be 'true' or 'false')"},
|
|
}, {
|
|
name: "feature flag that is unknown",
|
|
envvar: "FFLAG_TEST_WILL_NEVER_EXIST",
|
|
value: "true",
|
|
expectedLog: []string{"Ignored envvar 'FFLAG_TEST_WILL_NEVER_EXIST': unknown feature"},
|
|
}, {
|
|
name: "enable a feature flag with a description",
|
|
envvar: "FFLAG_TEST_SOME_FEATURE",
|
|
value: "true",
|
|
expectedLog: []string{
|
|
"Feature flag: some_feature=true (from envvar). A feature that does something, with a description",
|
|
},
|
|
}, {
|
|
name: "enable a deprecated feature",
|
|
envvar: "FFLAG_TEST_NEW_STANDARD",
|
|
value: "true",
|
|
expectedLog: []string{
|
|
"Envvar 'FFLAG_TEST_NEW_STANDARD': the flag is deprecated. In 2.0 we'll do T34.256w by default",
|
|
"Feature flag: new_standard=true (from envvar). This implements the new standard T34.256w",
|
|
},
|
|
}, {
|
|
name: "enable a feature that was retired in v1.5",
|
|
envvar: "FFLAG_TEST_WAS_ADOPTED",
|
|
value: "true",
|
|
expectedLog: []string{
|
|
"Ignored envvar 'FFLAG_TEST_WAS_ADOPTED': the flag is retired. " +
|
|
"The trinket was implemented in 1.5",
|
|
},
|
|
}, {
|
|
// this could happen in theory, but only if environment variables
|
|
// are parsed after configuration files, which is not a good idea
|
|
// because they are more useful asap
|
|
name: "disable a feature flag already set",
|
|
envvar: "FFLAG_TEST_EXPERIMENTAL1",
|
|
value: "false",
|
|
},
|
|
}
|
|
|
|
fr := setUp(t)
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
logger, hook := logtest.NewNullLogger()
|
|
logger.SetLevel(logrus.DebugLevel)
|
|
t.Setenv(tc.envvar, tc.value)
|
|
err := fr.SetFromEnv(logger)
|
|
cstest.RequireErrorMessage(t, err, tc.expectedErr)
|
|
for _, expectedMessage := range tc.expectedLog {
|
|
cstest.RequireLogContains(t, hook, expectedMessage)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetFromYaml(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
yml string
|
|
expectedLog []string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "empty file",
|
|
yml: "",
|
|
// no error
|
|
}, {
|
|
name: "invalid yaml",
|
|
yml: "bad! content, bad!",
|
|
expectedErr: "failed to parse feature flags: [1:1] string was used where sequence is expected\n > 1 | bad! content, bad!\n ^",
|
|
}, {
|
|
name: "invalid feature flag name",
|
|
yml: "- not_a_feature",
|
|
expectedLog: []string{"Ignored feature flag 'not_a_feature': unknown feature"},
|
|
}, {
|
|
name: "invalid value (must be a list)",
|
|
yml: "experimental1: true",
|
|
expectedErr: "failed to parse feature flags: [1:14] value was used where sequence is expected\n > 1 | experimental1: true\n ^",
|
|
}, {
|
|
name: "enable a feature flag",
|
|
yml: "- experimental1",
|
|
expectedLog: []string{"Feature flag: experimental1=true (from config file)"},
|
|
}, {
|
|
name: "enable a deprecated feature",
|
|
yml: "- new_standard",
|
|
expectedLog: []string{
|
|
"Feature 'new_standard': the flag is deprecated. In 2.0 we'll do T34.256w by default",
|
|
"Feature flag: new_standard=true (from config file). This implements the new standard T34.256w",
|
|
},
|
|
}, {
|
|
name: "enable a retired feature",
|
|
yml: "- was_adopted",
|
|
expectedLog: []string{
|
|
"Ignored feature flag 'was_adopted': the flag is retired. The trinket was implemented in 1.5",
|
|
},
|
|
},
|
|
}
|
|
|
|
fr := setUp(t)
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
logger, hook := logtest.NewNullLogger()
|
|
logger.SetLevel(logrus.DebugLevel)
|
|
err := fr.SetFromYaml(strings.NewReader(tc.yml), logger)
|
|
cstest.RequireErrorMessage(t, err, tc.expectedErr)
|
|
for _, expectedMessage := range tc.expectedLog {
|
|
cstest.RequireLogContains(t, hook, expectedMessage)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetFromYamlFile(t *testing.T) {
|
|
tmpfile, err := os.CreateTemp("", "test")
|
|
require.NoError(t, err)
|
|
|
|
defer os.Remove(tmpfile.Name())
|
|
|
|
// write the config file
|
|
_, err = tmpfile.Write([]byte("- experimental1"))
|
|
require.NoError(t, err)
|
|
require.NoError(t, tmpfile.Close())
|
|
|
|
fr := setUp(t)
|
|
logger, hook := logtest.NewNullLogger()
|
|
logger.SetLevel(logrus.DebugLevel)
|
|
|
|
err = fr.SetFromYamlFile(tmpfile.Name(), logger)
|
|
require.NoError(t, err)
|
|
|
|
cstest.RequireLogContains(t, hook, "Feature flag: experimental1=true (from config file)")
|
|
}
|
|
|
|
func TestGetEnabledFeatures(t *testing.T) {
|
|
fr := setUp(t)
|
|
|
|
feat1, err := fr.GetFeature("new_standard")
|
|
require.NoError(t, err)
|
|
feat1.Set(true)
|
|
|
|
feat2, err := fr.GetFeature("experimental1")
|
|
require.NoError(t, err)
|
|
feat2.Set(true)
|
|
|
|
expected := []string{
|
|
"experimental1",
|
|
"new_standard",
|
|
}
|
|
|
|
require.Equal(t, expected, fr.GetEnabledFeatures())
|
|
}
|