diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d9e3e7f90..c8cfb51ea 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 4874624be..e1a7573de 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/controller/contact_test.go b/api/controller/contact_test.go index 8c398b528..c0eb92cc8 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.go b/api/handler/contact.go index ba31d1f2d..9d5647464 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/contact_test.go b/api/handler/contact_test.go index cc2ebc028..4b64b4fde 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/api/handler/handler.go b/api/handler/handler.go index 27647a8c3..1ac3f953d 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 8d515afba..58f19aaaa 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 0d9e26f31..a53a3f1b8 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 80d65066d..d3df8e40d 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 4647bb3f1..75fa2ceea 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/config_test.go b/cmd/api/config_test.go index 4777e70ea..c55a67d68 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/cmd/api/main.go b/cmd/api/main.go index 9f87d2fd9..6a9f78e7d 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/go.mod b/go.mod index 7e02d5d77..b309808bd 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 @@ -176,12 +177,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 ea5e36f2f..a013bebd9 100644 --- a/go.sum +++ b/go.sum @@ -279,9 +279,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= @@ -523,6 +527,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 c0432ca21..eab4e915e 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/helpers_test.go b/helpers_test.go index d9c3ca4ee..4b764d6a6 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.go b/senders/discord/init.go index e991d089f..926746244 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/discord/init_test.go b/senders/discord/init_test.go index 73c2609af..11043df99 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.go b/senders/mail/mail.go index 39ce73344..10406e043 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/mail/mail_test.go b/senders/mail/mail_test.go index 3c3036130..57cc27fb3 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.go b/senders/mattermost/sender.go index 36cbabb37..1b0f85c4a 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/mattermost/sender_internal_test.go b/senders/mattermost/sender_internal_test.go index 1ae3da0a2..2b6bb7a64 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 a93a627a9..35e71665f 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 c47baa562..333182219 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.go b/senders/msteams/msteams.go index 4bfd282f8..8e7e03bcc 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/msteams/msteams_test.go b/senders/msteams/msteams_test.go index faf612731..3fa71726a 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.go b/senders/opsgenie/init.go index 83c205165..72913a760 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/opsgenie/init_test.go b/senders/opsgenie/init_test.go index e39702834..5c8c952a6 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 db81c15a2..048184e44 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.go b/senders/pagerduty/init.go index 0e58afc2c..cbb36c5b6 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/pagerduty/init_test.go b/senders/pagerduty/init_test.go index df0ecbbe7..7edd32231 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.go b/senders/pushover/pushover.go index d8f0c4b69..ecd88d1c4 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/pushover/pushover_test.go b/senders/pushover/pushover_test.go index a25b6ccdd..1416501d0 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.go b/senders/script/script.go index c78e37dde..d7b7351d1 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/script/script_test.go b/senders/script/script_test.go index 0c2d5cb67..83d500636 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.go b/senders/slack/slack.go index 28976c6c6..d1a5f4d2d 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/slack/slack_test.go b/senders/slack/slack_test.go index 43471896f..37cd85eda 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.go b/senders/telegram/init.go index 4659c1f69..f49332c94 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{} @@ -80,8 +81,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 +125,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) +} diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go index f55cdfcd0..b5a0bbc4e 100644 --- a/senders/telegram/init_test.go +++ b/senders/telegram/init_test.go @@ -1,12 +1,17 @@ package telegram import ( + "errors" "fmt" + "strings" "testing" "time" + "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) { @@ -14,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{}) }) @@ -31,3 +39,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) + }) +} diff --git a/senders/twilio/twilio.go b/senders/twilio/twilio.go index d8fc8a77a..7fff15cf4 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/twilio/twilio_test.go b/senders/twilio/twilio_test.go index 11b8715e3..1cd1b7753 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.go b/senders/victorops/init.go index e47e53485..c623f8208 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/victorops/init_test.go b/senders/victorops/init_test.go index 05c2111a0..e34b6abfe 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.go b/senders/webhook/webhook.go index 3812a8799..036c877cb 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{ diff --git a/senders/webhook/webhook_test.go b/senders/webhook/webhook_test.go index 07369f11c..845054881 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() {