Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add emergency contacts api #1082

Open
wants to merge 9 commits into
base: feat/add-emergency-contacts-database-functions
Choose a base branch
from
31 changes: 23 additions & 8 deletions api/controller/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ 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"
)

// 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")
errContactAlreadyEmergency = errors.New("this contact is being used with emergency contact")

errNotPermittedStr = "you are not permitted"
)

// GetAllContacts gets all moira contacts.
func GetAllContacts(database moira.Database) (*dto.ContactList, *api.ErrorResponse) {
Expand Down Expand Up @@ -60,7 +65,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
Expand Down Expand Up @@ -117,7 +122,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
Expand Down Expand Up @@ -163,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)
Expand Down Expand Up @@ -234,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)
Expand All @@ -258,12 +273,12 @@ 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) {
_, err := dataBase.GetContact(contactID)
if errors.Is(err, database.ErrNil) {
if errors.Is(err, moiradb.ErrNil) {
return false, nil
}
if err != nil {
Expand Down
32 changes: 29 additions & 3 deletions api/controller/contact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -219,7 +220,7 @@ func TestCreateContact(t *testing.T) {
Value: contactValue,
Type: notAllowedContactType,
}
expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType)
expectedErr := api.ErrorInvalidRequest(errNotAllowedContactType)
err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "")
So(err, ShouldResemble, expectedErr)
})
Expand Down Expand Up @@ -350,7 +351,7 @@ func TestCreateContact(t *testing.T) {
Value: contactValue,
Type: notAllowedContactType,
}
expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType)
expectedErr := api.ErrorInvalidRequest(errNotAllowedContactType)
err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID)
So(err, ShouldResemble, expectedErr)
})
Expand Down Expand Up @@ -564,7 +565,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, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin})
So(err, ShouldResemble, expectedErr)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -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.HeartbeatNotifier},
}
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))
})
})
})
}
Expand Down
116 changes: 116 additions & 0 deletions api/controller/emergency_contact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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"
"github.com/moira-alert/moira/datatypes"
)

// ErrEmptyEmergencyContactID error occurring when user did not specify contact id.
var ErrEmptyEmergencyContactID = errors.New("emergency contact id can not be empty")
kissken marked this conversation as resolved.
Show resolved Hide resolved

// 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 datatypes.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 := datatypes.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 := datatypes.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
}
Loading
Loading