Repository: moira-alert/moira
Pull request #1100 (2024-09-26 12:42:13)

feat(api): contact template validation
# Add contact template validation

Previously contact template validation in web config was on the frontend, I moved it to the backend and added validation of the templates themselves

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

Build and push Docker images with tag: feat-contact-validation.2024-09-27.372c4c8
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-contact-validation.2024-09-27.372c4c8
github-actions[bot]: <!-- add-pr-comment:add-pr-comment -->

Build and push Docker images with tag: feat-contact-validation.2024-09-27.372c4c8

Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d9e3e7f9..c8cfb51e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -5,8 +5,6 @@ on:
     branches: [ master ]
   pull_request:
     branches: [ master ]
-  schedule:
-    - cron: '33 20 * * 2'
 
 jobs:
   analyze:
diff --git a/api/controller/contact.go b/api/controller/contact.go
index 4874624b..e1a7573d 100644
--- a/api/controller/contact.go
+++ b/api/controller/contact.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
+	"regexp"
 	"time"
 
 	"github.com/go-graphite/carbonapi/date"
@@ -53,6 +54,7 @@ func GetContactById(database moira.Database, contactID string) (*dto.Contact, *a
 func CreateContact(
 	dataBase moira.Database,
 	auth *api.Authorization,
+	contactsTemplate []api.WebContact,
 	contact *dto.Contact,
 	userLogin,
 	teamID string,
@@ -74,6 +76,7 @@ func CreateContact(
 		Type:  contact.Type,
 		Value: contact.Value,
 	}
+
 	if contactData.ID == "" {
 		uuid4, err := uuid.NewV4()
 		if err != nil {
@@ -90,12 +93,18 @@ func CreateContact(
 		}
 	}
 
+	if err := validateContact(contactsTemplate, contactData); err != nil {
+		return api.ErrorInvalidRequest(err)
+	}
+
 	if err := dataBase.SaveContact(&contactData); err != nil {
 		return api.ErrorInternalServer(err)
 	}
+
 	contact.User = contactData.User
 	contact.ID = contactData.ID
 	contact.TeamID = contactData.Team
+
 	return nil
 }
 
@@ -103,6 +112,7 @@ func CreateContact(
 func UpdateContact(
 	dataBase moira.Database,
 	auth *api.Authorization,
+	contactsTemplate []api.WebContact,
 	contactDTO dto.Contact,
 	contactData moira.ContactData,
 ) (dto.Contact, *api.ErrorResponse) {
@@ -119,6 +129,10 @@ func UpdateContact(
 		contactData.Team = contactDTO.TeamID
 	}
 
+	if err := validateContact(contactsTemplate, contactData); err != nil {
+		return contactDTO, api.ErrorInvalidRequest(err)
+	}
+
 	if err := dataBase.SaveContact(&contactData); err != nil {
 		return contactDTO, api.ErrorInternalServer(err)
 	}
@@ -265,3 +279,19 @@ func isAllowedToUseContactType(auth *api.Authorization, userLogin string, contac
 
 	return isAllowedContactType || isAdmin || !isAuthEnabled
 }
+
+func validateContact(contactsTemplate []api.WebContact, contact moira.ContactData) error {
+	var validationPattern string
+	for _, contactTemplate := range contactsTemplate {
+		if contactTemplate.ContactType == contact.Type {
+			validationPattern = contactTemplate.ValidationRegex
+			break
+		}
+	}
+
+	if matched, err := regexp.MatchString(validationPattern, contact.Value); !matched || err != nil {
+		return fmt.Errorf("contact value doesn't match regex: '%s'", validationPattern)
+	}
+
+	return nil
+}
diff --git a/api/handler/contact.go b/api/handler/contact.go
index ba31d1f2..9d564746 100644
--- a/api/handler/contact.go
+++ b/api/handler/contact.go
@@ -101,8 +101,16 @@ func createNewContact(writer http.ResponseWriter, request *http.Request) {
 
 	userLogin := middleware.GetLogin(request)
 	auth := middleware.GetAuth(request)
-
-	if err := controller.CreateContact(database, auth, contact, userLogin, contact.TeamID); err != nil {
+	contactsTemplate := middleware.GetContactsTemplate(request)
+
+	if err := controller.CreateContact(
+		database,
+		auth,
+		contactsTemplate,
+		contact,
+		userLogin,
+		contact.TeamID,
+	); err != nil {
 		render.Render(writer, request, err) //nolint
 		return
 	}
@@ -155,8 +163,15 @@ func updateContact(writer http.ResponseWriter, request *http.Request) {
 	contactData := request.Context().Value(contactKey).(moira.ContactData)
 
 	auth := middleware.GetAuth(request)
-
-	contactDTO, err := controller.UpdateContact(database, auth, contactDTO, contactData)
+	contactsTemplate := middleware.GetContactsTemplate(request)
+
+	contactDTO, err := controller.UpdateContact(
+		database,
+		auth,
+		contactsTemplate,
+		contactDTO,
+		contactData,
+	)
 	if err != nil {
 		render.Render(writer, request, err) //nolint
 		return
diff --git a/api/handler/handler.go b/api/handler/handler.go
index 27647a8c..1ac3f953 100644
--- a/api/handler/handler.go
+++ b/api/handler/handler.go
@@ -38,6 +38,11 @@ func NewHandler(
 ) http.Handler {
 	database = db
 	searchIndex = index
+	var contactsTemplate []api.WebContact
+	if webConfig != nil {
+		contactsTemplate = webConfig.Contacts
+	}
+
 	router := chi.NewRouter()
 	router.Use(render.SetContentType(render.ContentTypeJSON))
 	router.Use(moiramiddle.UserContext)
@@ -111,7 +116,9 @@ func NewHandler(
 			router.Route("/subscription", subscription)
 			router.Route("/notification", notification)
 			router.Route("/teams", teams)
-			router.Route("/contact", func(router chi.Router) {
+			router.With(moiramiddle.ContactsTemplateContext(
+				contactsTemplate,
+			)).Route("/contact", func(router chi.Router) {
 				contact(router)
 				contactEvents(router)
 			})
diff --git a/api/handler/team_contact.go b/api/handler/team_contact.go
index 8d515afb..58f19aaa 100644
--- a/api/handler/team_contact.go
+++ b/api/handler/team_contact.go
@@ -40,8 +40,16 @@ func createNewTeamContact(writer http.ResponseWriter, request *http.Request) {
 
 	teamID := middleware.GetTeamID(request)
 	auth := middleware.GetAuth(request)
+	contactsTemplate := middleware.GetContactsTemplate(request)
 
-	if err := controller.CreateContact(database, auth, contact, "", teamID); err != nil {
+	if err := controller.CreateContact(
+		database,
+		auth,
+		contactsTemplate,
+		contact,
+		"",
+		teamID,
+	); err != nil {
 		render.Render(writer, request, err) //nolint:errcheck
 		return
 	}
diff --git a/api/middleware/context.go b/api/middleware/context.go
index 0d9e26f3..a53a3f1b 100644
--- a/api/middleware/context.go
+++ b/api/middleware/context.go
@@ -36,6 +36,16 @@ func SearchIndexContext(searcher moira.Searcher) func(next http.Handler) http.Ha
 	}
 }
 
+// ContactsTemplateContext sets to requests context contacts template.
+func ContactsTemplateContext(contactsTemplate []api.WebContact) func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+			ctx := context.WithValue(request.Context(), contactsTemplateKey, contactsTemplate)
+			next.ServeHTTP(writer, request.WithContext(ctx))
+		})
+	}
+}
+
 // UserContext get x-webauth-user header and sets it in request context, if header is empty sets empty string.
 func UserContext(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go
index 80d65066..d3df8e40 100644
--- a/api/middleware/middleware.go
+++ b/api/middleware/middleware.go
@@ -20,6 +20,7 @@ func (key ContextKey) String() string {
 var (
 	databaseKey          ContextKey = "database"
 	searcherKey          ContextKey = "searcher"
+	contactsTemplateKey  ContextKey = "contactsTemplate"
 	triggerIDKey         ContextKey = "triggerID"
 	clustersMetricTTLKey ContextKey = "clustersMetricTTL"
 	populateKey          ContextKey = "populated"
@@ -49,6 +50,11 @@ func GetDatabase(request *http.Request) moira.Database {
 	return request.Context().Value(databaseKey).(moira.Database)
 }
 
+// GetContactsTemplate gets contacts template from request context.
+func GetContactsTemplate(request *http.Request) []api.WebContact {
+	return request.Context().Value(contactsTemplateKey).([]api.WebContact)
+}
+
 // GetLogin gets user login string from request context, which was sets in UserContext middleware.
 func GetLogin(request *http.Request) string {
 	if request.Context() != nil && request.Context().Value(loginKey) != nil {
diff --git a/cmd/api/config.go b/cmd/api/config.go
index 4647bb3f..75fa2cee 100644
--- a/cmd/api/config.go
+++ b/cmd/api/config.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"fmt"
+	"regexp"
 	"time"
 
 	"github.com/moira-alert/moira"
@@ -136,6 +138,21 @@ func (auth *authorization) toApiConfig(webConfig *webConfig) api.Authorization {
 	}
 }
 
+func (config *webConfig) validate() error {
+	for _, contactTemplate := range config.ContactsTemplate {
+		validationRegex := contactTemplate.ValidationRegex
+		if validationRegex == "" {
+			continue
+		}
+
+		if _, err := regexp.Compile(validationRegex); err != nil {
+			return fmt.Errorf("contact template regex error '%s': %w", validationRegex, err)
+		}
+	}
+
+	return nil
+}
+
 func (config *webConfig) getSettings(isRemoteEnabled bool, remotes cmd.RemotesConfig) *api.WebConfig {
 	webContacts := make([]api.WebContact, 0, len(config.ContactsTemplate))
 
diff --git a/cmd/api/main.go b/cmd/api/main.go
index 9f87d2fd..6a9f78e7 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -58,6 +58,11 @@ func main() {
 		os.Exit(1)
 	}
 
+	if err = applicationConfig.Web.validate(); err != nil {
+		fmt.Fprintf(os.Stderr, "Can not configure web config: %s\n", err.Error())
+		os.Exit(1)
+	}
+
 	apiConfig := applicationConfig.API.getSettings(
 		applicationConfig.ClustersMetricTTL(),
 		applicationConfig.Web.getFeatureFlags(),
diff --git a/senders/telegram/init.go b/senders/telegram/init.go
index 4659c1f6..aabd2a4f 100644
--- a/senders/telegram/init.go
+++ b/senders/telegram/init.go
@@ -80,8 +80,9 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca
 
 	sender.logger = logger
 	sender.bot, err = telebot.NewBot(telebot.Settings{
-		Token:  cfg.APIToken,
-		Poller: &telebot.LongPoller{Timeout: pollerTimeout},
+		Token:   cfg.APIToken,
+		Poller:  &telebot.LongPoller{Timeout: pollerTimeout},
+		OnError: sender.customOnErrorFunc,
 	})
 	if err != nil {
 		return sender.removeTokenFromError(err)
@@ -123,3 +124,13 @@ func (sender *Sender) runTelebot(contactType string) {
 func telegramLockKey(contactType string) string {
 	return telegramLockPrefix + contactType
 }
+
+const errorInsideTelebotMsg = "Error inside telebot"
+
+func (sender *Sender) customOnErrorFunc(err error, _ telebot.Context) {
+	err = sender.removeTokenFromError(err)
+
+	sender.logger.Warning().
+		Error(err).
+		Msg(errorInsideTelebotMsg)
+}


Test patch:
diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go
index 8c398b52..c0eb92cc 100644
--- a/api/controller/contact_test.go
+++ b/api/controller/contact_test.go
@@ -127,6 +127,13 @@ func TestCreateContact(t *testing.T) {
 		},
 	}
 
+	contactsTemplate := []api.WebContact{
+		{
+			ContactType:     contactType,
+			ValidationRegex: "@mail.com",
+		},
+	}
+
 	Convey("Create for user", t, func() {
 		Convey("Success", func() {
 			contact := &dto.Contact{
@@ -134,7 +141,7 @@ func TestCreateContact(t *testing.T) {
 				Type:  contactType,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, userLogin)
 		})
@@ -153,7 +160,7 @@ func TestCreateContact(t *testing.T) {
 			}
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil)
 			dataBase.EXPECT().SaveContact(&expectedContact).Return(nil)
-			err := CreateContact(dataBase, auth, &contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, &contact, userLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, userLogin)
 			So(contact.ID, ShouldResemble, contact.ID)
@@ -166,7 +173,7 @@ func TestCreateContact(t *testing.T) {
 				Type:  contactType,
 			}
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, nil)
-			err := CreateContact(dataBase, auth, contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("contact with this ID already exists")))
 		})
 
@@ -178,10 +185,34 @@ func TestCreateContact(t *testing.T) {
 			}
 			err := fmt.Errorf("oooops! Can not write contact")
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, err)
-			expected := CreateContact(dataBase, auth, contact, userLogin, "")
+			expected := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(expected, ShouldResemble, api.ErrorInternalServer(err))
 		})
 
+		contactsTemplate = []api.WebContact{
+			{
+				ContactType:     contactType,
+				ValidationRegex: "@yandex.ru",
+			},
+		}
+
+		Convey("Error invalid contact value", func() {
+			contact := &dto.Contact{
+				Value: contactValue,
+				Type:  contactType,
+			}
+			expectedErr := api.ErrorInvalidRequest(fmt.Errorf("contact value doesn't match regex: '%s'", "@yandex.ru"))
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
+			So(err, ShouldResemble, expectedErr)
+		})
+
+		contactsTemplate = []api.WebContact{
+			{
+				ContactType:     contactType,
+				ValidationRegex: "@mail.com",
+			},
+		}
+
 		Convey("Error create now allowed contact", func() {
 			contact := &dto.Contact{
 				ID:    uuid.Must(uuid.NewV4()).String(),
@@ -189,7 +220,7 @@ func TestCreateContact(t *testing.T) {
 				Type:  notAllowedContactType,
 			}
 			expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType)
-			err := CreateContact(dataBase, auth, contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(err, ShouldResemble, expectedErr)
 		})
 
@@ -215,7 +246,7 @@ func TestCreateContact(t *testing.T) {
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil)
 			dataBase.EXPECT().SaveContact(&expectedContact).Return(nil)
 
-			err := CreateContact(dataBase, auth, contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(err, ShouldBeNil)
 		})
 
@@ -226,7 +257,7 @@ func TestCreateContact(t *testing.T) {
 			}
 			err := fmt.Errorf("oooops! Can not write contact")
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(err)
-			expected := CreateContact(dataBase, auth, contact, userLogin, "")
+			expected := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(expected, ShouldResemble, &api.ErrorResponse{
 				ErrorText:      err.Error(),
 				HTTPStatusCode: http.StatusInternalServerError,
@@ -243,7 +274,7 @@ func TestCreateContact(t *testing.T) {
 				Type:  contactType,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, "", teamID)
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
 			So(err, ShouldBeNil)
 			So(contact.TeamID, ShouldResemble, teamID)
 		})
@@ -262,7 +293,7 @@ func TestCreateContact(t *testing.T) {
 			}
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil)
 			dataBase.EXPECT().SaveContact(&expectedContact).Return(nil)
-			err := CreateContact(dataBase, auth, &contact, "", teamID)
+			err := CreateContact(dataBase, auth, contactsTemplate, &contact, "", teamID)
 			So(err, ShouldBeNil)
 			So(contact.TeamID, ShouldResemble, teamID)
 			So(contact.ID, ShouldResemble, contact.ID)
@@ -284,7 +315,7 @@ func TestCreateContact(t *testing.T) {
 			}
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil)
 			dataBase.EXPECT().SaveContact(&expectedContact).Return(nil)
-			err := CreateContact(dataBase, auth, &contact, "", teamID)
+			err := CreateContact(dataBase, auth, contactsTemplate, &contact, "", teamID)
 			So(err, ShouldBeNil)
 			So(contact.TeamID, ShouldResemble, teamID)
 			So(contact.Name, ShouldResemble, expectedContact.Name)
@@ -297,7 +328,7 @@ func TestCreateContact(t *testing.T) {
 				Type:  contactType,
 			}
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, nil)
-			err := CreateContact(dataBase, auth, contact, "", teamID)
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
 			So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("contact with this ID already exists")))
 		})
 
@@ -309,7 +340,7 @@ func TestCreateContact(t *testing.T) {
 			}
 			err := fmt.Errorf("oooops! Can not write contact")
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, err)
-			expected := CreateContact(dataBase, auth, contact, "", teamID)
+			expected := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
 			So(expected, ShouldResemble, api.ErrorInternalServer(err))
 		})
 
@@ -320,7 +351,7 @@ func TestCreateContact(t *testing.T) {
 				Type:  notAllowedContactType,
 			}
 			expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType)
-			err := CreateContact(dataBase, auth, contact, "", teamID)
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
 			So(err, ShouldResemble, expectedErr)
 		})
 
@@ -346,7 +377,7 @@ func TestCreateContact(t *testing.T) {
 			dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil)
 			dataBase.EXPECT().SaveContact(&expectedContact).Return(nil)
 
-			err := CreateContact(dataBase, auth, contact, "", teamID)
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
 			So(err, ShouldBeNil)
 		})
 
@@ -357,7 +388,7 @@ func TestCreateContact(t *testing.T) {
 			}
 			err := fmt.Errorf("oooops! Can not write contact")
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(err)
-			expected := CreateContact(dataBase, auth, contact, "", teamID)
+			expected := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
 			So(expected, ShouldResemble, &api.ErrorResponse{
 				ErrorText:      err.Error(),
 				HTTPStatusCode: http.StatusInternalServerError,
@@ -389,6 +420,8 @@ func TestAdminsCreatesContact(t *testing.T) {
 		},
 	}
 
+	contactsTemplate := []api.WebContact{}
+
 	Convey("Create for user", t, func() {
 		Convey("The same user", func() {
 			contact := &dto.Contact{
@@ -397,7 +430,7 @@ func TestAdminsCreatesContact(t *testing.T) {
 				User:  userLogin,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, userLogin)
 		})
@@ -409,7 +442,7 @@ func TestAdminsCreatesContact(t *testing.T) {
 				User:  adminLogin,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, adminLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, adminLogin)
 		})
@@ -421,7 +454,7 @@ func TestAdminsCreatesContact(t *testing.T) {
 				User:  adminLogin,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, userLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, userLogin)
 		})
@@ -433,7 +466,7 @@ func TestAdminsCreatesContact(t *testing.T) {
 				User:  userLogin,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, adminLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, userLogin)
 		})
@@ -445,7 +478,7 @@ func TestAdminsCreatesContact(t *testing.T) {
 				User:  userLogin,
 			}
 			dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil)
-			err := CreateContact(dataBase, auth, contact, adminLogin, "")
+			err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "")
 			So(err, ShouldBeNil)
 			So(contact.User, ShouldResemble, userLogin)
 		})
@@ -472,6 +505,13 @@ func TestUpdateContact(t *testing.T) {
 		},
 	}
 
+	contactsTemplate := []api.WebContact{
+		{
+			ContactType:     contactType,
+			ValidationRegex: "@mail.com",
+		},
+	}
+
 	Convey("User update", t, func() {
 		Convey("Success", func() {
 			contactDTO := dto.Contact{
@@ -488,7 +528,7 @@ func TestUpdateContact(t *testing.T) {
 				User:  userLogin,
 			}
 			dataBase.EXPECT().SaveContact(&contact).Return(nil)
-			expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
 			So(err, ShouldBeNil)
 			So(expectedContact.User, ShouldResemble, userLogin)
 			So(expectedContact.ID, ShouldResemble, contactID)
@@ -512,7 +552,7 @@ func TestUpdateContact(t *testing.T) {
 				User:  newUser,
 			}
 			dataBase.EXPECT().SaveContact(&contact).Return(nil)
-			expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
 			So(err, ShouldBeNil)
 			So(expectedContact.User, ShouldResemble, newUser)
 			So(expectedContact.ID, ShouldResemble, contactID)
@@ -526,13 +566,41 @@ func TestUpdateContact(t *testing.T) {
 			}
 			expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType)
 			contactID := uuid.Must(uuid.NewV4()).String()
-			expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
 			So(err, ShouldResemble, expectedErr)
 			So(expectedContact.User, ShouldResemble, contactDTO.User)
 			So(expectedContact.ID, ShouldResemble, contactDTO.ID)
 			So(expectedContact.Name, ShouldResemble, contactDTO.Name)
 		})
 
+		contactsTemplate = []api.WebContact{
+			{
+				ContactType:     contactType,
+				ValidationRegex: "@yandex.ru",
+			},
+		}
+
+		Convey("Error invalid contact value", func() {
+			contactDTO := dto.Contact{
+				Value: contactValue,
+				Type:  contactType,
+			}
+			expectedErr := api.ErrorInvalidRequest(fmt.Errorf("contact value doesn't match regex: '%s'", "@yandex.ru"))
+			contactID := uuid.Must(uuid.NewV4()).String()
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
+			So(err, ShouldResemble, expectedErr)
+			So(expectedContact.User, ShouldResemble, contactDTO.User)
+			So(expectedContact.ID, ShouldResemble, contactDTO.ID)
+			So(expectedContact.Name, ShouldResemble, contactDTO.Name)
+		})
+
+		contactsTemplate = []api.WebContact{
+			{
+				ContactType:     contactType,
+				ValidationRegex: "@mail.com",
+			},
+		}
+
 		Convey("Successfully update not allowed contact with disabled auth", func() {
 			auth.Enabled = false
 			defer func() {
@@ -554,7 +622,7 @@ func TestUpdateContact(t *testing.T) {
 			}
 
 			dataBase.EXPECT().SaveContact(&contact).Return(nil)
-			expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
 			So(err, ShouldBeNil)
 			So(expectedContact.User, ShouldResemble, userLogin)
 			So(expectedContact.ID, ShouldResemble, contactID)
@@ -575,7 +643,7 @@ func TestUpdateContact(t *testing.T) {
 			}
 			err := fmt.Errorf("oooops")
 			dataBase.EXPECT().SaveContact(&contact).Return(err)
-			expectedContact, actual := UpdateContact(dataBase, auth, contactDTO, contact)
+			expectedContact, actual := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, contact)
 			So(actual, ShouldResemble, api.ErrorInternalServer(err))
 			So(expectedContact.User, ShouldResemble, contactDTO.User)
 			So(expectedContact.ID, ShouldResemble, contactDTO.ID)
@@ -596,7 +664,7 @@ func TestUpdateContact(t *testing.T) {
 				Team:  teamID,
 			}
 			dataBase.EXPECT().SaveContact(&contact).Return(nil)
-			expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID})
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, Team: teamID})
 			So(err, ShouldBeNil)
 			So(expectedContact.TeamID, ShouldResemble, teamID)
 			So(expectedContact.ID, ShouldResemble, contactID)
@@ -617,7 +685,7 @@ func TestUpdateContact(t *testing.T) {
 				Team:  newTeam,
 			}
 			dataBase.EXPECT().SaveContact(&contact).Return(nil)
-			expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID})
+			expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, Team: teamID})
 			So(err, ShouldBeNil)
 			So(expectedContact.TeamID, ShouldResemble, newTeam)
 			So(expectedContact.ID, ShouldResemble, contactID)
@@ -637,7 +705,7 @@ func TestUpdateContact(t *testing.T) {
 			}
 			err := fmt.Errorf("oooops")
 			dataBase.EXPECT().SaveContact(&contact).Return(err)
-			expectedContact, actual := UpdateContact(dataBase, auth, contactDTO, contact)
+			expectedContact, actual := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, contact)
 			So(actual, ShouldResemble, api.ErrorInternalServer(err))
 			So(expectedContact.TeamID, ShouldResemble, contactDTO.TeamID)
 			So(expectedContact.ID, ShouldResemble, contactDTO.ID)
@@ -965,3 +1033,50 @@ func Test_isContactExists(t *testing.T) {
 		})
 	})
 }
+
+func TestValidateContact(t *testing.T) {
+	const (
+		contactType  = "phone"
+		contactValue = "+79998887766"
+	)
+
+	Convey("Test validateContact", t, func() {
+		contact := moira.ContactData{
+			Type:  contactType,
+			Value: contactValue,
+		}
+
+		Convey("With empty contactsTemplate", func() {
+			contactsTemplate := []api.WebContact{}
+
+			err := validateContact(contactsTemplate, contact)
+			So(err, ShouldBeNil)
+		})
+
+		Convey("With not matched regex pattern", func() {
+			contactsTemplate := []api.WebContact{
+				{
+					ContactType:     contactType,
+					ValidationRegex: "^9\\d{9}$",
+				},
+			}
+
+			notMatchedErr := fmt.Errorf("contact value doesn't match regex: '%s'", "^9\\d{9}$")
+
+			err := validateContact(contactsTemplate, contact)
+			So(err, ShouldResemble, notMatchedErr)
+		})
+
+		Convey("With matched regex pattern", func() {
+			contactsTemplate := []api.WebContact{
+				{
+					ContactType:     contactType,
+					ValidationRegex: `^\+79\d{9}$`,
+				},
+			}
+
+			err := validateContact(contactsTemplate, contact)
+			So(err, ShouldBeNil)
+		})
+	})
+}
diff --git a/api/handler/contact_test.go b/api/handler/contact_test.go
index cc2ebc02..4b64b4fd 100644
--- a/api/handler/contact_test.go
+++ b/api/handler/contact_test.go
@@ -20,13 +20,14 @@ import (
 )
 
 const (
-	ContactIDKey   = "contactID"
-	ContactKey     = "contact"
-	AuthKey        = "auth"
-	LoginKey       = "login"
-	defaultContact = "testContact"
-	defaultLogin   = "testLogin"
-	defaultTeamID  = "testTeamID"
+	testContactIDKey        = "contactID"
+	testContactKey          = "contact"
+	testAuthKey             = "auth"
+	testLoginKey            = "login"
+	testContactsTemplateKey = "contactsTemplate"
+	defaultContact          = "testContact"
+	defaultLogin            = "testLogin"
+	defaultTeamID           = "testTeamID"
 )
 
 func TestGetAllContacts(t *testing.T) {
@@ -136,8 +137,8 @@ func TestGetContactById(t *testing.T) {
 			}
 
 			testRequest := httptest.NewRequest(http.MethodGet, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ID: contactID}))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ID: contactID}))
 
 			getContactById(responseWriter, testRequest)
 
@@ -165,8 +166,8 @@ func TestGetContactById(t *testing.T) {
 			}
 
 			testRequest := httptest.NewRequest(http.MethodGet, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ID: contactID}))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ID: contactID}))
 
 			getContactById(responseWriter, testRequest)
 
@@ -203,6 +204,13 @@ func TestCreateNewContact(t *testing.T) {
 			},
 		}
 
+		contactsTemplate := []api.WebContact{
+			{
+				ContactType:     "mail",
+				ValidationRegex: "@skbkontur.ru",
+			},
+		}
+
 		newContactDto := &dto.Contact{
 			ID:     defaultContact,
 			Name:   "Mail Alerts",
@@ -228,8 +236,9 @@ func TestCreateNewContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
 			testRequest.Header.Add("content-type", "application/json")
 
 			createNewContact(responseWriter, testRequest)
@@ -260,8 +269,9 @@ func TestCreateNewContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
 			testRequest.Header.Add("content-type", "application/json")
 
 			createNewContact(responseWriter, testRequest)
@@ -301,8 +311,9 @@ func TestCreateNewContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
 			testRequest.Header.Add("content-type", "application/json")
 
 			createNewContact(responseWriter, testRequest)
@@ -332,8 +343,9 @@ func TestCreateNewContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
 			testRequest.Header.Add("content-type", "application/json")
 
 			createNewContact(responseWriter, testRequest)
@@ -351,6 +363,52 @@ func TestCreateNewContact(t *testing.T) {
 			So(response.StatusCode, ShouldEqual, http.StatusInternalServerError)
 		})
 
+		Convey("Invalid request when trying to create a new contact with invalid value", func() {
+			expected := &api.ErrorResponse{
+				StatusText: "Invalid request",
+				ErrorText:  "contact value doesn't match regex: '@yandex.ru'",
+			}
+			jsonContact, err := json.Marshal(newContactDto)
+			So(err, ShouldBeNil)
+
+			contactsTemplate = []api.WebContact{
+				{
+					ContactType:     "mail",
+					ValidationRegex: "@yandex.ru",
+				},
+			}
+
+			mockDb.EXPECT().GetContact(newContactDto.ID).Return(moira.ContactData{}, db.ErrNil).Times(1)
+			database = mockDb
+
+			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
+			testRequest.Header.Add("content-type", "application/json")
+
+			createNewContact(responseWriter, testRequest)
+
+			response := responseWriter.Result()
+			defer response.Body.Close()
+			contentBytes, err := io.ReadAll(response.Body)
+			So(err, ShouldBeNil)
+			contents := string(contentBytes)
+			actual := &api.ErrorResponse{}
+			err = json.Unmarshal([]byte(contents), actual)
+			So(err, ShouldBeNil)
+
+			So(actual, ShouldResemble, expected)
+			So(response.StatusCode, ShouldEqual, http.StatusBadRequest)
+		})
+
+		contactsTemplate = []api.WebContact{
+			{
+				ContactType:     "mail",
+				ValidationRegex: "@skbkontur.ru",
+			},
+		}
+
 		Convey("Trying to create a contact when both userLogin and teamID specified", func() {
 			newContactDto.TeamID = defaultTeamID
 			defer func() {
@@ -365,8 +423,9 @@ func TestCreateNewContact(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
 			testRequest.Header.Add("content-type", "application/json")
 
 			createNewContact(responseWriter, testRequest)
@@ -405,6 +464,13 @@ func TestUpdateContact(t *testing.T) {
 			TeamID: "",
 		}
 
+		contactsTemplate := []api.WebContact{
+			{
+				ContactType:     "mail",
+				ValidationRegex: "@skbkontur.ru",
+			},
+		}
+
 		Convey("Successful contact updated", func() {
 			jsonContact, err := json.Marshal(updatedContactDto)
 			So(err, ShouldBeNil)
@@ -419,15 +485,15 @@ func TestUpdateContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact))
-
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Name:  updatedContactDto.Name,
 				Type:  updatedContactDto.Type,
 				Value: updatedContactDto.Value,
 				User:  updatedContactDto.User,
 			}))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{
 				AllowedContactTypes: map[string]struct{}{
 					updatedContactDto.Type: {},
 				},
@@ -460,15 +526,15 @@ func TestUpdateContact(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact))
-
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Name:  updatedContactDto.Name,
 				Type:  updatedContactDto.Type,
 				Value: updatedContactDto.Value,
 				User:  updatedContactDto.User,
 			}))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{
 				AllowedContactTypes: map[string]struct{}{
 					updatedContactDto.Type: {},
 				},
@@ -495,6 +561,60 @@ func TestUpdateContact(t *testing.T) {
 			So(response.StatusCode, ShouldEqual, http.StatusBadRequest)
 		})
 
+		Convey("Invalid request when trying to update contact with invalid value", func() {
+			expected := &api.ErrorResponse{
+				StatusText: "Invalid request",
+				ErrorText:  "contact value doesn't match regex: '@yandex.ru'",
+			}
+			jsonContact, err := json.Marshal(updatedContactDto)
+			So(err, ShouldBeNil)
+
+			contactsTemplate = []api.WebContact{
+				{
+					ContactType:     "mail",
+					ValidationRegex: "@yandex.ru",
+				},
+			}
+
+			testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
+				ID:    contactID,
+				Name:  updatedContactDto.Name,
+				Type:  updatedContactDto.Type,
+				Value: updatedContactDto.Value,
+				User:  updatedContactDto.User,
+			}))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{
+				AllowedContactTypes: map[string]struct{}{
+					updatedContactDto.Type: {},
+				},
+			}))
+
+			testRequest.Header.Add("content-type", "application/json")
+
+			updateContact(responseWriter, testRequest)
+
+			response := responseWriter.Result()
+			defer response.Body.Close()
+			contentBytes, err := io.ReadAll(response.Body)
+			So(err, ShouldBeNil)
+			contents := string(contentBytes)
+			actual := &api.ErrorResponse{}
+			err = json.Unmarshal([]byte(contents), actual)
+			So(err, ShouldBeNil)
+
+			So(actual, ShouldResemble, expected)
+			So(response.StatusCode, ShouldEqual, http.StatusBadRequest)
+		})
+
+		contactsTemplate = []api.WebContact{
+			{
+				ContactType:     "mail",
+				ValidationRegex: "@skbkontur.ru",
+			},
+		}
+
 		Convey("Internal error when trying to update contact", func() {
 			expected := &api.ErrorResponse{
 				StatusText: "Internal Server Error",
@@ -513,13 +633,14 @@ func TestUpdateContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  updatedContactDto.Type,
 				Value: updatedContactDto.Value,
 				User:  updatedContactDto.User,
 			}))
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{
 				AllowedContactTypes: map[string]struct{}{
 					updatedContactDto.Type: {},
 				},
@@ -561,7 +682,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -587,7 +708,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -614,7 +735,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -642,7 +763,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -679,7 +800,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -718,7 +839,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -762,7 +883,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -797,7 +918,7 @@ func TestRemoveContact(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{
 				ID:    contactID,
 				Type:  "mail",
 				Value: "moira@skbkontur.ru",
@@ -837,7 +958,7 @@ func TestSendTestContactNotification(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPost, "/contact/"+contactID+"/test", nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID))
 
 			sendTestContactNotification(responseWriter, testRequest)
 
@@ -861,7 +982,7 @@ func TestSendTestContactNotification(t *testing.T) {
 			database = mockDb
 
 			testRequest := httptest.NewRequest(http.MethodPost, "/contact/"+contactID+"/test", nil)
-			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID))
+			testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID))
 
 			sendTestContactNotification(responseWriter, testRequest)
 
diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go
index 4777e70e..c55a67d6 100644
--- a/cmd/api/config_test.go
+++ b/cmd/api/config_test.go
@@ -248,3 +248,38 @@ func Test_webConfig_getSettings(t *testing.T) {
 		})
 	})
 }
+
+func Test_webConfig_validate(t *testing.T) {
+	Convey("With empty web config", t, func() {
+		config := webConfig{}
+
+		err := config.validate()
+		So(err, ShouldBeNil)
+	})
+
+	Convey("With invalid contact template pattern", t, func() {
+		config := webConfig{
+			ContactsTemplate: []webContact{
+				{
+					ValidationRegex: "**",
+				},
+			},
+		}
+
+		err := config.validate()
+		So(err, ShouldNotBeNil)
+	})
+
+	Convey("With valid contact template pattern", t, func() {
+		config := webConfig{
+			ContactsTemplate: []webContact{
+				{
+					ValidationRegex: ".*",
+				},
+			},
+		}
+
+		err := config.validate()
+		So(err, ShouldBeNil)
+	})
+}
diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go
index f55cdfcd..9179cb92 100644
--- a/senders/telegram/init_test.go
+++ b/senders/telegram/init_test.go
@@ -1,10 +1,15 @@
 package telegram
 
 import (
+	"errors"
 	"fmt"
+	"strings"
 	"testing"
 	"time"
 
+	mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert"
+	"go.uber.org/mock/gomock"
+
 	logging "github.com/moira-alert/moira/logging/zerolog_adapter"
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -31,3 +36,26 @@ func TestInit(t *testing.T) {
 		})
 	})
 }
+
+func Test_customOnErrorFunc(t *testing.T) {
+	Convey("test customOnErrorFunc hides credential and logs", t, func() {
+		mockCtrl := gomock.NewController(t)
+		defer mockCtrl.Finish()
+
+		logger := mock_moira_alert.NewMockLogger(mockCtrl)
+		eventsBuilder := mock_moira_alert.NewMockEventBuilder(mockCtrl)
+
+		sender := Sender{
+			logger:   logger,
+			apiToken: "1111111111:SecretTokenabc_987654321hellokonturmoira",
+		}
+
+		err := fmt.Errorf("https://some.api.of.telegram/bot%s/update failed to update", sender.apiToken)
+
+		logger.EXPECT().Warning().Return(eventsBuilder).AnyTimes()
+		eventsBuilder.EXPECT().Error(errors.New(strings.ReplaceAll(err.Error(), sender.apiToken, hidden))).Return(eventsBuilder)
+		eventsBuilder.EXPECT().Msg(errorInsideTelebotMsg)
+
+		sender.customOnErrorFunc(err, nil)
+	})
+}