diff --git a/internal/api/graphql/graph/baseResolver/issue.go b/internal/api/graphql/graph/baseResolver/issue.go index 065db6df..397fa2dd 100644 --- a/internal/api/graphql/graph/baseResolver/issue.go +++ b/internal/api/graphql/graph/baseResolver/issue.go @@ -112,6 +112,10 @@ func IssueBaseResolver(app app.Heureka, ctx context.Context, filter *model.Issue IssueMatchTargetRemediationDate: nil, //@todo Implement } + if filter.State != nil { + f.State = entity.StateFilterType(*filter.State) + } + opt := GetIssueListOptions(requestedFields) issues, err := app.ListIssues(f, opt) @@ -179,6 +183,10 @@ func IssueNameBaseResolver(app app.Heureka, ctx context.Context, filter *model.I IssueMatchTargetRemediationDate: nil, //@todo Implement } + if filter.State != nil { + f.State = entity.StateFilterType(*filter.State) + } + opt := GetListOptions(requestedFields) names, err := app.ListIssueNames(f, opt) diff --git a/internal/api/graphql/graph/schema/issue.graphqls b/internal/api/graphql/graph/schema/issue.graphqls index 370f5a80..ce1ab8ee 100644 --- a/internal/api/graphql/graph/schema/issue.graphqls +++ b/internal/api/graphql/graph/schema/issue.graphqls @@ -59,6 +59,7 @@ input IssueFilter { primaryName: [String], issueMatchStatus: [IssueMatchStatusValues], issueType: [IssueTypes], + state: Int componentVersionId: [String], diff --git a/internal/database/mariadb/issue.go b/internal/database/mariadb/issue.go index a56b9ebc..e52eab74 100644 --- a/internal/database/mariadb/issue.go +++ b/internal/database/mariadb/issue.go @@ -51,7 +51,12 @@ func (s *SqlDatabase) getIssueFilterString(filter *entity.IssueFilter) string { fl = append(fl, buildFilterQuery(filter.Type, "I.issue_type = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.PrimaryName, "I.issue_primary_name = ?", OP_OR)) fl = append(fl, buildFilterQuery(filter.Search, wildCardFilterQuery, OP_OR)) - fl = append(fl, "I.issue_deleted_at IS NULL") + //TODO: create common function + if filter.State == entity.Active { + fl = append(fl, "I.issue_deleted_at IS NULL") + } else if filter.State == entity.Deleted { + fl = append(fl, "I.issue_deleted_at IS NOT NULL") + } return combineFilterQueries(fl, OP_AND) } diff --git a/internal/e2e/common/issue.go b/internal/e2e/common/issue.go index 8fc26276..0a05d336 100644 --- a/internal/e2e/common/issue.go +++ b/internal/e2e/common/issue.go @@ -9,6 +9,7 @@ import ( "os" "github.com/cloudoperators/heureka/internal/api/graphql/graph/model" + "github.com/cloudoperators/heureka/internal/entity" util2 "github.com/cloudoperators/heureka/pkg/util" "github.com/machinebox/graphql" @@ -77,6 +78,30 @@ func QueryUpdateIssue(port string, issue Issue, iid string) *model.Issue { return &respData.Issue } +func QueryDeleteIssue(port string, iid string) string { + // create a queryCollection (safe to share across requests) + client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", port)) + + //@todo may need to make this more fault proof?! What if the test is executed from the root dir? does it still work? + b, err := os.ReadFile("../api/graphql/graph/queryCollection/issue/delete.graphql") + Expect(err).To(BeNil()) + str := string(b) + req := graphql.NewRequest(str) + + req.Var("id", iid) + + req.Header.Set("Cache-Control", "no-cache") + ctx := context.Background() + + var respData struct { + Id string `json:"deleteIssue"` + } + if err := util2.RequestWithBackoff(func() error { return client.Run(ctx, req, &respData) }); err != nil { + logrus.WithError(err).WithField("request", req).Fatalln("Error while unmarshaling") + } + return respData.Id +} + func QueryGetIssue(port string, issuePrimaryName string) *model.IssueConnection { // create a queryCollection (safe to share across requests) client := graphql.NewClient(fmt.Sprintf("http://localhost:%s/query", port)) @@ -87,7 +112,7 @@ func QueryGetIssue(port string, issuePrimaryName string) *model.IssueConnection str := string(b) req := graphql.NewRequest(str) - req.Var("filter", map[string]string{"primaryName": issuePrimaryName}) + req.Var("filter", map[string]string{"primaryName": issuePrimaryName, "state": fmt.Sprintf("%d", entity.All)}) req.Var("first", 1) req.Var("after", "0") diff --git a/internal/e2e/metadata_test.go b/internal/e2e/metadata_test.go index fed32e26..3ad1be3c 100644 --- a/internal/e2e/metadata_test.go +++ b/internal/e2e/metadata_test.go @@ -37,6 +37,7 @@ func createTestIssue(port string) string { Expect(issue.Type.String()).To(Equal(testCreatedIssueType)) return issue.ID } + func updateTestIssue(port string, iid string) { issue := e2e_common.QueryUpdateIssue(port, e2e_common.Issue{PrimaryName: testIssuePrimaryName, Description: testUpdatedIssueDescription, Type: testUpdatedIssueType}, iid) Expect(*issue.PrimaryName).To(Equal(testIssuePrimaryName)) @@ -44,12 +45,23 @@ func updateTestIssue(port string, iid string) { Expect(issue.Type.String()).To(Equal(testUpdatedIssueType)) } +func deleteTestIssue(port string, iid string) { + issueId := e2e_common.QueryDeleteIssue(port, iid) + Expect(issueId).To(Equal(iid)) +} + func getTestIssue(port string) model.Issue { issues := e2e_common.QueryGetIssue(port, testIssuePrimaryName) Expect(issues.TotalCount).To(Equal(1)) return *issues.Edges[0].Node } +func parseTimeExpectNoError(t string) time.Time { + tt, err := time.Parse(dbDateLayout, t) + Expect(err).Should(BeNil()) + return tt +} + var _ = Describe("Creating and updating entity via API", Label("e2e", "Entities"), func() { var s *server.Server var cfg util.Config @@ -74,22 +86,21 @@ var _ = Describe("Creating and updating entity via API", Label("e2e", "Entities" createTestIssue(cfg.Port) issue = getTestIssue(cfg.Port) }) - It("shall assign CreatedBy, CreatedAt, UpdatedBy and UpdatedAt metadata fields and shall keep nil in DeltedAt metadata fields", func() { + It("shall assign CreatedBy, CreatedAt, UpdatedBy and UpdatedAt metadata fields and shall keep zero value in DeltedAt metadata fields", func() { Expect(*issue.Description).To(Equal(testCreatedIssueDescription)) Expect(issue.Type.String()).To(Equal(testCreatedIssueType)) Expect(issue.Metadata).To(Not(BeNil())) Expect(*issue.Metadata.CreatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) - createdAt, err := time.Parse(dbDateLayout, *issue.Metadata.CreatedAt) - Expect(err).Should(BeNil()) - Expect(createdAt).Should(BeTemporally("~", time.Now().UTC(), 3*time.Second)) + createdAt := parseTimeExpectNoError(*issue.Metadata.CreatedAt) + deletedAt := parseTimeExpectNoError(*issue.Metadata.DeletedAt) + updatedAt := parseTimeExpectNoError(*issue.Metadata.UpdatedAt) + Expect(createdAt).Should(BeTemporally("~", time.Now().UTC(), 3*time.Second)) Expect(*issue.Metadata.UpdatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) - - updatedAt, err := time.Parse(dbDateLayout, *issue.Metadata.UpdatedAt) - Expect(err).Should(BeNil()) Expect(updatedAt).To(Equal(createdAt)) + Expect(deletedAt).To(Equal(time.Unix(0, 0).Local())) }) }) When("Issue is updated via API", func() { @@ -100,24 +111,82 @@ var _ = Describe("Creating and updating entity via API", Label("e2e", "Entities" updateTestIssue(cfg.Port, iid) issue = getTestIssue(cfg.Port) }) - It("shall assign UpdatedBy and UpdatedAt metadata fields and shall keep nil in DeletedAt metadata field", func() { + It("shall assign UpdatedBy and UpdatedAt metadata fields and shall keep zero value in DeletedAt metadata field", func() { Expect(*issue.Description).To(Equal(testUpdatedIssueDescription)) Expect(issue.Type.String()).To(Equal(testUpdatedIssueType)) Expect(issue.Metadata).To(Not(BeNil())) Expect(*issue.Metadata.CreatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) - createdAt, err := time.Parse(dbDateLayout, *issue.Metadata.CreatedAt) - Expect(err).Should(BeNil()) - Expect(createdAt).Should(BeTemporally("~", time.Now().UTC(), 3*time.Second)) + createdAt := parseTimeExpectNoError(*issue.Metadata.CreatedAt) + deletedAt := parseTimeExpectNoError(*issue.Metadata.DeletedAt) + updatedAt := parseTimeExpectNoError(*issue.Metadata.UpdatedAt) + Expect(createdAt).Should(BeTemporally("~", time.Now().UTC(), 3*time.Second)) Expect(*issue.Metadata.UpdatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) - - updatedAt, err := time.Parse(dbDateLayout, *issue.Metadata.UpdatedAt) - Expect(err).Should(BeNil()) Expect(updatedAt).Should(BeTemporally("~", time.Now().UTC(), 2*time.Second)) Expect(updatedAt).Should(BeTemporally(">", createdAt)) + Expect(deletedAt).To(Equal(time.Unix(0, 0).Local())) }) }) + When("Issue is deleted via API", func() { + var issue model.Issue + BeforeEach(func() { + iid := createTestIssue(cfg.Port) + time.Sleep(1100 * time.Millisecond) + deleteTestIssue(cfg.Port, iid) + issue = getTestIssue(cfg.Port) + }) + It("shall assign UpdatedBy, DeletedAt and UpdatedAt metadata fields on delete", func() { + Expect(*issue.Description).To(Equal(testCreatedIssueDescription)) + Expect(issue.Type.String()).To(Equal(testCreatedIssueType)) + + Expect(issue.Metadata).To(Not(BeNil())) + Expect(*issue.Metadata.CreatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) + + createdAt := parseTimeExpectNoError(*issue.Metadata.CreatedAt) + deletedAt := parseTimeExpectNoError(*issue.Metadata.DeletedAt) + updatedAt := parseTimeExpectNoError(*issue.Metadata.UpdatedAt) + + Expect(createdAt).Should(BeTemporally("~", time.Now().UTC(), 3*time.Second)) + Expect(*issue.Metadata.UpdatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) + Expect(deletedAt).Should(BeTemporally("~", time.Now().UTC(), 2*time.Second)) + Expect(deletedAt).Should(BeTemporally(">", createdAt)) + Expect(deletedAt).To(Equal(updatedAt)) + }) + }) + /* When("Issue is deleted via API", func() { + var issue model.Issue + var updatedAt0 time.Time + BeforeEach(func() { + iid := createTestIssue(cfg.Port) + time.Sleep(1100 * time.Millisecond) + updateTestIssue(cfg.Port, iid) + + issue = getTestIssue(cfg.Port) + updatedAt0 = parseTimeExpectNoError(*issue.Metadata.UpdatedAt) + + time.Sleep(1100 * time.Millisecond) + deleteTestIssue(cfg.Port, iid) + issue = getTestIssue(cfg.Port) + }) + It("shall assign UpdatedBy and DeletedAt metadata fields on delete and UpdatedAt should not change after delete", func() { + Expect(*issue.Description).To(Equal(testUpdatedIssueDescription)) + Expect(issue.Type.String()).To(Equal(testUpdatedIssueType)) + + Expect(issue.Metadata).To(Not(BeNil())) + Expect(*issue.Metadata.CreatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) + + createdAt := parseTimeExpectNoError(*issue.Metadata.CreatedAt) + Expect(createdAt).Should(BeTemporally("~", time.Now().UTC(), 3*time.Second)) + + Expect(*issue.Metadata.UpdatedBy).To(Equal(fmt.Sprintf("%d", e2e_common.SystemUserId))) + + updatedAt := parseTimeExpectNoError(*issue.Metadata.UpdatedAt) + Expect(updatedAt0).Should(BeTemporally(">", createdAt)) + Expect(updatedAt).Should(BeTemporally(">", updatedAt0)) + }) + + })*/ }) diff --git a/internal/entity/common.go b/internal/entity/common.go index dd1ecafc..8b39e57f 100644 --- a/internal/entity/common.go +++ b/internal/entity/common.go @@ -227,3 +227,11 @@ type Metadata struct { UpdatedBy int64 `json:"updated_by"` DeletedAt time.Time `json:"deleted_at,omitempty"` } + +type StateFilterType int + +const ( + Active StateFilterType = 0 + Deleted StateFilterType = 1 + All StateFilterType = 2 +) diff --git a/internal/entity/issue.go b/internal/entity/issue.go index 44e52129..dc4f5a10 100644 --- a/internal/entity/issue.go +++ b/internal/entity/issue.go @@ -51,18 +51,19 @@ type IssueResult struct { type IssueFilter struct { Paginated - PrimaryName []*string `json:"primary_name"` - ServiceCCRN []*string `json:"service_ccrn"` - Type []*string `json:"type"` - Id []*int64 `json:"id"` - ActivityId []*int64 `json:"activity_id"` - IssueMatchId []*int64 `json:"issue_match_id"` - ComponentVersionId []*int64 `json:"component_version_id"` - IssueVariantId []*int64 `json:"issue_variant_id"` - Search []*string `json:"search"` - IssueMatchStatus []*string `json:"issue_match_status"` - IssueMatchDiscoveryDate *TimeFilter `json:"issue_match_discovery_date"` - IssueMatchTargetRemediationDate *TimeFilter `json:"issue_match_target_remediation_date"` + PrimaryName []*string `json:"primary_name"` + ServiceCCRN []*string `json:"service_ccrn"` + Type []*string `json:"type"` + Id []*int64 `json:"id"` + ActivityId []*int64 `json:"activity_id"` + IssueMatchId []*int64 `json:"issue_match_id"` + ComponentVersionId []*int64 `json:"component_version_id"` + IssueVariantId []*int64 `json:"issue_variant_id"` + Search []*string `json:"search"` + IssueMatchStatus []*string `json:"issue_match_status"` + IssueMatchDiscoveryDate *TimeFilter `json:"issue_match_discovery_date"` + IssueMatchTargetRemediationDate *TimeFilter `json:"issue_match_target_remediation_date"` + State StateFilterType `json:"state"` } type IssueAggregations struct {