Parcourir la source

CI: refactoring pkg/csplugin tests (#2247)

mmetc il y a 2 ans
Parent
commit
12c32d507c

+ 1 - 1
go.mod

@@ -71,7 +71,7 @@ require (
 	github.com/bluele/gcache v0.0.2
 	github.com/bluele/gcache v0.0.2
 	github.com/cespare/xxhash/v2 v2.1.2
 	github.com/cespare/xxhash/v2 v2.1.2
 	github.com/coreos/go-systemd/v22 v22.5.0
 	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/goccy/go-yaml v1.9.7
 	github.com/gofrs/uuid v4.0.0+incompatible
 	github.com/gofrs/uuid v4.0.0+incompatible
 	github.com/golang-jwt/jwt/v4 v4.2.0
 	github.com/golang-jwt/jwt/v4 v4.2.0

+ 2 - 2
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/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 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
 github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
 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 h1:t4VYnDlAd0RjDM2SlILalbwfCrQxtJSMGdQOR0zwkE4=
 github.com/crowdsecurity/grokky v0.2.1/go.mod h1:33usDIYzGDsgX1kHAThCbseso6JuWNJXOzRQDGXHtWM=
 github.com/crowdsecurity/grokky v0.2.1/go.mod h1:33usDIYzGDsgX1kHAThCbseso6JuWNJXOzRQDGXHtWM=
 github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5lirI+bdc=
 github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5lirI+bdc=

+ 0 - 17
pkg/csplugin/broker.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"os"
 	"os"
-	"path/filepath"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"text/template"
 	"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) {
 func getUUID() (string, error) {
 	uuidv4, err := uuid.NewRandom()
 	uuidv4, err := uuid.NewRandom()
 	if err != nil {
 	if err != nil {

+ 164 - 0
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
+}

+ 76 - 323
pkg/csplugin/broker_test.go

@@ -5,11 +5,6 @@ package csplugin
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"os"
 	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
-	"reflect"
-	"runtime"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -25,116 +20,39 @@ import (
 	"github.com/crowdsecurity/crowdsec/pkg/models"
 	"github.com/crowdsecurity/crowdsec/pkg/models"
 )
 )
 
 
-var testPath string
 
 
-func setPluginPermTo744(t *testing.T) {
-	setPluginPermTo(t, "744")
+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 setPluginPermTo722(t *testing.T) {
-	setPluginPermTo(t, "722")
-}
+func (s *PluginSuite) readconfig() (PluginConfig) {
+	var config PluginConfig
+	t := s.T()
 
 
-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)
+	orig, err := os.ReadFile(s.pluginConfig)
+	require.NoError(t, err,"unable to read config file %s", s.pluginConfig)
 
 
-			assert.Equal(t, tc.want, got)
-			assert.Equal(t, tc.want1, got1)
-		})
-	}
+	err = yaml.Unmarshal(orig, &config)
+	require.NoError(t, err,"unable to unmarshal config file")
+	
+	return config
 }
 }
 
 
-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)
 
 
-			if !reflect.DeepEqual(got, tc.want) {
-				t.Errorf("listFilesAtPath() = %v, want %v", got, tc.want)
-			}
-		})
-	}
+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 TestBrokerInit(t *testing.T) {
-	if runtime.GOOS == "windows" {
-		t.Skip("Skipping test on windows")
-	}
 
 
+func (s *PluginSuite) TestBrokerInit() {
 	tests := []struct {
 	tests := []struct {
 		name        string
 		name        string
 		action      func(*testing.T)
 		action      func(*testing.T)
@@ -143,28 +61,30 @@ func TestBrokerInit(t *testing.T) {
 	}{
 	}{
 		{
 		{
 			name:   "valid config",
 			name:   "valid config",
-			action: setPluginPermTo744,
 		},
 		},
 		{
 		{
 			name:        "group writable binary",
 			name:        "group writable binary",
 			expectedErr: "notification-dummy is world writable",
 			expectedErr: "notification-dummy is world writable",
-			action:      setPluginPermTo722,
+			action:      s.permissionSetter(0o722),
 		},
 		},
 		{
 		{
 			name:        "group writable binary",
 			name:        "group writable binary",
 			expectedErr: "notification-dummy is group writable",
 			expectedErr: "notification-dummy is group writable",
-			action:      setPluginPermTo724,
+			action:      s.permissionSetter(0o724),
 		},
 		},
 		{
 		{
 			name:        "no plugin dir",
 			name:        "no plugin dir",
 			expectedErr: cstest.FileNotFoundMessage,
 			expectedErr: cstest.FileNotFoundMessage,
-			action:      tearDown,
+			action: func(t *testing.T) {
+				err := os.RemoveAll(s.runDir)
+				require.NoError(t, err)
+			},
 		},
 		},
 		{
 		{
 			name:        "no plugin binary",
 			name:        "no plugin binary",
 			expectedErr: "binary for plugin dummy_default not found",
 			expectedErr: "binary for plugin dummy_default not found",
 			action: func(t *testing.T) {
 			action: func(t *testing.T) {
-				err := os.Remove(path.Join(testPath, "notification-dummy"))
+				err := os.Remove(s.pluginBinary)
 				require.NoError(t, err)
 				require.NoError(t, err)
 			},
 			},
 		},
 		},
@@ -174,7 +94,6 @@ func TestBrokerInit(t *testing.T) {
 			procCfg: csconfig.PluginCfg{
 			procCfg: csconfig.PluginCfg{
 				User: "123445555551122toto",
 				User: "123445555551122toto",
 			},
 			},
-			action: setPluginPermTo744,
 		},
 		},
 		{
 		{
 			name:        "only specify group",
 			name:        "only specify group",
@@ -182,7 +101,6 @@ func TestBrokerInit(t *testing.T) {
 			procCfg: csconfig.PluginCfg{
 			procCfg: csconfig.PluginCfg{
 				Group: "123445555551122toto",
 				Group: "123445555551122toto",
 			},
 			},
-			action: setPluginPermTo744,
 		},
 		},
 		{
 		{
 			name:        "Fails to run as root",
 			name:        "Fails to run as root",
@@ -191,7 +109,6 @@ func TestBrokerInit(t *testing.T) {
 				User:  "root",
 				User:  "root",
 				Group: "root",
 				Group: "root",
 			},
 			},
-			action: setPluginPermTo744,
 		},
 		},
 		{
 		{
 			name:        "Invalid user and group",
 			name:        "Invalid user and group",
@@ -200,7 +117,6 @@ func TestBrokerInit(t *testing.T) {
 				User:  "toto1234",
 				User:  "toto1234",
 				Group: "toto1234",
 				Group: "toto1234",
 			},
 			},
-			action: setPluginPermTo744,
 		},
 		},
 		{
 		{
 			name:        "Valid user and invalid group",
 			name:        "Valid user and invalid group",
@@ -209,79 +125,33 @@ func TestBrokerInit(t *testing.T) {
 				User:  "nobody",
 				User:  "nobody",
 				Group: "toto1234",
 				Group: "toto1234",
 			},
 			},
-			action: setPluginPermTo744,
 		},
 		},
 	}
 	}
 
 
 	for _, tc := range tests {
 	for _, tc := range tests {
 		tc := tc
 		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 {
 			if tc.action != nil {
 				tc.action(t)
 				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)
 			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
 	var alerts []models.Alert
 	DefaultEmptyTicker = 50 * time.Millisecond
 	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)
 	assert.NoError(t, err)
-	tomb := tomb.Tomb{}
 
 
+	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
 
 
 	// send one item, it should be processed right now
 	// send one item, it should be processed right now
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
 	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")
 	require.NoError(t, err, "Error reading file")
 
 
 	err = json.Unmarshal(content, &alerts)
 	err = json.Unmarshal(content, &alerts)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 	assert.Len(t, alerts, 1)
 	assert.Len(t, alerts, 1)
 
 
 	// remove it
 	// remove it
@@ -313,34 +183,24 @@ func TestBrokerNoThreshold(t *testing.T) {
 	assert.Len(t, alerts, 1)
 	assert.Len(t, alerts, 1)
 }
 }
 
 
-func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) {
+func (s *PluginSuite) TestBrokerRunGroupAndTimeThreshold_TimeFirst() {
 	// test grouping by "time"
 	// test grouping by "time"
 	DefaultEmptyTicker = 50 * time.Millisecond
 	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
 	// set groupwait and groupthreshold, should honor whichever comes first
-	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
+	cfg := s.readconfig()
 	cfg.GroupThreshold = 4
 	cfg.GroupThreshold = 4
 	cfg.GroupWait = 1 * time.Second
 	cfg.GroupWait = 1 * time.Second
-	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)
 	assert.NoError(t, err)
-	tomb := tomb.Tomb{}
 
 
+	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
+
 	// send data
 	// send data
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
 	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)
 	err = json.Unmarshal(content, &alerts)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, alerts, 3)
 	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
 	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
 	// set groupwait and groupthreshold, should honor whichever comes first
-	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
+	cfg := s.readconfig()
 	cfg.GroupThreshold = 4
 	cfg.GroupThreshold = 4
 	cfg.GroupWait = 4 * time.Second
 	cfg.GroupWait = 4 * time.Second
-	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)
 	assert.NoError(t, err)
-	tomb := tomb.Tomb{}
 
 
+	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
 
 
 	// send data
 	// send data
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@@ -411,41 +255,24 @@ func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) {
 	err = json.Unmarshal(content, &alerts)
 	err = json.Unmarshal(content, &alerts)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, alerts, 4)
 	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"
 	// test grouping by "size"
 	DefaultEmptyTicker = 50 * time.Millisecond
 	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
 	// set groupwait
-	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
+	cfg := s.readconfig()
 	cfg.GroupThreshold = 4
 	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)
 	assert.NoError(t, err)
-	tomb := tomb.Tomb{}
 
 
+	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
 
 
 	// send data
 	// send data
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@@ -466,39 +293,23 @@ func TestBrokerRunGroupThreshold(t *testing.T) {
 	err = json.Unmarshal(content, &alerts)
 	err = json.Unmarshal(content, &alerts)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, alerts, 4)
 	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
 	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
 	// set groupwait
-	raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
+	cfg := s.readconfig()
 	cfg.GroupWait = 1 * time.Second
 	cfg.GroupWait = 1 * time.Second
-	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)
 	assert.NoError(t, err)
-	tomb := tomb.Tomb{}
 
 
+	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
 
 
 	// send data
 	// send data
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
 	pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
@@ -516,32 +327,18 @@ func TestBrokerRunTimeThreshold(t *testing.T) {
 	err = json.Unmarshal(content, &alerts)
 	err = json.Unmarshal(content, &alerts)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, alerts, 1)
 	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
 	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",
-	})
+
+	t := s.T()
+	
+	pb, err := s.InitBroker(nil)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	tomb := tomb.Tomb{}
 
 
+	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
 
 
 	assert.NoFileExists(t, "./out")
 	assert.NoFileExists(t, "./out")
 
 
@@ -559,47 +356,3 @@ func TestBrokerRunSimple(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	assert.Len(t, alerts, 2)
 	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")
-}

+ 26 - 204
pkg/csplugin/broker_win_test.go

@@ -3,16 +3,12 @@
 package csplugin
 package csplugin
 
 
 import (
 import (
-	"log"
 	"os"
 	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
-	"reflect"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"gopkg.in/tomb.v2"
 	"gopkg.in/tomb.v2"
 
 
 	"github.com/crowdsecurity/go-cs-lib/pkg/cstest"
 	"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
 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 {
 	tests := []struct {
 		name        string
 		name        string
-		action      func()
-		errContains string
-		wantErr     bool
+		action      func(*testing.T)
 		procCfg     csconfig.PluginCfg
 		procCfg     csconfig.PluginCfg
+		expectedErr string
 	}{
 	}{
 		{
 		{
 			name:    "valid config",
 			name:    "valid config",
-			wantErr: false,
 		},
 		},
 		{
 		{
 			name:        "no plugin dir",
 			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",
 			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)
 	assert.NoError(t, err)
+
 	tomb := tomb.Tomb{}
 	tomb := tomb.Tomb{}
 	go pb.Run(&tomb)
 	go pb.Run(&tomb)
-	defer pb.Kill()
 
 
 	assert.NoFileExists(t, "./out")
 	assert.NoFileExists(t, "./out")
 	defer os.Remove("./out")
 	defer os.Remove("./out")
@@ -220,44 +83,3 @@ func TestBrokerRun(t *testing.T) {
 	assert.FileExists(t, ".\\out")
 	assert.FileExists(t, ".\\out")
 	assert.Equal(t, types.GetLineCountForFile(".\\out"), 2)
 	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)
-	}
-}

+ 22 - 0
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
+}
+

+ 57 - 0
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)
+		})
+	}
+}

+ 0 - 0
pkg/csplugin/tests/notifications/dummy.yaml → pkg/csplugin/testdata/dummy.yaml


+ 49 - 0
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)
+		})
+	}
+}

+ 49 - 0
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)
+		})
+	}
+}