Repository: moira-alert/moira
Pull request #1099 (2024-09-26 09:00:26)

feat(notifier): add validation notifier config
# Add validation notifier config

Added validation of the notifier's senders config, required fields are now checked with [validator](https://github.com/go-playground/validator)

almostinf: /build
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-validate-notifier-config.2024-09-27.8db8d2a
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-validate-notifier-config.2024-09-27.8db8d2a
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-validate-notifier-config.2024-09-27.8db8d2a
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-validate-notifier-config.2024-09-27.8db8d2a
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-validate-notifier-config.2024-09-27.8db8d2a

Patch:
diff --git a/go.mod b/go.mod
index c92f76c3..a2d3a9c5 100644
--- a/go.mod
+++ b/go.mod
@@ -46,6 +46,7 @@ require (
 require github.com/prometheus/common v0.37.0
 
 require (
+	github.com/go-playground/validator/v10 v10.4.1
 	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/mattermost/mattermost/server/public v0.1.1
 	github.com/mitchellh/mapstructure v1.5.0
@@ -183,12 +184,15 @@ require (
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/spec v0.20.9 // indirect
 	github.com/go-openapi/swag v0.22.4 // indirect
+	github.com/go-playground/locales v0.13.0 // indirect
+	github.com/go-playground/universal-translator v0.17.0 // indirect
 	github.com/hashicorp/go-hclog v1.6.3 // indirect
 	github.com/hashicorp/go-plugin v1.6.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/huandu/xstrings v1.3.3 // indirect
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
+	github.com/leodido/go-urn v1.2.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mitchellh/copystructure v1.0.0 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
diff --git a/go.sum b/go.sum
index 089f98d5..0eacd324 100644
--- a/go.sum
+++ b/go.sum
@@ -637,9 +637,13 @@ github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/
 github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
 github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
 github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
 github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
@@ -898,6 +902,7 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
diff --git a/helpers.go b/helpers.go
index c0432ca2..eab4e915 100644
--- a/helpers.go
+++ b/helpers.go
@@ -5,6 +5,8 @@ import (
 	"math"
 	"strings"
 	"time"
+
+	"github.com/go-playground/validator/v10"
 )
 
 // BytesScanner allows to scan for subslices separated by separator.
@@ -250,3 +252,9 @@ func MergeToSorted[T Comparable](arr1, arr2 []T) ([]T, error) {
 
 	return merged, nil
 }
+
+// ValidateStruct is a default generic function that uses a validator to validate structure fields.
+func ValidateStruct(s any) error {
+	validator := validator.New()
+	return validator.Struct(s)
+}
diff --git a/senders/discord/init.go b/senders/discord/init.go
index e991d089..92674624 100644
--- a/senders/discord/init.go
+++ b/senders/discord/init.go
@@ -20,7 +20,7 @@ const (
 // Structure that represents the Discord configuration in the YAML file.
 type config struct {
 	ContactType string `mapstructure:"contact_type"`
-	Token       string `mapstructure:"token"`
+	Token       string `mapstructure:"token" validate:"required"`
 	FrontURI    string `mapstructure:"front_uri"`
 }
 
@@ -42,9 +42,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to discord config: %w", err)
 	}
 
-	if cfg.Token == "" {
-		return fmt.Errorf("cannot read the discord token from the config")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("discord config validation error: %w", err)
 	}
+
 	sender.session, err = discordgo.New("Bot " + cfg.Token)
 	if err != nil {
 		return fmt.Errorf("error creating discord session: %w", err)
diff --git a/senders/mail/mail.go b/senders/mail/mail.go
index 39ce7334..10406e04 100644
--- a/senders/mail/mail.go
+++ b/senders/mail/mail.go
@@ -14,10 +14,10 @@ import (
 
 // Structure that represents the Mail configuration in the YAML file.
 type config struct {
-	MailFrom     string `mapstructure:"mail_from"`
+	MailFrom     string `mapstructure:"mail_from" validate:"required"`
 	SMTPHello    string `mapstructure:"smtp_hello"`
-	SMTPHost     string `mapstructure:"smtp_host"`
-	SMTPPort     int64  `mapstructure:"smtp_port"`
+	SMTPHost     string `mapstructure:"smtp_host" validate:"required"`
+	SMTPPort     int64  `mapstructure:"smtp_port" validate:"required"`
 	InsecureTLS  bool   `mapstructure:"insecure_tls"`
 	FrontURI     string `mapstructure:"front_uri"`
 	SMTPPass     string `mapstructure:"smtp_pass"`
@@ -64,6 +64,10 @@ func (sender *Sender) fillSettings(senderSettings interface{}, logger moira.Logg
 		return fmt.Errorf("failed to decode senderSettings to mail config: %w", err)
 	}
 
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("mail config validation error: %w", err)
+	}
+
 	sender.logger = logger
 	sender.From = cfg.MailFrom
 	sender.SMTPHello = cfg.SMTPHello
@@ -76,12 +80,11 @@ func (sender *Sender) fillSettings(senderSettings interface{}, logger moira.Logg
 	sender.TemplateFile = cfg.TemplateFile
 	sender.location = location
 	sender.dateTimeFormat = dateTimeFormat
+
 	if sender.Username == "" {
 		sender.Username = sender.From
 	}
-	if sender.From == "" {
-		return fmt.Errorf("mail_from can't be empty")
-	}
+
 	return nil
 }
 
@@ -106,11 +109,13 @@ func (sender *Sender) tryDial() error {
 		return err
 	}
 	defer t.Close()
+
 	if sender.SMTPHello != "" {
 		if err := t.Hello(sender.SMTPHello); err != nil {
 			return err
 		}
 	}
+
 	if sender.Password != "" {
 		tlsConfig := &tls.Config{
 			InsecureSkipVerify: sender.InsecureTLS,
@@ -123,5 +128,6 @@ func (sender *Sender) tryDial() error {
 			return err
 		}
 	}
+
 	return nil
 }
diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go
index 36cbabb3..1b0f85c4 100644
--- a/senders/mattermost/sender.go
+++ b/senders/mattermost/sender.go
@@ -18,10 +18,10 @@ import (
 
 // Structure that represents the Mattermost configuration in the YAML file.
 type config struct {
-	Url          string            `mapstructure:"url"`
+	Url          string            `mapstructure:"url" validate:"required,url"`
 	InsecureTLS  bool              `mapstructure:"insecure_tls"`
-	APIToken     string            `mapstructure:"api_token"`
-	FrontURI     string            `mapstructure:"front_uri"`
+	APIToken     string            `mapstructure:"api_token" validate:"required"`
+	FrontURI     string            `mapstructure:"front_uri" validate:"required"`
 	UseEmoji     bool              `mapstructure:"use_emoji"`
 	DefaultEmoji string            `mapstructure:"default_emoji"`
 	EmojiMap     map[string]string `mapstructure:"emoji_map"`
@@ -53,8 +53,8 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to mattermost config: %w", err)
 	}
 
-	if cfg.Url == "" {
-		return fmt.Errorf("can not read Mattermost url from config")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("mattermost config validation error: %w", err)
 	}
 
 	client := model.NewAPIv4Client(cfg.Url)
@@ -68,20 +68,13 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 	}
 
 	sender.client = client
-
-	if cfg.APIToken == "" {
-		return fmt.Errorf("can not read Mattermost api_token from config")
-	}
 	sender.client.SetToken(cfg.APIToken)
 
-	if cfg.FrontURI == "" {
-		return fmt.Errorf("can not read Mattermost front_uri from config")
-	}
-
 	emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap)
 	if err != nil {
 		return fmt.Errorf("cannot initialize mattermost sender, err: %w", err)
 	}
+
 	sender.logger = logger
 	sender.formatter = msgformat.NewHighlightSyntaxFormatter(
 		emojiProvider,
diff --git a/senders/msteams/msteams.go b/senders/msteams/msteams.go
index 4bfd282f..8e7e03bc 100644
--- a/senders/msteams/msteams.go
+++ b/senders/msteams/msteams.go
@@ -29,15 +29,19 @@ const (
 	quotes            = "```"
 )
 
-var throttleWarningFact = Fact{
-	Name:  "Warning",
-	Value: "Please, *fix your system or tune this trigger* to generate less events.",
-}
+var (
+	throttleWarningFact = Fact{
+		Name:  "Warning",
+		Value: "Please, *fix your system or tune this trigger* to generate less events.",
+	}
 
-var headers = map[string]string{
-	"User-Agent":   "Moira",
-	"Content-Type": "application/json",
-}
+	headers = map[string]string{
+		"User-Agent":   "Moira",
+		"Content-Type": "application/json",
+	}
+
+	defaultClientTimeout = 30 * time.Second
+)
 
 // Structure that represents the MSTeams configuration in the YAML file.
 type config struct {
@@ -62,13 +66,18 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to msteams config: %w", err)
 	}
 
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("msteams config validation error: %w", err)
+	}
+
 	sender.logger = logger
 	sender.location = location
 	sender.frontURI = cfg.FrontURI
 	sender.maxEvents = cfg.MaxEvents
 	sender.client = &http.Client{
-		Timeout: time.Duration(30) * time.Second, //nolint
+		Timeout: defaultClientTimeout,
 	}
+
 	return nil
 }
 
diff --git a/senders/opsgenie/init.go b/senders/opsgenie/init.go
index 83c20516..72913a76 100644
--- a/senders/opsgenie/init.go
+++ b/senders/opsgenie/init.go
@@ -13,8 +13,7 @@ import (
 
 // Structure that represents the OpsGenie configuration in the YAML file.
 type config struct {
-	APIKey   string `mapstructure:"api_key"`
-	FrontURI string `mapstructure:"front_uri"`
+	APIKey string `mapstructure:"api_key" validate:"required"`
 }
 
 // Sender implements the Sender interface for opsgenie.
@@ -27,7 +26,6 @@ type Sender struct {
 	imageStoreID         string
 	imageStore           moira.ImageStore
 	imageStoreConfigured bool
-	frontURI             string
 }
 
 // Init initializes the opsgenie sender.
@@ -39,11 +37,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to opsgenie config: %w", err)
 	}
 
-	sender.apiKey = cfg.APIKey
-	if sender.apiKey == "" {
-		return fmt.Errorf("cannot read the api_key from the sender settings")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("opsgenie config validation error: %w", err)
 	}
 
+	sender.apiKey = cfg.APIKey
 	sender.imageStoreID, sender.imageStore, sender.imageStoreConfigured = senders.ReadImageStoreConfig(senderSettings, sender.ImageStores, logger)
 
 	sender.client, err = alert.NewClient(&client.Config{
@@ -53,7 +51,6 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("error while creating opsgenie client: %w", err)
 	}
 
-	sender.frontURI = cfg.FrontURI
 	sender.logger = logger
 	sender.location = location
 	return nil
diff --git a/senders/pagerduty/init.go b/senders/pagerduty/init.go
index 0e58afc2..cbb36c5b 100644
--- a/senders/pagerduty/init.go
+++ b/senders/pagerduty/init.go
@@ -39,5 +39,6 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 
 	sender.logger = logger
 	sender.location = location
+
 	return nil
 }
diff --git a/senders/pushover/pushover.go b/senders/pushover/pushover.go
index d8f0c4b6..ecd88d1c 100644
--- a/senders/pushover/pushover.go
+++ b/senders/pushover/pushover.go
@@ -19,7 +19,7 @@ const (
 
 // Structure that represents the Pushover configuration in the YAML file.
 type config struct {
-	APIToken string `mapstructure:"api_token"`
+	APIToken string `mapstructure:"api_token" validate:"required"`
 	FrontURI string `mapstructure:"front_uri"`
 }
 
@@ -41,14 +41,16 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to pushover config: %w", err)
 	}
 
-	sender.apiToken = cfg.APIToken
-	if sender.apiToken == "" {
-		return fmt.Errorf("can not read pushover api_token from config")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("pushover config validation error: %w", err)
 	}
+
+	sender.apiToken = cfg.APIToken
 	sender.client = pushover_client.New(sender.apiToken)
 	sender.logger = logger
 	sender.frontURI = cfg.FrontURI
 	sender.location = location
+
 	return nil
 }
 
diff --git a/senders/script/script.go b/senders/script/script.go
index c78e37dd..d7b7351d 100644
--- a/senders/script/script.go
+++ b/senders/script/script.go
@@ -15,7 +15,7 @@ import (
 
 // Structure that represents the Script configuration in the YAML file.
 type config struct {
-	Exec string `mapstructure:"exec"`
+	Exec string `mapstructure:"exec" validate:"required"`
 }
 
 // Sender implements moira sender interface via script execution.
@@ -40,6 +40,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to script config: %w", err)
 	}
 
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("script config validation error: %w", err)
+	}
+
 	_, _, err = parseExec(cfg.Exec)
 	if err != nil {
 		return err
diff --git a/senders/slack/slack.go b/senders/slack/slack.go
index 28976c6c..d1a5f4d2 100644
--- a/senders/slack/slack.go
+++ b/senders/slack/slack.go
@@ -31,7 +31,7 @@ var (
 
 // Structure that represents the Slack configuration in the YAML file.
 type config struct {
-	APIToken     string            `mapstructure:"api_token"`
+	APIToken     string            `mapstructure:"api_token" validate:"required"`
 	UseEmoji     bool              `mapstructure:"use_emoji"`
 	FrontURI     string            `mapstructure:"front_uri"`
 	DefaultEmoji string            `mapstructure:"default_emoji"`
@@ -54,13 +54,15 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to slack config: %w", err)
 	}
 
-	if cfg.APIToken == "" {
-		return fmt.Errorf("can not read slack api_token from config")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("slack config validation error: %w", err)
 	}
+
 	emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap)
 	if err != nil {
 		return fmt.Errorf("cannot initialize slack sender, err: %w", err)
 	}
+
 	sender.logger = logger
 	sender.emojiProvider = emojiProvider
 	sender.formatter = msgformat.NewHighlightSyntaxFormatter(
@@ -75,7 +77,9 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		eventStringFormatter,
 		codeBlockStart,
 		codeBlockEnd)
+
 	sender.client = slack_client.New(cfg.APIToken)
+
 	return nil
 }
 
diff --git a/senders/telegram/init.go b/senders/telegram/init.go
index aabd2a4f..f49332c9 100644
--- a/senders/telegram/init.go
+++ b/senders/telegram/init.go
@@ -27,7 +27,7 @@ var pollerTimeout = 10 * time.Second
 // Structure that represents the Telegram configuration in the YAML file.
 type config struct {
 	ContactType string `mapstructure:"contact_type"`
-	APIToken    string `mapstructure:"api_token"`
+	APIToken    string `mapstructure:"api_token" validate:"required"`
 	FrontURI    string `mapstructure:"front_uri"`
 }
 
@@ -66,9 +66,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to telegram config: %w", err)
 	}
 
-	if cfg.APIToken == "" {
-		return fmt.Errorf("can not read telegram api_token from config")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("telegram config validation error: %w", err)
 	}
+
 	sender.apiToken = cfg.APIToken
 
 	emojiProvider := telegramEmojiProvider{}
diff --git a/senders/twilio/twilio.go b/senders/twilio/twilio.go
index d8fc8a77..7fff15cf 100644
--- a/senders/twilio/twilio.go
+++ b/senders/twilio/twilio.go
@@ -11,10 +11,10 @@ import (
 
 // Structure that represents the Twilio configuration in the YAML file.
 type config struct {
-	Type          string `mapstructure:"sender_type"`
-	APIAsid       string `mapstructure:"api_asid"`
-	APIAuthToken  string `mapstructure:"api_authtoken"`
-	APIFromPhone  string `mapstructure:"api_fromphone"`
+	Type          string `mapstructure:"sender_type" validate:"required"`
+	APIAsid       string `mapstructure:"api_asid" validate:"required"`
+	APIAuthToken  string `mapstructure:"api_authtoken" validate:"required"`
+	APIFromPhone  string `mapstructure:"api_fromphone" validate:"required"`
 	VoiceURL      string `mapstructure:"voiceurl"`
 	TwimletsEcho  bool   `mapstructure:"twimlets_echo"`
 	AppendMessage bool   `mapstructure:"append_message"`
@@ -43,19 +43,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 	if err != nil {
 		return fmt.Errorf("failed to decode senderSettings to twilio config: %w", err)
 	}
-	apiType := cfg.Type
-
-	if cfg.APIAsid == "" {
-		return fmt.Errorf("can not read [%s] api_sid param from config", apiType)
-	}
 
-	if cfg.APIAuthToken == "" {
-		return fmt.Errorf("can not read [%s] api_authtoken param from config", apiType)
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("twilio config validation error: %w", err)
 	}
 
-	if cfg.APIFromPhone == "" {
-		return fmt.Errorf("can not read [%s] api_fromphone param from config", apiType)
-	}
+	apiType := cfg.Type
 
 	twilioClient := twilio_client.NewClient(cfg.APIAsid, cfg.APIAuthToken)
 
@@ -65,6 +58,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		logger:       logger,
 		location:     location,
 	}
+
 	switch apiType {
 	case "twilio sms":
 		sender.sender = &twilioSenderSms{tSender}
diff --git a/senders/victorops/init.go b/senders/victorops/init.go
index e47e5348..c623f820 100644
--- a/senders/victorops/init.go
+++ b/senders/victorops/init.go
@@ -12,7 +12,7 @@ import (
 
 // Structure that represents the VictorOps configuration in the YAML file.
 type config struct {
-	RoutingURL string `mapstructure:"routing_url"`
+	RoutingURL string `mapstructure:"routing_url" validate:"required"`
 	ImageStore string `mapstructure:"image_store"`
 	FrontURI   string `mapstructure:"front_uri"`
 }
@@ -40,11 +40,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to victorops config: %w", err)
 	}
 
-	sender.routingURL = cfg.RoutingURL
-	if sender.routingURL == "" {
-		return fmt.Errorf("cannot read the routing url from the yaml config")
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("victorops config validation error: %w", err)
 	}
 
+	sender.routingURL = cfg.RoutingURL
+
 	sender.imageStoreID = cfg.ImageStore
 	if sender.imageStoreID == "" {
 		logger.Warning().Msg("Cannot read image_store from the config, will not be able to attach plot images to events")
diff --git a/senders/webhook/webhook.go b/senders/webhook/webhook.go
index 3812a879..036c877c 100644
--- a/senders/webhook/webhook.go
+++ b/senders/webhook/webhook.go
@@ -1,7 +1,6 @@
 package webhook
 
 import (
-	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -11,11 +10,9 @@ import (
 	"github.com/moira-alert/moira"
 )
 
-var ErrMissingURL = errors.New("can not read url from config")
-
 // Structure that represents the Webhook configuration in the YAML file.
 type config struct {
-	URL      string            `mapstructure:"url"`
+	URL      string            `mapstructure:"url" validate:"required"`
 	Body     string            `mapstructure:"body"`
 	Headers  map[string]string `mapstructure:"headers"`
 	User     string            `mapstructure:"user"`
@@ -42,13 +39,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 		return fmt.Errorf("failed to decode senderSettings to webhook config: %w", err)
 	}
 
-	sender.url = cfg.URL
-	if sender.url == "" {
-		return ErrMissingURL
+	if err = moira.ValidateStruct(cfg); err != nil {
+		return fmt.Errorf("webhook config validation error: %w", err)
 	}
 
+	sender.url = cfg.URL
 	sender.body = cfg.Body
-
 	sender.user, sender.password = cfg.User, cfg.Password
 
 	sender.headers = map[string]string{


Test patch:
diff --git a/helpers_test.go b/helpers_test.go
index d9c3ca4e..4b764d6a 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -290,25 +290,25 @@ func TestMergeToSorted(t *testing.T) {
 		})
 
 		Convey("Test with one nil array", func() {
-			merged, err := MergeToSorted[myInt](nil, []myInt{1, 2, 3})
+			merged, err := MergeToSorted(nil, []myInt{1, 2, 3})
 			So(err, ShouldBeNil)
 			So(merged, ShouldResemble, []myInt{1, 2, 3})
 		})
 
 		Convey("Test with two arrays", func() {
-			merged, err := MergeToSorted[myInt]([]myInt{4, 5}, []myInt{1, 2, 3})
+			merged, err := MergeToSorted([]myInt{4, 5}, []myInt{1, 2, 3})
 			So(err, ShouldBeNil)
 			So(merged, ShouldResemble, []myInt{1, 2, 3, 4, 5})
 		})
 
 		Convey("Test with empty array", func() {
-			merged, err := MergeToSorted[myInt]([]myInt{-4, 5}, []myInt{})
+			merged, err := MergeToSorted([]myInt{-4, 5}, []myInt{})
 			So(err, ShouldBeNil)
 			So(merged, ShouldResemble, []myInt{-4, 5})
 		})
 
 		Convey("Test with sorted values but mixed up", func() {
-			merged, err := MergeToSorted[myInt]([]myInt{1, 9, 10}, []myInt{4, 8, 12})
+			merged, err := MergeToSorted([]myInt{1, 9, 10}, []myInt{4, 8, 12})
 			So(err, ShouldBeNil)
 			So(merged, ShouldResemble, []myInt{1, 4, 8, 9, 10, 12})
 		})
@@ -333,9 +333,55 @@ func TestMergeToSorted(t *testing.T) {
 			}
 
 			expected := append(arr2, arr1...)
-			merged, err := MergeToSorted[myTest](arr1, arr2)
+			merged, err := MergeToSorted(arr1, arr2)
 			So(err, ShouldBeNil)
 			So(merged, ShouldResemble, expected)
 		})
 	})
 }
+
+func TestValidateStruct(t *testing.T) {
+	type ValidationStruct struct {
+		TestInt  int    `validate:"required,gt=0"`
+		TestURL  string `validate:"required,url"`
+		TestBool bool
+	}
+
+	const (
+		validURL = "https://github.com/moira-alert/moira"
+		validInt = 1
+	)
+
+	Convey("Test ValidateStruct", t, func() {
+		Convey("With TestInt less than zero", func() {
+			testStruct := ValidationStruct{
+				TestInt: -1,
+				TestURL: validURL,
+			}
+
+			err := ValidateStruct(testStruct)
+			So(err, ShouldNotBeNil)
+		})
+
+		Convey("With invalid TestURL format", func() {
+			testStruct := ValidationStruct{
+				TestInt:  validInt,
+				TestURL:  "test",
+				TestBool: true,
+			}
+
+			err := ValidateStruct(testStruct)
+			So(err, ShouldNotBeNil)
+		})
+
+		Convey("With valid structure", func() {
+			testStruct := ValidationStruct{
+				TestInt: validInt,
+				TestURL: validURL,
+			}
+
+			err := ValidateStruct(testStruct)
+			So(err, ShouldBeNil)
+		})
+	})
+}
diff --git a/senders/discord/init_test.go b/senders/discord/init_test.go
index 73c2609a..11043df9 100644
--- a/senders/discord/init_test.go
+++ b/senders/discord/init_test.go
@@ -1,10 +1,11 @@
 package discord
 
 import (
-	"fmt"
+	"errors"
 	"testing"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/moira-alert/moira"
 
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
@@ -31,9 +32,14 @@ func TestInit(t *testing.T) {
 	location, _ := time.LoadLocation("UTC")
 	Convey("Init tests", t, func() {
 		sender := Sender{DataBase: &MockDB{}}
-		Convey("Empty map", func() {
-			err := sender.Init(map[string]interface{}{}, logger, nil, "")
-			So(err, ShouldResemble, fmt.Errorf("cannot read the discord token from the config"))
+
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty token", func() {
+			senderSettings := map[string]interface{}{}
+
+			err := sender.Init(senderSettings, logger, nil, "")
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{DataBase: &MockDB{}})
 		})
 
@@ -42,7 +48,9 @@ func TestInit(t *testing.T) {
 				"token":     "123",
 				"front_uri": "http://moira.uri",
 			}
-			sender.Init(senderSettings, logger, location, "15:04") //nolint
+
+			err := sender.Init(senderSettings, logger, location, "15:04") //nolint
+			So(err, ShouldBeNil)
 			So(sender.frontURI, ShouldResemble, "http://moira.uri")
 			So(sender.session.Token, ShouldResemble, "Bot 123")
 			So(sender.logger, ShouldResemble, logger)
diff --git a/senders/mail/mail_test.go b/senders/mail/mail_test.go
index 3c303613..57cc27fb 100644
--- a/senders/mail/mail_test.go
+++ b/senders/mail/mail_test.go
@@ -1,33 +1,90 @@
 package mail
 
 import (
-	"fmt"
+	"errors"
 	"testing"
 
+	"github.com/go-playground/validator/v10"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
+const (
+	defaultMailFrom     = "test-mail-from"
+	defaultSMTPHost     = "test-smtp-host"
+	defaultSMTPPort     = 80
+	defaultSMTPHello    = "test-smtp-hello"
+	defaultInsecureTLS  = true
+	defaultFrontURI     = "test-front-uri"
+	defaultSMTPPass     = "test-smtp-pass"
+	defaultSMTPUser     = "test-smtp-user"
+	defaultTemplateFile = "test-template-file"
+)
+
 func TestFillSettings(t *testing.T) {
-	Convey("Empty map", t, func() {
+	Convey("Test fillSettings", t, func() {
 		sender := Sender{}
-		err := sender.fillSettings(map[string]interface{}{}, nil, nil, "")
-		So(err, ShouldResemble, fmt.Errorf("mail_from can't be empty"))
-		So(sender, ShouldResemble, Sender{})
-	})
 
-	Convey("Has From", t, func() {
-		sender := Sender{}
-		settings := map[string]interface{}{"mail_from": "123"}
-		Convey("No username", func() {
-			err := sender.fillSettings(settings, nil, nil, "")
-			So(err, ShouldBeNil)
-			So(sender, ShouldResemble, Sender{From: "123", Username: "123"})
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty mail_from", func() {
+			senderSettings := map[string]interface{}{
+				"smtp_host": defaultSMTPHost,
+				"smtp_port": defaultSMTPPort,
+			}
+
+			err := sender.fillSettings(senderSettings, nil, nil, "")
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
 		})
-		Convey("Has username", func() {
-			settings["smtp_user"] = "user"
-			err := sender.fillSettings(settings, nil, nil, "")
+
+		Convey("With empty smpt_host", func() {
+			senderSettings := map[string]interface{}{
+				"mail_from": defaultMailFrom,
+				"smtp_port": defaultSMTPPort,
+			}
+
+			err := sender.fillSettings(senderSettings, nil, nil, "")
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
+		})
+
+		Convey("With empty smpt_port", func() {
+			senderSettings := map[string]interface{}{
+				"mail_from": defaultMailFrom,
+				"smtp_host": defaultSMTPHost,
+			}
+
+			err := sender.fillSettings(senderSettings, nil, nil, "")
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
+		})
+
+		Convey("With full settings", func() {
+			senderSettings := map[string]interface{}{
+				"mail_from":     defaultMailFrom,
+				"smtp_host":     defaultSMTPHost,
+				"smtp_port":     defaultSMTPPort,
+				"smtp_hello":    defaultSMTPHello,
+				"insecure_tls":  defaultInsecureTLS,
+				"front_uri":     defaultFrontURI,
+				"smtp_user":     defaultSMTPUser,
+				"smtp_pass":     defaultSMTPPass,
+				"template_file": defaultTemplateFile,
+			}
+
+			err := sender.fillSettings(senderSettings, nil, nil, "")
 			So(err, ShouldBeNil)
-			So(sender, ShouldResemble, Sender{From: "123", Username: "user"})
+			So(sender, ShouldResemble, Sender{
+				From:         defaultMailFrom,
+				SMTPHello:    defaultSMTPHello,
+				SMTPHost:     defaultSMTPHost,
+				SMTPPort:     80,
+				FrontURI:     defaultFrontURI,
+				InsecureTLS:  defaultInsecureTLS,
+				Username:     defaultSMTPUser,
+				Password:     defaultSMTPPass,
+				TemplateFile: defaultTemplateFile,
+			})
 		})
 	})
 }
diff --git a/senders/mattermost/sender_internal_test.go b/senders/mattermost/sender_internal_test.go
index 1ae3da0a..2b6bb7a6 100644
--- a/senders/mattermost/sender_internal_test.go
+++ b/senders/mattermost/sender_internal_test.go
@@ -21,7 +21,7 @@ func TestSendEvents(t *testing.T) {
 
 	Convey("Given configured sender", t, func() {
 		senderSettings := map[string]interface{}{ // redundant, but necessary config
-			"url":          "qwerty",
+			"url":          "https://mattermost.com/",
 			"api_token":    "qwerty",
 			"front_uri":    "qwerty",
 			"insecure_tls": true,
diff --git a/senders/mattermost/sender_manual_test.go b/senders/mattermost/sender_manual_test.go
index a93a627a..35e71665 100644
--- a/senders/mattermost/sender_manual_test.go
+++ b/senders/mattermost/sender_manual_test.go
@@ -18,7 +18,7 @@ func TestSender(t *testing.T) {
 	logger, _ := logging.ConfigureLog("stdout", "debug", "test", true)
 
 	const (
-		url       = "http://localhost:8065"
+		url       = "https://mattermost.com/"
 		apiToken  = "8pdo6yoiutgidgxs9qxhbo7w4h"
 		channelID = "3y6ab8rptfdr9m1hzskghpxwsc"
 	)
diff --git a/senders/mattermost/sender_test.go b/senders/mattermost/sender_test.go
index c47baa56..33318221 100644
--- a/senders/mattermost/sender_test.go
+++ b/senders/mattermost/sender_test.go
@@ -1,69 +1,79 @@
 package mattermost
 
 import (
+	"errors"
 	"testing"
 
+	"github.com/go-playground/validator/v10"
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
+const (
+	defaultURL         = "https://mattermost.com/"
+	defaultAPIToken    = "test-api-token"
+	defaultFrontURI    = "test-front-uri"
+	defaultInsecureTLS = true
+	defaultUseEmoji    = true
+	defaultEmoji       = "test-emoji"
+)
+
+var defaultEmojiMap = map[string]string{
+	"OK": ":dance_mops:",
+}
+
 func TestInit(t *testing.T) {
 	logger, _ := logging.ConfigureLog("stdout", "debug", "test", true)
+
 	Convey("Init tests", t, func() {
-		sender := &Sender{}
+		sender := Sender{}
 
-		Convey("No url", func() {
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty url", func() {
 			senderSettings := map[string]interface{}{
-				"api_token":    "qwerty",
-				"front_uri":    "qwerty",
-				"insecure_tls": true,
+				"api_token": defaultAPIToken,
+				"front_uri": defaultFrontURI,
 			}
+
 			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
 		})
 
-		Convey("Empty url", func() {
+		Convey("With empty api_token", func() {
 			senderSettings := map[string]interface{}{
-				"url":          "",
-				"api_token":    "qwerty",
-				"front_uri":    "qwerty",
-				"insecure_tls": true,
+				"url":       defaultURL,
+				"front_uri": defaultFrontURI,
 			}
-			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
-		})
 
-		Convey("No api_token", func() {
-			senderSettings := map[string]interface{}{"url": "qwerty", "front_uri": "qwerty"}
 			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
 		})
 
-		Convey("Empty api_token", func() {
-			senderSettings := map[string]interface{}{"url": "qwerty", "front_uri": "qwerty", "api_token": ""}
-			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
-		})
-
-		Convey("No front_uri", func() {
-			senderSettings := map[string]interface{}{"url": "qwerty", "api_token": "qwerty"}
-			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
-		})
+		Convey("With empty front_uri", func() {
+			senderSettings := map[string]interface{}{
+				"url":       defaultURL,
+				"api_token": defaultAPIToken,
+			}
 
-		Convey("Empty front_uri", func() {
-			senderSettings := map[string]interface{}{"url": "qwerty", "api_token": "qwerty", "front_uri": ""}
 			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
 		})
 
-		Convey("Full config", func() {
+		Convey("With full config", func() {
 			senderSettings := map[string]interface{}{
-				"url":          "qwerty",
-				"api_token":    "qwerty",
-				"front_uri":    "qwerty",
-				"insecure_tls": true,
+				"url":           defaultURL,
+				"api_token":     defaultAPIToken,
+				"front_uri":     defaultFrontURI,
+				"insecure_tls":  defaultInsecureTLS,
+				"use_emoji":     defaultUseEmoji,
+				"default_emoji": defaultEmoji,
+				"emoji_map":     defaultEmojiMap,
 			}
+
 			err := sender.Init(senderSettings, logger, nil, "")
 			So(err, ShouldBeNil)
 		})
diff --git a/senders/msteams/msteams_test.go b/senders/msteams/msteams_test.go
index faf61273..3fa71726 100644
--- a/senders/msteams/msteams_test.go
+++ b/senders/msteams/msteams_test.go
@@ -11,18 +11,28 @@ import (
 	"gopkg.in/h2non/gock.v1"
 )
 
+const (
+	defaultFrontURI  = "test-front-uri"
+	defaultMaxEvents = -1
+)
+
 func TestInit(t *testing.T) {
 	logger, _ := logging.ConfigureLog("stdout", "debug", "test", true)
+
 	Convey("Init tests", t, func() {
 		sender := Sender{}
+
 		senderSettings := map[string]interface{}{
-			"max_events": -1,
+			"max_events": defaultMaxEvents,
+			"front_uri":  defaultFrontURI,
 		}
+
 		Convey("Minimal settings", func() {
 			err := sender.Init(senderSettings, logger, nil, "")
 			So(err, ShouldResemble, nil)
 			So(sender, ShouldNotResemble, Sender{})
-			So(sender.maxEvents, ShouldResemble, -1)
+			So(sender.maxEvents, ShouldResemble, defaultMaxEvents)
+			So(sender.frontURI, ShouldResemble, defaultFrontURI)
 		})
 	})
 }
diff --git a/senders/opsgenie/init_test.go b/senders/opsgenie/init_test.go
index e3970283..5c8c952a 100644
--- a/senders/opsgenie/init_test.go
+++ b/senders/opsgenie/init_test.go
@@ -1,10 +1,11 @@
 package opsgenie
 
 import (
-	"fmt"
+	"errors"
 	"testing"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/moira-alert/moira"
 	"go.uber.org/mock/gomock"
 
@@ -25,9 +26,13 @@ func TestInit(t *testing.T) {
 			"s3": imageStore,
 		}}
 
-		Convey("Empty map", func() {
-			err := sender.Init(map[string]interface{}{}, logger, nil, "")
-			So(err, ShouldResemble, fmt.Errorf("cannot read the api_key from the sender settings"))
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty api_key", func() {
+			senderSettings := map[string]interface{}{}
+
+			err := sender.Init(senderSettings, logger, nil, "")
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{
 				ImageStores: map[string]moira.ImageStore{
 					"s3": imageStore,
@@ -35,7 +40,7 @@ func TestInit(t *testing.T) {
 			})
 		})
 
-		Convey("Has settings", func() {
+		Convey("With full settings", func() {
 			imageStore.EXPECT().IsEnabled().Return(true)
 			senderSettings := map[string]interface{}{
 				"api_key":     "testkey",
@@ -44,7 +49,6 @@ func TestInit(t *testing.T) {
 			}
 			sender.Init(senderSettings, logger, location, "15:04") //nolint
 			So(sender.apiKey, ShouldResemble, "testkey")
-			So(sender.frontURI, ShouldResemble, "http://moira.uri")
 			So(sender.logger, ShouldResemble, logger)
 			So(sender.location, ShouldResemble, location)
 		})
diff --git a/senders/opsgenie/send_test.go b/senders/opsgenie/send_test.go
index db81c15a..048184e4 100644
--- a/senders/opsgenie/send_test.go
+++ b/senders/opsgenie/send_test.go
@@ -146,7 +146,6 @@ func TestMakeCreateAlertRequest(t *testing.T) {
 	imageStore := mock_moira_alert.NewMockImageStore(mockCtrl)
 
 	sender := Sender{
-		frontURI:             "https://my-moira.com",
 		location:             location,
 		logger:               logger,
 		imageStoreConfigured: true,
diff --git a/senders/pagerduty/init_test.go b/senders/pagerduty/init_test.go
index df0ecbbe..7edd3223 100644
--- a/senders/pagerduty/init_test.go
+++ b/senders/pagerduty/init_test.go
@@ -37,6 +37,7 @@ func TestInit(t *testing.T) {
 			So(sender.imageStoreConfigured, ShouldResemble, true)
 			So(sender.imageStore, ShouldResemble, imageStore)
 		})
+
 		Convey("Wrong image_store name", func() {
 			senderSettings := map[string]interface{}{
 				"front_uri":   "http://moira.uri",
@@ -46,6 +47,7 @@ func TestInit(t *testing.T) {
 			So(sender.imageStoreConfigured, ShouldResemble, false)
 			So(sender.imageStore, ShouldResemble, nil)
 		})
+
 		Convey("image store not configured", func() {
 			imageStore.EXPECT().IsEnabled().Return(false)
 			senderSettings := map[string]interface{}{
diff --git a/senders/pushover/pushover_test.go b/senders/pushover/pushover_test.go
index a25b6ccd..1416501d 100644
--- a/senders/pushover/pushover_test.go
+++ b/senders/pushover/pushover_test.go
@@ -2,36 +2,51 @@ package pushover
 
 import (
 	"bytes"
-	"fmt"
+	"errors"
 	"testing"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	pushover_client "github.com/gregdel/pushover"
 	"github.com/moira-alert/moira"
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
-func TestSender_Init(t *testing.T) {
+func TestInit(t *testing.T) {
 	logger, _ := logging.ConfigureLog("stdout", "debug", "test", true)
+
+	validatorErr := validator.ValidationErrors{}
+
 	Convey("Empty map", t, func() {
 		sender := Sender{}
-		err := sender.Init(map[string]interface{}{}, logger, nil, "")
-		So(err, ShouldResemble, fmt.Errorf("can not read pushover api_token from config"))
+		senderSettings := map[string]interface{}{}
+
+		err := sender.Init(senderSettings, logger, nil, "")
+		So(errors.As(err, &validatorErr), ShouldBeTrue)
 		So(sender, ShouldResemble, Sender{})
 	})
 
 	Convey("Settings has api_token", t, func() {
 		sender := Sender{}
-		err := sender.Init(map[string]interface{}{"api_token": "123"}, logger, nil, "")
+		senderSettings := map[string]interface{}{
+			"api_token": "123",
+		}
+
+		err := sender.Init(senderSettings, logger, nil, "")
 		So(err, ShouldBeNil)
 		So(sender, ShouldResemble, Sender{apiToken: "123", client: pushover_client.New("123"), logger: logger})
 	})
 
 	Convey("Settings has all data", t, func() {
 		sender := Sender{}
+		senderSettings := map[string]interface{}{
+			"api_token": "123",
+			"front_uri": "321",
+		}
 		location, _ := time.LoadLocation("UTC")
-		err := sender.Init(map[string]interface{}{"api_token": "123", "front_uri": "321"}, logger, location, "")
+
+		err := sender.Init(senderSettings, logger, location, "")
 		So(err, ShouldBeNil)
 		So(sender, ShouldResemble, Sender{apiToken: "123", client: pushover_client.New("123"), frontURI: "321", logger: logger, location: location})
 	})
diff --git a/senders/script/script_test.go b/senders/script/script_test.go
index 0c2d5cb6..83d50063 100644
--- a/senders/script/script_test.go
+++ b/senders/script/script_test.go
@@ -1,9 +1,11 @@
 package script
 
 import (
+	"errors"
 	"fmt"
 	"testing"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/moira-alert/moira"
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
 	. "github.com/smartystreets/goconvey/convey"
@@ -50,9 +52,11 @@ func TestInit(t *testing.T) {
 		sender := Sender{}
 		settings := map[string]interface{}{}
 
+		validatorErr := validator.ValidationErrors{}
+
 		Convey("Empty exec", func() {
 			err := sender.Init(settings, logger, nil, "")
-			So(err, ShouldResemble, fmt.Errorf("file  not found"))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{})
 		})
 
diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go
index 43471896..37cd85ed 100644
--- a/senders/slack/slack_test.go
+++ b/senders/slack/slack_test.go
@@ -1,10 +1,12 @@
 package slack
 
 import (
+	"errors"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/moira-alert/moira"
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
 	. "github.com/smartystreets/goconvey/convey"
@@ -15,15 +17,12 @@ func TestInit(t *testing.T) {
 	Convey("Init tests", t, func() {
 		sender := Sender{}
 		senderSettings := map[string]interface{}{}
-		Convey("Empty map", func() {
-			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
-		})
 
-		Convey("has empty api_token", func() {
-			senderSettings["api_token"] = ""
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty api_token", func() {
 			err := sender.Init(senderSettings, logger, nil, "")
-			So(err, ShouldNotBeNil)
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 		})
 
 		Convey("has api_token", func() {
diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go
index 9179cb92..b5a0bbc4 100644
--- a/senders/telegram/init_test.go
+++ b/senders/telegram/init_test.go
@@ -7,11 +7,11 @@ import (
 	"testing"
 	"time"
 
-	mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert"
-	"go.uber.org/mock/gomock"
-
+	"github.com/go-playground/validator/v10"
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
+	mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert"
 	. "github.com/smartystreets/goconvey/convey"
+	"go.uber.org/mock/gomock"
 )
 
 func TestInit(t *testing.T) {
@@ -19,9 +19,12 @@ func TestInit(t *testing.T) {
 	location, _ := time.LoadLocation("UTC")
 	Convey("Init tests", t, func() {
 		sender := Sender{}
-		Convey("Empty map", func() {
+
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty api_token", func() {
 			err := sender.Init(map[string]interface{}{}, logger, nil, "")
-			So(err, ShouldResemble, fmt.Errorf("can not read telegram api_token from config"))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{})
 		})
 
diff --git a/senders/twilio/twilio_test.go b/senders/twilio/twilio_test.go
index 11b8715e..1cd1b775 100644
--- a/senders/twilio/twilio_test.go
+++ b/senders/twilio/twilio_test.go
@@ -1,11 +1,13 @@
 package twilio
 
 import (
+	"errors"
 	"fmt"
 	"testing"
 	"time"
 
 	twilio_client "github.com/carlosdp/twiliogo"
+	"github.com/go-playground/validator/v10"
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -16,9 +18,12 @@ func TestInit(t *testing.T) {
 		logger, _ := logging.ConfigureLog("stdout", "debug", "test", true)
 		location, _ := time.LoadLocation("UTC")
 		settings := map[string]interface{}{}
+
+		validatorErr := validator.ValidationErrors{}
+
 		Convey("no api asid", func() {
 			err := sender.Init(settings, logger, nil, "15:04")
-			So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_sid param from config", ""))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{})
 		})
 
@@ -26,7 +31,7 @@ func TestInit(t *testing.T) {
 
 		Convey("no api authtoken", func() {
 			err := sender.Init(settings, logger, nil, "15:04")
-			So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_authtoken param from config", ""))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{})
 		})
 
@@ -34,7 +39,7 @@ func TestInit(t *testing.T) {
 
 		Convey("no api fromphone", func() {
 			err := sender.Init(settings, logger, nil, "15:04")
-			So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_fromphone param from config", ""))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{})
 		})
 
@@ -42,7 +47,15 @@ func TestInit(t *testing.T) {
 
 		Convey("no api type", func() {
 			err := sender.Init(settings, logger, nil, "15:04")
-			So(err, ShouldResemble, fmt.Errorf("wrong twilio type: %s", ""))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
+			So(sender, ShouldResemble, Sender{})
+		})
+
+		settings["sender_type"] = "test"
+
+		Convey("with unknown api type", func() {
+			err := sender.Init(settings, logger, nil, "15:04")
+			So(err, ShouldResemble, fmt.Errorf("wrong twilio type: %s", "test"))
 			So(sender, ShouldResemble, Sender{})
 		})
 
diff --git a/senders/victorops/init_test.go b/senders/victorops/init_test.go
index 05c2111a..e34b6abf 100644
--- a/senders/victorops/init_test.go
+++ b/senders/victorops/init_test.go
@@ -1,10 +1,11 @@
 package victorops
 
 import (
-	"fmt"
+	"errors"
 	"testing"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/moira-alert/moira"
 	"github.com/moira-alert/moira/senders/victorops/api"
 	"go.uber.org/mock/gomock"
@@ -25,9 +26,12 @@ func TestInit(t *testing.T) {
 		sender := Sender{ImageStores: map[string]moira.ImageStore{
 			"s3": imageStore,
 		}}
-		Convey("Empty map", func() {
+
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty routing url", func() {
 			err := sender.Init(map[string]interface{}{}, logger, nil, "")
-			So(err, ShouldResemble, fmt.Errorf("cannot read the routing url from the yaml config"))
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 			So(sender, ShouldResemble, Sender{
 				ImageStores: map[string]moira.ImageStore{
 					"s3": imageStore,
diff --git a/senders/webhook/webhook_test.go b/senders/webhook/webhook_test.go
index 07369f11..84505488 100644
--- a/senders/webhook/webhook_test.go
+++ b/senders/webhook/webhook_test.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"encoding/base64"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
@@ -12,6 +13,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/go-playground/validator/v10"
 	"github.com/moira-alert/moira"
 
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
@@ -38,12 +40,14 @@ var (
 
 func TestSender_Init(t *testing.T) {
 	Convey("Test Init function", t, func() {
-		Convey("With empty settings", func() {
+		validatorErr := validator.ValidationErrors{}
+
+		Convey("With empty url", func() {
 			settings := map[string]interface{}{}
 			sender := Sender{}
 
 			err := sender.Init(settings, logger, location, dateTimeFormat)
-			So(err, ShouldResemble, ErrMissingURL)
+			So(errors.As(err, &validatorErr), ShouldBeTrue)
 		})
 
 		Convey("With only url", func() {