From 12c32d507c450cd99c30116696aa880aec766264 Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Thu, 1 Jun 2023 10:33:08 +0200 Subject: [PATCH] CI: refactoring pkg/csplugin tests (#2247) --- go.mod | 2 +- go.sum | 4 +- pkg/csplugin/broker.go | 17 - pkg/csplugin/broker_suite_test.go | 164 +++++++ pkg/csplugin/broker_test.go | 405 ++++-------------- pkg/csplugin/broker_win_test.go | 230 ++-------- pkg/csplugin/listfiles.go | 22 + pkg/csplugin/listfiles_test.go | 57 +++ .../notifications => testdata}/dummy.yaml | 0 pkg/csplugin/utils_test.go | 49 +++ pkg/csplugin/utils_windows_test.go | 49 +++ 11 files changed, 449 insertions(+), 550 deletions(-) create mode 100644 pkg/csplugin/broker_suite_test.go create mode 100644 pkg/csplugin/listfiles.go create mode 100644 pkg/csplugin/listfiles_test.go rename pkg/csplugin/{tests/notifications => testdata}/dummy.yaml (100%) create mode 100644 pkg/csplugin/utils_test.go create mode 100644 pkg/csplugin/utils_windows_test.go diff --git a/go.mod b/go.mod index b2be8cec4..d2874ad05 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/bluele/gcache v0.0.2 github.com/cespare/xxhash/v2 v2.1.2 github.com/coreos/go-systemd/v22 v22.5.0 - github.com/crowdsecurity/go-cs-lib v0.0.0-20230522120244-fa545c12e7ee + github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd github.com/goccy/go-yaml v1.9.7 github.com/gofrs/uuid v4.0.0+incompatible github.com/golang-jwt/jwt/v4 v4.2.0 diff --git a/go.sum b/go.sum index 9d8cca9e7..4a63b3839 100644 --- a/go.sum +++ b/go.sum @@ -172,8 +172,8 @@ github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU= github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk= -github.com/crowdsecurity/go-cs-lib v0.0.0-20230522120244-fa545c12e7ee h1:YD/SYJ0otjlyAxuLRzJNXTQyXcpccefTvaHextBT9mQ= -github.com/crowdsecurity/go-cs-lib v0.0.0-20230522120244-fa545c12e7ee/go.mod h1:9JJLSpGj1ZXnROV3xAcJvS/HTaUvuA8K3gGOpO4tfVc= +github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd h1:Y70ceDKAKYFXTnxEjXuBDSh07umvDhbX3PCCYhdtsZ0= +github.com/crowdsecurity/go-cs-lib v0.0.0-20230531105801-4c1535c2b3bd/go.mod h1:9JJLSpGj1ZXnROV3xAcJvS/HTaUvuA8K3gGOpO4tfVc= github.com/crowdsecurity/grokky v0.2.1 h1:t4VYnDlAd0RjDM2SlILalbwfCrQxtJSMGdQOR0zwkE4= github.com/crowdsecurity/grokky v0.2.1/go.mod h1:33usDIYzGDsgX1kHAThCbseso6JuWNJXOzRQDGXHtWM= github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5lirI+bdc= diff --git a/pkg/csplugin/broker.go b/pkg/csplugin/broker.go index fcde41831..46c7ac6ed 100644 --- a/pkg/csplugin/broker.go +++ b/pkg/csplugin/broker.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "strings" "sync" "text/template" @@ -375,22 +374,6 @@ func setRequiredFields(pluginCfg *PluginConfig) { } -// helper which gives paths to all files in the given directory non-recursively -func listFilesAtPath(path string) ([]string, error) { - filePaths := make([]string, 0) - files, err := os.ReadDir(path) - if err != nil { - return nil, err - } - for _, file := range files { - if file.IsDir() { - continue - } - filePaths = append(filePaths, filepath.Join(path, file.Name())) - } - return filePaths, nil -} - func getUUID() (string, error) { uuidv4, err := uuid.NewRandom() if err != nil { diff --git a/pkg/csplugin/broker_suite_test.go b/pkg/csplugin/broker_suite_test.go new file mode 100644 index 000000000..4c7cdd6eb --- /dev/null +++ b/pkg/csplugin/broker_suite_test.go @@ -0,0 +1,164 @@ +package csplugin + +import ( + "io" + "os" + "os/exec" + "path" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" +) + + +type PluginSuite struct { + suite.Suite + + // where the plugin is built - temporary directory for the suite + buildDir string + // full path to the built plugin binary + builtBinary string + + runDir string // temporary directory for each test + pluginDir string // (config_paths.plugin_dir) + notifDir string // (config_paths.notification_dir) + pluginBinary string // full path to the plugin binary (unique for each test) + pluginConfig string // full path to the notification config (unique for each test) + + pluginBroker *PluginBroker +} + + +func TestPluginSuite(t *testing.T) { + suite.Run(t, new(PluginSuite)) +} + + +func (s *PluginSuite) SetupSuite() { + var err error + + t := s.T() + + s.buildDir, err = os.MkdirTemp("", "cs_plugin_test_build") + require.NoError(t, err) + + s.builtBinary = path.Join(s.buildDir, "notification-dummy") + + if runtime.GOOS == "windows" { + s.builtBinary += ".exe" + } + + cmd := exec.Command("go", "build", "-o", s.builtBinary, "../../plugins/notifications/dummy/") + err = cmd.Run() + require.NoError(t, err, "while building dummy plugin") +} + + +func (s *PluginSuite) TearDownSuite() { + t := s.T() + err := os.RemoveAll(s.buildDir) + require.NoError(t, err) +} + + +func copyFile(src string, dst string) error { + s, err := os.Open(src) + if err != nil { + return err + } + defer s.Close() + + d, err := os.Create(dst) + if err != nil { + return err + } + defer d.Close() + + _, err = io.Copy(d, s) + if err != nil { + return err + } + + err = d.Sync() + if err != nil { + return err + } + + return nil +} + +func (s *PluginSuite) SetupTest() { + s.SetupSubTest() +} + +func (s *PluginSuite) TearDownTest() { + s.TearDownSubTest() +} + + +func (s *PluginSuite) SetupSubTest() { + var err error + t := s.T() + + s.runDir, err = os.MkdirTemp("", "cs_plugin_test") + require.NoError(t, err) + + s.pluginDir = path.Join(s.runDir, "bin") + err = os.MkdirAll(path.Join(s.runDir, "bin"), 0o755) + require.NoError(t, err, "while creating bin dir") + + s.notifDir = path.Join(s.runDir, "config") + err = os.MkdirAll(s.notifDir, 0o755) + require.NoError(t, err, "while creating config dir") + + s.pluginBinary = path.Join(s.pluginDir, "notification-dummy") + + if runtime.GOOS == "windows" { + s.pluginBinary += ".exe" + } + + err = copyFile(s.builtBinary, s.pluginBinary) + require.NoError(t, err, "while copying built binary") + err = os.Chmod(s.pluginBinary, 0o744) + require.NoError(t, err, "chmod 0744 %s", s.pluginBinary) + + s.pluginConfig = path.Join(s.notifDir, "dummy.yaml") + err = copyFile("testdata/dummy.yaml", s.pluginConfig) + require.NoError(t, err, "while copying plugin config") +} + +func (s *PluginSuite) TearDownSubTest() { + t := s.T() + if s.pluginBroker != nil { + s.pluginBroker.Kill() + s.pluginBroker = nil + } + + err := os.RemoveAll(s.runDir) + if runtime.GOOS != "windows" { + require.NoError(t, err) + } + + os.Remove("./out") +} + +func (s *PluginSuite) InitBroker(procCfg *csconfig.PluginCfg) (*PluginBroker, error) { + pb := PluginBroker{} + if procCfg == nil { + procCfg = &csconfig.PluginCfg{} + } + profiles := csconfig.NewDefaultConfig().API.Server.Profiles + profiles = append(profiles, &csconfig.ProfileCfg{ + Notifications: []string{"dummy_default"}, + }) + err := pb.Init(procCfg, profiles, &csconfig.ConfigurationPaths{ + PluginDir: s.pluginDir, + NotificationDir: s.notifDir, + }) + s.pluginBroker = &pb + return s.pluginBroker, err +} diff --git a/pkg/csplugin/broker_test.go b/pkg/csplugin/broker_test.go index 344deefd7..fc329693c 100644 --- a/pkg/csplugin/broker_test.go +++ b/pkg/csplugin/broker_test.go @@ -5,11 +5,6 @@ package csplugin import ( "encoding/json" "os" - "os/exec" - "path" - "path/filepath" - "reflect" - "runtime" "testing" "time" @@ -25,116 +20,39 @@ import ( "github.com/crowdsecurity/crowdsec/pkg/models" ) -var testPath string -func setPluginPermTo744(t *testing.T) { - setPluginPermTo(t, "744") -} - -func setPluginPermTo722(t *testing.T) { - setPluginPermTo(t, "722") -} - -func setPluginPermTo724(t *testing.T) { - setPluginPermTo(t, "724") -} -func TestGetPluginNameAndTypeFromPath(t *testing.T) { - setUp(t) - defer tearDown(t) - type args struct { - path string - } - tests := []struct { - name string - args args - want string - want1 string - expectedErr string - }{ - { - name: "valid plugin name, single dash", - args: args{ - path: path.Join(testPath, "notification-gitter"), - }, - want: "notification", - want1: "gitter", - }, - { - name: "invalid plugin name", - args: args{ - path: "./tests/gitter", - }, - expectedErr: "plugin name ./tests/gitter is invalid. Name should be like {type-name}", - }, - { - name: "valid plugin name, multiple dash", - args: args{ - path: "./tests/notification-instant-slack", - }, - want: "notification-instant", - want1: "slack", - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got, got1, err := getPluginTypeAndSubtypeFromPath(tc.args.path) - cstest.RequireErrorContains(t, err, tc.expectedErr) - - assert.Equal(t, tc.want, got) - assert.Equal(t, tc.want1, got1) - }) +func (s *PluginSuite) permissionSetter(perm os.FileMode) func(*testing.T) { + return func(t *testing.T) { + err := os.Chmod(s.pluginBinary, perm) + require.NoError(t, err, "chmod %s %s", perm, s.pluginBinary) } } -func TestListFilesAtPath(t *testing.T) { - setUp(t) - defer tearDown(t) - type args struct { - path string - } - tests := []struct { - name string - args args - want []string - expectedErr string - }{ - { - name: "valid directory", - args: args{ - path: testPath, - }, - want: []string{ - filepath.Join(testPath, "notification-gitter"), - filepath.Join(testPath, "slack"), - }, - }, - { - name: "invalid directory", - args: args{ - path: "./foo/bar/", - }, - expectedErr: "open ./foo/bar/: " + cstest.FileNotFoundMessage, - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got, err := listFilesAtPath(tc.args.path) - cstest.RequireErrorContains(t, err, tc.expectedErr) +func (s *PluginSuite) readconfig() (PluginConfig) { + var config PluginConfig + t := s.T() - if !reflect.DeepEqual(got, tc.want) { - t.Errorf("listFilesAtPath() = %v, want %v", got, tc.want) - } - }) - } + orig, err := os.ReadFile(s.pluginConfig) + require.NoError(t, err,"unable to read config file %s", s.pluginConfig) + + err = yaml.Unmarshal(orig, &config) + require.NoError(t, err,"unable to unmarshal config file") + + return config } -func TestBrokerInit(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Skipping test on windows") - } +func (s *PluginSuite) writeconfig(config PluginConfig) { + t := s.T() + data, err := yaml.Marshal(&config) + require.NoError(t, err,"unable to marshal config file") + + err = os.WriteFile(s.pluginConfig, data, 0644) + require.NoError(t, err,"unable to write config file %s", s.pluginConfig) +} + + +func (s *PluginSuite) TestBrokerInit() { tests := []struct { name string action func(*testing.T) @@ -143,28 +61,30 @@ func TestBrokerInit(t *testing.T) { }{ { name: "valid config", - action: setPluginPermTo744, }, { name: "group writable binary", expectedErr: "notification-dummy is world writable", - action: setPluginPermTo722, + action: s.permissionSetter(0o722), }, { name: "group writable binary", expectedErr: "notification-dummy is group writable", - action: setPluginPermTo724, + action: s.permissionSetter(0o724), }, { name: "no plugin dir", expectedErr: cstest.FileNotFoundMessage, - action: tearDown, + action: func(t *testing.T) { + err := os.RemoveAll(s.runDir) + require.NoError(t, err) + }, }, { name: "no plugin binary", expectedErr: "binary for plugin dummy_default not found", action: func(t *testing.T) { - err := os.Remove(path.Join(testPath, "notification-dummy")) + err := os.Remove(s.pluginBinary) require.NoError(t, err) }, }, @@ -174,7 +94,6 @@ func TestBrokerInit(t *testing.T) { procCfg: csconfig.PluginCfg{ User: "123445555551122toto", }, - action: setPluginPermTo744, }, { name: "only specify group", @@ -182,7 +101,6 @@ func TestBrokerInit(t *testing.T) { procCfg: csconfig.PluginCfg{ Group: "123445555551122toto", }, - action: setPluginPermTo744, }, { name: "Fails to run as root", @@ -191,7 +109,6 @@ func TestBrokerInit(t *testing.T) { User: "root", Group: "root", }, - action: setPluginPermTo744, }, { name: "Invalid user and group", @@ -200,7 +117,6 @@ func TestBrokerInit(t *testing.T) { User: "toto1234", Group: "toto1234", }, - action: setPluginPermTo744, }, { name: "Valid user and invalid group", @@ -209,79 +125,33 @@ func TestBrokerInit(t *testing.T) { User: "nobody", Group: "toto1234", }, - action: setPluginPermTo744, }, } for _, tc := range tests { tc := tc - t.Run(tc.name, func(t *testing.T) { - defer tearDown(t) - buildDummyPlugin(t) + s.Run(tc.name, func() { + t := s.T() if tc.action != nil { tc.action(t) } - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) - err := pb.Init(&tc.procCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) - defer pb.Kill() + _, err := s.InitBroker(&tc.procCfg) cstest.RequireErrorContains(t, err, tc.expectedErr) }) } } -func readconfig(t *testing.T, path string) ([]byte, PluginConfig) { - var config PluginConfig - orig, err := os.ReadFile("tests/notifications/dummy.yaml") - require.NoError(t, err,"unable to read config file %s", path) - - err = yaml.Unmarshal(orig, &config) - require.NoError(t, err,"unable to unmarshal config file") - - return orig, config -} - -func writeconfig(t *testing.T, config PluginConfig, path string) { - data, err := yaml.Marshal(&config) - require.NoError(t, err,"unable to marshal config file") - - err = os.WriteFile(path, data, 0644) - require.NoError(t, err,"unable to write config file %s", path) -} - -func TestBrokerNoThreshold(t *testing.T) { +func (s *PluginSuite) TestBrokerNoThreshold() { var alerts []models.Alert DefaultEmptyTicker = 50 * time.Millisecond - buildDummyPlugin(t) - setPluginPermTo744(t) - defer tearDown(t) - - // init - pluginCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) - - // default config - err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) + t := s.T() + pb, err := s.InitBroker(nil) assert.NoError(t, err) - tomb := tomb.Tomb{} + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() // send one item, it should be processed right now pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} @@ -292,7 +162,7 @@ func TestBrokerNoThreshold(t *testing.T) { require.NoError(t, err, "Error reading file") err = json.Unmarshal(content, &alerts) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, alerts, 1) // remove it @@ -313,34 +183,24 @@ func TestBrokerNoThreshold(t *testing.T) { assert.Len(t, alerts, 1) } -func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) { +func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_TimeFirst() { // test grouping by "time" DefaultEmptyTicker = 50 * time.Millisecond - buildDummyPlugin(t) - setPluginPermTo744(t) - defer tearDown(t) - // init - pluginCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) + t := s.T() + // set groupwait and groupthreshold, should honor whichever comes first - raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") + cfg := s.readconfig() cfg.GroupThreshold = 4 cfg.GroupWait = 1 * time.Second - writeconfig(t, cfg, "tests/notifications/dummy.yaml") - err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) - assert.NoError(t, err) - tomb := tomb.Tomb{} + s.writeconfig(cfg) + pb, err := s.InitBroker(nil) + assert.NoError(t, err) + + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() + // send data pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} @@ -357,40 +217,24 @@ func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) { err = json.Unmarshal(content, &alerts) assert.NoError(t, err) assert.Len(t, alerts, 3) - - // restore config - err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644) - require.NoError(t, err,"unable to write config file") } -func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) { +func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_CountFirst() { DefaultEmptyTicker = 50 * time.Millisecond - buildDummyPlugin(t) - setPluginPermTo(t, "744") - defer tearDown(t) - // init - pluginCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) + t := s.T() // set groupwait and groupthreshold, should honor whichever comes first - raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") + cfg := s.readconfig() cfg.GroupThreshold = 4 cfg.GroupWait = 4 * time.Second - writeconfig(t, cfg, "tests/notifications/dummy.yaml") - err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) - assert.NoError(t, err) - tomb := tomb.Tomb{} + s.writeconfig(cfg) + pb, err := s.InitBroker(nil) + assert.NoError(t, err) + + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() // send data pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} @@ -411,41 +255,24 @@ func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) { err = json.Unmarshal(content, &alerts) assert.NoError(t, err) assert.Len(t, alerts, 4) - - // restore config - err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644) - require.NoError(t, err,"unable to write config file") } -func TestBrokerRunGroupThreshold(t *testing.T) { +func (s *PluginSuite) TestBrokerRunGroupThreshold() { // test grouping by "size" DefaultEmptyTicker = 50 * time.Millisecond - buildDummyPlugin(t) - setPluginPermTo(t, "744") - defer tearDown(t) - // init - pluginCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) + t := s.T() // set groupwait - raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") + cfg := s.readconfig() cfg.GroupThreshold = 4 - writeconfig(t, cfg, "tests/notifications/dummy.yaml") - err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) + s.writeconfig(cfg) + pb, err := s.InitBroker(nil) assert.NoError(t, err) - tomb := tomb.Tomb{} + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() // send data pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} @@ -466,39 +293,23 @@ func TestBrokerRunGroupThreshold(t *testing.T) { err = json.Unmarshal(content, &alerts) assert.NoError(t, err) assert.Len(t, alerts, 4) - - // restore config - err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644) - require.NoError(t, err, "unable to write config file") } -func TestBrokerRunTimeThreshold(t *testing.T) { +func (s *PluginSuite) TestBrokerRunTimeThreshold() { DefaultEmptyTicker = 50 * time.Millisecond - buildDummyPlugin(t) - setPluginPermTo(t, "744") - defer tearDown(t) - // init - pluginCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) + t := s.T() // set groupwait - raw, cfg := readconfig(t, "tests/notifications/dummy.yaml") + cfg := s.readconfig() cfg.GroupWait = 1 * time.Second - writeconfig(t, cfg, "tests/notifications/dummy.yaml") - err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) - assert.NoError(t, err) - tomb := tomb.Tomb{} + s.writeconfig(cfg) + pb, err := s.InitBroker(nil) + assert.NoError(t, err) + + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() // send data pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}} @@ -516,32 +327,18 @@ func TestBrokerRunTimeThreshold(t *testing.T) { err = json.Unmarshal(content, &alerts) assert.NoError(t, err) assert.Len(t, alerts, 1) - - // restore config - err = os.WriteFile("tests/notifications/dummy.yaml", raw, 0644) - require.NoError(t, err, "unable to write config file %s", err) } -func TestBrokerRunSimple(t *testing.T) { +func (s *PluginSuite) TestBrokerRunSimple() { DefaultEmptyTicker = 50 * time.Millisecond - buildDummyPlugin(t) - setPluginPermTo(t, "744") - defer tearDown(t) - pluginCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) - err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) - assert.NoError(t, err) - tomb := tomb.Tomb{} + t := s.T() + + pb, err := s.InitBroker(nil) + assert.NoError(t, err) + + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() assert.NoFileExists(t, "./out") @@ -559,47 +356,3 @@ func TestBrokerRunSimple(t *testing.T) { assert.NoError(t, err) assert.Len(t, alerts, 2) } - -func buildDummyPlugin(t *testing.T) { - dir, err := os.MkdirTemp("./tests", "cs_plugin_test") - require.NoError(t, err) - - cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy"), "../../plugins/notifications/dummy/") - err = cmd.Run() - require.NoError(t, err, "while building dummy plugin") - - testPath = dir - os.Remove("./out") -} - -func setPluginPermTo(t *testing.T, perm string) { - if runtime.GOOS != "windows" { - err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run() - require.NoError(t, err, "chmod 744 %s", path.Join(testPath, "notification-dummy")) - } -} - -func setUp(t *testing.T) { - dir, err := os.MkdirTemp("./", "cs_plugin_test") - require.NoError(t, err) - - f, err := os.Create(path.Join(dir, "slack")) - require.NoError(t, err) - - f.Close() - f, err = os.Create(path.Join(dir, "notification-gitter")) - require.NoError(t, err) - - f.Close() - err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666) - require.NoError(t, err) - - testPath = dir -} - -func tearDown(t *testing.T) { - err := os.RemoveAll(testPath) - require.NoError(t, err) - - os.Remove("./out") -} diff --git a/pkg/csplugin/broker_win_test.go b/pkg/csplugin/broker_win_test.go index a3169bb29..8076ce67f 100644 --- a/pkg/csplugin/broker_win_test.go +++ b/pkg/csplugin/broker_win_test.go @@ -3,16 +3,12 @@ package csplugin import ( - "log" "os" - "os/exec" - "path" - "path/filepath" - "reflect" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/tomb.v2" "github.com/crowdsecurity/go-cs-lib/pkg/cstest" @@ -27,188 +23,55 @@ Due to the complexity of file permission modification with go on windows, we onl not if it will actually reject plugins with invalid permissions */ -var testPath string - -func TestGetPluginNameAndTypeFromPath(t *testing.T) { - setUp() - defer tearDown() - type args struct { - path string - } - tests := []struct { - name string - args args - want string - want1 string - wantErr bool - }{ - { - name: "valid plugin name, single dash", - args: args{ - path: path.Join(testPath, "notification-gitter"), - }, - want: "notification", - want1: "gitter", - wantErr: false, - }, - { - name: "invalid plugin name", - args: args{ - path: ".\\tests\\gitter.exe", - }, - want: "", - want1: "", - wantErr: true, - }, - { - name: "valid plugin name, multiple dash", - args: args{ - path: ".\\tests\\notification-instant-slack.exe", - }, - want: "notification-instant", - want1: "slack", - wantErr: false, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - got, got1, err := getPluginTypeAndSubtypeFromPath(tt.args.path) - if (err != nil) != tt.wantErr { - t.Errorf("getPluginNameAndTypeFromPath() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("getPluginNameAndTypeFromPath() got = %v, want %v", got, tt.want) - } - if got1 != tt.want1 { - t.Errorf("getPluginNameAndTypeFromPath() got1 = %v, want %v", got1, tt.want1) - } - }) - } -} - -func TestListFilesAtPath(t *testing.T) { - setUp() - defer tearDown() - type args struct { - path string - } - tests := []struct { - name string - args args - want []string - wantErr bool - }{ - { - name: "valid directory", - args: args{ - path: testPath, - }, - want: []string{ - filepath.Join(testPath, "notification-gitter"), - filepath.Join(testPath, "slack"), - }, - }, - { - name: "invalid directory", - args: args{ - path: "./foo/bar/", - }, - wantErr: true, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - got, err := listFilesAtPath(tt.args.path) - if (err != nil) != tt.wantErr { - t.Errorf("listFilesAtPath() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("listFilesAtPath() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestBrokerInit(t *testing.T) { +func (s *PluginSuite) TestBrokerInit() { tests := []struct { name string - action func() - errContains string - wantErr bool + action func(*testing.T) procCfg csconfig.PluginCfg + expectedErr string }{ { name: "valid config", - wantErr: false, }, { name: "no plugin dir", - wantErr: true, - errContains: cstest.FileNotFoundMessage, - action: tearDown, + expectedErr: cstest.PathNotFoundMessage, + action: func(t *testing.T) { + err := os.RemoveAll(s.runDir) + require.NoError(t, err) + }, }, { name: "no plugin binary", - wantErr: true, - errContains: "binary for plugin dummy_default not found", - action: func() { - err := os.Remove(path.Join(testPath, "notification-dummy.exe")) - if err != nil { - t.Fatal(err) - } + expectedErr: "binary for plugin dummy_default not found", + action: func(t *testing.T) { + err := os.Remove(s.pluginBinary) + require.NoError(t, err) }, }, } - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - defer tearDown() - buildDummyPlugin() - if test.action != nil { - test.action() + for _, tc := range tests { + tc := tc + s.Run(tc.name, func() { + t := s.T() + if tc.action != nil { + tc.action(t) } - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) - err := pb.Init(&test.procCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) - defer pb.Kill() - if test.wantErr { - assert.ErrorContains(t, err, test.errContains) - } else { - assert.NoError(t, err) - } - + _, err := s.InitBroker(&tc.procCfg) + cstest.RequireErrorContains(t, err, tc.expectedErr) }) } } -func TestBrokerRun(t *testing.T) { - buildDummyPlugin() - defer tearDown() - procCfg := csconfig.PluginCfg{} - pb := PluginBroker{} - profiles := csconfig.NewDefaultConfig().API.Server.Profiles - profiles = append(profiles, &csconfig.ProfileCfg{ - Notifications: []string{"dummy_default"}, - }) - err := pb.Init(&procCfg, profiles, &csconfig.ConfigurationPaths{ - PluginDir: testPath, - NotificationDir: "./tests/notifications", - }) +func (s *PluginSuite) TestBrokerRun() { + t := s.T() + + pb, err := s.InitBroker(nil) assert.NoError(t, err) + tomb := tomb.Tomb{} go pb.Run(&tomb) - defer pb.Kill() assert.NoFileExists(t, "./out") defer os.Remove("./out") @@ -220,44 +83,3 @@ func TestBrokerRun(t *testing.T) { assert.FileExists(t, ".\\out") assert.Equal(t, types.GetLineCountForFile(".\\out"), 2) } - -func buildDummyPlugin() { - dir, err := os.MkdirTemp(".\\tests", "cs_plugin_test") - if err != nil { - log.Fatal(err) - } - cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy.exe"), "../../plugins/notifications/dummy/") - if err := cmd.Run(); err != nil { - log.Fatal(err) - } - testPath = dir -} - -func setUp() { - dir, err := os.MkdirTemp("./", "cs_plugin_test") - if err != nil { - log.Fatal(err) - } - f, err := os.Create(path.Join(dir, "slack")) - if err != nil { - log.Fatal(err) - } - f.Close() - f, err = os.Create(path.Join(dir, "notification-gitter")) - if err != nil { - log.Fatal(err) - } - f.Close() - err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666) - if err != nil { - log.Fatal(err) - } - testPath = dir -} - -func tearDown() { - err := os.RemoveAll(testPath) - if err != nil { - log.Fatal(err) - } -} diff --git a/pkg/csplugin/listfiles.go b/pkg/csplugin/listfiles.go new file mode 100644 index 000000000..2dea44f4f --- /dev/null +++ b/pkg/csplugin/listfiles.go @@ -0,0 +1,22 @@ +package csplugin + +import ( + "os" + "path/filepath" +) + +// helper which gives paths to all files in the given directory non-recursively +func listFilesAtPath(path string) ([]string, error) { + filePaths := make([]string, 0) + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + for _, file := range files { + if ! file.IsDir() { + filePaths = append(filePaths, filepath.Join(path, file.Name())) + } + } + return filePaths, nil +} + diff --git a/pkg/csplugin/listfiles_test.go b/pkg/csplugin/listfiles_test.go new file mode 100644 index 000000000..8bcedaa1f --- /dev/null +++ b/pkg/csplugin/listfiles_test.go @@ -0,0 +1,57 @@ +package csplugin + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/crowdsecurity/go-cs-lib/pkg/cstest" +) + +func TestListFilesAtPath(t *testing.T) { + dir, err := os.MkdirTemp("", "test-listfiles") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(dir) + }) + _, err = os.Create(filepath.Join(dir, "notification-gitter")) + require.NoError(t, err) + _, err = os.Create(filepath.Join(dir, "slack")) + require.NoError(t, err) + err = os.Mkdir(filepath.Join(dir, "somedir"), 0755) + require.NoError(t, err) + _, err = os.Create(filepath.Join(dir, "somedir", "inner")) + require.NoError(t, err) + + tests := []struct { + name string + path string + want []string + expectedErr string + }{ + { + name: "valid directory", + path: dir, + want: []string{ + filepath.Join(dir, "notification-gitter"), + filepath.Join(dir, "slack"), + }, + }, + { + name: "invalid directory", + path: "./foo/bar/", + expectedErr: "open ./foo/bar/: " + cstest.PathNotFoundMessage, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got, err := listFilesAtPath(tc.path) + cstest.RequireErrorContains(t, err, tc.expectedErr) + assert.ElementsMatch(t, tc.want, got) + }) + } +} diff --git a/pkg/csplugin/tests/notifications/dummy.yaml b/pkg/csplugin/testdata/dummy.yaml similarity index 100% rename from pkg/csplugin/tests/notifications/dummy.yaml rename to pkg/csplugin/testdata/dummy.yaml diff --git a/pkg/csplugin/utils_test.go b/pkg/csplugin/utils_test.go new file mode 100644 index 000000000..b4ac1e7e7 --- /dev/null +++ b/pkg/csplugin/utils_test.go @@ -0,0 +1,49 @@ +//go:build linux || freebsd || netbsd || openbsd || solaris || !windows + +package csplugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/crowdsecurity/go-cs-lib/pkg/cstest" +) + +func TestGetPluginNameAndTypeFromPath(t *testing.T) { + tests := []struct { + name string + path string + want string + want1 string + expectedErr string + }{ + { + name: "valid plugin name, single dash", + path: "/path/to/notification-gitter", + want: "notification", + want1: "gitter", + }, + { + name: "invalid plugin name", + path: "/path/to/gitter", + expectedErr: "plugin name /path/to/gitter is invalid. Name should be like {type-name}", + }, + { + name: "valid plugin name, multiple dash", + path: "/path/to/notification-instant-slack", + want: "notification-instant", + want1: "slack", + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got, got1, err := getPluginTypeAndSubtypeFromPath(tc.path) + cstest.RequireErrorContains(t, err, tc.expectedErr) + + assert.Equal(t, tc.want, got) + assert.Equal(t, tc.want1, got1) + }) + } +} diff --git a/pkg/csplugin/utils_windows_test.go b/pkg/csplugin/utils_windows_test.go new file mode 100644 index 000000000..9161fd45b --- /dev/null +++ b/pkg/csplugin/utils_windows_test.go @@ -0,0 +1,49 @@ +//go:build windows + +package csplugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/crowdsecurity/go-cs-lib/pkg/cstest" +) + +func TestGetPluginNameAndTypeFromPath(t *testing.T) { + tests := []struct { + name string + path string + want string + want1 string + expectedErr string + }{ + { + name: "valid plugin name, single dash", + path: "c:\\path\\to\\notification-gitter", + want: "notification", + want1: "gitter", + }, + { + name: "invalid plugin name", + path: "c:\\path\\to\\gitter.exe", + expectedErr: "plugin name c:\\path\\to\\gitter.exe is invalid. Name should be like {type-name}", + }, + { + name: "valid plugin name, multiple dash", + path: "c:\\path\\to\\notification-instant-slack.exe", + want: "notification-instant", + want1: "slack", + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got, got1, err := getPluginTypeAndSubtypeFromPath(tc.path) + cstest.RequireErrorContains(t, err, tc.expectedErr) + + assert.Equal(t, tc.want, got) + assert.Equal(t, tc.want1, got1) + }) + } +}