From 49e78f86ccf13093feff3041c7d24e9c2da80bcb Mon Sep 17 00:00:00 2001 From: almostinf Date: Tue, 10 Sep 2024 14:02:39 +0300 Subject: [PATCH 1/4] Revert "remove new changes in api" This reverts commit 56b489dcee92b6b05b96ec721f532fb41e8df773. --- api/controller/contact.go | 13 +- api/controller/contact_test.go | 6 +- api/controller/emergency_contact.go | 115 ++++ api/controller/emergency_contact_test.go | 341 ++++++++++++ api/controller/team.go | 22 +- api/controller/team_test.go | 54 +- api/controller/user.go | 20 +- api/controller/user_test.go | 100 +++- api/dto/contact.go | 13 +- api/dto/emergency_contact.go | 73 +++ api/dto/emergency_contact_test.go | 43 ++ api/dto/team.go | 7 +- api/dto/user.go | 5 +- api/handler/contact.go | 8 +- api/handler/emergency_contact.go | 185 +++++++ api/handler/emergency_contact_test.go | 665 +++++++++++++++++++++++ api/handler/handler.go | 4 + api/handler/team.go | 1 + api/handler/team_emergency_contact.go | 53 ++ 19 files changed, 1665 insertions(+), 63 deletions(-) create mode 100644 api/controller/emergency_contact.go create mode 100644 api/controller/emergency_contact_test.go create mode 100644 api/dto/emergency_contact.go create mode 100644 api/dto/emergency_contact_test.go create mode 100644 api/handler/emergency_contact.go create mode 100644 api/handler/emergency_contact_test.go create mode 100644 api/handler/team_emergency_contact.go diff --git a/api/controller/contact.go b/api/controller/contact.go index 4874624be..d325ac844 100644 --- a/api/controller/contact.go +++ b/api/controller/contact.go @@ -15,8 +15,11 @@ import ( "github.com/moira-alert/moira/database" ) -// ErrNotAllowedContactType means that this type of contact is not allowed to be created. -var ErrNotAllowedContactType = errors.New("cannot create contact with not allowed contact type") +var ( + // errNotAllowedContactType means that this type of contact is not allowed to be created. + errNotAllowedContactType = errors.New("cannot create contact with not allowed contact type") + errNotPermittedStr = "you are not permitted" +) // GetAllContacts gets all moira contacts. func GetAllContacts(database moira.Database) (*dto.ContactList, *api.ErrorResponse) { @@ -58,7 +61,7 @@ func CreateContact( teamID string, ) *api.ErrorResponse { if !isAllowedToUseContactType(auth, userLogin, contact.Type) { - return api.ErrorInvalidRequest(ErrNotAllowedContactType) + return api.ErrorInvalidRequest(errNotAllowedContactType) } // Only admins are allowed to create contacts for other users @@ -107,7 +110,7 @@ func UpdateContact( contactData moira.ContactData, ) (dto.Contact, *api.ErrorResponse) { if !isAllowedToUseContactType(auth, contactDTO.User, contactDTO.Type) { - return contactDTO, api.ErrorInvalidRequest(ErrNotAllowedContactType) + return contactDTO, api.ErrorInvalidRequest(errNotAllowedContactType) } contactData.Type = contactDTO.Type @@ -244,7 +247,7 @@ func CheckUserPermissionsForContact( return contactData, nil } - return moira.ContactData{}, api.ErrorForbidden("you are not permitted") + return moira.ContactData{}, api.ErrorForbidden(errNotPermittedStr) } func isContactExists(dataBase moira.Database, contactID string) (bool, error) { diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go index 8c398b528..f3a388bc2 100644 --- a/api/controller/contact_test.go +++ b/api/controller/contact_test.go @@ -188,7 +188,7 @@ func TestCreateContact(t *testing.T) { Value: contactValue, Type: notAllowedContactType, } - expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) + expectedErr := api.ErrorInvalidRequest(errNotAllowedContactType) err := CreateContact(dataBase, auth, contact, userLogin, "") So(err, ShouldResemble, expectedErr) }) @@ -319,7 +319,7 @@ func TestCreateContact(t *testing.T) { Value: contactValue, Type: notAllowedContactType, } - expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) + expectedErr := api.ErrorInvalidRequest(errNotAllowedContactType) err := CreateContact(dataBase, auth, contact, "", teamID) So(err, ShouldResemble, expectedErr) }) @@ -524,7 +524,7 @@ func TestUpdateContact(t *testing.T) { Value: contactValue, Type: notAllowedContactType, } - expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) + expectedErr := api.ErrorInvalidRequest(errNotAllowedContactType) contactID := uuid.Must(uuid.NewV4()).String() expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldResemble, expectedErr) diff --git a/api/controller/emergency_contact.go b/api/controller/emergency_contact.go new file mode 100644 index 000000000..11ca281fc --- /dev/null +++ b/api/controller/emergency_contact.go @@ -0,0 +1,115 @@ +package controller + +import ( + "errors" + "fmt" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/dto" + moiradb "github.com/moira-alert/moira/database" +) + +// ErrEmptyEmergencyContactID error occurring when user did not specify contact id. +var ErrEmptyEmergencyContactID = errors.New("emergency contact id can not be empty") + +// GetEmergencyContacts a method for obtaining all emergency contacts. +func GetEmergencyContacts(database moira.Database) (*dto.EmergencyContactList, *api.ErrorResponse) { + emergencyContacts, err := database.GetEmergencyContacts() + if err != nil { + return nil, api.ErrorInternalServer(err) + } + + return dto.FromEmergencyContacts(emergencyContacts), nil +} + +// GetEmergencyContact a method for obtaining a specific emergency contact. +func GetEmergencyContact(database moira.Database, contactID string) (*dto.EmergencyContact, *api.ErrorResponse) { + emergencyContact, err := database.GetEmergencyContact(contactID) + if err != nil { + if errors.Is(err, moiradb.ErrNil) { + return nil, api.ErrorNotFound(fmt.Sprintf("emergency contact with ID '%s' does not exists", contactID)) + } + + return nil, api.ErrorInternalServer(err) + } + + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + return &emergencyContactDTO, nil +} + +func verifyEmergencyContactAccess( + database moira.Database, + auth *api.Authorization, + emergencyContact moira.EmergencyContact, + userLogin string, +) *api.ErrorResponse { + contact, err := database.GetContact(emergencyContact.ContactID) + if err != nil { + return api.ErrorInternalServer(err) + } + + // Only admins are allowed to create an emergency contacts for other users + if !auth.IsAdmin(userLogin) && contact.User != "" && contact.User != userLogin { + return api.ErrorInvalidRequest(fmt.Errorf("cannot create an emergency contact using someone else's contact_id '%s'", emergencyContact.ContactID)) + } + + return nil +} + +// CreateEmergencyContact a method for creating emergency contact. +func CreateEmergencyContact( + database moira.Database, + auth *api.Authorization, + emergencyContactDTO *dto.EmergencyContact, + userLogin string, +) (dto.SaveEmergencyContactResponse, *api.ErrorResponse) { + if emergencyContactDTO == nil { + return dto.SaveEmergencyContactResponse{}, nil + } + + emergencyContact := moira.EmergencyContact(*emergencyContactDTO) + if emergencyContact.ContactID == "" { + return dto.SaveEmergencyContactResponse{}, api.ErrorInvalidRequest(ErrEmptyEmergencyContactID) + } + + if err := verifyEmergencyContactAccess(database, auth, emergencyContact, userLogin); err != nil { + return dto.SaveEmergencyContactResponse{}, err + } + + if err := database.SaveEmergencyContact(emergencyContact); err != nil { + return dto.SaveEmergencyContactResponse{}, api.ErrorInternalServer(err) + } + + return dto.SaveEmergencyContactResponse{ + ContactID: emergencyContact.ContactID, + }, nil +} + +// UpdateEmergencyContact a method to update the emergency contact. +func UpdateEmergencyContact(database moira.Database, contactID string, emergencyContactDTO *dto.EmergencyContact) (dto.SaveEmergencyContactResponse, *api.ErrorResponse) { + if emergencyContactDTO == nil { + return dto.SaveEmergencyContactResponse{}, nil + } + + emergencyContact := moira.EmergencyContact(*emergencyContactDTO) + emergencyContact.ContactID = contactID + + if err := database.SaveEmergencyContact(emergencyContact); err != nil { + return dto.SaveEmergencyContactResponse{}, api.ErrorInternalServer(err) + } + + return dto.SaveEmergencyContactResponse{ + ContactID: emergencyContact.ContactID, + }, nil +} + +// RemoveEmergencyContact a method to delete the emergency contact. +func RemoveEmergencyContact(database moira.Database, contactID string) *api.ErrorResponse { + if err := database.RemoveEmergencyContact(contactID); err != nil { + return api.ErrorInternalServer(err) + } + + return nil +} diff --git a/api/controller/emergency_contact_test.go b/api/controller/emergency_contact_test.go new file mode 100644 index 000000000..07dfc8b37 --- /dev/null +++ b/api/controller/emergency_contact_test.go @@ -0,0 +1,341 @@ +package controller + +import ( + "errors" + "fmt" + "testing" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/dto" + moiradb "github.com/moira-alert/moira/database" + mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" + . "github.com/smartystreets/goconvey/convey" + "go.uber.org/mock/gomock" +) + +var ( + testContactID = "test-contact-id" + testContactID2 = "test-contact-id2" + + testEmergencyContact = moira.EmergencyContact{ + ContactID: testContactID, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } + testEmergencyContact2 = moira.EmergencyContact{ + ContactID: testContactID2, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeRedisDisconnected}, + } + + testEmergencyContactDTO = dto.EmergencyContact{ + ContactID: testContactID, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } + testEmergencyContact2DTO = dto.EmergencyContact{ + ContactID: testContactID2, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeRedisDisconnected}, + } +) + +func TestGetEmergencyContacts(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + defer mockCtrl.Finish() + + Convey("Test GetEmergencyContacts", t, func() { + Convey("With nil response from database", func() { + database.EXPECT().GetEmergencyContacts().Return(nil, nil) + expectedEmergencyContactList := &dto.EmergencyContactList{ + List: make([]dto.EmergencyContact, 0), + } + + emergencyContactList, err := GetEmergencyContacts(database) + So(err, ShouldBeNil) + So(emergencyContactList, ShouldResemble, expectedEmergencyContactList) + }) + + Convey("With some saved emergency contacts in database", func() { + database.EXPECT().GetEmergencyContacts().Return([]*moira.EmergencyContact{&testEmergencyContact, &testEmergencyContact2}, nil) + expectedEmergencyContactList := &dto.EmergencyContactList{ + List: []dto.EmergencyContact{testEmergencyContactDTO, testEmergencyContact2DTO}, + } + + emergencyContactList, err := GetEmergencyContacts(database) + So(err, ShouldBeNil) + So(emergencyContactList, ShouldResemble, expectedEmergencyContactList) + }) + }) +} + +func TestGetEmergencyContact(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + defer mockCtrl.Finish() + + Convey("Test GetEmergencyContact", t, func() { + Convey("With unexisted emergency contact", func() { + database.EXPECT().GetEmergencyContact(testContactID).Return(moira.EmergencyContact{}, moiradb.ErrNil) + + emergencyContact, err := GetEmergencyContact(database, testContactID) + So(err, ShouldResemble, api.ErrorNotFound(fmt.Sprintf("emergency contact with ID '%s' does not exists", testContactID))) + So(emergencyContact, ShouldBeNil) + }) + + Convey("With undefined db error", func() { + expectedErr := errors.New("test-error") + database.EXPECT().GetEmergencyContact(testContactID).Return(moira.EmergencyContact{}, expectedErr) + + emergencyContact, err := GetEmergencyContact(database, testContactID) + So(err, ShouldResemble, api.ErrorInternalServer(expectedErr)) + So(emergencyContact, ShouldBeNil) + }) + + Convey("Successfully get emergency contact", func() { + database.EXPECT().GetEmergencyContact(testContactID).Return(testEmergencyContact, nil) + + emergencyContact, err := GetEmergencyContact(database, testContactID) + So(err, ShouldBeNil) + So(emergencyContact, ShouldResemble, &testEmergencyContactDTO) + }) + }) +} + +func TestVerifyEmergencyContactAccess(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + defer mockCtrl.Finish() + + const ( + admin = "admin" + user = "user" + ) + + contact := moira.ContactData{ + ID: testContactID, + User: user, + } + + Convey("Test verifyEmergencyContactAccess", t, func() { + Convey("With disabled auth and admin", func() { + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: false, + } + + database.EXPECT().GetContact(testContactID).Return(contact, nil) + + err := verifyEmergencyContactAccess(database, auth, testEmergencyContact, admin) + So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("cannot create an emergency contact using someone else's contact_id '%s'", testContactID))) + }) + + Convey("With disabled auth and user", func() { + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: false, + } + + database.EXPECT().GetContact(testContactID).Return(contact, nil) + + err := verifyEmergencyContactAccess(database, auth, testEmergencyContact, user) + So(err, ShouldBeNil) + }) + + Convey("With enabled auth and admin", func() { + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: true, + } + + database.EXPECT().GetContact(testContactID).Return(contact, nil) + + err := verifyEmergencyContactAccess(database, auth, testEmergencyContact, admin) + So(err, ShouldBeNil) + }) + + Convey("With enabled auth and user", func() { + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: true, + } + + database.EXPECT().GetContact(testContactID).Return(contact, nil) + + err := verifyEmergencyContactAccess(database, auth, testEmergencyContact, user) + So(err, ShouldBeNil) + }) + + Convey("With database error", func() { + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: true, + } + + dbErr := errors.New("get contact error") + database.EXPECT().GetContact(testContactID).Return(moira.ContactData{}, dbErr) + + err := verifyEmergencyContactAccess(database, auth, testEmergencyContact, user) + So(err, ShouldResemble, api.ErrorInternalServer(dbErr)) + }) + + Convey("With empty userLogin", func() { + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: true, + } + + database.EXPECT().GetContact(testContactID).Return(contact, nil) + + err := verifyEmergencyContactAccess(database, auth, testEmergencyContact, user) + So(err, ShouldBeNil) + }) + }) +} + +func TestCreateEmergencyContact(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + defer mockCtrl.Finish() + + const ( + admin = "admin" + user = "user" + ) + + contact := moira.ContactData{ + ID: testContactID, + User: user, + } + + auth := &api.Authorization{ + AdminList: map[string]struct{}{ + admin: {}, + }, + Enabled: true, + } + + Convey("Test CreateEmergencyContact", t, func() { + Convey("With nil emergency contact dto", func() { + response, err := CreateEmergencyContact(database, auth, nil, user) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{}) + }) + + Convey("With empty emergency contact id", func() { + emergencyContactDTO := dto.EmergencyContact{} + response, err := CreateEmergencyContact(database, auth, &emergencyContactDTO, user) + So(err, ShouldResemble, api.ErrorInvalidRequest(ErrEmptyEmergencyContactID)) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{}) + }) + + Convey("With get contact database error", func() { + dbErr := errors.New("get contact error") + database.EXPECT().GetContact(testContactID).Return(moira.ContactData{}, dbErr) + + response, err := CreateEmergencyContact(database, auth, &testEmergencyContactDTO, user) + So(err, ShouldResemble, api.ErrorInternalServer(dbErr)) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{}) + }) + + Convey("With save emergency contact database error", func() { + dbErr := errors.New("create emergency contact error") + database.EXPECT().GetContact(testContactID).Return(contact, nil) + database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(dbErr) + + response, err := CreateEmergencyContact(database, auth, &testEmergencyContactDTO, user) + So(err, ShouldResemble, api.ErrorInternalServer(dbErr)) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{}) + }) + + Convey("Without any errors", func() { + database.EXPECT().GetContact(testContactID).Return(contact, nil) + database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) + + response, err := CreateEmergencyContact(database, auth, &testEmergencyContactDTO, user) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{ + ContactID: testContactID, + }) + }) + }) +} + +func TestUpdateEmergencyContact(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + defer mockCtrl.Finish() + + Convey("Test UpdateEmergencyContact", t, func() { + Convey("With nil emergency contact dto", func() { + response, err := UpdateEmergencyContact(database, testContactID, nil) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{}) + }) + + Convey("With empty contact id", func() { + emergencyContactDTO := dto.EmergencyContact{ + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } + database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) + + response, err := UpdateEmergencyContact(database, testContactID, &emergencyContactDTO) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{ + ContactID: testContactID, + }) + }) + + Convey("With full filled emergency contact dto", func() { + database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) + + response, err := UpdateEmergencyContact(database, testContactID, &testEmergencyContactDTO) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{ + ContactID: testContactID, + }) + }) + + Convey("With database error", func() { + dbErr := errors.New("update emergency contact error") + database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(dbErr) + + response, err := UpdateEmergencyContact(database, testContactID, &testEmergencyContactDTO) + So(err, ShouldResemble, api.ErrorInternalServer(dbErr)) + So(response, ShouldResemble, dto.SaveEmergencyContactResponse{}) + }) + }) +} + +func TestRemoveEmergencyContact(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + defer mockCtrl.Finish() + + Convey("Test RemoveEmergencyContact", t, func() { + Convey("Successfully removed emergency contact", func() { + database.EXPECT().RemoveEmergencyContact(testContactID).Return(nil) + + err := RemoveEmergencyContact(database, testContactID) + So(err, ShouldBeNil) + }) + + Convey("With database error", func() { + dbErr := errors.New("remove emergency contact error") + database.EXPECT().RemoveEmergencyContact(testContactID).Return(dbErr) + + err := RemoveEmergencyContact(database, testContactID) + So(err, ShouldResemble, api.ErrorInternalServer(dbErr)) + }) + }) +} diff --git a/api/controller/team.go b/api/controller/team.go index 3c35ac0e2..a170a773c 100644 --- a/api/controller/team.go +++ b/api/controller/team.go @@ -428,9 +428,10 @@ func CheckUserPermissionsForTeam( // GetTeamSettings gets team contacts and subscriptions. func GetTeamSettings(database moira.Database, teamID string) (dto.TeamSettings, *api.ErrorResponse) { teamSettings := dto.TeamSettings{ - TeamID: teamID, - Contacts: make([]moira.ContactData, 0), - Subscriptions: make([]moira.SubscriptionData, 0), + TeamID: teamID, + Contacts: make([]moira.ContactData, 0), + Subscriptions: make([]moira.SubscriptionData, 0), + EmergencyContacts: make([]dto.EmergencyContact, 0), } subscriptionIDs, err := database.GetTeamSubscriptionIDs(teamID) @@ -442,11 +443,13 @@ func GetTeamSettings(database moira.Database, teamID string) (dto.TeamSettings, if err != nil { return dto.TeamSettings{}, api.ErrorInternalServer(err) } + for _, subscription := range subscriptions { if subscription != nil { teamSettings.Subscriptions = append(teamSettings.Subscriptions, *subscription) } } + contactIDs, err := database.GetTeamContactIDs(teamID) if err != nil { return dto.TeamSettings{}, api.ErrorInternalServer(err) @@ -456,10 +459,23 @@ func GetTeamSettings(database moira.Database, teamID string) (dto.TeamSettings, if err != nil { return dto.TeamSettings{}, api.ErrorInternalServer(err) } + for _, contact := range contacts { if contact != nil { teamSettings.Contacts = append(teamSettings.Contacts, *contact) } } + + emergencyContacts, err := database.GetEmergencyContactsByIDs(contactIDs) + if err != nil { + return dto.TeamSettings{}, api.ErrorInternalServer(err) + } + + for _, emergencyContact := range emergencyContacts { + if emergencyContact != nil { + teamSettings.EmergencyContacts = append(teamSettings.EmergencyContacts, dto.EmergencyContact(*emergencyContact)) + } + } + return teamSettings, nil } diff --git a/api/controller/team_test.go b/api/controller/team_test.go index 0eb937e4f..eb167b913 100644 --- a/api/controller/team_test.go +++ b/api/controller/team_test.go @@ -642,30 +642,38 @@ func TestGetTeamSettings(t *testing.T) { subscriptions := []*moira.SubscriptionData{{ID: subscriptionIDs[0]}, {ID: subscriptionIDs[1]}} contactIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} contacts := []*moira.ContactData{{ID: contactIDs[0]}, {ID: contactIDs[1]}} + emergencyContacts := []*moira.EmergencyContact{{ContactID: contactIDs[0]}, {ContactID: contactIDs[1]}} + database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(subscriptionIDs, nil) database.EXPECT().GetSubscriptions(subscriptionIDs).Return(subscriptions, nil) database.EXPECT().GetTeamContactIDs(teamID).Return(contactIDs, nil) database.EXPECT().GetContacts(contactIDs).Return(contacts, nil) + database.EXPECT().GetEmergencyContactsByIDs(contactIDs).Return(emergencyContacts, nil) + settings, err := GetTeamSettings(database, teamID) So(err, ShouldBeNil) So(settings, ShouldResemble, dto.TeamSettings{ - TeamID: teamID, - Contacts: []moira.ContactData{*contacts[0], *contacts[1]}, - Subscriptions: []moira.SubscriptionData{*subscriptions[0], *subscriptions[1]}, + TeamID: teamID, + Contacts: []moira.ContactData{*contacts[0], *contacts[1]}, + Subscriptions: []moira.SubscriptionData{*subscriptions[0], *subscriptions[1]}, + EmergencyContacts: []dto.EmergencyContact{dto.EmergencyContact(*emergencyContacts[0]), dto.EmergencyContact(*emergencyContacts[1])}, }) }) Convey("No contacts and subscriptions", t, func() { - database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) - database.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) - database.EXPECT().GetTeamContactIDs(teamID).Return(make([]string, 0), nil) - database.EXPECT().GetContacts(make([]string, 0)).Return(make([]*moira.ContactData, 0), nil) + database.EXPECT().GetTeamSubscriptionIDs(teamID).Return([]string{}, nil) + database.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil) + database.EXPECT().GetTeamContactIDs(teamID).Return([]string{}, nil) + database.EXPECT().GetContacts([]string{}).Return([]*moira.ContactData{}, nil) + database.EXPECT().GetEmergencyContactsByIDs([]string{}).Return([]*moira.EmergencyContact{}, nil) + settings, err := GetTeamSettings(database, teamID) So(err, ShouldBeNil) So(settings, ShouldResemble, dto.TeamSettings{ - TeamID: teamID, - Contacts: make([]moira.ContactData, 0), - Subscriptions: make([]moira.SubscriptionData, 0), + TeamID: teamID, + Contacts: make([]moira.ContactData, 0), + Subscriptions: make([]moira.SubscriptionData, 0), + EmergencyContacts: make([]dto.EmergencyContact, 0), }) }) @@ -673,36 +681,62 @@ func TestGetTeamSettings(t *testing.T) { Convey("GetTeamSubscriptionIDs", func() { expected := fmt.Errorf("can not read ids") database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(nil, expected) + settings, err := GetTeamSettings(database, teamID) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldResemble, dto.TeamSettings{}) }) + Convey("GetSubscriptions", func() { expected := fmt.Errorf("can not read subscriptions") database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) database.EXPECT().GetSubscriptions(make([]string, 0)).Return(nil, expected) + settings, err := GetTeamSettings(database, teamID) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldResemble, dto.TeamSettings{}) }) + Convey("GetTeamContactIDs", func() { expected := fmt.Errorf("can not read contact ids") database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) database.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) database.EXPECT().GetTeamContactIDs(teamID).Return(nil, expected) + settings, err := GetTeamSettings(database, teamID) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldResemble, dto.TeamSettings{}) }) + Convey("GetContacts", func() { expected := fmt.Errorf("can not read contacts") subscriptionIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} subscriptions := []*moira.SubscriptionData{{ID: subscriptionIDs[0]}, {ID: subscriptionIDs[1]}} contactIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(subscriptionIDs, nil) database.EXPECT().GetSubscriptions(subscriptionIDs).Return(subscriptions, nil) database.EXPECT().GetTeamContactIDs(teamID).Return(contactIDs, nil) database.EXPECT().GetContacts(contactIDs).Return(nil, expected) + settings, err := GetTeamSettings(database, teamID) + + So(err, ShouldResemble, api.ErrorInternalServer(expected)) + So(settings, ShouldResemble, dto.TeamSettings{}) + }) + + Convey("GetEmergencyContacts", func() { + expected := fmt.Errorf("can not read emergency contacts") + subscriptionIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + subscriptions := []*moira.SubscriptionData{{ID: subscriptionIDs[0]}, {ID: subscriptionIDs[1]}} + contactIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + contacts := []*moira.ContactData{{ID: contactIDs[0]}, {ID: contactIDs[1]}} + + database.EXPECT().GetTeamSubscriptionIDs(teamID).Return(subscriptionIDs, nil) + database.EXPECT().GetSubscriptions(subscriptionIDs).Return(subscriptions, nil) + database.EXPECT().GetTeamContactIDs(teamID).Return(contactIDs, nil) + database.EXPECT().GetContacts(contactIDs).Return(contacts, nil) + database.EXPECT().GetEmergencyContactsByIDs(contactIDs).Return(nil, expected) + settings, err := GetTeamSettings(database, teamID) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldResemble, dto.TeamSettings{}) diff --git a/api/controller/user.go b/api/controller/user.go index 0b3d969d6..bea7a77c8 100644 --- a/api/controller/user.go +++ b/api/controller/user.go @@ -14,8 +14,9 @@ func GetUserSettings(database moira.Database, userLogin string, auth *api.Author AuthEnabled: auth.IsEnabled(), Role: auth.GetRole(userLogin), }, - Contacts: make([]moira.ContactData, 0), - Subscriptions: make([]moira.SubscriptionData, 0), + Contacts: make([]moira.ContactData, 0), + Subscriptions: make([]moira.SubscriptionData, 0), + EmergencyContacts: make([]dto.EmergencyContact, 0), } subscriptionIDs, err := database.GetUserSubscriptionIDs(userLogin) @@ -27,11 +28,13 @@ func GetUserSettings(database moira.Database, userLogin string, auth *api.Author if err != nil { return nil, api.ErrorInternalServer(err) } + for _, subscription := range subscriptions { if subscription != nil { userSettings.Subscriptions = append(userSettings.Subscriptions, *subscription) } } + contactIDs, err := database.GetUserContactIDs(userLogin) if err != nil { return nil, api.ErrorInternalServer(err) @@ -41,10 +44,23 @@ func GetUserSettings(database moira.Database, userLogin string, auth *api.Author if err != nil { return nil, api.ErrorInternalServer(err) } + for _, contact := range contacts { if contact != nil { userSettings.Contacts = append(userSettings.Contacts, *contact) } } + + emergencyContacts, err := database.GetEmergencyContactsByIDs(contactIDs) + if err != nil { + return nil, api.ErrorInternalServer(err) + } + + for _, emergencyContact := range emergencyContacts { + if emergencyContact != nil { + userSettings.EmergencyContacts = append(userSettings.EmergencyContacts, dto.EmergencyContact(*emergencyContact)) + } + } + return userSettings, nil } diff --git a/api/controller/user_test.go b/api/controller/user_test.go index 23f913927..24800464e 100644 --- a/api/controller/user_test.go +++ b/api/controller/user_test.go @@ -23,32 +23,42 @@ func TestGetUserSettings(t *testing.T) { Convey("Success get user data", t, func() { subscriptionIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} subscriptions := []*moira.SubscriptionData{{ID: subscriptionIDs[0]}, {ID: subscriptionIDs[1]}} + contactIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} contacts := []*moira.ContactData{{ID: contactIDs[0]}, {ID: contactIDs[1]}} + + emergencyContacts := []*moira.EmergencyContact{{ContactID: contactIDs[0]}, {ContactID: contactIDs[1]}} + database.EXPECT().GetUserSubscriptionIDs(login).Return(subscriptionIDs, nil) database.EXPECT().GetSubscriptions(subscriptionIDs).Return(subscriptions, nil) database.EXPECT().GetUserContactIDs(login).Return(contactIDs, nil) database.EXPECT().GetContacts(contactIDs).Return(contacts, nil) + database.EXPECT().GetEmergencyContactsByIDs(contactIDs).Return(emergencyContacts, nil) + settings, err := GetUserSettings(database, login, auth) So(err, ShouldBeNil) So(settings, ShouldResemble, &dto.UserSettings{ - User: dto.User{Login: login}, - Contacts: []moira.ContactData{*contacts[0], *contacts[1]}, - Subscriptions: []moira.SubscriptionData{*subscriptions[0], *subscriptions[1]}, + User: dto.User{Login: login}, + Contacts: []moira.ContactData{*contacts[0], *contacts[1]}, + Subscriptions: []moira.SubscriptionData{*subscriptions[0], *subscriptions[1]}, + EmergencyContacts: []dto.EmergencyContact{dto.EmergencyContact(*emergencyContacts[0]), dto.EmergencyContact(*emergencyContacts[1])}, }) }) - Convey("No contacts and subscriptions", t, func() { - database.EXPECT().GetUserSubscriptionIDs(login).Return(make([]string, 0), nil) - database.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) - database.EXPECT().GetUserContactIDs(login).Return(make([]string, 0), nil) - database.EXPECT().GetContacts(make([]string, 0)).Return(make([]*moira.ContactData, 0), nil) + Convey("No contacts, subscriptions and emergency contacts", t, func() { + database.EXPECT().GetUserSubscriptionIDs(login).Return([]string{}, nil) + database.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil) + database.EXPECT().GetUserContactIDs(login).Return([]string{}, nil) + database.EXPECT().GetContacts([]string{}).Return([]*moira.ContactData{}, nil) + database.EXPECT().GetEmergencyContactsByIDs([]string{}).Return([]*moira.EmergencyContact{}, nil) + settings, err := GetUserSettings(database, login, auth) So(err, ShouldBeNil) So(settings, ShouldResemble, &dto.UserSettings{ - User: dto.User{Login: login}, - Contacts: make([]moira.ContactData, 0), - Subscriptions: make([]moira.SubscriptionData, 0), + User: dto.User{Login: login}, + Contacts: make([]moira.ContactData, 0), + Subscriptions: make([]moira.SubscriptionData, 0), + EmergencyContacts: make([]dto.EmergencyContact, 0), }) }) @@ -57,30 +67,36 @@ func TestGetUserSettings(t *testing.T) { authFull := &api.Authorization{Enabled: true, AdminList: map[string]struct{}{adminLogin: {}}} Convey("User is not admin", func() { - database.EXPECT().GetUserSubscriptionIDs(login).Return(make([]string, 0), nil) - database.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) - database.EXPECT().GetUserContactIDs(login).Return(make([]string, 0), nil) - database.EXPECT().GetContacts(make([]string, 0)).Return(make([]*moira.ContactData, 0), nil) + database.EXPECT().GetUserSubscriptionIDs(login).Return([]string{}, nil) + database.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil) + database.EXPECT().GetUserContactIDs(login).Return([]string{}, nil) + database.EXPECT().GetContacts([]string{}).Return([]*moira.ContactData{}, nil) + database.EXPECT().GetEmergencyContactsByIDs([]string{}).Return([]*moira.EmergencyContact{}, nil) + settings, err := GetUserSettings(database, login, authFull) So(err, ShouldBeNil) So(settings, ShouldResemble, &dto.UserSettings{ - User: dto.User{Login: login, Role: api.RoleUser, AuthEnabled: true}, - Contacts: make([]moira.ContactData, 0), - Subscriptions: make([]moira.SubscriptionData, 0), + User: dto.User{Login: login, Role: api.RoleUser, AuthEnabled: true}, + Contacts: make([]moira.ContactData, 0), + Subscriptions: make([]moira.SubscriptionData, 0), + EmergencyContacts: make([]dto.EmergencyContact, 0), }) }) Convey("User is admin", func() { - database.EXPECT().GetUserSubscriptionIDs(adminLogin).Return(make([]string, 0), nil) - database.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) - database.EXPECT().GetUserContactIDs(adminLogin).Return(make([]string, 0), nil) - database.EXPECT().GetContacts(make([]string, 0)).Return(make([]*moira.ContactData, 0), nil) + database.EXPECT().GetUserSubscriptionIDs(adminLogin).Return([]string{}, nil) + database.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil) + database.EXPECT().GetUserContactIDs(adminLogin).Return([]string{}, nil) + database.EXPECT().GetContacts([]string{}).Return([]*moira.ContactData{}, nil) + database.EXPECT().GetEmergencyContactsByIDs([]string{}).Return([]*moira.EmergencyContact{}, nil) + settings, err := GetUserSettings(database, adminLogin, authFull) So(err, ShouldBeNil) So(settings, ShouldResemble, &dto.UserSettings{ - User: dto.User{Login: adminLogin, Role: api.RoleAdmin, AuthEnabled: true}, - Contacts: make([]moira.ContactData, 0), - Subscriptions: make([]moira.SubscriptionData, 0), + User: dto.User{Login: adminLogin, Role: api.RoleAdmin, AuthEnabled: true}, + Contacts: make([]moira.ContactData, 0), + Subscriptions: make([]moira.SubscriptionData, 0), + EmergencyContacts: make([]dto.EmergencyContact, 0), }) }) }) @@ -89,37 +105,63 @@ func TestGetUserSettings(t *testing.T) { Convey("GetUserSubscriptionIDs", func() { expected := fmt.Errorf("can not read ids") database.EXPECT().GetUserSubscriptionIDs(login).Return(nil, expected) + settings, err := GetUserSettings(database, login, auth) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldBeNil) }) + Convey("GetSubscriptions", func() { expected := fmt.Errorf("can not read subscriptions") - database.EXPECT().GetUserSubscriptionIDs(login).Return(make([]string, 0), nil) - database.EXPECT().GetSubscriptions(make([]string, 0)).Return(nil, expected) + database.EXPECT().GetUserSubscriptionIDs(login).Return([]string{}, nil) + database.EXPECT().GetSubscriptions([]string{}).Return(nil, expected) + settings, err := GetUserSettings(database, login, auth) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldBeNil) }) + Convey("GetUserContactIDs", func() { expected := fmt.Errorf("can not read contact ids") - database.EXPECT().GetUserSubscriptionIDs(login).Return(make([]string, 0), nil) - database.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) + database.EXPECT().GetUserSubscriptionIDs(login).Return([]string{}, nil) + database.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil) database.EXPECT().GetUserContactIDs(login).Return(nil, expected) + settings, err := GetUserSettings(database, login, auth) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldBeNil) }) + Convey("GetContacts", func() { expected := fmt.Errorf("can not read contacts") subscriptionIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} subscriptions := []*moira.SubscriptionData{{ID: subscriptionIDs[0]}, {ID: subscriptionIDs[1]}} contactIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + database.EXPECT().GetUserSubscriptionIDs(login).Return(subscriptionIDs, nil) database.EXPECT().GetSubscriptions(subscriptionIDs).Return(subscriptions, nil) database.EXPECT().GetUserContactIDs(login).Return(contactIDs, nil) database.EXPECT().GetContacts(contactIDs).Return(nil, expected) + + settings, err := GetUserSettings(database, login, auth) + So(err, ShouldResemble, api.ErrorInternalServer(expected)) + So(settings, ShouldBeNil) + }) + + Convey("GetEmergencyContacts", func() { + expected := fmt.Errorf("can not read emergency contacts") + subscriptionIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + subscriptions := []*moira.SubscriptionData{{ID: subscriptionIDs[0]}, {ID: subscriptionIDs[1]}} + contactIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + contacts := []*moira.ContactData{{ID: contactIDs[0]}, {ID: contactIDs[1]}} + + database.EXPECT().GetUserSubscriptionIDs(login).Return(subscriptionIDs, nil) + database.EXPECT().GetSubscriptions(subscriptionIDs).Return(subscriptions, nil) + database.EXPECT().GetUserContactIDs(login).Return(contactIDs, nil) + database.EXPECT().GetContacts(contactIDs).Return(contacts, nil) + database.EXPECT().GetEmergencyContactsByIDs(contactIDs).Return(nil, expected) settings, err := GetUserSettings(database, login, auth) + So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(settings, ShouldBeNil) }) diff --git a/api/dto/contact.go b/api/dto/contact.go index 376b190c5..70d717d81 100644 --- a/api/dto/contact.go +++ b/api/dto/contact.go @@ -2,12 +2,18 @@ package dto import ( + "errors" "fmt" "net/http" "github.com/moira-alert/moira" ) +var ( + errEmptyContactType = errors.New("contact type can not be empty") + errUserLoginAndTeamIDFilledIn = errors.New("contact cannot have both the user field and the team_id field filled in") +) + type ContactList struct { List []*moira.ContactData `json:"list"` } @@ -31,13 +37,16 @@ func (*Contact) Render(w http.ResponseWriter, r *http.Request) error { func (contact *Contact) Bind(r *http.Request) error { if contact.Type == "" { - return fmt.Errorf("contact type can not be empty") + return errEmptyContactType } + if contact.Value == "" { return fmt.Errorf("contact value of type %s can not be empty", contact.Type) } + if contact.User != "" && contact.TeamID != "" { - return fmt.Errorf("contact cannot have both the user field and the team_id field filled in") + return errUserLoginAndTeamIDFilledIn } + return nil } diff --git a/api/dto/emergency_contact.go b/api/dto/emergency_contact.go new file mode 100644 index 000000000..3202075e6 --- /dev/null +++ b/api/dto/emergency_contact.go @@ -0,0 +1,73 @@ +package dto + +import ( + "errors" + "fmt" + "net/http" + + "github.com/moira-alert/moira" +) + +// ErrEmptyEmergencyTypes means that the user has not specified any emergency types. +var ErrEmptyEmergencyTypes = errors.New("emergency types can not be empty") + +// EmergencyContact is the DTO structure for contacts to which notifications will go in the event of special internal Moira problems. +type EmergencyContact struct { + ContactID string `json:"contact_id" example:"1dd38765-c5be-418d-81fa-7a5f879c2315"` + EmergencyTypes []moira.EmergencyContactType `json:"emergency_types" example:"notifier_off"` +} + +// Render is a function that implements chi Renderer interface for EmergencyContact. +func (*EmergencyContact) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +// Bind is a method that implements Binder interface from chi and checks that validity of data in request. +func (emergencyContact *EmergencyContact) Bind(r *http.Request) error { + if len(emergencyContact.EmergencyTypes) == 0 { + return ErrEmptyEmergencyTypes + } + + for _, emergencyType := range emergencyContact.EmergencyTypes { + if !emergencyType.IsValid() { + return fmt.Errorf("'%s' emergency type doesn't exist", emergencyType) + } + } + + return nil +} + +// EmergencyContactList is the DTO structure for list of contacts to which notifications will go in the event of special internal Moira problems. +type EmergencyContactList struct { + List []EmergencyContact `json:"list"` +} + +// Render is a function that implements chi Renderer interface for EmergencyContactList. +func (*EmergencyContactList) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +// FromEmergencyContacts a method that converts emergency contacts to dto emergency ccontact list. +func FromEmergencyContacts(emergencyContacts []*moira.EmergencyContact) *EmergencyContactList { + emergencyContactsDTO := &EmergencyContactList{ + List: make([]EmergencyContact, 0, len(emergencyContacts)), + } + + for _, emergencyContact := range emergencyContacts { + if emergencyContact != nil { + emergencyContactsDTO.List = append(emergencyContactsDTO.List, EmergencyContact(*emergencyContact)) + } + } + + return emergencyContactsDTO +} + +// SaveEmergencyContactResponse is the DTO structure which is returned in the methods of saving contact. +type SaveEmergencyContactResponse struct { + ContactID string `json:"contact_id" example:"1dd38765-c5be-418d-81fa-7a5f879c2315"` +} + +// Render is a function that implements chi Renderer interface for SaveEmergencyContactResponse. +func (SaveEmergencyContactResponse) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} diff --git a/api/dto/emergency_contact_test.go b/api/dto/emergency_contact_test.go new file mode 100644 index 000000000..2d47bde2d --- /dev/null +++ b/api/dto/emergency_contact_test.go @@ -0,0 +1,43 @@ +package dto + +import ( + "testing" + + "github.com/moira-alert/moira" + . "github.com/smartystreets/goconvey/convey" +) + +var ( + testContactID = "test-contact-id" + + testEmergencyContact = moira.EmergencyContact{ + ContactID: testContactID, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } +) + +func TestFromEmergencyContacts(t *testing.T) { + Convey("Test FromEmergencyContacts", t, func() { + Convey("With nil emergency contacts", func() { + expectedEmergencyContactList := &EmergencyContactList{ + List: make([]EmergencyContact, 0), + } + emergencyContactList := FromEmergencyContacts(nil) + So(emergencyContactList, ShouldResemble, expectedEmergencyContactList) + }) + + Convey("With some emergency contacts", func() { + expectedEmergencyContactList := &EmergencyContactList{ + List: []EmergencyContact{ + EmergencyContact(testEmergencyContact), + }, + } + emergencyContacts := []*moira.EmergencyContact{ + &testEmergencyContact, + nil, + } + emergencyContactList := FromEmergencyContacts(emergencyContacts) + So(emergencyContactList, ShouldResemble, expectedEmergencyContactList) + }) + }) +} diff --git a/api/dto/team.go b/api/dto/team.go index 9b08cb7ee..16a1a242b 100644 --- a/api/dto/team.go +++ b/api/dto/team.go @@ -96,9 +96,10 @@ func (TeamMembers) Render(w http.ResponseWriter, r *http.Request) error { } type TeamSettings struct { - TeamID string `json:"team_id" example:"d5d98eb3-ee18-4f75-9364-244f67e23b54"` - Contacts []moira.ContactData `json:"contacts"` - Subscriptions []moira.SubscriptionData `json:"subscriptions"` + TeamID string `json:"team_id" example:"d5d98eb3-ee18-4f75-9364-244f67e23b54"` + Contacts []moira.ContactData `json:"contacts"` + Subscriptions []moira.SubscriptionData `json:"subscriptions"` + EmergencyContacts []EmergencyContact `json:"emergency_contacts"` } func (TeamSettings) Render(w http.ResponseWriter, r *http.Request) error { diff --git a/api/dto/user.go b/api/dto/user.go index e86438486..634a55074 100644 --- a/api/dto/user.go +++ b/api/dto/user.go @@ -10,8 +10,9 @@ import ( type UserSettings struct { User - Contacts []moira.ContactData `json:"contacts"` - Subscriptions []moira.SubscriptionData `json:"subscriptions"` + Contacts []moira.ContactData `json:"contacts"` + Subscriptions []moira.SubscriptionData `json:"subscriptions"` + EmergencyContacts []EmergencyContact `json:"emergency_contacts"` } func (*UserSettings) Render(w http.ResponseWriter, r *http.Request) error { diff --git a/api/handler/contact.go b/api/handler/contact.go index ba31d1f2d..fef5a9ca6 100644 --- a/api/handler/contact.go +++ b/api/handler/contact.go @@ -81,7 +81,7 @@ func getContactById(writer http.ResponseWriter, request *http.Request) { // nolint: gofmt,goimports // -// @summary Creates a new contact notification for the current user +// @summary Creates a new contact for the current user // @id create-new-contact // @tags contact // @accept json @@ -131,8 +131,9 @@ func contactFilter(next http.Handler) http.Handler { // nolint: gofmt,goimports // -// @summary Updates an existing notification contact to the values passed in the request body +// @summary Updates an existing contact to the values passed in the request body // @id update-contact +// @tags contact // @accept json // @produce json // @param contactID path string true "ID of the contact to update" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) @@ -144,7 +145,6 @@ func contactFilter(next http.Handler) http.Handler { // @failure 422 {object} api.ErrorRenderExample "Render error" // @failure 500 {object} api.ErrorInternalServerExample "Internal server error" // @router /contact/{contactID} [put] -// @tags contact func updateContact(writer http.ResponseWriter, request *http.Request) { contactDTO := dto.Contact{} if err := render.Bind(request, &contactDTO); err != nil { @@ -195,13 +195,13 @@ func removeContact(writer http.ResponseWriter, request *http.Request) { // @id send-test-contact-notification // @accept json // @produce json +// @tags contact // @param contactID path string true "The ID of the target contact" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) // @success 200 "Test successful" // @failure 403 {object} api.ErrorForbiddenExample "Forbidden" // @failure 404 {object} api.ErrorNotFoundExample "Resource not found" // @failure 500 {object} api.ErrorInternalServerExample "Internal server error" // @router /contact/{contactID}/test [post] -// @tags contact func sendTestContactNotification(writer http.ResponseWriter, request *http.Request) { contactID := middleware.GetContactID(request) err := controller.SendTestContactNotification(database, contactID) diff --git a/api/handler/emergency_contact.go b/api/handler/emergency_contact.go new file mode 100644 index 000000000..72681f4e4 --- /dev/null +++ b/api/handler/emergency_contact.go @@ -0,0 +1,185 @@ +package handler + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/controller" + "github.com/moira-alert/moira/api/dto" + "github.com/moira-alert/moira/api/middleware" +) + +func emergencyContact(router chi.Router) { + router.With(middleware.AdminOnlyMiddleware()).Get("/", getEmergencyContacts) + router.Post("/", createEmergencyContact) + router.Route("/{contactId}", func(router chi.Router) { + router.Use(middleware.ContactContext) + router.Use(emergencyContactFilter) + router.Use(contactFilter) + router.Get("/", getEmergencyContactByID) + router.Put("/", updateEmergencyContact) + router.Delete("/", removeEmergencyContact) + }) +} + +// nolint: gofmt,goimports +// +// @summary Gets all Moira emergency contacts +// @id get-all-emergency-contacts +// @tags emergency-contact +// @produce json +// @success 200 {object} dto.EmergencyContactList "Contacts fetched successfully" +// @failure 422 {object} api.ErrorRenderExample "Render error" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /emergency-contact [get] +func getEmergencyContacts(writer http.ResponseWriter, request *http.Request) { + emergencyContacts, err := controller.GetEmergencyContacts(database) + if err != nil { + render.Render(writer, request, err) //nolint + return + } + + if err := render.Render(writer, request, emergencyContacts); err != nil { + render.Render(writer, request, api.ErrorRender(err)) //nolint + return + } +} + +// nolint: gofmt,goimports +// +// @summary Get emergency contact by it's contact ID +// @id get-emergency-contact-by-id +// @tags emergency-contact +// @produce json +// @param contactID path string true "Contact ID" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @success 200 {object} dto.EmergencyContact "Successfully received contact" +// @failure 403 {object} api.ErrorForbiddenExample "Forbidden" +// @failure 404 {object} api.ErrorNotFoundExample "Resource not found" +// @failure 422 {object} api.ErrorRenderExample "Render error" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /emergency-contact/{contactID} [get] +func getEmergencyContactByID(writer http.ResponseWriter, request *http.Request) { + contactID := middleware.GetContactID(request) + + emergencyContact, err := controller.GetEmergencyContact(database, contactID) + if err != nil { + render.Render(writer, request, err) //nolint + return + } + + if err := render.Render(writer, request, emergencyContact); err != nil { + render.Render(writer, request, api.ErrorRender(err)) //nolint + return + } +} + +// nolint: gofmt,goimports +// +// @summary Creates a new emergency contact for the current user +// @id create-emergency-contact +// @tags emergency-contact +// @accept json +// @produce json +// @param emergency-contact body dto.EmergencyContact true "Emergency contact data" +// @success 200 {object} dto.SaveEmergencyContactResponse "Emergency contact created successfully" +// @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" +// @failure 422 {object} api.ErrorRenderExample "Render error" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /emergency-contact [post] +func createEmergencyContact(writer http.ResponseWriter, request *http.Request) { + emergencyContactDTO := &dto.EmergencyContact{} + if err := render.Bind(request, emergencyContactDTO); err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + userLogin := middleware.GetLogin(request) + auth := middleware.GetAuth(request) + + response, err := controller.CreateEmergencyContact(database, auth, emergencyContactDTO, userLogin) + if err != nil { + render.Render(writer, request, err) //nolint + return + } + + if err := render.Render(writer, request, response); err != nil { + render.Render(writer, request, api.ErrorRender(err)) //nolint + return + } +} + +// nolint: gofmt,goimports +// +// @summary Updates an existing contact to the values passed in the request body +// @id update-emergency-contact +// @tags emergency-contact +// @accept json +// @produce json +// @param contactID path string true "ID of the contact to update" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @param emergency-contact body dto.EmergencyContact true "Updated emergency contact data" +// @success 200 {object} dto.SaveEmergencyContactResponse "Updated emergency contact" +// @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" +// @failure 403 {object} api.ErrorForbiddenExample "Forbidden" +// @failure 404 {object} api.ErrorNotFoundExample "Resource not found" +// @failure 422 {object} api.ErrorRenderExample "Render error" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /emergency-contact/{contactID} [put] +func updateEmergencyContact(writer http.ResponseWriter, request *http.Request) { + emergencyContactDTO := &dto.EmergencyContact{} + if err := render.Bind(request, emergencyContactDTO); err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + contactID := middleware.GetContactID(request) + + response, err := controller.UpdateEmergencyContact(database, contactID, emergencyContactDTO) + if err != nil { + render.Render(writer, request, err) //nolint + return + } + + if err := render.Render(writer, request, response); err != nil { + render.Render(writer, request, api.ErrorRender(err)) //nolint + return + } +} + +// nolint: gofmt,goimports +// +// @summary Deletes emergency contact for the current user +// @id remove-emergency-contact +// @accept json +// @produce json +// @tags emergency-contact +// @param contactID path string true "ID of the emergency contact to remove" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @success 200 "Emergency contact has been deleted" +// @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" +// @failure 403 {object} api.ErrorForbiddenExample "Forbidden" +// @failure 404 {object} api.ErrorNotFoundExample "Resource not found" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /emergency-contact/{contactID} [delete] +func removeEmergencyContact(writer http.ResponseWriter, request *http.Request) { + contactID := middleware.GetContactID(request) + + if err := controller.RemoveEmergencyContact(database, contactID); err != nil { + render.Render(writer, request, err) //nolint + return + } +} + +// emergencyContactFilter is middleware for check emergency contact existence. +func emergencyContactFilter(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + contactID := middleware.GetContactID(request) + + if _, err := controller.GetEmergencyContact(database, contactID); err != nil { + render.Render(writer, request, err) //nolint + return + } + + next.ServeHTTP(writer, request) + }) +} diff --git a/api/handler/emergency_contact_test.go b/api/handler/emergency_contact_test.go new file mode 100644 index 000000000..48b77830a --- /dev/null +++ b/api/handler/emergency_contact_test.go @@ -0,0 +1,665 @@ +package handler + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/controller" + "github.com/moira-alert/moira/api/dto" + "github.com/moira-alert/moira/api/middleware" + moiradb "github.com/moira-alert/moira/database" + mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" + . "github.com/smartystreets/goconvey/convey" + "go.uber.org/mock/gomock" +) + +var ( + testContactID = "test-contact-id" + testContactID2 = "test-contact-id2" + + testEmergencyContact = moira.EmergencyContact{ + ContactID: testContactID, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } + testEmergencyContact2 = moira.EmergencyContact{ + ContactID: testContactID2, + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeRedisDisconnected}, + } + + login = "testLogin" + + testContact = moira.ContactData{ + ID: testContactID, + User: login, + } +) + +func TestGetEmergencyContacts(t *testing.T) { + Convey("Test getEmergencyContacts", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + + Convey("Successfully get emergency contacts", func() { + mockDb.EXPECT().GetEmergencyContacts().Return([]*moira.EmergencyContact{ + &testEmergencyContact, + &testEmergencyContact2, + }, nil) + database = mockDb + + expectedEmergencyContactList := &dto.EmergencyContactList{ + List: []dto.EmergencyContact{ + dto.EmergencyContact(testEmergencyContact), + dto.EmergencyContact(testEmergencyContact2), + }, + } + + testRequest := httptest.NewRequest(http.MethodGet, "/emergency-contact", nil) + + getEmergencyContacts(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + actual := &dto.EmergencyContactList{} + err := json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expectedEmergencyContactList) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Internal server error from database", func() { + dbErr := errors.New("get emergency contacts error") + mockDb.EXPECT().GetEmergencyContacts().Return(nil, dbErr) + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: dbErr.Error(), + } + + testRequest := httptest.NewRequest(http.MethodGet, "/emergency-contact", nil) + + getEmergencyContacts(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + actual := &api.ErrorResponse{} + err := json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + }) +} + +func TestGetEmergencyContactByID(t *testing.T) { + Convey("Test getEmergencyContactByID", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + + Convey("Successfully get emergency contact by id", func() { + mockDb.EXPECT().GetEmergencyContact(testContactID).Return(testEmergencyContact, nil) + database = mockDb + + expectedEmergencyContactDTO := dto.EmergencyContact(testEmergencyContact) + testRequest := httptest.NewRequest(http.MethodGet, "/emergency-contact/"+testContactID, nil) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + + getEmergencyContactByID(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &dto.EmergencyContact{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, &expectedEmergencyContactDTO) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Not found error from database", func() { + dbErr := moiradb.ErrNil + mockDb.EXPECT().GetEmergencyContact(testContactID).Return(moira.EmergencyContact{}, dbErr) + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Resource not found", + ErrorText: fmt.Sprintf("emergency contact with ID '%s' does not exists", testContactID), + } + + testRequest := httptest.NewRequest(http.MethodGet, "/emergency-contact/"+testContactID, nil) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + + getEmergencyContactByID(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusNotFound) + }) + + Convey("Internal error from database", func() { + dbErr := errors.New("get emergency contact error") + mockDb.EXPECT().GetEmergencyContact(testContactID).Return(moira.EmergencyContact{}, dbErr) + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: dbErr.Error(), + } + + testRequest := httptest.NewRequest(http.MethodGet, "/emergency-contact/"+testContactID, nil) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + + getEmergencyContactByID(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + }) +} + +func TestCreateEmergencyContact(t *testing.T) { + Convey("Test createEmergencyContact", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + + auth := &api.Authorization{ + Enabled: true, + AdminList: map[string]struct{}{login: {}}, + } + + Convey("Successfully create emergency contact", func() { + emergencyContactDTO := dto.EmergencyContact(testEmergencyContact) + + expectedResponse := &dto.SaveEmergencyContactResponse{ + ContactID: testContactID, + } + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + mockDb.EXPECT().GetContact(testContactID).Return(testContact, nil) + mockDb.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest.Header.Add("content-type", "application/json") + + createEmergencyContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &dto.SaveEmergencyContactResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expectedResponse) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Try to create emergency contact without contact id", func() { + emergencyContact := moira.EmergencyContact{ + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: controller.ErrEmptyEmergencyContactID.Error(), + } + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest.Header.Add("content-type", "application/json") + + createEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("Try to create emergency contact without emergency types", func() { + emergencyContact := moira.EmergencyContact{ + ContactID: testContactID, + } + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: dto.ErrEmptyEmergencyTypes.Error(), + } + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest.Header.Add("content-type", "application/json") + + createEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("Try to create emergency contact with invalid emergency type", func() { + emergencyContact := moira.EmergencyContact{ + ContactID: testContactID, + EmergencyTypes: []moira.EmergencyContactType{ + "notifier_on", + }, + } + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "'notifier_on' emergency type doesn't exist", + } + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest.Header.Add("content-type", "application/json") + + createEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("Internal server error with get contact database error", func() { + emergencyContactDTO := dto.EmergencyContact(testEmergencyContact) + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + dbErr := errors.New("get contact error") + + mockDb.EXPECT().GetContact(testContactID).Return(moira.ContactData{}, dbErr) + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: dbErr.Error(), + } + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest.Header.Add("content-type", "application/json") + + createEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("Internal server error with save emergency contact database error", func() { + emergencyContactDTO := dto.EmergencyContact(testEmergencyContact) + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + dbErr := errors.New("save emergency contact error") + + mockDb.EXPECT().GetContact(testContactID).Return(testContact, nil) + mockDb.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(dbErr) + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: dbErr.Error(), + } + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest.Header.Add("content-type", "application/json") + + createEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + }) +} + +func TestUpdateEmergencyContact(t *testing.T) { + Convey("Test updateEmergencyContact", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + + Convey("Successfully update emergency contact", func() { + emergencyContactDTO := dto.EmergencyContact(testEmergencyContact) + + expectedResponse := &dto.SaveEmergencyContactResponse{ + ContactID: testContactID, + } + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + mockDb.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/emergency-contact/"+testContactID, bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + testRequest.Header.Add("content-type", "application/json") + + updateEmergencyContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &dto.SaveEmergencyContactResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expectedResponse) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Successfully update emergency contact without contact id in dto", func() { + emergencyContact := moira.EmergencyContact{ + EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + } + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + expectedResponse := &dto.SaveEmergencyContactResponse{ + ContactID: testContactID, + } + + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + mockDb.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/emergency-contact/"+testContactID, bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + testRequest.Header.Add("content-type", "application/json") + + updateEmergencyContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &dto.SaveEmergencyContactResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expectedResponse) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Invalid Request without emergency types in dto", func() { + emergencyContact := moira.EmergencyContact{ + ContactID: testContactID, + } + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + expectedErr := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: dto.ErrEmptyEmergencyTypes.Error(), + } + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/emergency-contact/"+testContactID, bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + testRequest.Header.Add("content-type", "application/json") + + updateEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("Invalid Request with undefined emergency type in dto", func() { + emergencyContact := moira.EmergencyContact{ + ContactID: testContactID, + EmergencyTypes: []moira.EmergencyContactType{ + "notifier_on", + }, + } + emergencyContactDTO := dto.EmergencyContact(emergencyContact) + + expectedErr := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "'notifier_on' emergency type doesn't exist", + } + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/emergency-contact/"+testContactID, bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + testRequest.Header.Add("content-type", "application/json") + + updateEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("Internal Server Error with database error", func() { + emergencyContactDTO := dto.EmergencyContact(testEmergencyContact) + + dbErr := errors.New("update emergency contact error") + expectedErr := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: dbErr.Error(), + } + jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) + So(err, ShouldBeNil) + + mockDb.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(dbErr) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/emergency-contact/"+testContactID, bytes.NewBuffer(jsonEmergencyContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + testRequest.Header.Add("content-type", "application/json") + + updateEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + }) +} + +func TestRemoveEmergencyContact(t *testing.T) { + Convey("Test removeEmergencyContact", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + + Convey("Successfully remove emergency contact", func() { + mockDb.EXPECT().RemoveEmergencyContact(testContactID).Return(nil) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact/"+testContactID, http.NoBody) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + + removeEmergencyContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Internal server error with remove emergency contact", func() { + dbErr := errors.New("remove emergency contact error") + mockDb.EXPECT().RemoveEmergencyContact(testContactID).Return(dbErr) + database = mockDb + + expectedErr := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: dbErr.Error(), + } + + testRequest := httptest.NewRequest(http.MethodDelete, "/emergency-contact/"+testContactID, http.NoBody) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, testContactID)) + + removeEmergencyContact(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, expectedErr) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + }) +} diff --git a/api/handler/handler.go b/api/handler/handler.go index 27647a8c3..da7875d4d 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -59,6 +59,9 @@ func NewHandler( // @tag.name contact // @tag.description APIs for working with Moira contacts. For more details, see // + // @tag.name emergency-contact + // @tag.description APIs for working with Moira emergency contacts + // // @tag.name config // @tag.description View Moira's runtime configuration. For more details, see // @@ -115,6 +118,7 @@ func NewHandler( contact(router) contactEvents(router) }) + router.Route("/emergency-contact", emergencyContact) router.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/api/swagger/doc.json"), )) diff --git a/api/handler/team.go b/api/handler/team.go index 294c09817..be09b766e 100644 --- a/api/handler/team.go +++ b/api/handler/team.go @@ -29,6 +29,7 @@ func teams(router chi.Router) { router.Get("/settings", getTeamSettings) router.Route("/subscriptions", teamSubscription) router.Route("/contacts", teamContact) + router.Route("/emergency-contacts", teamEmergencyContact) }) } diff --git a/api/handler/team_emergency_contact.go b/api/handler/team_emergency_contact.go new file mode 100644 index 000000000..07c222371 --- /dev/null +++ b/api/handler/team_emergency_contact.go @@ -0,0 +1,53 @@ +package handler + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/render" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/controller" + "github.com/moira-alert/moira/api/dto" + "github.com/moira-alert/moira/api/middleware" +) + +func teamEmergencyContact(router chi.Router) { + router.Post("/", createTeamEmergencyContact) +} + +// nolint: gofmt,goimports +// +// @summary Create team emergency contact +// @id create-team-emergency-contact +// @tags teamEmergencyContact +// @accept json +// @produce json +// @param teamID path string true "The ID of team" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @param emergency-contact body dto.EmergencyContact true "Emergency contact data" +// @success 200 {object} dto.SaveEmergencyContactResponse "Team emergency contact created successfully" +// @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" +// @failure 403 {object} api.ErrorForbiddenExample "Forbidden" +// @failure 404 {object} api.ErrorNotFoundExample "Resource not found" +// @failure 422 {object} api.ErrorRenderExample "Render error" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /teams/{teamID}/emergency-contacts [post] +func createTeamEmergencyContact(writer http.ResponseWriter, request *http.Request) { + emergencyContactDTO := &dto.EmergencyContact{} + if err := render.Bind(request, emergencyContactDTO); err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint:errcheck + return + } + + auth := middleware.GetAuth(request) + + response, err := controller.CreateEmergencyContact(database, auth, emergencyContactDTO, "") + if err != nil { + render.Render(writer, request, err) //nolint:errcheck + return + } + + if err := render.Render(writer, request, response); err != nil { + render.Render(writer, request, api.ErrorRender(err)) //nolint:errcheck + return + } +} From ab5ec278218a30561c811410ec00860a2a930f85 Mon Sep 17 00:00:00 2001 From: almostinf Date: Mon, 23 Sep 2024 19:52:06 +0300 Subject: [PATCH 2/4] merge base branch --- api/controller/emergency_contact_test.go | 10 ++++----- api/dto/emergency_contact.go | 16 +++++++------- api/dto/emergency_contact_test.go | 2 +- api/handler/emergency_contact_test.go | 28 ++++++++++++------------ database/redis/emergency_contact.go | 2 +- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/api/controller/emergency_contact_test.go b/api/controller/emergency_contact_test.go index 07dfc8b37..ea43ec99f 100644 --- a/api/controller/emergency_contact_test.go +++ b/api/controller/emergency_contact_test.go @@ -20,20 +20,20 @@ var ( testEmergencyContact = moira.EmergencyContact{ ContactID: testContactID, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } testEmergencyContact2 = moira.EmergencyContact{ ContactID: testContactID2, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeRedisDisconnected}, + HeartbeatTypes: []moira.HeartbeatType{moira.HearbeatTypeNotSet}, } testEmergencyContactDTO = dto.EmergencyContact{ ContactID: testContactID, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } testEmergencyContact2DTO = dto.EmergencyContact{ ContactID: testContactID2, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeRedisDisconnected}, + HeartbeatTypes: []moira.HeartbeatType{moira.HearbeatTypeNotSet}, } ) @@ -285,7 +285,7 @@ func TestUpdateEmergencyContact(t *testing.T) { Convey("With empty contact id", func() { emergencyContactDTO := dto.EmergencyContact{ - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) diff --git a/api/dto/emergency_contact.go b/api/dto/emergency_contact.go index 3202075e6..04b1dcb0e 100644 --- a/api/dto/emergency_contact.go +++ b/api/dto/emergency_contact.go @@ -8,13 +8,13 @@ import ( "github.com/moira-alert/moira" ) -// ErrEmptyEmergencyTypes means that the user has not specified any emergency types. -var ErrEmptyEmergencyTypes = errors.New("emergency types can not be empty") +// ErrEmptyHeartbeatTypes means that the user has not specified any heartbeat types. +var ErrEmptyHeartbeatTypes = errors.New("heartbeat types can not be empty") // EmergencyContact is the DTO structure for contacts to which notifications will go in the event of special internal Moira problems. type EmergencyContact struct { - ContactID string `json:"contact_id" example:"1dd38765-c5be-418d-81fa-7a5f879c2315"` - EmergencyTypes []moira.EmergencyContactType `json:"emergency_types" example:"notifier_off"` + ContactID string `json:"contact_id" example:"1dd38765-c5be-418d-81fa-7a5f879c2315"` + HeartbeatTypes []moira.HeartbeatType `json:"heartbeat_types" example:"notifier_off"` } // Render is a function that implements chi Renderer interface for EmergencyContact. @@ -24,13 +24,13 @@ func (*EmergencyContact) Render(w http.ResponseWriter, r *http.Request) error { // Bind is a method that implements Binder interface from chi and checks that validity of data in request. func (emergencyContact *EmergencyContact) Bind(r *http.Request) error { - if len(emergencyContact.EmergencyTypes) == 0 { - return ErrEmptyEmergencyTypes + if len(emergencyContact.HeartbeatTypes) == 0 { + return ErrEmptyHeartbeatTypes } - for _, emergencyType := range emergencyContact.EmergencyTypes { + for _, emergencyType := range emergencyContact.HeartbeatTypes { if !emergencyType.IsValid() { - return fmt.Errorf("'%s' emergency type doesn't exist", emergencyType) + return fmt.Errorf("'%s' heartbeat type doesn't exist", emergencyType) } } diff --git a/api/dto/emergency_contact_test.go b/api/dto/emergency_contact_test.go index 2d47bde2d..3e9b75e24 100644 --- a/api/dto/emergency_contact_test.go +++ b/api/dto/emergency_contact_test.go @@ -12,7 +12,7 @@ var ( testEmergencyContact = moira.EmergencyContact{ ContactID: testContactID, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } ) diff --git a/api/handler/emergency_contact_test.go b/api/handler/emergency_contact_test.go index 48b77830a..b068f73b7 100644 --- a/api/handler/emergency_contact_test.go +++ b/api/handler/emergency_contact_test.go @@ -27,11 +27,11 @@ var ( testEmergencyContact = moira.EmergencyContact{ ContactID: testContactID, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } testEmergencyContact2 = moira.EmergencyContact{ ContactID: testContactID2, - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeRedisDisconnected}, + HeartbeatTypes: []moira.HeartbeatType{moira.HearbeatTypeNotSet}, } login = "testLogin" @@ -246,7 +246,7 @@ func TestCreateEmergencyContact(t *testing.T) { Convey("Try to create emergency contact without contact id", func() { emergencyContact := moira.EmergencyContact{ - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } emergencyContactDTO := dto.EmergencyContact(emergencyContact) @@ -280,7 +280,7 @@ func TestCreateEmergencyContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) - Convey("Try to create emergency contact without emergency types", func() { + Convey("Try to create emergency contact without heartbeat types", func() { emergencyContact := moira.EmergencyContact{ ContactID: testContactID, } @@ -293,7 +293,7 @@ func TestCreateEmergencyContact(t *testing.T) { expectedErr := &api.ErrorResponse{ StatusText: "Invalid request", - ErrorText: dto.ErrEmptyEmergencyTypes.Error(), + ErrorText: dto.ErrEmptyHeartbeatTypes.Error(), } testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) @@ -316,10 +316,10 @@ func TestCreateEmergencyContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) - Convey("Try to create emergency contact with invalid emergency type", func() { + Convey("Try to create emergency contact with invalid heartbeat type", func() { emergencyContact := moira.EmergencyContact{ ContactID: testContactID, - EmergencyTypes: []moira.EmergencyContactType{ + HeartbeatTypes: []moira.HeartbeatType{ "notifier_on", }, } @@ -332,7 +332,7 @@ func TestCreateEmergencyContact(t *testing.T) { expectedErr := &api.ErrorResponse{ StatusText: "Invalid request", - ErrorText: "'notifier_on' emergency type doesn't exist", + ErrorText: "'notifier_on' heartbeat type doesn't exist", } testRequest := httptest.NewRequest(http.MethodPost, "/emergency-contact", bytes.NewBuffer(jsonEmergencyContact)) @@ -472,7 +472,7 @@ func TestUpdateEmergencyContact(t *testing.T) { Convey("Successfully update emergency contact without contact id in dto", func() { emergencyContact := moira.EmergencyContact{ - EmergencyTypes: []moira.EmergencyContactType{moira.EmergencyTypeNotifierOff}, + HeartbeatTypes: []moira.HeartbeatType{moira.HeartbeatNotifierOff}, } emergencyContactDTO := dto.EmergencyContact(emergencyContact) @@ -505,7 +505,7 @@ func TestUpdateEmergencyContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusOK) }) - Convey("Invalid Request without emergency types in dto", func() { + Convey("Invalid Request without heartbeat types in dto", func() { emergencyContact := moira.EmergencyContact{ ContactID: testContactID, } @@ -513,7 +513,7 @@ func TestUpdateEmergencyContact(t *testing.T) { expectedErr := &api.ErrorResponse{ StatusText: "Invalid request", - ErrorText: dto.ErrEmptyEmergencyTypes.Error(), + ErrorText: dto.ErrEmptyHeartbeatTypes.Error(), } jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) So(err, ShouldBeNil) @@ -539,10 +539,10 @@ func TestUpdateEmergencyContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) - Convey("Invalid Request with undefined emergency type in dto", func() { + Convey("Invalid Request with undefined heartbeat type in dto", func() { emergencyContact := moira.EmergencyContact{ ContactID: testContactID, - EmergencyTypes: []moira.EmergencyContactType{ + HeartbeatTypes: []moira.HeartbeatType{ "notifier_on", }, } @@ -550,7 +550,7 @@ func TestUpdateEmergencyContact(t *testing.T) { expectedErr := &api.ErrorResponse{ StatusText: "Invalid request", - ErrorText: "'notifier_on' emergency type doesn't exist", + ErrorText: "'notifier_on' heartbeat type doesn't exist", } jsonEmergencyContact, err := json.Marshal(emergencyContactDTO) So(err, ShouldBeNil) diff --git a/database/redis/emergency_contact.go b/database/redis/emergency_contact.go index 5a0fc1975..5a35f5d0d 100644 --- a/database/redis/emergency_contact.go +++ b/database/redis/emergency_contact.go @@ -85,7 +85,7 @@ func (connector *DbConnector) getEmergencyContactIDs() ([]string, error) { return emergencyContactIDs, nil } -// GetHeartbeatTypeContactIDs a method for obtaining contact IDs by specific emergency type. +// GetHeartbeatTypeContactIDs a method for obtaining contact IDs by specific heartbeat type. func (connector *DbConnector) GetHeartbeatTypeContactIDs(heartbeatType moira.HeartbeatType) ([]string, error) { c := *connector.client ctx := connector.context From 5bbcb9136ac4a012e004277c635a856897e344ef Mon Sep 17 00:00:00 2001 From: almostinf Date: Fri, 1 Nov 2024 11:47:48 +0300 Subject: [PATCH 3/4] handle emergency contact removing in contacts --- api/controller/contact.go | 22 +++++-- api/controller/contact_test.go | 26 ++++++++ api/controller/emergency_contact_test.go | 4 +- api/handler/contact_test.go | 82 ++++++++++++++++++++++++ api/handler/emergency_contact_test.go | 2 +- 5 files changed, 128 insertions(+), 8 deletions(-) diff --git a/api/controller/contact.go b/api/controller/contact.go index cd880e15b..38829b49f 100644 --- a/api/controller/contact.go +++ b/api/controller/contact.go @@ -13,13 +13,15 @@ import ( "github.com/moira-alert/moira" "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/dto" - "github.com/moira-alert/moira/database" + moiradb "github.com/moira-alert/moira/database" ) var ( // errNotAllowedContactType means that this type of contact is not allowed to be created. - errNotAllowedContactType = errors.New("cannot create contact with not allowed contact type") - errNotPermittedStr = "you are not permitted" + errNotAllowedContactType = errors.New("cannot create contact with not allowed contact type") + errContactAlreadyEmergency = errors.New("this contact is being used with emergency contact") + + errNotPermittedStr = "you are not permitted" ) // GetAllContacts gets all moira contacts. @@ -166,6 +168,16 @@ func RemoveContact(database moira.Database, contactID string, userLogin string, subscriptionIDs = append(subscriptionIDs, teamSubscriptionIDs...) } + _, err := database.GetEmergencyContact(contactID) + isEmergencyContactExist := !errors.Is(err, moiradb.ErrNil) + if err != nil && isEmergencyContactExist { + return api.ErrorInternalServer(err) + } + + if isEmergencyContactExist { + return api.ErrorInvalidRequest(errContactAlreadyEmergency) + } + subscriptions, err := database.GetSubscriptions(subscriptionIDs) if err != nil { return api.ErrorInternalServer(err) @@ -237,7 +249,7 @@ func CheckUserPermissionsForContact( ) (moira.ContactData, *api.ErrorResponse) { contactData, err := dataBase.GetContact(contactID) if err != nil { - if errors.Is(err, database.ErrNil) { + if errors.Is(err, moiradb.ErrNil) { return moira.ContactData{}, api.ErrorNotFound(fmt.Sprintf("contact with ID '%s' does not exists", contactID)) } return moira.ContactData{}, api.ErrorInternalServer(err) @@ -266,7 +278,7 @@ func CheckUserPermissionsForContact( func isContactExists(dataBase moira.Database, contactID string) (bool, error) { _, err := dataBase.GetContact(contactID) - if errors.Is(err, database.ErrNil) { + if errors.Is(err, moiradb.ErrNil) { return false, nil } if err != nil { diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go index 2bc4934f5..8144f2cf4 100644 --- a/api/controller/contact_test.go +++ b/api/controller/contact_test.go @@ -12,6 +12,7 @@ import ( "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/dto" "github.com/moira-alert/moira/database" + "github.com/moira-alert/moira/datatypes" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" "go.uber.org/mock/gomock" @@ -776,6 +777,7 @@ func TestRemoveContact(t *testing.T) { defer mockCtrl.Finish() dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) Convey("Without subscriptions", func() { + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetUserSubscriptionIDs(userLogin).Return(make([]string, 0), nil) dataBase.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) dataBase.EXPECT().RemoveContact(contactID).Return(nil) @@ -789,6 +791,7 @@ func TestRemoveContact(t *testing.T) { ID: uuid.Must(uuid.NewV4()).String(), } + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetUserSubscriptionIDs(userLogin).Return([]string{subscription.ID}, nil) dataBase.EXPECT().GetSubscriptions([]string{subscription.ID}).Return([]*moira.SubscriptionData{subscription}, nil) dataBase.EXPECT().RemoveContact(contactID).Return(nil) @@ -806,6 +809,7 @@ func TestRemoveContact(t *testing.T) { Convey("GetSubscriptions", func() { expectedError := fmt.Errorf("oooops! Can not read user subscriptions") dataBase.EXPECT().GetUserSubscriptionIDs(userLogin).Return(make([]string, 0), nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetSubscriptions(make([]string, 0)).Return(nil, expectedError) err := RemoveContact(dataBase, contactID, userLogin, "") So(err, ShouldResemble, api.ErrorInternalServer(expectedError)) @@ -819,6 +823,7 @@ func TestRemoveContact(t *testing.T) { subscriptionSubstring := fmt.Sprintf("%s (tags: %s)", subscription.ID, strings.Join(subscription.Tags, ", ")) expectedError := fmt.Errorf("this contact is being used in following subscriptions: %s", subscriptionSubstring) dataBase.EXPECT().GetUserSubscriptionIDs(userLogin).Return([]string{subscription.ID}, nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetSubscriptions([]string{subscription.ID}).Return([]*moira.SubscriptionData{&subscription}, nil) err := RemoveContact(dataBase, contactID, userLogin, "") So(err, ShouldResemble, api.ErrorInvalidRequest(expectedError)) @@ -832,6 +837,7 @@ func TestRemoveContact(t *testing.T) { dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) Convey("Without subscriptions", func() { dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetSubscriptions(make([]string, 0)).Return(make([]*moira.SubscriptionData, 0), nil) dataBase.EXPECT().RemoveContact(contactID).Return(nil) err := RemoveContact(dataBase, contactID, "", teamID) @@ -845,6 +851,7 @@ func TestRemoveContact(t *testing.T) { } dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return([]string{subscription.ID}, nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetSubscriptions([]string{subscription.ID}).Return([]*moira.SubscriptionData{subscription}, nil) dataBase.EXPECT().RemoveContact(contactID).Return(nil) err := RemoveContact(dataBase, contactID, "", teamID) @@ -861,6 +868,7 @@ func TestRemoveContact(t *testing.T) { Convey("GetSubscriptions", func() { expectedError := fmt.Errorf("oooops! Can not read team subscriptions") dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetSubscriptions(make([]string, 0)).Return(nil, expectedError) err := RemoveContact(dataBase, contactID, "", teamID) So(err, ShouldResemble, api.ErrorInternalServer(expectedError)) @@ -874,10 +882,28 @@ func TestRemoveContact(t *testing.T) { subscriptionSubstring := fmt.Sprintf("%s (tags: %s)", subscription.ID, strings.Join(subscription.Tags, ", ")) expectedError := fmt.Errorf("this contact is being used in following subscriptions: %s", subscriptionSubstring) dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return([]string{subscription.ID}, nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, database.ErrNil) dataBase.EXPECT().GetSubscriptions([]string{subscription.ID}).Return([]*moira.SubscriptionData{&subscription}, nil) err := RemoveContact(dataBase, contactID, "", teamID) So(err, ShouldResemble, api.ErrorInvalidRequest(expectedError)) }) + Convey("With emergency contact", func() { + emergencyContact := datatypes.EmergencyContact{ + ContactID: contactID, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + } + dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(emergencyContact, nil) + err := RemoveContact(dataBase, contactID, "", teamID) + So(err, ShouldResemble, api.ErrorInvalidRequest(errContactAlreadyEmergency)) + }) + Convey("With get emergency contact error", func() { + testErr := errors.New("test error") + dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) + dataBase.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, testErr) + err := RemoveContact(dataBase, contactID, "", teamID) + So(err, ShouldResemble, api.ErrorInternalServer(testErr)) + }) }) }) } diff --git a/api/controller/emergency_contact_test.go b/api/controller/emergency_contact_test.go index 82bba84ba..b0c5c0d14 100644 --- a/api/controller/emergency_contact_test.go +++ b/api/controller/emergency_contact_test.go @@ -25,7 +25,7 @@ var ( } testEmergencyContact2 = datatypes.EmergencyContact{ ContactID: testContactID2, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HearbeatTypeNotSet}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatTypeNotSet}, } testEmergencyContactDTO = dto.EmergencyContact{ @@ -34,7 +34,7 @@ var ( } testEmergencyContact2DTO = dto.EmergencyContact{ ContactID: testContactID2, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HearbeatTypeNotSet}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatTypeNotSet}, } ) diff --git a/api/handler/contact_test.go b/api/handler/contact_test.go index 4b64b4fde..8369c504b 100644 --- a/api/handler/contact_test.go +++ b/api/handler/contact_test.go @@ -14,6 +14,7 @@ import ( "github.com/moira-alert/moira/api/dto" "github.com/moira-alert/moira/api/middleware" db "github.com/moira-alert/moira/database" + "github.com/moira-alert/moira/datatypes" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" "go.uber.org/mock/gomock" @@ -677,6 +678,7 @@ func TestRemoveContact(t *testing.T) { contactID := defaultContact Convey("Successful deletion of a contact without user, team id and subscriptions", func() { + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil).Times(1) mockDb.EXPECT().RemoveContact(contactID).Return(nil).Times(1) database = mockDb @@ -703,6 +705,7 @@ func TestRemoveContact(t *testing.T) { Convey("Successful deletion of a contact without team id and subscriptions", func() { mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil).Times(1) mockDb.EXPECT().RemoveContact(contactID).Return(nil).Times(1) database = mockDb @@ -730,6 +733,7 @@ func TestRemoveContact(t *testing.T) { Convey("Successful deletion of a contact without user id and subscriptions", func() { mockDb.EXPECT().GetTeamSubscriptionIDs(defaultTeamID).Return([]string{}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil).Times(1) mockDb.EXPECT().RemoveContact(contactID).Return(nil).Times(1) database = mockDb @@ -758,6 +762,7 @@ func TestRemoveContact(t *testing.T) { Convey("Successful deletion of a contact without subscriptions", func() { mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{}, nil).Times(1) mockDb.EXPECT().GetTeamSubscriptionIDs(defaultTeamID).Return([]string{}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil).Times(1) mockDb.EXPECT().RemoveContact(contactID).Return(nil).Times(1) database = mockDb @@ -791,6 +796,7 @@ func TestRemoveContact(t *testing.T) { } mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{"test"}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{"test"}).Return([]*moira.SubscriptionData{ { Contacts: []string{"testContact"}, @@ -823,6 +829,79 @@ func TestRemoveContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) + Convey("Error when deleting a contact, the user has existing emergency contact", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "this contact is being used with emergency contact", + } + + emergencyContact := datatypes.EmergencyContact{ + ContactID: contactID, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + } + + mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{"test"}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(emergencyContact, nil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ + ID: contactID, + Type: "mail", + Value: "moira@skbkontur.ru", + User: defaultLogin, + })) + testRequest.Header.Add("content-type", "application/json") + + removeContact(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) + }) + + Convey("Internal error when deleting a contact, failed to get emergency contact", func() { + expected := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: testErr.Error(), + } + + mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{"test"}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, testErr).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ + ID: contactID, + Type: "mail", + Value: "moira@skbkontur.ru", + User: defaultLogin, + })) + testRequest.Header.Add("content-type", "application/json") + + removeContact(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.StatusInternalServerError) + }) + Convey("Error when deleting a contact, the team has existing subscriptions", func() { expected := &api.ErrorResponse{ StatusText: "Invalid request", @@ -830,6 +909,7 @@ func TestRemoveContact(t *testing.T) { } mockDb.EXPECT().GetTeamSubscriptionIDs(defaultTeamID).Return([]string{"test"}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{"test"}).Return([]*moira.SubscriptionData{ { Contacts: []string{"testContact"}, @@ -870,6 +950,7 @@ func TestRemoveContact(t *testing.T) { mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{"test1"}, nil).Times(1) mockDb.EXPECT().GetTeamSubscriptionIDs(defaultTeamID).Return([]string{"test2"}, nil).Times(1) + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{"test1", "test2"}).Return([]*moira.SubscriptionData{ { Contacts: []string{"testContact"}, @@ -913,6 +994,7 @@ func TestRemoveContact(t *testing.T) { ErrorText: testErr.Error(), } + mockDb.EXPECT().GetEmergencyContact(contactID).Return(datatypes.EmergencyContact{}, db.ErrNil).Times(1) mockDb.EXPECT().GetSubscriptions([]string{}).Return([]*moira.SubscriptionData{}, nil).Times(1) mockDb.EXPECT().RemoveContact(contactID).Return(testErr).Times(1) database = mockDb diff --git a/api/handler/emergency_contact_test.go b/api/handler/emergency_contact_test.go index c05972cc0..d2ac2774a 100644 --- a/api/handler/emergency_contact_test.go +++ b/api/handler/emergency_contact_test.go @@ -32,7 +32,7 @@ var ( } testEmergencyContact2 = datatypes.EmergencyContact{ ContactID: testContactID2, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HearbeatTypeNotSet}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatTypeNotSet}, } login = "testLogin" From 82fcf1ce956a6840b670b0f6501942c0db74bf4a Mon Sep 17 00:00:00 2001 From: almostinf Date: Tue, 5 Nov 2024 17:44:48 +0300 Subject: [PATCH 4/4] fix api with new heartbeat types --- api/controller/contact_test.go | 2 +- api/controller/emergency_contact_test.go | 6 +++--- api/dto/emergency_contact_test.go | 2 +- api/handler/contact_test.go | 2 +- api/handler/emergency_contact_test.go | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go index 8144f2cf4..7b2411448 100644 --- a/api/controller/contact_test.go +++ b/api/controller/contact_test.go @@ -890,7 +890,7 @@ func TestRemoveContact(t *testing.T) { Convey("With emergency contact", func() { emergencyContact := datatypes.EmergencyContact{ ContactID: contactID, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } dataBase.EXPECT().GetTeamSubscriptionIDs(teamID).Return(make([]string, 0), nil) dataBase.EXPECT().GetEmergencyContact(contactID).Return(emergencyContact, nil) diff --git a/api/controller/emergency_contact_test.go b/api/controller/emergency_contact_test.go index b0c5c0d14..85b62cd31 100644 --- a/api/controller/emergency_contact_test.go +++ b/api/controller/emergency_contact_test.go @@ -21,7 +21,7 @@ var ( testEmergencyContact = datatypes.EmergencyContact{ ContactID: testContactID, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } testEmergencyContact2 = datatypes.EmergencyContact{ ContactID: testContactID2, @@ -30,7 +30,7 @@ var ( testEmergencyContactDTO = dto.EmergencyContact{ ContactID: testContactID, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } testEmergencyContact2DTO = dto.EmergencyContact{ ContactID: testContactID2, @@ -286,7 +286,7 @@ func TestUpdateEmergencyContact(t *testing.T) { Convey("With empty contact id", func() { emergencyContactDTO := dto.EmergencyContact{ - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } database.EXPECT().SaveEmergencyContact(testEmergencyContact).Return(nil) diff --git a/api/dto/emergency_contact_test.go b/api/dto/emergency_contact_test.go index 4e4790dfd..8d0725fae 100644 --- a/api/dto/emergency_contact_test.go +++ b/api/dto/emergency_contact_test.go @@ -12,7 +12,7 @@ var ( testEmergencyContact = datatypes.EmergencyContact{ ContactID: testContactID, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } ) diff --git a/api/handler/contact_test.go b/api/handler/contact_test.go index 8369c504b..e307fb090 100644 --- a/api/handler/contact_test.go +++ b/api/handler/contact_test.go @@ -837,7 +837,7 @@ func TestRemoveContact(t *testing.T) { emergencyContact := datatypes.EmergencyContact{ ContactID: contactID, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } mockDb.EXPECT().GetUserSubscriptionIDs(defaultLogin).Return([]string{"test"}, nil).Times(1) diff --git a/api/handler/emergency_contact_test.go b/api/handler/emergency_contact_test.go index d2ac2774a..ac4bd67b4 100644 --- a/api/handler/emergency_contact_test.go +++ b/api/handler/emergency_contact_test.go @@ -28,7 +28,7 @@ var ( testEmergencyContact = datatypes.EmergencyContact{ ContactID: testContactID, - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } testEmergencyContact2 = datatypes.EmergencyContact{ ContactID: testContactID2, @@ -247,7 +247,7 @@ func TestCreateEmergencyContact(t *testing.T) { Convey("Try to create emergency contact without contact id", func() { emergencyContact := datatypes.EmergencyContact{ - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } emergencyContactDTO := dto.EmergencyContact(emergencyContact) @@ -473,7 +473,7 @@ func TestUpdateEmergencyContact(t *testing.T) { Convey("Successfully update emergency contact without contact id in dto", func() { emergencyContact := datatypes.EmergencyContact{ - HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff}, + HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifier}, } emergencyContactDTO := dto.EmergencyContact(emergencyContact)