From c7e4998308e1ca6d3c63ac2d48acac173ea4b0e9 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:13:19 +0700 Subject: [PATCH] feat: get all teams (#1126) --- api/constants.go | 13 + api/controller/team.go | 61 +++++ api/controller/team_test.go | 265 +++++++++++++++++++ api/dto/team.go | 29 +++ api/handler/constants.go | 6 + api/handler/team.go | 49 +++- api/handler/team_test.go | 140 ++++++++++ api/middleware/context.go | 61 +++++ api/middleware/context_test.go | 457 +++++++++++++++++++-------------- api/middleware/middleware.go | 16 +- database/redis/reply/team.go | 23 ++ database/redis/teams.go | 8 + database/redis/teams_test.go | 78 ++++++ interfaces.go | 1 + mock/moira-alert/database.go | 15 ++ 15 files changed, 1027 insertions(+), 195 deletions(-) create mode 100644 api/constants.go create mode 100644 api/handler/team_test.go diff --git a/api/constants.go b/api/constants.go new file mode 100644 index 000000000..2cf6ed7de --- /dev/null +++ b/api/constants.go @@ -0,0 +1,13 @@ +package api + +// SortOrder represents the sorting order for entities. +type SortOrder string + +const ( + // NoSortOrder means that entities may be unsorted. + NoSortOrder SortOrder = "" + // AscSortOrder means that entities should be ordered ascending (example: from 1 to 9). + AscSortOrder SortOrder = "asc" + // DescSortOrder means that entities should be ordered descending (example: from 9 to 1). + DescSortOrder SortOrder = "desc" +) diff --git a/api/controller/team.go b/api/controller/team.go index 3c35ac0e2..ef1c14e82 100644 --- a/api/controller/team.go +++ b/api/controller/team.go @@ -3,6 +3,8 @@ package controller import ( "errors" "fmt" + "regexp" + "slices" "strings" "github.com/go-redis/redis/v8" @@ -81,6 +83,65 @@ func GetTeam(dataBase moira.Database, teamID string) (dto.TeamModel, *api.ErrorR return teamModel, nil } +// SearchTeams is a controller function that returns all teams. +func SearchTeams(dataBase moira.Database, page, size int64, textRegexp *regexp.Regexp, sortOrder api.SortOrder) (dto.TeamsList, *api.ErrorResponse) { + teams, err := dataBase.GetAllTeams() + if err != nil { + return dto.TeamsList{}, api.ErrorInternalServer(fmt.Errorf("cannot get teams from database: %w", err)) + } + + filteredTeams := make([]moira.Team, 0) + for _, team := range teams { + if textRegexp.MatchString(team.Name) || textRegexp.MatchString(team.ID) { + filteredTeams = append(filteredTeams, team) + } + } + + teams = filteredTeams + + if sortOrder == api.AscSortOrder || sortOrder == api.DescSortOrder { + slices.SortFunc(teams, func(first, second moira.Team) int { + cmpRes := strings.Compare(strings.ToLower(first.Name), strings.ToLower(second.Name)) + if sortOrder == api.DescSortOrder { + return cmpRes * -1 + } else { + return cmpRes + } + }) + } + + total := int64(len(teams)) + + if page < 0 || (page > 0 && size < 0) { + return dto.TeamsList{ + List: []dto.TeamModel{}, + Page: page, + Size: size, + Total: total, + }, nil + } + + if page >= 0 && size >= 0 { + shift := page * size + if shift < int64(len(teams)) { + teams = teams[shift:] + } else { + teams = []moira.Team{} + } + + if size <= int64(len(teams)) { + teams = teams[:size] + } + } + + model := dto.NewTeamsList(teams) + model.Page = page + model.Size = size + model.Total = total + + return model, nil +} + // GetUserTeams is a controller function that returns a teams in which user is a member bu user ID. func GetUserTeams(dataBase moira.Database, userID string) (dto.UserTeams, *api.ErrorResponse) { teams, err := dataBase.GetUserTeams(userID) diff --git a/api/controller/team_test.go b/api/controller/team_test.go index 0eb937e4f..cc010b8eb 100644 --- a/api/controller/team_test.go +++ b/api/controller/team_test.go @@ -3,6 +3,7 @@ package controller import ( "errors" "fmt" + "regexp" "testing" "github.com/gofrs/uuid" @@ -193,6 +194,270 @@ func TestGetTeam(t *testing.T) { }) } +func TestSearchTeams(t *testing.T) { + Convey("SearchTeams", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) + + teams := []moira.Team{ + { + ID: "first-team-id", + Name: "First team", + }, + { + ID: "second-team-id", + Name: "Second team", + }, + { + ID: "third-team-id", + Name: "Third team", + }, + { + ID: "fourth-team-id", + Name: "Fourth team", + }, + { + ID: "fifth-team-id", + Name: "Fifth team", + }, + { + ID: "sixth-team-id", + Name: "Sixth team", + }, + { + ID: "seventh-team-id", + Name: "Seventh team", + }, + } + + teamModels := dto.NewTeamsList(teams).List + + anyText := regexp.MustCompile(".*") + + var ( + firstPage int64 = 0 + allTeamsSize int64 = -1 + ) + + Convey("with page < 0 returns empty list", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + var ( + page int64 = -1 + total = int64(len(teamModels)) + ) + + response, err := SearchTeams(dataBase, page, allTeamsSize, anyText, api.NoSortOrder) + + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: []dto.TeamModel{}, + Page: page, + Size: allTeamsSize, + Total: total, + }) + }) + + Convey("with page > 0 and size < 0, returns empty list", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + var ( + page int64 = 1 + total = int64(len(teamModels)) + ) + + response, err := SearchTeams(dataBase, page, allTeamsSize, anyText, api.NoSortOrder) + + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: []dto.TeamModel{}, + Page: page, + Size: allTeamsSize, + Total: total, + }) + }) + + Convey("when database returns error", func() { + dbErr := errors.New("test db err") + + dataBase.EXPECT().GetAllTeams().Return(nil, dbErr) + + response, err := SearchTeams(dataBase, firstPage, allTeamsSize, anyText, api.NoSortOrder) + + So(err, ShouldResemble, api.ErrorInternalServer(fmt.Errorf("cannot get teams from database: %w", dbErr))) + So(response, ShouldResemble, dto.TeamsList{}) + }) + + Convey("get all teams default options", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + total := int64(len(teamModels)) + + response, err := SearchTeams(dataBase, firstPage, allTeamsSize, anyText, api.NoSortOrder) + + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: teamModels, + Page: firstPage, + Size: allTeamsSize, + Total: total, + }) + }) + + Convey("with paginating", func() { + Convey("page and size in range of teams", func() { + var ( + page0 int64 = 0 + page1 int64 = 1 + size int64 = 3 + total = int64(len(teamModels)) + ) + + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + + response, err := SearchTeams(dataBase, page0, size, anyText, api.NoSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: teamModels[:size], + Page: page0, + Size: size, + Total: total, + }) + + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + + response, err = SearchTeams(dataBase, page1, size, anyText, api.NoSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: teamModels[page1*size : page1*size+size], + Page: page1, + Size: size, + Total: total, + }) + }) + + Convey("page ok, but size out of range", func() { + var ( + page int64 = 1 + size int64 = 5 + total = int64(len(teamModels)) + ) + + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + + response, err := SearchTeams(dataBase, page, size, anyText, api.NoSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: teamModels[page*size:], + Page: page, + Size: size, + Total: total, + }) + }) + + Convey("page and size out of range", func() { + var ( + page int64 = 2 + size int64 = 5 + total = int64(len(teamModels)) + ) + + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + + response, err := SearchTeams(dataBase, page, size, anyText, api.NoSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: []dto.TeamModel{}, + Page: page, + Size: size, + Total: total, + }) + }) + }) + + Convey("with text regexp", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + textRegexp := regexp.MustCompile(".*th-team-id") + total := int64(len(teamModels)) - 3 + + response, err := SearchTeams(dataBase, firstPage, allTeamsSize, textRegexp, api.NoSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: teamModels[3:], + Page: firstPage, + Size: allTeamsSize, + Total: total, + }) + }) + + Convey("with sorting", func() { + Convey("when asc", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + total := int64(len(teamModels)) + + response, err := SearchTeams(dataBase, firstPage, allTeamsSize, anyText, api.AscSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: []dto.TeamModel{ + teamModels[4], + teamModels[0], + teamModels[3], + teamModels[1], + teamModels[6], + teamModels[5], + teamModels[2], + }, + Page: firstPage, + Size: allTeamsSize, + Total: total, + }) + }) + + Convey("when desc", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + total := int64(len(teamModels)) + + response, err := SearchTeams(dataBase, firstPage, allTeamsSize, anyText, api.DescSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: []dto.TeamModel{ + teamModels[2], + teamModels[5], + teamModels[6], + teamModels[1], + teamModels[3], + teamModels[0], + teamModels[4], + }, + Page: firstPage, + Size: allTeamsSize, + Total: total, + }) + }) + }) + + Convey("with all options", func() { + dataBase.EXPECT().GetAllTeams().Return(teams, nil) + textRegexp := regexp.MustCompile(".*th-team-id") + var ( + total = int64(len(teamModels)) - 3 + page int64 = 1 + size int64 = 2 + ) + + response, err := SearchTeams(dataBase, page, size, textRegexp, api.DescSortOrder) + So(err, ShouldBeNil) + So(response, ShouldResemble, dto.TeamsList{ + List: []dto.TeamModel{ + teamModels[3], + teamModels[4], + }, + Page: page, + Size: size, + Total: total, + }) + }) + }) +} + func TestGetUserTeams(t *testing.T) { Convey("GetUserTeams", t, func() { mockCtrl := gomock.NewController(t) diff --git a/api/dto/team.go b/api/dto/team.go index 9b08cb7ee..a9be69b23 100644 --- a/api/dto/team.go +++ b/api/dto/team.go @@ -95,12 +95,41 @@ func (TeamMembers) Render(w http.ResponseWriter, r *http.Request) error { return nil } +// TeamSettings is a structure that contains info about team: contacts and subscriptions. type TeamSettings struct { TeamID string `json:"team_id" example:"d5d98eb3-ee18-4f75-9364-244f67e23b54"` Contacts []moira.ContactData `json:"contacts"` Subscriptions []moira.SubscriptionData `json:"subscriptions"` } +// Render is a function that implements chi Renderer interface for TeamSettings. func (TeamSettings) Render(w http.ResponseWriter, r *http.Request) error { return nil } + +// TeamsList is a structure that represents a list of existing teams in db. +type TeamsList struct { + List []TeamModel `json:"list"` + Page int64 `json:"page" example:"0" format:"int64"` + Size int64 `json:"size" example:"100" format:"int64"` + Total int64 `json:"total" example:"10" format:"int64"` +} + +// Render is a function that implements chi Renderer interface for TeamsList. +func (TeamsList) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +// NewTeamsList constructs TeamsList out of []moira.Team. +// TeamsList.Page, TeamsList.Size and TeamsList.Total are not filled. +func NewTeamsList(teams []moira.Team) TeamsList { + models := make([]TeamModel, 0, len(teams)) + + for _, team := range teams { + models = append(models, NewTeamModel(team)) + } + + return TeamsList{ + List: models, + } +} diff --git a/api/handler/constants.go b/api/handler/constants.go index 16585b0f1..4d2f55bad 100644 --- a/api/handler/constants.go +++ b/api/handler/constants.go @@ -16,3 +16,9 @@ const ( contactEventsDefaultPage = 0 contactEventsDefaultSize = -1 ) + +const ( + getAllTeamsDefaultPage = 0 + getAllTeamsDefaultSize = -1 + getAllTeamsDefaultRegexTemplate = ".*" +) diff --git a/api/handler/team.go b/api/handler/team.go index 294c09817..19e28b347 100644 --- a/api/handler/team.go +++ b/api/handler/team.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "regexp" "github.com/go-chi/chi" "github.com/go-chi/render" @@ -12,7 +13,13 @@ import ( ) func teams(router chi.Router) { - router.Get("/", getAllTeams) + router.With( + middleware.AdminOnlyMiddleware(), + middleware.Paginate(getAllTeamsDefaultPage, getAllTeamsDefaultSize), + middleware.SearchTextContext(regexp.MustCompile(getAllTeamsDefaultRegexTemplate)), + middleware.SortOrderContext(api.AscSortOrder), + ).Get("/all", searchTeams) + router.Get("/", getAllTeamsForUser) router.Post("/", createTeam) router.Route("/{teamId}", func(router chi.Router) { router.Use(middleware.TeamContext) @@ -81,15 +88,15 @@ func createTeam(writer http.ResponseWriter, request *http.Request) { // nolint: gofmt,goimports // -// @summary Get all teams -// @id get-all-teams +// @summary Get all teams for user +// @id get-all-teams-for-user // @tags team // @produce json // @success 200 {object} dto.UserTeams "Teams fetched successfully" // @failure 422 {object} api.ErrorRenderExample "Render error" // @failure 500 {object} api.ErrorInternalServerExample "Internal server error" // @router /teams [get] -func getAllTeams(writer http.ResponseWriter, request *http.Request) { +func getAllTeamsForUser(writer http.ResponseWriter, request *http.Request) { user := middleware.GetLogin(request) response, err := controller.GetUserTeams(database, user) if err != nil { @@ -131,6 +138,40 @@ func getTeam(writer http.ResponseWriter, request *http.Request) { } } +// nolint: gofmt,goimports +// +// @summary Get all Moira teams +// @id get-all-teams +// @tags team +// @produce json +// @param size query int false "Number of items to be displayed on one page. if size = -1 then all teams returned" default(-1) +// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param searchText query string false "Regular expression which will be applied to team id or team name than filtering teams" default(.*) +// @param sort query string false "String to set sort order (by name). On empty - no order, asc - ascending, desc - descending" default(asc) +// @success 200 {object} dto.TeamsList "Teams fetched successfully" +// @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" +// @failure 403 {object} api.ErrorForbiddenExample "Forbidden" +// @failure 422 {object} api.ErrorRenderExample "Render error" +// @failure 500 {object} api.ErrorInternalServerExample "Internal server error" +// @router /teams/all [get] +func searchTeams(writer http.ResponseWriter, request *http.Request) { + page := middleware.GetPage(request) + size := middleware.GetSize(request) + textRegex := middleware.GetSearchText(request) + sort := middleware.GetSortOrder(request) + + response, err := controller.SearchTeams(database, page, size, textRegex, sort) + 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 + } +} + // nolint: gofmt,goimports // // @summary Update existing team diff --git a/api/handler/team_test.go b/api/handler/team_test.go new file mode 100644 index 000000000..8d261aa4c --- /dev/null +++ b/api/handler/team_test.go @@ -0,0 +1,140 @@ +package handler + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "regexp" + "strconv" + "testing" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/dto" + "github.com/moira-alert/moira/api/middleware" + mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" + + . "github.com/smartystreets/goconvey/convey" + "go.uber.org/mock/gomock" +) + +func fillContextForTestSearchTeams(ctx context.Context, testPage, testSize int64, searchText *regexp.Regexp, sort api.SortOrder) context.Context { + ctx = middleware.SetContextValueForTest(ctx, "page", testPage) + ctx = middleware.SetContextValueForTest(ctx, "size", testSize) + ctx = middleware.SetContextValueForTest(ctx, "searchText", searchText) + ctx = middleware.SetContextValueForTest(ctx, "sort", sort) + + return ctx +} + +func Test_searchTeams(t *testing.T) { + Convey("Test searching teams", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + database = mockDb + + var ( + defaultTestPage int64 = getAllTeamsDefaultPage + defaultTestSize int64 = getAllTeamsDefaultSize + defaultTestSearchText = regexp.MustCompile(getAllTeamsDefaultRegexTemplate) + defaultTestSortOrder = api.NoSortOrder + ) + + testTeamsCount := 7 + testTeams := make([]moira.Team, 0, testTeamsCount) + for i := 0; i < testTeamsCount; i++ { + iStr := strconv.FormatInt(int64(i), 10) + + testTeams = append(testTeams, moira.Team{ + ID: "team-" + iStr, + Name: "Test team " + iStr, + }) + } + + Convey("when everything ok returns ok", func() { + mockDb.EXPECT().GetAllTeams().Return(testTeams, nil) + + testRequest := httptest.NewRequest(http.MethodGet, "/api/teams/all", nil) + + testRequest = testRequest.WithContext( + fillContextForTestSearchTeams( + testRequest.Context(), + defaultTestPage, + defaultTestSize, + defaultTestSearchText, + defaultTestSortOrder)) + testRequest.Header.Add("content-type", "application/json") + + total := int64(len(testTeams)) + + expectedDTO := dto.NewTeamsList(testTeams) + expectedDTO.Page = defaultTestPage + expectedDTO.Size = defaultTestSize + expectedDTO.Total = total + + searchTeams(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + + content, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + var gotDTO dto.TeamsList + err = json.Unmarshal(content, &gotDTO) + So(err, ShouldBeNil) + So(gotDTO, ShouldResemble, expectedDTO) + }) + + Convey("when db returns error returns internal server error", func() { + dbErr := errors.New("some error from db") + + mockDb.EXPECT().GetAllTeams().Return(nil, dbErr) + + testRequest := httptest.NewRequest(http.MethodGet, "/api/teams/all", nil) + + testRequest = testRequest.WithContext( + fillContextForTestSearchTeams( + testRequest.Context(), + defaultTestPage, + defaultTestSize, + defaultTestSearchText, + defaultTestSortOrder)) + testRequest.Header.Add("content-type", "application/json") + + type errorResponse struct { + StatusText string `json:"status"` + ErrorText string `json:"error,omitempty"` + } + + expectedErrResponseFromController := api.ErrorInternalServer(fmt.Errorf("cannot get teams from database: %w", dbErr)) + expectedDTO := errorResponse{ + StatusText: expectedErrResponseFromController.StatusText, + ErrorText: expectedErrResponseFromController.ErrorText, + } + + searchTeams(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + + content, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + var gotDTO errorResponse + err = json.Unmarshal(content, &gotDTO) + So(err, ShouldBeNil) + So(gotDTO, ShouldResemble, expectedDTO) + }) + }) +} diff --git a/api/middleware/context.go b/api/middleware/context.go index 67d6d7c1e..aa5a0fbc3 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strconv" "strings" "time" @@ -351,3 +352,63 @@ func LimitsContext(limit api.LimitsConfig) func(next http.Handler) http.Handler }) } } + +// SearchTextContext compiles and puts search text regex to request context. +func SearchTextContext(defaultRegex *regexp.Regexp) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + var searchTextRegex *regexp.Regexp + + searchText := urlValues.Get("searchText") + if searchText != "" { + searchTextRegex, err = regexp.Compile(searchText) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("failed to parse searchText template '%s': %w", searchText, err))) //nolint + return + } + } else { + searchTextRegex = defaultRegex + } + + ctx := context.WithValue(request.Context(), searchTextContextKey, searchTextRegex) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} + +// SortOrderContext puts sort order to request context. +func SortOrderContext(defaultSortOrder api.SortOrder) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + queryParamName := "sort" + + var sortOrder api.SortOrder + if !urlValues.Has(queryParamName) { + sortOrder = defaultSortOrder + } else { + sortVal := api.SortOrder(urlValues.Get(queryParamName)) + switch sortVal { + case api.NoSortOrder, api.AscSortOrder, api.DescSortOrder: + sortOrder = sortVal + default: + sortOrder = defaultSortOrder + } + } + + ctx := context.WithValue(request.Context(), sortOrderContextKey, sortOrder) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} diff --git a/api/middleware/context_test.go b/api/middleware/context_test.go index 6031e77ee..325d9d359 100644 --- a/api/middleware/context_test.go +++ b/api/middleware/context_test.go @@ -1,20 +1,141 @@ package middleware import ( + "context" + "fmt" "io" "net/http" "net/http/httptest" + "regexp" "testing" + "github.com/moira-alert/moira/api" + . "github.com/smartystreets/goconvey/convey" ) const expectedBadRequest = `{"status":"Invalid request","error":"invalid URL escape \"%\""} ` +func testRequestOk( + url string, + middlewareFunc func(next http.Handler) http.Handler, + contextVals map[ContextKey]interface{}, +) { + responseWriter := httptest.NewRecorder() + + testRequest := httptest.NewRequest(http.MethodGet, url, nil) + for contextKey, val := range contextVals { + ctx := context.WithValue(testRequest.Context(), contextKey, val) + testRequest = testRequest.WithContext(ctx) + } + + handler := func(w http.ResponseWriter, r *http.Request) {} + + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) +} + +func testRequestFails( + url string, + middlewareFunc func(next http.Handler) http.Handler, + contextVals map[ContextKey]interface{}, + failedRequestStr string, + failedRequestStatusCode int, +) { + responseWriter := httptest.NewRecorder() + + testRequest := httptest.NewRequest(http.MethodGet, url, nil) + for contextKey, val := range contextVals { + ctx := context.WithValue(testRequest.Context(), contextKey, val) + testRequest = testRequest.WithContext(ctx) + } + + handler := func(w http.ResponseWriter, r *http.Request) {} + + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, failedRequestStr) + So(response.StatusCode, ShouldEqual, failedRequestStatusCode) +} + +func TestAdminOnlyMiddleware(t *testing.T) { + Convey("Checking authorization", t, func() { + auth := api.Authorization{ + Enabled: true, + AdminList: map[string]struct{}{ + "admin": {}, + }, + AllowedContactTypes: map[string]struct{}{}, + } + + Convey("with enabled auth", func() { + Convey("admin access ok", func() { + testRequestOk( + "/test", + AdminOnlyMiddleware(), + map[ContextKey]interface{}{ + authKey: &auth, + loginKey: "admin", + }, + ) + }) + + Convey("non admin access forbidden", func() { + testRequestFails( + "/test", + AdminOnlyMiddleware(), + map[ContextKey]interface{}{ + authKey: &auth, + loginKey: "user", + }, + "{\"status\":\"Forbidden\",\"error\":\"Only administrators can use this\"}\n", + http.StatusForbidden, + ) + }) + }) + + Convey("with auth disabled", func() { + auth.Enabled = false + + Convey("admin access ok", func() { + testRequestOk( + "/test", + AdminOnlyMiddleware(), + map[ContextKey]interface{}{ + authKey: &auth, + loginKey: "admin", + }, + ) + }) + + Convey("non admin access ok", func() { + testRequestOk( + "/test", + AdminOnlyMiddleware(), + map[ContextKey]interface{}{ + authKey: &auth, + loginKey: "", + }, + ) + }) + }) + }) +} + func TestPaginateMiddleware(t *testing.T) { Convey("checking correctness of parameters", t, func() { - responseWriter := httptest.NewRecorder() defaultPage := int64(1) defaultSize := int64(10) @@ -22,42 +143,26 @@ func TestPaginateMiddleware(t *testing.T) { parameters := []string{"p=0&size=100", "p=0", "size=100", "", "p=test&size=100", "p=0&size=test"} for _, param := range parameters { - testRequest := httptest.NewRequest(http.MethodGet, "/test?"+param, nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := Paginate(defaultPage, defaultSize) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) + testRequestOk( + "/test?"+param, + Paginate(defaultPage, defaultSize), + nil) } }) Convey("with wrong url query parameters", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?p=0%&size=100", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := Paginate(defaultPage, defaultSize) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + testRequestFails( + "/test?p=0%&size=100", + Paginate(defaultPage, defaultSize), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } func TestPagerMiddleware(t *testing.T) { Convey("checking correctness of parameters", t, func() { - responseWriter := httptest.NewRecorder() defaultCreatePager := false defaultPagerID := "test" @@ -65,80 +170,48 @@ func TestPagerMiddleware(t *testing.T) { parameters := []string{"pagerID=test&createPager=true", "pagerID=test", "createPager=true", "", "pagerID=-1&createPager=true", "pagerID=test&createPager=-1"} for _, param := range parameters { - testRequest := httptest.NewRequest(http.MethodGet, "/test?"+param, nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := Pager(defaultCreatePager, defaultPagerID) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) + testRequestOk( + "/test?"+param, + Pager(defaultCreatePager, defaultPagerID), + nil) } }) Convey("with wrong url query parameters", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?pagerID=test%&createPager=true", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := Pager(defaultCreatePager, defaultPagerID) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + testRequestFails( + "/test?pagerID=test%&createPager=true", + Pager(defaultCreatePager, defaultPagerID), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } func TestPopulateMiddleware(t *testing.T) { Convey("checking correctness of parameter", t, func() { - responseWriter := httptest.NewRecorder() defaultPopulated := false Convey("with correct parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?populated=true", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := Populate(defaultPopulated) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) + testRequestOk( + "/test?populated=true", + Populate(defaultPopulated), + nil) }) Convey("with wrong url query parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?populated%=true", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := Populate(defaultPopulated) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + testRequestFails( + "/test?populated%=true", + Populate(defaultPopulated), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } func TestDateRangeMiddleware(t *testing.T) { Convey("checking correctness of parameters", t, func() { - responseWriter := httptest.NewRecorder() defaultFrom := "-1hour" defaultTo := "now" @@ -146,162 +219,166 @@ func TestDateRangeMiddleware(t *testing.T) { parameters := []string{"from=-2hours&to=now", "from=-2hours", "to=now", "", "from=-2&to=now", "from=-2hours&to=-1"} for _, param := range parameters { - testRequest := httptest.NewRequest(http.MethodGet, "/test?"+param, nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := DateRange(defaultFrom, defaultTo) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) + testRequestOk( + "/test?"+param, + DateRange(defaultFrom, defaultTo), + nil) } }) Convey("with wrong url query parameters", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?from=-2hours%&to=now", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := DateRange(defaultFrom, defaultTo) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + testRequestFails( + "/test?from=-2hours%&to=now", + DateRange(defaultFrom, defaultTo), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } func TestTargetNameMiddleware(t *testing.T) { Convey("checking correctness of parameter", t, func() { - responseWriter := httptest.NewRecorder() defaultTargetName := "test" Convey("with correct parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?target=test", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := TargetName(defaultTargetName) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) + testRequestOk( + "/test?target=test", + TargetName(defaultTargetName), + nil) }) Convey("with wrong url query parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?target%=test", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := TargetName(defaultTargetName) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + testRequestFails( + "/test?target%=test", + TargetName(defaultTargetName), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } -func TestMetricProviderMiddleware(t *testing.T) { +func TestMetricContextMiddleware(t *testing.T) { Convey("Check metric provider", t, func() { - responseWriter := httptest.NewRecorder() defaultMetric := ".*" Convey("status ok with correct query paramete", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?metric=test%5C.metric.*", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := MetricContext(defaultMetric) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) + testRequestOk( + "/test?metric=test%5C.metric.*", + MetricContext(defaultMetric), + nil) }) Convey("status bad request with wrong url query parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?metric%=test", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := MetricContext(defaultMetric) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + testRequestFails( + "/test?metric%=test", + MetricContext(defaultMetric), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } -func TestStatesProviderMiddleware(t *testing.T) { +func TestStatesContextMiddleware(t *testing.T) { Convey("Checking states provide", t, func() { - responseWriter := httptest.NewRecorder() - Convey("ok with correct states list", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := StatesContext() - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + testRequestOk( + "/test?states=OK%2CERROR", + StatesContext(), + nil) + }) - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() + Convey("bad request with bad states list", func() { + testRequestFails( + "/test?states=OK%2CERROR%2Cwarn", + StatesContext(), + nil, + "{\"status\":\"Invalid request\",\"error\":\"bad state in query parameter: warn\"}\n", + http.StatusBadRequest) + }) - So(response.StatusCode, ShouldEqual, http.StatusOK) + Convey("bad request with wrong url query parameter", func() { + testRequestFails( + "/test?states%=test", + StatesContext(), + nil, + expectedBadRequest, + http.StatusBadRequest) }) + }) +} - Convey("bad request with bad states list", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR%2Cwarn", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} +func TestSearchTextContext(t *testing.T) { + Convey("Checkins search text context", t, func() { + defaultSearchText := regexp.MustCompile(".*") - middlewareFunc := StatesContext() - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + Convey("status ok with correct query parameter", func() { + testRequestOk( + "/test?searchText=test%5Ctext.*", + SearchTextContext(defaultSearchText), + nil) + }) - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() + Convey("status ok with empty query parameter", func() { + testRequestOk( + "/test?searchText=", + SearchTextContext(defaultSearchText), + nil) + }) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + Convey("status bad request with wrong url query parameter", func() { + testRequestFails( + "/test?searchText%=test", + SearchTextContext(defaultSearchText), + nil, + expectedBadRequest, + http.StatusBadRequest) }) - Convey("bad request with wrong url query parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?states%=test", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} + Convey("status bad request with bad regexp", func() { + testRequestFails( + "/test?searchText=*", + SearchTextContext(defaultSearchText), + nil, + "{\"status\":\"Invalid request\",\"error\":\"failed to parse searchText template '*': error parsing regexp: missing argument to repetition operator: `*`\"}\n", + http.StatusBadRequest) + }) + }) +} - middlewareFunc := StatesContext() - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) +func TestSortOrderContext(t *testing.T) { + Convey("Checking sort order context", t, func() { + defaultSortOrder := api.NoSortOrder + + Convey("with no query parameter", func() { + testRequestOk( + "/test", + SortOrderContext(defaultSortOrder), + nil) + }) - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) + Convey("with correct query parameter", func() { + sortOrders := []api.SortOrder{api.NoSortOrder, api.AscSortOrder, api.DescSortOrder, "some"} - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + for i, givenSortOrder := range sortOrders { + Convey(fmt.Sprintf("case %d: sord order '%s'", i+1, givenSortOrder), func() { + testRequestOk( + fmt.Sprintf("/test?sort=%s", givenSortOrder), + SortOrderContext(defaultSortOrder), + nil) + }) + } + }) + + Convey("status bad request with wrong url query parameter", func() { + testRequestFails( + "/test?sort%=test", + SortOrderContext(defaultSortOrder), + nil, + expectedBadRequest, + http.StatusBadRequest) }) }) } diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index 14b8e8ca8..f4fe1b142 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -3,6 +3,7 @@ package middleware import ( "context" "net/http" + "regexp" "time" "github.com/moira-alert/moira" @@ -43,7 +44,10 @@ var ( metricContextKey ContextKey = "metric" statesContextKey ContextKey = "states" limitsContextKey ContextKey = "limits" - anonymousUser = "anonymous" + searchTextContextKey ContextKey = "searchText" + sortOrderContextKey ContextKey = "sort" + + anonymousUser = "anonymous" ) // GetDatabase gets moira.Database realization from request context. @@ -186,3 +190,13 @@ func GetStates(request *http.Request) map[string]struct{} { func GetLimits(request *http.Request) api.LimitsConfig { return request.Context().Value(limitsContextKey).(api.LimitsConfig) } + +// GetSearchText returns search text regexp. +func GetSearchText(request *http.Request) *regexp.Regexp { + return request.Context().Value(searchTextContextKey).(*regexp.Regexp) +} + +// GetSortOrder returns api.SortOrder. +func GetSortOrder(request *http.Request) api.SortOrder { + return request.Context().Value(sortOrderContextKey).(api.SortOrder) +} diff --git a/database/redis/reply/team.go b/database/redis/reply/team.go index d7c58eb80..0cca54539 100644 --- a/database/redis/reply/team.go +++ b/database/redis/reply/team.go @@ -56,3 +56,26 @@ func NewTeam(rep *redis.StringCmd) (moira.Team, error) { } return teamSE.toTeam(), nil } + +func UnmarshalAllTeams(rsp *redis.StringStringMapCmd) ([]moira.Team, error) { + teamsMap, err := rsp.Result() + if err != nil { + return nil, err + } + + resTeams := make([]moira.Team, 0, len(teamsMap)) + for teamID, marshaledTeam := range teamsMap { + teamSE := teamStorageElement{} + err = json.Unmarshal([]byte(marshaledTeam), &teamSE) + if err != nil { + return nil, fmt.Errorf("failed to parse team json %s: %w", marshaledTeam, err) + } + + team := teamSE.toTeam() + team.ID = teamID + + resTeams = append(resTeams, team) + } + + return resTeams, nil +} diff --git a/database/redis/teams.go b/database/redis/teams.go index be94b4c58..b0ba8ada0 100644 --- a/database/redis/teams.go +++ b/database/redis/teams.go @@ -23,6 +23,14 @@ func (connector *DbConnector) SaveTeam(teamID string, team moira.Team) error { return nil } +func (connector *DbConnector) GetAllTeams() ([]moira.Team, error) { + c := *connector.client + + response := c.HGetAll(connector.context, teamsKey) + + return reply.UnmarshalAllTeams(response) +} + // GetTeam retrieves team from redis by it's id. func (connector *DbConnector) GetTeam(teamID string) (moira.Team, error) { c := *connector.client diff --git a/database/redis/teams_test.go b/database/redis/teams_test.go index d3db66077..252500e14 100644 --- a/database/redis/teams_test.go +++ b/database/redis/teams_test.go @@ -1,6 +1,7 @@ package redis import ( + "fmt" "testing" "github.com/moira-alert/moira" @@ -147,3 +148,80 @@ func TestTeamStoring(t *testing.T) { So(actualUsers, ShouldHaveLength, 0) }) } + +func TestGetAllTeams(t *testing.T) { + Convey("Test getting all teams", t, func() { + logger, _ := logging.GetLogger("dataBase") + dataBase := NewTestDatabase(logger) + dataBase.Flush() + defer dataBase.Flush() + + Convey("with empty db returns no err and empty teams slice", func() { + teams, err := dataBase.GetAllTeams() + So(err, ShouldBeNil) + So(teams, ShouldHaveLength, 0) + }) + + testTeams := []moira.Team{ + { + ID: "teamID_1", + Name: "First team", + }, + { + ID: "teamID_2", + Name: "Second team", + }, + { + ID: "teamID_3", + Name: "Third team", + }, + { + ID: "teamID_4", + Name: "Fourth team", + }, + { + ID: "teamID_5", + Name: "Fifth team", + }, + } + + Convey("with some teams returns all", func() { + type expectedTeamCase struct { + moira.Team + count int + } + + mapOfExpectedTeams := make(map[string]expectedTeamCase) + + for _, team := range testTeams { + mapOfExpectedTeams[team.ID] = expectedTeamCase{ + Team: team, + count: 0, + } + + err := dataBase.SaveTeam(team.ID, team) + So(err, ShouldBeNil) + } + + gotTeams, err := dataBase.GetAllTeams() + So(err, ShouldBeNil) + So(gotTeams, ShouldHaveLength, len(mapOfExpectedTeams)) + + Convey("check equality of teams", func() { + for _, team := range gotTeams { + Convey(fmt.Sprintf("for team with id: %s", team.ID), func() { + expectedTeam, exists := mapOfExpectedTeams[team.ID] + So(exists, ShouldBeTrue) + So(team, ShouldResemble, expectedTeam.Team) + + if exists { + expectedTeam.count += 1 + mapOfExpectedTeams[team.ID] = expectedTeam + So(expectedTeam.count, ShouldEqual, 1) + } + }) + } + }) + }) + }) +} diff --git a/interfaces.go b/interfaces.go index 94fc776bd..29ec10eaa 100644 --- a/interfaces.go +++ b/interfaces.go @@ -143,6 +143,7 @@ type Database interface { // Teams management SaveTeam(teamID string, team Team) error GetTeam(teamID string) (Team, error) + GetAllTeams() ([]Team, error) SaveTeamsAndUsers(teamID string, users []string, usersTeams map[string][]string) error GetUserTeams(userID string) ([]string, error) GetTeamUsers(teamID string) ([]string, error) diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go index a9953446c..8f199fb96 100644 --- a/mock/moira-alert/database.go +++ b/mock/moira-alert/database.go @@ -341,6 +341,21 @@ func (mr *MockDatabaseMockRecorder) GetAllContacts() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllContacts", reflect.TypeOf((*MockDatabase)(nil).GetAllContacts)) } +// GetAllTeams mocks base method. +func (m *MockDatabase) GetAllTeams() ([]moira.Team, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllTeams") + ret0, _ := ret[0].([]moira.Team) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllTeams indicates an expected call of GetAllTeams. +func (mr *MockDatabaseMockRecorder) GetAllTeams() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTeams", reflect.TypeOf((*MockDatabase)(nil).GetAllTeams)) +} + // GetAllTriggerIDs mocks base method. func (m *MockDatabase) GetAllTriggerIDs() ([]string, error) { m.ctrl.T.Helper()