From a11cd0419dbc3f101b97a8be2233abe55ba21ef3 Mon Sep 17 00:00:00 2001 From: David Rochow Date: Fri, 18 Oct 2024 14:02:27 +0200 Subject: [PATCH] feat(issue_matching): make issue matching work (#280) * feat(serviceIssueVariant): added service_issue_variant acces on DB layer * feat(issueMatching): replaced issueVariantMap building with more efficient serviceIssueVariant DB layer query * test(serviceIssueMatch): added unit test for get service issue match from component instance * fix: adding updated mocks * Automatic application of license header * draft: intermediate unit-test progress * Automatic application of license header * test: updated unittests for issue_matching * fix:added missing parameter * fix: added missing parameter * docs: adjusted wording of comment * fix: flaky test * fix: generated merged mocks * Automatic application of license header --------- Co-authored-by: License Bot --- go.sum | 2 + .../issue_match/issue_match_handler_events.go | 81 ++---- .../issue_match/issue_match_handler_test.go | 266 +++--------------- .../issue_variant_handler_test.go | 4 +- internal/database/interface.go | 1 + internal/database/mariadb/entity.go | 29 +- .../database/mariadb/service_issue_variant.go | 130 +++++++++ .../mariadb/service_issue_variant_test.go | 183 ++++++++++++ internal/e2e/issue_variant_query_test.go | 2 +- internal/entity/common.go | 3 +- internal/entity/issue_variant.go | 21 ++ internal/entity/test/issue_variant.go | 14 +- internal/entity/test/service_issue_variant.go | 24 ++ internal/mocks/mock_Database.go | 60 +++- internal/mocks/mock_Heureka.go | 2 +- 15 files changed, 528 insertions(+), 294 deletions(-) create mode 100644 internal/database/mariadb/service_issue_variant.go create mode 100644 internal/database/mariadb/service_issue_variant_test.go create mode 100644 internal/entity/test/service_issue_variant.go diff --git a/go.sum b/go.sum index 86e3334c..e56bb43e 100644 --- a/go.sum +++ b/go.sum @@ -219,6 +219,8 @@ github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/vektah/gqlparser/v2 v2.5.17 h1:9At7WblLV7/36nulgekUgIaqHZWn5hxqluxrxGUhOmI= github.com/vektah/gqlparser/v2 v2.5.17/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= +github.com/vikstrous/dataloadgen v0.0.6 h1:A7s/fI3QNnH80CA9vdNbWK7AsbLjIxNHpZnV+VnOT1s= +github.com/vikstrous/dataloadgen v0.0.6/go.mod h1:8vuQVpBH0ODbMKAPUdCAPcOGezoTIhgAjgex51t4vbg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/app/issue_match/issue_match_handler_events.go b/internal/app/issue_match/issue_match_handler_events.go index 69153bcd..6a241d8f 100644 --- a/internal/app/issue_match/issue_match_handler_events.go +++ b/internal/app/issue_match/issue_match_handler_events.go @@ -10,7 +10,6 @@ import ( "github.com/cloudoperators/heureka/internal/app/event" "github.com/cloudoperators/heureka/internal/database" "github.com/cloudoperators/heureka/internal/entity" - "github.com/samber/lo" "github.com/sirupsen/logrus" ) @@ -93,77 +92,43 @@ func OnComponentInstanceCreate(db database.Database, event event.Event) { } // BuildIssueVariantMap builds a map of issue id to issue variant for the given issues and component instance id +// it does take the first issue_variant with the highest priority for the respective component instance. +// This is archived by utilizing database.GetServiceIssueVariants that does return ALL issue variants for a given +// component instance id together with the priorty and afterwards identifying for each issue the variant with the highest +// priority // -// - It fetches the services and issue repositories related to the component instance -// - identifies the issue repositories with the highest priority -// - fetches the issue variants for this repositories and the given issues -// - and finally creates a map of the relevant issueVariants where in case of multiple issue variants the highest -// severity variant is taken. In case of multiple variants of the same severity the first one is taken. -// -// @Todo DISCUSS may move getting issues here as well and iterate in the calling function over the issueVariantMap.... -// @Todo DISCUSS function still long but mainly due to getting data and logging steps, congnitive complexity is not that high here -// -// @Todo DISCUSS this is essential business logic, does it belong here or should it live somewhere else? -func BuildIssueVariantMap(db database.Database, componentInstanceID int64, componentVersionID int64) (map[int64]entity.IssueVariant, error) { +// Returns a map of issue id to issue variant +func BuildIssueVariantMap(db database.Database, componentInstanceID int64, componentVersionID int64) (map[int64]entity.ServiceIssueVariant, error) { l := logrus.WithFields(logrus.Fields{ "event": "BuildIssueVariantMap", "componentInstanceID": componentInstanceID, "componentVersionID": componentVersionID, }) - // Get Issues based on ComponentVersion ID - issues, err := db.GetIssues(&entity.IssueFilter{ComponentVersionId: []*int64{&componentVersionID}}) - if err != nil { - l.WithField("event-step", "FetchIssues").WithError(err).Error("Error while fetching issues related to component Version") - return nil, err - } - - // Get Services based on ComponentInstance ID - services, err := db.GetServices(&entity.ServiceFilter{ComponentInstanceId: []*int64{&componentInstanceID}}) - if err != nil { - l.WithField("event-step", "FetchServices").WithError(err).Error("Error while fetching services related to Component Instance") - return nil, err - } - - // Get Issue Repositories - serviceIds := lo.Map(services, func(service entity.Service, _ int) *int64 { return &service.Id }) - repositories, err := db.GetIssueRepositories(&entity.IssueRepositoryFilter{ServiceId: serviceIds}) - if err != nil { - l.WithField("event-step", "FetchIssueRepositories").WithField("serviceIds", serviceIds).WithError(err).Error("Error while fetching issue repositories related to services that are related to the component instance") - return nil, err - } - - if len(repositories) < 1 { - l.WithField("event-step", "FetchIssueRepositories").WithField("serviceIds", serviceIds).Error("No issue repositories found that are related to the services") - return nil, NewIssueMatchHandlerError("No issue repositories found that are related to the services") - } // Get Issue Variants - // - getting high prio - maxPriorityIr := lo.MaxBy(repositories, func(item entity.IssueRepository, max entity.IssueRepository) bool { - return item.Priority > max.Priority - }) - issueVariants, err := db.GetIssueVariants(&entity.IssueVariantFilter{ - IssueId: lo.Map(issues, func(i entity.Issue, _ int) *int64 { return &i.Id }), - IssueRepositoryId: []*int64{&maxPriorityIr.Id}, - }) - + issueVariants, err := db.GetServiceIssueVariants(&entity.ServiceIssueVariantFilter{ComponentInstanceId: []*int64{&componentInstanceID}}) if err != nil { - l.WithField("event-step", "FetchIssueVariants").WithError(err).Error("Error while fetching issue variants related to issue repositories") - return nil, err + l.WithField("event-step", "FetchIssueVariants").WithError(err).Error("Error while fetching issue variants") + return nil, NewIssueMatchHandlerError("Error while fetching issue variants") } + //No issue variants found, if len(issueVariants) < 1 { l.WithField("event-step", "FetchIssueVariants").Error("No issue variants found that are related to the issue repository") return nil, NewIssueMatchHandlerError("No issue variants found that are related to the issue repository") } // create a map of issue id to variants for easy access - var issueVariantMap = make(map[int64]entity.IssueVariant) + var issueVariantMap = make(map[int64]entity.ServiceIssueVariant) for _, variant := range issueVariants { - // if there are multiple variants with the same priority on their repositories we take the highest severity one + if _, ok := issueVariantMap[variant.IssueId]; ok { - if issueVariantMap[variant.IssueId].Severity.Score < variant.Severity.Score { + // if there are multiple variants with the same priority on their repositories we take the highest severity one + // if serverity and score are the same the first occuring issue variant is taken + if issueVariantMap[variant.IssueId].Priority < variant.Priority { + issueVariantMap[variant.IssueId] = variant + } else if issueVariantMap[variant.IssueId].Severity.Score < variant.Severity.Score { issueVariantMap[variant.IssueId] = variant } } else { @@ -189,7 +154,7 @@ func OnComponentVersionAssignmentToComponentInstance(db database.Database, compo l.WithField("issueVariantMap", issueVariantMap) if err != nil { - l.WithField("event-step", "BuildIssueVariants").WithError(err).Error("Error while fetching issues related to component Version") + l.WithField("event-step", "BuildIssueVariants").WithError(err).Error("Error while fetching issues related to component instance") return } @@ -204,6 +169,7 @@ func OnComponentVersionAssignmentToComponentInstance(db database.Database, compo IssueId: []*int64{&issueId}, ComponentInstanceId: []*int64{&componentInstanceID}, }) + if err != nil { l.WithField("event-step", "FetchIssueMatches").WithError(err).Error("Error while fetching issue matches related to assigned Component Instance") return @@ -217,7 +183,10 @@ func OnComponentVersionAssignmentToComponentInstance(db database.Database, compo } // Create new issue match + // currently a static user is assumed to be used, this going to change in future to either a configured user or a dynamically + // infered user from the component version issue macht issue_match := &entity.IssueMatch{ + UserId: 1, //@todo discuss whatever we use a static system user or infer the user from the ComponentVersionIssue Status: entity.IssueMatchStatusValuesNew, Severity: issueVariantMap[issueId].Severity, //we got two simply take the first one ComponentInstanceId: componentInstanceID, @@ -234,8 +203,12 @@ func OnComponentVersionAssignmentToComponentInstance(db database.Database, compo } } +// GetTargetRemediationTimeline returns the target remediation timeline for a given severity and creation date +// In a first iteration this is going to be obtained from a static configuration in environment variables or configuration file +// In future this is potentially going to be dynamically inferred from individual service configurations in addition +// +// @todo get the configuration from environment variables or configuration file func GetTargetRemediationTimeline(severity entity.Severity, creationDate time.Time) time.Time { - //@todo get the configuration from environment variables or configuration file switch entity.SeverityValues(severity.Value) { case entity.SeverityValuesLow: return creationDate.AddDate(0, 6, 0) diff --git a/internal/app/issue_match/issue_match_handler_test.go b/internal/app/issue_match/issue_match_handler_test.go index 825e1991..30a1a3bd 100644 --- a/internal/app/issue_match/issue_match_handler_test.go +++ b/internal/app/issue_match/issue_match_handler_test.go @@ -372,13 +372,9 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC service.Id = 1 // Mocks - db.On("GetIssues", &entity.IssueFilter{ComponentVersionId: []*int64{&componentVersionID}}).Return([]entity.Issue{}, nil) - db.On("GetServices", &entity.ServiceFilter{ComponentInstanceId: []*int64{&componentInstanceID}}).Return([]entity.Service{service}, nil) - db.On("GetIssueRepositories", &entity.IssueRepositoryFilter{ServiceId: []*int64{lo.ToPtr(int64(1))}}).Return([]entity.IssueRepository{ir}, nil) - db.On("GetIssueVariants", &entity.IssueVariantFilter{ - IssueId: []*int64{}, - IssueRepositoryId: []*int64{lo.ToPtr(int64(1))}, - }).Return([]entity.IssueVariant{}, nil) + db.On("GetServiceIssueVariants", &entity.ServiceIssueVariantFilter{ + ComponentInstanceId: []*int64{lo.ToPtr(int64(1))}, + }).Return([]entity.ServiceIssueVariant{}, nil) }) It("should return an empty map", func() { @@ -391,44 +387,11 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC When("all data is retrieved successfully", func() { BeforeEach(func() { - // Fake issues - issue1 := test.NewFakeIssueEntity() - issue1.Id = 1 - issue2 := test.NewFakeIssueEntity() - issue2.Id = 2 - - // Fake service - service := test.NewFakeServiceEntity() - service.Id = 1 - - // Fake issue repository - ir := test.NewFakeIssueRepositoryEntity() - ir.Id = 1 - ir.Priority = 1 - - // Fake issue variants - iv1 := test.NewFakeIssueVariantEntity() - iv1.Id = 1 - iv1.IssueId = 1 - iv1.IssueRepositoryId = 1 - - iv2 := test.NewFakeIssueVariantEntity() - iv2.Id = 1 - iv2.IssueId = 2 - iv2.IssueRepositoryId = 2 - - issues := []entity.Issue{issue1, issue2} - services := []entity.Service{service} - repositories := []entity.IssueRepository{ir} - variants := []entity.IssueVariant{iv1, iv2} - + variants := test.NNewFakeServiceIssueVariantEntity(2, 10, nil) // Mocks - db.On("GetIssues", &entity.IssueFilter{ComponentVersionId: []*int64{&componentVersionID}}).Return(issues, nil) - db.On("GetServices", &entity.ServiceFilter{ComponentInstanceId: []*int64{&componentInstanceID}}).Return(services, nil) - db.On("GetIssueRepositories", &entity.IssueRepositoryFilter{ServiceId: []*int64{lo.ToPtr(int64(1))}}).Return(repositories, nil) - db.On("GetIssueVariants", mock.MatchedBy(func(filter *entity.IssueVariantFilter) bool { + db.On("GetServiceIssueVariants", mock.MatchedBy(func(filter *entity.ServiceIssueVariantFilter) bool { // Check that IssueId and IssueRepositoryId are not nil, but don't care about their contents - return filter.IssueId != nil && filter.IssueRepositoryId != nil + return filter.ComponentInstanceId != nil })).Return(variants, nil) }) @@ -441,39 +404,16 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC }) When("multiple issue repository with different priority", func() { + var v2 entity.ServiceIssueVariant BeforeEach(func() { - issues := test.NNewFakeIssueEntities(1) - issues[0].Id = 1 - - issueVariants := test.NNewFakeIssueVariants(2) - - repositories := test.NNewFakeIssueRepositories(2) - repositories[0].Id = 111 - repositories[1].Id = 222 - repositories[0].Priority = 111 - repositories[1].Priority = 222 - - services := test.NNewFakeServiceEntities(1) - - // 2 Issue Variants from different issue repositories - issueVariants[0].IssueRepositoryId = repositories[0].Id - issueVariants[0].IssueId = issues[0].Id - issueVariants[0].SecondaryName = "issueVariant1" - - issueVariants[1].IssueRepositoryId = repositories[1].Id - issueVariants[1].IssueId = issues[0].Id - issueVariants[1].SecondaryName = "issueVariant2" - + v1 := test.NewFakeServiceIssueVariantEntity(100, lo.ToPtr(int64(1))) + v2 = test.NewFakeServiceIssueVariantEntity(200, lo.ToPtr(int64(1))) + variants := []entity.ServiceIssueVariant{v1, v2} // Mocks - db.On("GetIssues", &entity.IssueFilter{ComponentVersionId: []*int64{&componentVersionID}}).Return(issues, nil) - db.On("GetServices", &entity.ServiceFilter{ComponentInstanceId: []*int64{&componentInstanceID}}).Return(services, nil) - db.On("GetIssueRepositories", &entity.IssueRepositoryFilter{ServiceId: []*int64{&services[0].Id}}).Return(repositories, nil) - db.On("GetIssueVariants", mock.MatchedBy(func(filter *entity.IssueVariantFilter) bool { - // Make sure the right filter is used - return filter.IssueId != nil && *filter.IssueRepositoryId[0] == repositories[1].Id - - // We return only issueVariant2 because it has the issue repository with the higher priority - })).Return([]entity.IssueVariant{issueVariants[1]}, nil) + db.On("GetServiceIssueVariants", mock.MatchedBy(func(filter *entity.ServiceIssueVariantFilter) bool { + // Check that IssueId and IssueRepositoryId are not nil, but don't care about their contents + return filter.ComponentInstanceId != nil + })).Return(variants, nil) }) It("it should chose the issue repository with the highest priority", func() { result, err := im.BuildIssueVariantMap(db, componentInstanceID, componentVersionID) @@ -481,190 +421,51 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC Expect(err).To(BeNil()) Expect(result).To(HaveLen(1)) Expect(result).To(HaveKey(int64(1))) - - // we take int64(1) as index because this is the issueId - Expect(result[int64(1)].IssueRepositoryId).To(Equal(int64(222))) - + Expect(result[1].Id).To(BeEquivalentTo(v2.Id)) }) }) When("multiple issue repository with same priority", func() { - var ( - issues []entity.Issue - issueVariants []entity.IssueVariant - repositories []entity.IssueRepository - services []entity.Service - ) BeforeEach(func() { - issues = test.NNewFakeIssueEntities(1) - issues[0].Id = 1 - - issueVariants = test.NNewFakeIssueVariants(3) - repositories = test.NNewFakeIssueRepositories(3) - repositories[0].Id = 111 - repositories[1].Id = 222 - repositories[2].Id = 333 - repositories[0].Priority = 1 - repositories[1].Priority = 1 - repositories[2].Priority = 1 - - services = test.NNewFakeServiceEntities(1) - - // Issue Variants from different issue repositories - issueVariants[0].IssueRepositoryId = repositories[0].Id - issueVariants[0].IssueId = issues[0].Id - issueVariants[1].IssueRepositoryId = repositories[1].Id - issueVariants[1].IssueId = issues[0].Id - issueVariants[2].IssueRepositoryId = repositories[2].Id - issueVariants[2].IssueId = issues[0].Id - + variants := test.NNewFakeServiceIssueVariantEntity(2, 10, lo.ToPtr(int64(1))) // Mocks - db.On("GetIssues", &entity.IssueFilter{ComponentVersionId: []*int64{&componentVersionID}}).Return(issues, nil) - db.On("GetServices", &entity.ServiceFilter{ComponentInstanceId: []*int64{&componentInstanceID}}).Return(services, nil) - db.On("GetIssueRepositories", &entity.IssueRepositoryFilter{ServiceId: []*int64{&services[0].Id}}).Return(repositories, nil) - db.On("GetIssueVariants", mock.MatchedBy(func(filter *entity.IssueVariantFilter) bool { + db.On("GetServiceIssueVariants", mock.MatchedBy(func(filter *entity.ServiceIssueVariantFilter) bool { // Check that IssueId and IssueRepositoryId are not nil, but don't care about their contents - return filter.IssueId != nil && filter.IssueRepositoryId != nil - })).Return(issueVariants, nil) + return filter.ComponentInstanceId != nil + })).Return(variants, nil) }) It("it should randomly chose one issue repository", func() { result, err := im.BuildIssueVariantMap(db, componentInstanceID, componentVersionID) Expect(err).To(BeNil()) Expect(result).To(HaveLen(1)) - Expect(result).To(HaveKey(int64(issues[0].Id))) + Expect(result).To(HaveKey(int64(1))) - iv, ok := result[issues[0].Id] + iv, ok := result[1] Expect(ok).To(BeTrue()) - Expect(iv).To(BeAssignableToTypeOf(entity.IssueVariant{})) + Expect(iv).To(BeAssignableToTypeOf(entity.ServiceIssueVariant{})) }) }) - - When("multiple variants exist for the same issue", Label("IssueVariant"), func() { - var ( - issue entity.Issue - service entity.Service - ir1 entity.IssueRepository - ir2 entity.IssueRepository - issueVariant1 entity.IssueVariant - issueVariant2 entity.IssueVariant - ) - BeforeEach(func() { - // Fake issue - issue = test.NewFakeIssueEntity() - issue.Id = 1 - - // Fake service - service = test.NewFakeServiceEntity() - service.Id = 1 - - // Fake issue repositories - ir1 = test.NewFakeIssueRepositoryEntity() - ir1.Id = 1 - - ir2 = test.NewFakeIssueRepositoryEntity() - ir2.Id = 2 - - // Fake issue variants (with same IssueId) - issueVariant1 = test.NewFakeIssueVariantEntity() - issueVariant1.Id = 1 - issueVariant1.IssueId = issue.Id - issueVariant1.IssueRepositoryId = ir1.Id - issueVariant1.SecondaryName = "IV1" - issueVariant1.Severity.Score = 7.7 - - issueVariant2 = test.NewFakeIssueVariantEntity() - issueVariant2.Id = 2 - issueVariant2.IssueId = issue.Id // Same issue id - issueVariant2.IssueRepositoryId = ir2.Id - issueVariant2.SecondaryName = "IV2" - issueVariant2.Severity.Score = 6.6 - - issues := []entity.Issue{issue} - services := []entity.Service{service} - repositories := []entity.IssueRepository{ir1, ir2} - variants := []entity.IssueVariant{issueVariant1, issueVariant2} - - db.On("GetIssues", &entity.IssueFilter{ComponentVersionId: []*int64{&componentVersionID}}).Return(issues, nil) - db.On("GetServices", &entity.ServiceFilter{ComponentInstanceId: []*int64{&componentInstanceID}}).Return(services, nil) - db.On("GetIssueRepositories", &entity.IssueRepositoryFilter{ServiceId: []*int64{&service.Id}}).Return(repositories, nil) - db.On("GetIssueVariants", mock.MatchedBy(func(filter *entity.IssueVariantFilter) bool { - // Check that IssueId and IssueRepositoryId are not nil, but don't care about their contents - return filter.IssueId != nil && filter.IssueRepositoryId != nil - })).Return(variants, nil) - }) - - It("should choose the highest severity variant", func() { - result, err := im.BuildIssueVariantMap(db, componentInstanceID, componentVersionID) - - Expect(err).To(BeNil()) - Expect(result).To(HaveLen(1)) - - Expect(issueVariant1.Id).To(Equal(int64(1))) - Expect(issueVariant2.Id).To(Equal(int64(2))) - - Expect(result[issue.Id].Id).To(Equal(issueVariant1.Id)) - Expect(result[issue.Id].IssueRepositoryId).To(Equal(ir1.Id)) - }) - }) }) // Tests for OnComponentVersionAssignmentToComponentInstance Context("OnComponentVersionAssignmentToComponentInstance", func() { Context("when BuildIssueVariantMap succeeds", func() { BeforeEach(func() { - // Fake issues - issue1 := test.NewFakeIssueEntity() - issue1.Id = 1 - issue2 := test.NewFakeIssueEntity() - issue2.Id = 2 - - // Fake IssueRepository - ir := test.NewFakeIssueRepositoryEntity() - ir.Id = 1 - ir.Priority = 1 - - // Fake service - service := test.NewFakeServiceEntity() - service.Id = 1 - - // Fake issue variants - // - Issue Variant 1 - iv1 := test.NewFakeIssueVariantEntity() - iv1.Id = 1 - iv1.IssueId = issue1.Id - iv1.IssueRepositoryId = ir.Id - iv1.Severity.Score = 7.7 - - // - Issue Variant 2 - iv2 := test.NewFakeIssueVariantEntity() - iv2.Id = 2 - iv2.IssueId = issue1.Id - iv2.IssueRepositoryId = ir.Id - iv2.Severity.Score = 9.1 - - // - Issue Variant 3 - iv3 := test.NewFakeIssueVariantEntity() - iv3.Id = 3 - iv3.IssueId = issue2.Id - iv3.IssueRepositoryId = ir.Id - iv3.Severity.Score = 1.8 - - variants := []entity.IssueVariant{iv1, iv2, iv3} - - // Mock the necessary database calls for BuildIssueVariantMap - db.On("GetIssues", mock.Anything).Return([]entity.Issue{issue1, issue2}, nil) - db.On("GetServices", mock.Anything).Return([]entity.Service{service}, nil) - db.On("GetIssueRepositories", mock.Anything).Return([]entity.IssueRepository{ir}, nil) - db.On("GetIssueVariants", mock.MatchedBy(func(filter *entity.IssueVariantFilter) bool { - return filter.IssueId != nil && filter.IssueRepositoryId != nil + v1 := test.NewFakeServiceIssueVariantEntity(100, lo.ToPtr(int64(1))) + v2 := test.NewFakeServiceIssueVariantEntity(200, lo.ToPtr(int64(2))) + variants := []entity.ServiceIssueVariant{v1, v2} + // Mocks + db.On("GetServiceIssueVariants", mock.MatchedBy(func(filter *entity.ServiceIssueVariantFilter) bool { + // Check that IssueId and IssueRepositoryId are not nil, but don't care about their contents + return filter.ComponentInstanceId != nil })).Return(variants, nil) - db.On("GetIssueMatches", mock.Anything).Return([]entity.IssueMatch{}, nil) }) It("should create issue matches for each issue", func() { + db.On("GetIssueMatches", mock.Anything).Return([]entity.IssueMatch{}, nil) // Mock CreateIssueMatch db.On("CreateIssueMatch", mock.AnythingOfType("*entity.IssueMatch")).Return(&entity.IssueMatch{}, nil).Twice() im.OnComponentVersionAssignmentToComponentInstance(db, componentInstanceID, componentVersionID) @@ -678,9 +479,14 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC // Fake issues issueMatch := test.NewFakeIssueMatch() issueMatch.IssueId = 2 // issue2.Id + //when issueid is 2 return a fake issue match db.On("GetIssueMatches", mock.MatchedBy(func(filter *entity.IssueMatchFilter) bool { return *filter.IssueId[0] == int64(2) })).Return([]entity.IssueMatch{issueMatch}, nil) + //when it is not 2 return none + db.On("GetIssueMatches", mock.MatchedBy(func(filter *entity.IssueMatchFilter) bool { + return *filter.IssueId[0] != int64(2) + })).Return([]entity.IssueMatch{}, nil) }) It("should only create issue matches for new issues", func() { @@ -689,7 +495,7 @@ var _ = Describe("OnComponentInstanceCreate", Label("app", "OnComponentInstanceC im.OnComponentVersionAssignmentToComponentInstance(db, componentInstanceID, componentVersionID) // Verify that CreateIssueMatch was called only once (for the new issue) - // db.AssertNumberOfCalls(GinkgoT(), "CreateIssueMatch", 1) + db.AssertNumberOfCalls(GinkgoT(), "CreateIssueMatch", 1) }) }) diff --git a/internal/app/issue_variant/issue_variant_handler_test.go b/internal/app/issue_variant/issue_variant_handler_test.go index 538eb22f..60715a6a 100644 --- a/internal/app/issue_variant/issue_variant_handler_test.go +++ b/internal/app/issue_variant/issue_variant_handler_test.go @@ -215,7 +215,7 @@ var _ = Describe("When creating IssueVariant", Label("app", "CreateIssueVariant" BeforeEach(func() { db = mocks.NewMockDatabase(GinkgoT()) - issueVariant = test.NewFakeIssueVariantEntity() + issueVariant = test.NewFakeIssueVariantEntity(nil) first := 10 var after int64 after = 0 @@ -259,7 +259,7 @@ var _ = Describe("When updating IssueVariant", Label("app", "UpdateIssueVariant" BeforeEach(func() { db = mocks.NewMockDatabase(GinkgoT()) - issueVariant = test.NewFakeIssueVariantEntity() + issueVariant = test.NewFakeIssueVariantEntity(nil) first := 10 var after int64 after = 0 diff --git a/internal/database/interface.go b/internal/database/interface.go index 2d938b18..61a43f2d 100644 --- a/internal/database/interface.go +++ b/internal/database/interface.go @@ -18,6 +18,7 @@ type Database interface { RemoveComponentVersionFromIssue(int64, int64) error GetIssueNames(*entity.IssueFilter) ([]string, error) + GetServiceIssueVariants(*entity.ServiceIssueVariantFilter) ([]entity.ServiceIssueVariant, error) GetIssueVariants(*entity.IssueVariantFilter) ([]entity.IssueVariant, error) GetAllIssueVariantIds(*entity.IssueVariantFilter) ([]int64, error) CountIssueVariants(*entity.IssueVariantFilter) (int64, error) diff --git a/internal/database/mariadb/entity.go b/internal/database/mariadb/entity.go index d4481395..e839d67b 100644 --- a/internal/database/mariadb/entity.go +++ b/internal/database/mariadb/entity.go @@ -77,7 +77,8 @@ type DatabaseRow interface { ActivityHasIssueRow | ActivityHasServiceRow | IssueRepositoryServiceRow | - IssueMatchChangeRow + IssueMatchChangeRow | + ServiceIssueVariantRow } type IssueRow struct { @@ -366,6 +367,32 @@ func (ivwr *IssueVariantWithRepository) AsIssueVariantEntry() entity.IssueVarian } } +type ServiceIssueVariantRow struct { + IssueRepositoryRow + IssueVariantRow +} + +func (siv *ServiceIssueVariantRow) AsServiceIssueVariantEntry() entity.ServiceIssueVariant { + rep := siv.IssueRepositoryRow.AsIssueRepository() + return entity.ServiceIssueVariant{ + IssueVariant: entity.IssueVariant{ + Id: GetInt64Value(siv.IssueVariantRow.Id), + IssueRepositoryId: GetInt64Value(siv.IssueRepositoryRow.IssueRepositoryId), + IssueRepository: &rep, + SecondaryName: GetStringValue(siv.IssueVariantRow.SecondaryName), + IssueId: GetInt64Value(siv.IssueId), + Issue: nil, + Severity: entity.NewSeverity(GetStringValue(siv.Vector)), + Description: GetStringValue(siv.Description), + CreatedAt: GetTimeValue(siv.IssueVariantRow.CreatedAt), + DeletedAt: GetTimeValue(siv.IssueVariantRow.DeletedAt), + UpdatedAt: GetTimeValue(siv.IssueVariantRow.UpdatedAt), + }, + ServiceId: GetInt64Value(siv.IssueRepositoryServiceRow.ServiceId), + Priority: GetInt64Value(siv.Priority), + } +} + type ComponentRow struct { Id sql.NullInt64 `db:"component_id" json:"id"` Name sql.NullString `db:"component_name" json:"name"` diff --git a/internal/database/mariadb/service_issue_variant.go b/internal/database/mariadb/service_issue_variant.go new file mode 100644 index 00000000..20f29230 --- /dev/null +++ b/internal/database/mariadb/service_issue_variant.go @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package mariadb + +import ( + "fmt" + "github.com/cloudoperators/heureka/internal/entity" + "github.com/jmoiron/sqlx" + "github.com/sirupsen/logrus" +) + +func (s *SqlDatabase) ensureServiceIssueVariantFilter(f *entity.ServiceIssueVariantFilter) *entity.ServiceIssueVariantFilter { + var first = 1000 + var after int64 = 0 + if f == nil { + return &entity.ServiceIssueVariantFilter{ + Paginated: entity.Paginated{ + First: &first, + After: &after, + }, + ComponentInstanceId: nil, + } + } + + if f.After == nil { + f.After = &after + } + if f.First == nil { + f.First = &first + } + return f +} + +func (s *SqlDatabase) getServiceIssueVariantFilterString(filter *entity.ServiceIssueVariantFilter) string { + var fl []string + fl = append(fl, buildFilterQuery(filter.ComponentInstanceId, "CI.componentinstance_id = ?", OP_OR)) + fl = append(fl, "IV.issuevariant_deleted_at IS NULL") + + return combineFilterQueries(fl, OP_AND) +} + +func (s *SqlDatabase) buildServiceIssueVariantStatement(baseQuery string, filter *entity.ServiceIssueVariantFilter, withCursor bool, l *logrus.Entry) (*sqlx.Stmt, []interface{}, error) { + var query string + filter = s.ensureServiceIssueVariantFilter(filter) + l.WithFields(logrus.Fields{"filter": filter}) + + filterStr := s.getServiceIssueVariantFilterString(filter) + cursor := getCursor(filter.Paginated, filterStr, "IV.issuevariant_id > ?") + + whereClause := "" + if filterStr != "" || withCursor { + whereClause = fmt.Sprintf("WHERE %s", filterStr) + } + + // construct final query + if withCursor { + query = fmt.Sprintf(baseQuery, whereClause, cursor.Statement) + } else { + query = fmt.Sprintf(baseQuery, whereClause) + } + + //construct prepared statement and if where clause does exist add parameters + var stmt *sqlx.Stmt + var err error + + stmt, err = s.db.Preparex(query) + if err != nil { + msg := ERROR_MSG_PREPARED_STMT + l.WithFields( + logrus.Fields{ + "error": err, + "query": query, + "stmt": stmt, + }).Error(msg) + return nil, nil, fmt.Errorf("%s", msg) + } + + //adding parameters + var filterParameters []interface{} + filterParameters = buildQueryParameters(filterParameters, filter.ComponentInstanceId) + + if withCursor { + filterParameters = append(filterParameters, cursor.Value) + filterParameters = append(filterParameters, cursor.Limit) + } + + return stmt, filterParameters, nil +} + +func (s *SqlDatabase) GetServiceIssueVariants(filter *entity.ServiceIssueVariantFilter) ([]entity.ServiceIssueVariant, error) { + l := logrus.WithFields(logrus.Fields{ + "event": "database.GetIssueVariants", + }) + + baseQuery := ` + SELECT IRS.issuerepositoryservice_priority, IV.* FROM ComponentInstance CI + # Join path to Issue + INNER JOIN ComponentVersion CV on CI.componentinstance_component_version_id = CV.componentversion_id + INNER JOIN ComponentVersionIssue CVI on CV.componentversion_id = CVI.componentversionissue_component_version_id + INNER JOIN Issue I on CVI.componentversionissue_issue_id = I.issue_id + + # Join path to Repository + INNER JOIN Service S on CI.componentinstance_service_id = S.service_id + INNER JOIN IssueRepositoryService IRS on IRS.issuerepositoryservice_service_id = S.service_id + INNER JOIN IssueRepository IR on IR.issuerepository_id = IRS.issuerepositoryservice_issue_repository_id + + # Join to from repo and issue to IssueVariant + INNER JOIN IssueVariant IV on I.issue_id = IV.issuevariant_issue_id and IV.issuevariant_repository_id = IR.issuerepository_id + %s + %s ORDER BY IV.issuevariant_id LIMIT ? + ` + + stmt, filterParameters, err := s.buildServiceIssueVariantStatement(baseQuery, filter, true, l) + + if err != nil { + return nil, err + } + + defer stmt.Close() + + return performListScan( + stmt, + filterParameters, + l, + func(l []entity.ServiceIssueVariant, e ServiceIssueVariantRow) []entity.ServiceIssueVariant { + return append(l, e.AsServiceIssueVariantEntry()) + }, + ) +} diff --git a/internal/database/mariadb/service_issue_variant_test.go b/internal/database/mariadb/service_issue_variant_test.go new file mode 100644 index 00000000..fc8d442d --- /dev/null +++ b/internal/database/mariadb/service_issue_variant_test.go @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package mariadb_test + +import ( + "database/sql" + "fmt" + "github.com/brianvoe/gofakeit/v7" + "github.com/cloudoperators/heureka/internal/database/mariadb" + "github.com/cloudoperators/heureka/internal/database/mariadb/test" + "github.com/cloudoperators/heureka/internal/entity" + "github.com/goark/go-cvss/v3/metric" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/samber/lo" +) + +var _ = Describe("ServiceIssueVariant - ", Label("database", "IssueVariant"), func() { + var db *mariadb.SqlDatabase + var seeder *test.DatabaseSeeder + BeforeEach(func() { + var err error + db = dbm.NewTestSchema() + seeder, err = test.NewDatabaseSeeder(dbm.DbConfig()) + Expect(err).To(BeNil(), "Database Seeder Setup should work") + }) + + When("Getting ServiceIssueVariants", Label("GetServiceIssueVariants"), func() { + Context("and the database is empty", func() { + It("can perform the query", func() { + res, err := db.GetServiceIssueVariants(nil) + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + By("returning an empty list", func() { + Expect(res).To(BeEmpty()) + }) + }) + }) + Context("and we have 10 issue variants in the database", func() { + BeforeEach(func() { + seeder.SeedDbWithNFakeData(10) + }) + //this should work and give me all combinations back + Context("and using no filter", func() { + It("Should work", func() { + _, err := db.GetServiceIssueVariants(nil) + By("throwing no error", func() { + Expect(err).Should(BeNil()) + }) + }) + }) + }) + + //This is a testcase with a custom complex setup: + // We need to setup a whole bunch of data to test the filtering for service issue variants based by component instances + // The idea is to create + DescribeTable("and filtering for component instances", func(filterForInstances int, totalInstances int, totalIssues int) { + //Complex Setup + var allCI []mariadb.ComponentInstanceRow + var issueVariants []mariadb.IssueVariantRow + var issueRepositories []mariadb.BaseIssueRepositoryRow + issue_count := totalIssues / totalInstances + // create issue repository + issueRepositories = seeder.SeedIssueRepositories() + + for i := 0; i < totalInstances; i++ { + components := make([]mariadb.ComponentRow, 0) + services := make([]mariadb.BaseServiceRow, 0) + + // create the component + // we do this until it got successfully created to avoid failures through unique constraint violations + // this happens on bigger datasets due to the limited randomness of the fixtures + for len(components) == 0 { + components = seeder.SeedComponents(1) + } + + // create the service + // we do this until it got successfully created to avoid failures through unique constraint violations + // this happens on bigger datasets due to the limited randomness of the fixtures + for len(services) == 0 { + services = seeder.SeedServices(1) + } + + //create issues + issues := seeder.SeedIssues(issue_count) + + // create component version and adding each issue to the component version + componentVersions := seeder.SeedComponentVersions(1, components) + cvirows := make([]mariadb.ComponentVersionIssueRow, issue_count) + for idx, issue := range issues { + cvi := mariadb.ComponentVersionIssueRow{ + ComponentVersionId: componentVersions[0].Id, + IssueId: issue.Id, + } + + _, err := seeder.InsertFakeComponentVersionIssue(cvi) + Expect(err).To(BeNil()) + cvirows[idx] = cvi + } + + // create component instance + componentInstances := seeder.SeedComponentInstances(1, componentVersions, services) + allCI = append(allCI, componentInstances...) + + // create an issue variant per repo and issue (5 repos 10 issues) + variantList := make([]mariadb.IssueVariantRow, issue_count*5) + for idx, issue := range issues { + for irdx, ir := range issueRepositories { + variants := []string{fmt.Sprintf("GHSA-%d", i), fmt.Sprintf("RSHA-%d", i), fmt.Sprintf("VMSA-%d", i)} + v := test.GenerateRandomCVSS31Vector() + cvss, _ := metric.NewEnvironmental().Decode(v) + rating := cvss.Severity().String() + iv := mariadb.IssueVariantRow{ + IssueId: issue.Id, + IssueRepositoryId: ir.Id, + SecondaryName: sql.NullString{String: fmt.Sprintf("%s-%d-%d", gofakeit.RandomString(variants), gofakeit.Year(), gofakeit.Number(1000, 9999)), Valid: true}, + Description: issue.Description, + Vector: sql.NullString{String: v, Valid: true}, + Rating: sql.NullString{String: rating, Valid: true}, + } + id, err := seeder.InsertFakeIssueVariant(iv) + Expect(err).To(BeNil()) + iv.IssueId = sql.NullInt64{Int64: id, Valid: true} + variantList[(idx*5)+irdx] = iv + } + } + issueVariants = append(issueVariants, variantList...) + + // add to each repository the service with a increasing priority + // this means the last repository is always the highest priority one + for idx, ir := range issueRepositories { + irs := mariadb.IssueRepositoryServiceRow{ + IssueRepositoryId: ir.Id, + ServiceId: services[0].Id, + Priority: sql.NullInt64{Int64: int64(idx + 1), Valid: true}, + } + _, err := seeder.InsertFakeIssueRepositoryService(irs) + Expect(err).To(BeNil()) + } + } + + //Setup end + + //Except + By(fmt.Sprintf("having in total %d component instances with each %d issues across the repositories", filterForInstances, totalIssues), func() { + By("and filtering for this component instance", func() { + By("it can perform the query correctly", func() { + + //get instance ids to filter for based on count of instances that we want to filter for + cids := lo.Map(allCI, func(item mariadb.ComponentInstanceRow, _ int) *int64 { return lo.ToPtr(item.Id.Int64) }) + if len(cids) > filterForInstances { + cids = cids[:filterForInstances] + } + filter := &entity.ServiceIssueVariantFilter{ + Paginated: entity.Paginated{}, + ComponentInstanceId: cids, + } + res, err := db.GetServiceIssueVariants(filter) + + By("throwing no error", func() { + Expect(err).To(BeNil()) + }) + + By("and returning all the issue variants", func() { + Expect(len(res)).To(BeIdenticalTo((len(issueVariants) / totalInstances) * filterForInstances)) + }) + }) + + }) + }) + }, + Entry("1 of 1 component instance, with 10 issues", 1, 1, 10), + Entry("1 of 2 component instance, with 10 issues", 1, 2, 10), + Entry("1 of 1 component instance, with 100 issues", 1, 1, 100), + Entry("2 of 2 component instance, with 10 issues", 2, 2, 10), + Entry("4 of 100 component instance, with 50 issues", 4, 100, 50), + Entry("4 of 4 component instance, with 4 issues", 4, 4, 4), + ) + }) +}) diff --git a/internal/e2e/issue_variant_query_test.go b/internal/e2e/issue_variant_query_test.go index e073ee68..5ba4f242 100644 --- a/internal/e2e/issue_variant_query_test.go +++ b/internal/e2e/issue_variant_query_test.go @@ -218,7 +218,7 @@ var _ = Describe("Creating IssueVariant via API", Label("e2e", "IssueVariants"), var seedCollection *test.SeedCollection BeforeEach(func() { seedCollection = seeder.SeedDbWithNFakeData(10) - issueVariant = testentity.NewFakeIssueVariantEntity() + issueVariant = testentity.NewFakeIssueVariantEntity(nil) issueVariant.IssueRepositoryId = seedCollection.IssueRepositoryRows[0].Id.Int64 issueVariant.IssueId = seedCollection.IssueRows[0].Id.Int64 }) diff --git a/internal/entity/common.go b/internal/entity/common.go index d2a6cb1a..1a11dac2 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -46,7 +46,8 @@ type HeurekaEntity interface { IssueMatch | IssueMatchChange | HeurekaFilter | - IssueCount + IssueCount | + ServiceIssueVariant } type HeurekaFilter interface { diff --git a/internal/entity/issue_variant.go b/internal/entity/issue_variant.go index e7516a07..d42dea83 100644 --- a/internal/entity/issue_variant.go +++ b/internal/entity/issue_variant.go @@ -52,3 +52,24 @@ type IssueVariantResult struct { *IssueVariantAggregations *IssueVariant } + +type ServiceIssueVariant struct { + IssueVariant + ServiceId int64 `json:"service_id"` + Priority int64 `json:"priority"` +} + +type ServiceIssueVariantFilter struct { + Paginated + ComponentInstanceId []*int64 `json:"component_instance_id"` +} + +func NewServiceIssueVariantFilter() *ServiceIssueVariantFilter { + return &ServiceIssueVariantFilter{ + Paginated: Paginated{ + First: nil, + After: nil, + }, + ComponentInstanceId: nil, + } +} diff --git a/internal/entity/test/issue_variant.go b/internal/entity/test/issue_variant.go index 0a36a034..72437b67 100644 --- a/internal/entity/test/issue_variant.go +++ b/internal/entity/test/issue_variant.go @@ -9,7 +9,15 @@ import ( "github.com/cloudoperators/heureka/internal/entity" ) -func NewFakeIssueVariantEntity() entity.IssueVariant { +func NewFakeIssueVariantEntity(issue *int64) entity.IssueVariant { + var issueId int64 + + if issue == nil { + issueId = int64(gofakeit.Number(1, 10000000)) + } else { + issueId = *issue + } + vector := test.GenerateRandomCVSS31Vector() severity := entity.NewSeverity(vector) return entity.IssueVariant{ @@ -17,7 +25,7 @@ func NewFakeIssueVariantEntity() entity.IssueVariant { SecondaryName: gofakeit.Noun(), Description: gofakeit.Sentence(10), Severity: severity, - IssueId: 0, + IssueId: issueId, Issue: nil, IssueRepositoryId: 0, IssueRepository: nil, @@ -30,7 +38,7 @@ func NewFakeIssueVariantEntity() entity.IssueVariant { func NNewFakeIssueVariants(n int) []entity.IssueVariant { r := make([]entity.IssueVariant, n) for i := 0; i < n; i++ { - r[i] = NewFakeIssueVariantEntity() + r[i] = NewFakeIssueVariantEntity(nil) } return r } diff --git a/internal/entity/test/service_issue_variant.go b/internal/entity/test/service_issue_variant.go new file mode 100644 index 00000000..023680a4 --- /dev/null +++ b/internal/entity/test/service_issue_variant.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "github.com/cloudoperators/heureka/internal/entity" +) + +func NewFakeServiceIssueVariantEntity(prio int64, issueId *int64) entity.ServiceIssueVariant { + return entity.ServiceIssueVariant{ + IssueVariant: NewFakeIssueVariantEntity(issueId), + ServiceId: 0, + Priority: prio, + } +} + +func NNewFakeServiceIssueVariantEntity(n int, prio int64, issueId *int64) []entity.ServiceIssueVariant { + r := make([]entity.ServiceIssueVariant, n) + for i := 0; i < n; i++ { + r[i] = NewFakeServiceIssueVariantEntity(prio, issueId) + } + return r +} diff --git a/internal/mocks/mock_Database.go b/internal/mocks/mock_Database.go index 97964413..d40d75af 100644 --- a/internal/mocks/mock_Database.go +++ b/internal/mocks/mock_Database.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors // SPDX-License-Identifier: Apache-2.0 -// Code generated by mockery v2.46.1. DO NOT EDIT. +// Code generated by mockery v2.45.0. DO NOT EDIT. package mocks @@ -4239,6 +4239,64 @@ func (_c *MockDatabase_GetIssuesWithAggregations_Call) RunAndReturn(run func(*en return _c } +// GetServiceIssueVariants provides a mock function with given fields: _a0 +func (_m *MockDatabase) GetServiceIssueVariants(_a0 *entity.ServiceIssueVariantFilter) ([]entity.ServiceIssueVariant, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetServiceIssueVariants") + } + + var r0 []entity.ServiceIssueVariant + var r1 error + if rf, ok := ret.Get(0).(func(*entity.ServiceIssueVariantFilter) ([]entity.ServiceIssueVariant, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(*entity.ServiceIssueVariantFilter) []entity.ServiceIssueVariant); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]entity.ServiceIssueVariant) + } + } + + if rf, ok := ret.Get(1).(func(*entity.ServiceIssueVariantFilter) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDatabase_GetServiceIssueVariants_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServiceIssueVariants' +type MockDatabase_GetServiceIssueVariants_Call struct { + *mock.Call +} + +// GetServiceIssueVariants is a helper method to define mock.On call +// - _a0 *entity.ServiceIssueVariantFilter +func (_e *MockDatabase_Expecter) GetServiceIssueVariants(_a0 interface{}) *MockDatabase_GetServiceIssueVariants_Call { + return &MockDatabase_GetServiceIssueVariants_Call{Call: _e.mock.On("GetServiceIssueVariants", _a0)} +} + +func (_c *MockDatabase_GetServiceIssueVariants_Call) Run(run func(_a0 *entity.ServiceIssueVariantFilter)) *MockDatabase_GetServiceIssueVariants_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*entity.ServiceIssueVariantFilter)) + }) + return _c +} + +func (_c *MockDatabase_GetServiceIssueVariants_Call) Return(_a0 []entity.ServiceIssueVariant, _a1 error) *MockDatabase_GetServiceIssueVariants_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDatabase_GetServiceIssueVariants_Call) RunAndReturn(run func(*entity.ServiceIssueVariantFilter) ([]entity.ServiceIssueVariant, error)) *MockDatabase_GetServiceIssueVariants_Call { + _c.Call.Return(run) + return _c +} + // GetServiceNames provides a mock function with given fields: _a0 func (_m *MockDatabase) GetServiceNames(_a0 *entity.ServiceFilter) ([]string, error) { ret := _m.Called(_a0) diff --git a/internal/mocks/mock_Heureka.go b/internal/mocks/mock_Heureka.go index 867bac01..87603d69 100644 --- a/internal/mocks/mock_Heureka.go +++ b/internal/mocks/mock_Heureka.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors // SPDX-License-Identifier: Apache-2.0 -// Code generated by mockery v2.46.1. DO NOT EDIT. +// Code generated by mockery v2.45.0. DO NOT EDIT. package mocks